/**
 * Custom hook to handle all LSP integration logic
 * Consolidates LSP client setup, document lifecycle, completions, and hover
 */

import type { RefObject } from "react";
import { useEffect, useMemo, useRef } from "react";
import { extensionRegistry } from "@/extensions/registry/extension-registry";
import { useExtensionStore } from "@/extensions/registry/extension-store";
import { EDITOR_CONSTANTS } from "@/features/editor/config/constants";
import { useEditorLayout } from "@/features/editor/hooks/use-layout";
import { useSnippetCompletion } from "@/features/editor/hooks/use-snippet-completion";
import { LspClient } from "@/features/editor/lsp/lsp-client";
import { useLspStore } from "@/features/editor/lsp/lsp-store";
import { useDefinitionLink } from "@/features/editor/lsp/use-definition-link";
import { useGoToDefinition } from "@/features/editor/lsp/use-go-to-definition";
import { useHover } from "@/features/editor/lsp/use-hover";
import { useBufferStore } from "@/features/editor/stores/buffer-store";
import { useEditorUIStore } from "@/features/editor/stores/ui-store";
import { useFileSystemStore } from "@/features/file-system/controllers/store";
import { hasTextContent } from "@/features/panes/types/pane-content";
import { logger } from "../utils/logger";
import type { Position } from "../types/editor";

interface UseLspIntegrationOptions {
  enabled?: boolean;
  filePath: string | undefined;
  value: string;
  cursorPosition: Position;
  editorRef: RefObject<HTMLDivElement | null> | RefObject<HTMLTextAreaElement>;
}

/**
 * Check if file extension is supported by LSP
 */
const isFileSupported = (filePath: string | undefined): boolean => {
  if (!filePath) return false;
  // Use extension registry to check if LSP is supported for this file
  return extensionRegistry.isLspSupported(filePath);
};

/**
 * Hook that manages all LSP integration for the editor
 */
export const useLspIntegration = ({
  enabled = true,
  filePath,
  value,
  cursorPosition,
  editorRef,
}: UseLspIntegrationOptions) => {
  // Get LSP client instance (singleton)
  const lspClient = useMemo(() => LspClient.getInstance(), []);

  // Get workspace path
  const rootFolderPath = useFileSystemStore((state) => state.rootFolderPath);
  const installedExtensions = useExtensionStore.use.installedExtensions();

  // Check if current file is supported
  const isLspSupported = useMemo(() => isFileSupported(filePath), [filePath, installedExtensions]);

  // LSP store actions
  const lspActions = useLspStore.use.actions();

  // Snippet completion integration
  const snippetCompletion = useSnippetCompletion(filePath);

  // Get layout dimensions for hover position calculations
  const { charWidth, lineHeight } = useEditorLayout();

  // Use constant debounce for predictable completion behavior
  const completionTimerRef = useRef<NodeJS.Timeout | undefined>(undefined);

  // Track cursor position where completions were triggered (to hide on cursor movement)
  const completionTriggerOffsetRef = useRef<number | null>(null);

  // Track which file the last input was for (to avoid triggering completions on buffer switch)
  const lastInputFilePathRef = useRef<string | null>(null);

  // Track document versions per file path for LSP sync
  const documentVersionsRef = useRef<Map<string, number>>(new Map());

  // Track which documents have been opened (to avoid sending changes before open)
  const openedDocumentsRef = useRef<Set<string>>(new Set());

  // Get completion application state
  const isApplyingCompletion = useEditorUIStore.use.isApplyingCompletion();

  // Track when user actually types (not just cursor movement)
  const lastInputTimestamp = useEditorUIStore.use.lastInputTimestamp();

  // Set up LSP completion handlers
  useEffect(() => {
    if (!enabled) return;
    lspActions.setCompletionHandlers(lspClient.getCompletions.bind(lspClient), (fp: string) =>
      isFileSupported(fp),
    );
  }, [enabled, lspClient, lspActions]);

  // Set up hover functionality
  const hoverHandlers = useHover({
    getHover: lspClient.getHover.bind(lspClient),
    isLanguageSupported: (fp) => isFileSupported(fp),
    filePath: filePath || "",
    lineHeight,
    charWidth,
  });

  // Set up go-to-definition (Cmd+Click)
  const goToDefinitionHandlers = useGoToDefinition({
    getDefinition: lspClient.getDefinition.bind(lspClient),
    isLanguageSupported: (fp) => isFileSupported(fp),
    filePath: filePath || "",
    lineHeight,
    charWidth,
  });

  // Set up definition link highlighting (Cmd+hover)
  const definitionLinkHandlers = useDefinitionLink({
    filePath: filePath || "",
    content: value,
    lineHeight,
    charWidth,
    isLanguageSupported: isLspSupported,
    getDefinition: lspClient.getDefinition.bind(lspClient),
  });

  // Handle document lifecycle (open/close)
  useEffect(() => {
    if (!enabled) return;
    if (!filePath || !isLspSupported) return;

    // Derive workspace path from file path if rootFolderPath is not set
    // This handles cases where files are opened without a project folder
    const workspacePath = rootFolderPath || filePath.substring(0, filePath.lastIndexOf("/"));

    if (!workspacePath) {
      console.warn("LSP: Could not determine workspace path for", filePath);
      return;
    }

    const cleanupDocument = () => {
      const isStillOpen = useBufferStore
        .getState()
        .buffers.some((buffer) => hasTextContent(buffer) && buffer.path === filePath);

      if (isStillOpen) {
        return;
      }

      if (openedDocumentsRef.current.has(filePath)) {
        lspClient.notifyDocumentClose(filePath).catch((error) => {
          console.error("LSP document close error:", error);
        });
        lspClient.stopForFile(filePath).catch((error) => {
          console.error("LSP stop for file error:", error);
        });
      }

      documentVersionsRef.current.delete(filePath);
      openedDocumentsRef.current.delete(filePath);
    };

    if (openedDocumentsRef.current.has(filePath)) {
      return cleanupDocument;
    }

    // Start LSP server for this file and then notify about document open
    const initLsp = async () => {
      try {
        logger.debug("LspIntegration", `Starting LSP for ${filePath} in ${workspacePath}`);
        // Reset document version for this file
        // Rust sends version 1 on document open, so we start at 1
        // First change will increment to 2
        documentVersionsRef.current.set(filePath, 1);
        // Start LSP server for this file type
        const started = await lspClient.startForFile(filePath, workspacePath);
        if (!started) {
          return;
        }
        // Notify LSP about document open
        await lspClient.notifyDocumentOpen(filePath, value);
        // Mark document as opened so changes can be sent
        openedDocumentsRef.current.add(filePath);
        logger.debug("LspIntegration", `LSP started and document opened for ${filePath}`);
      } catch (error) {
        console.error("LSP initialization error:", error);
      }
    };

    initLsp();

    return cleanupDocument;
  }, [enabled, filePath, isLspSupported, lspClient, rootFolderPath, value]);

  // Handle document content changes
  useEffect(() => {
    if (!enabled) return;
    if (!filePath || !isLspSupported) return;

    // Only send changes after document is opened to avoid race condition
    if (!openedDocumentsRef.current.has(filePath)) {
      return;
    }

    // Increment document version for this file
    const currentVersion = documentVersionsRef.current.get(filePath) || 1;
    const newVersion = currentVersion + 1;
    documentVersionsRef.current.set(filePath, newVersion);

    lspClient.notifyDocumentChange(filePath, value, newVersion).catch((error) => {
      console.error("LSP document change error:", error);
    });
  }, [enabled, value, filePath, isLspSupported, lspClient]);

  // Handle completion triggers - only when user types (not on cursor movement)
  useEffect(() => {
    if (!enabled) return;
    // Safety: reset stuck isApplyingCompletion flag
    // This can happen if a previous completion application didn't complete properly
    if (isApplyingCompletion && lastInputTimestamp > 0) {
      useEditorUIStore.getState().actions.setIsApplyingCompletion(false);
    }

    // Only trigger completions when user actually types
    if (
      !filePath ||
      !editorRef.current ||
      !isLspSupported ||
      lastInputTimestamp === 0 ||
      !openedDocumentsRef.current.has(filePath)
    ) {
      if (completionTimerRef.current) {
        clearTimeout(completionTimerRef.current);
      }
      return;
    }

    // Skip if this is a buffer switch (filePath changed but typing happened in a different file)
    // This prevents completions from appearing when switching to a buffer where user didn't just type
    if (lastInputFilePathRef.current !== null && lastInputFilePathRef.current !== filePath) {
      return;
    }

    // Debounce completion trigger with fixed delay for predictable behavior
    completionTimerRef.current = setTimeout(() => {
      // Get latest value at trigger time (not from effect deps)
      const buffer = useBufferStore.getState().buffers.find((b) => b.path === filePath);
      if (!buffer || !hasTextContent(buffer)) return;

      // Store the cursor offset and file path where completion was triggered
      completionTriggerOffsetRef.current = cursorPosition.offset;
      lastInputFilePathRef.current = filePath;

      lspActions.requestCompletion({
        filePath,
        cursorPos: cursorPosition.offset,
        value: buffer.content, // Use latest content from store
        editorRef: editorRef as RefObject<HTMLDivElement | null>,
      });
    }, EDITOR_CONSTANTS.COMPLETION_DEBOUNCE_MS);

    return () => {
      if (completionTimerRef.current) {
        clearTimeout(completionTimerRef.current);
      }
    };
    // eslint-disable-next-line react-hooks/exhaustive-deps -- cursorPosition and isApplyingCompletion are read at render time, not as triggers
  }, [enabled, lastInputTimestamp, filePath, lspActions, isLspSupported, editorRef]);

  // Hide completions when cursor moves via navigation (not typing)
  // Navigation = cursor moves but lastInputTimestamp doesn't change
  const prevInputTimestampRef = useRef<number>(0);

  useEffect(() => {
    if (!enabled) return;
    const { isLspCompletionVisible } = useEditorUIStore.getState();

    // Only check if completions are visible
    if (!isLspCompletionVisible) {
      prevInputTimestampRef.current = lastInputTimestamp;
      return;
    }

    // If lastInputTimestamp changed, this cursor movement was caused by typing
    // Don't hide completions in this case
    if (lastInputTimestamp !== prevInputTimestampRef.current) {
      prevInputTimestampRef.current = lastInputTimestamp;
      return;
    }

    // lastInputTimestamp didn't change, so this is navigation (arrow keys, click, etc.)
    // Hide completions
    useEditorUIStore.getState().actions.setIsLspCompletionVisible(false);
    completionTriggerOffsetRef.current = null;
  }, [enabled, cursorPosition.offset, lastInputTimestamp]);

  return {
    lspClient,
    isLspSupported,
    snippetCompletion,
    hoverHandlers,
    goToDefinitionHandlers,
    definitionLinkHandlers,
  };
};
