"use client";

import React, { useState, useEffect, useRef, useCallback } from "react";
import { apiRequest } from "@/lib/api/client";
import type { KnowledgeEntryWithTags } from "@mosaic/shared";

interface LinkAutocompleteProps {
  /**
   * The textarea element to attach autocomplete to
   */
  textareaRef: React.RefObject<HTMLTextAreaElement | null>;
  /**
   * Callback when a link is selected
   */
  onInsert: (linkText: string) => void;
}

interface AutocompleteState {
  isOpen: boolean;
  query: string;
  position: { top: number; left: number };
  triggerIndex: number;
}

interface SearchResult {
  id: string;
  slug: string;
  title: string;
  summary?: string | null;
}

/**
 * LinkAutocomplete - Provides autocomplete for wiki-style links in markdown
 *
 * Detects when user types `[[` and shows a dropdown with matching entries.
 * Arrow keys navigate, Enter selects, Esc cancels.
 * Inserts `[[slug|title]]` on selection.
 */
export function LinkAutocomplete({
  textareaRef,
  onInsert,
}: LinkAutocompleteProps): React.ReactElement {
  const [state, setState] = useState<AutocompleteState>({
    isOpen: false,
    query: "",
    position: { top: 0, left: 0 },
    triggerIndex: -1,
  });
  const [results, setResults] = useState<SearchResult[]>([]);
  const [selectedIndex, setSelectedIndex] = useState<number>(0);
  const [isLoading, setIsLoading] = useState<boolean>(false);
  const [searchError, setSearchError] = useState<string | null>(null);
  const dropdownRef = useRef<HTMLDivElement>(null);
  const searchTimeoutRef = useRef<NodeJS.Timeout | null>(null);
  const abortControllerRef = useRef<AbortController | null>(null);
  const mirrorRef = useRef<HTMLDivElement | null>(null);
  const cursorSpanRef = useRef<HTMLSpanElement | null>(null);

  /**
   * Search for knowledge entries matching the query.
   * Accepts an AbortSignal to allow cancellation of in-flight requests,
   * preventing stale results from overwriting newer ones.
   */
  const searchEntries = useCallback(async (query: string, signal: AbortSignal): Promise<void> => {
    if (!query.trim()) {
      setResults([]);
      return;
    }

    setIsLoading(true);
    try {
      const response = await apiRequest<{
        data: KnowledgeEntryWithTags[];
        meta: {
          total: number;
          page: number;
          limit: number;
          totalPages: number;
        };
      }>(`/api/knowledge/search?q=${encodeURIComponent(query)}&limit=10`, {
        method: "GET",
        signal,
      });

      const searchResults: SearchResult[] = response.data.map((entry) => ({
        id: entry.id,
        slug: entry.slug,
        title: entry.title,
        summary: entry.summary,
      }));

      setResults(searchResults);
      setSelectedIndex(0);
      setSearchError(null);
    } catch (error) {
      // Ignore aborted requests - a newer search has superseded this one
      if (error instanceof DOMException && error.name === "AbortError") {
        return;
      }
      console.error("Failed to search entries:", error);
      setResults([]);
      setSearchError("Search unavailable — please try again");
    } finally {
      if (!signal.aborted) {
        setIsLoading(false);
      }
    }
  }, []);

  /**
   * Debounced search - waits 300ms after user stops typing.
   * Cancels any in-flight request via AbortController before firing a new one,
   * preventing race conditions where older results overwrite newer ones.
   */
  const debouncedSearch = useCallback(
    (query: string): void => {
      if (searchTimeoutRef.current) {
        clearTimeout(searchTimeoutRef.current);
      }

      // Abort any in-flight request from a previous search
      if (abortControllerRef.current) {
        abortControllerRef.current.abort();
      }

      searchTimeoutRef.current = setTimeout(() => {
        // Create a new AbortController for this search request
        const controller = new AbortController();
        abortControllerRef.current = controller;
        void searchEntries(query, controller.signal);
      }, 300);
    },
    [searchEntries]
  );

  /**
   * Calculate dropdown position relative to cursor.
   * Uses a persistent off-screen mirror element (via refs) to avoid
   * creating and removing DOM nodes on every keystroke, which causes
   * layout thrashing.
   */
  const calculateDropdownPosition = useCallback(
    (textarea: HTMLTextAreaElement, cursorIndex: number): { top: number; left: number } => {
      // Lazily create the mirror element once, then reuse it
      if (!mirrorRef.current) {
        const mirror = document.createElement("div");
        mirror.style.position = "absolute";
        mirror.style.visibility = "hidden";
        mirror.style.height = "auto";
        mirror.style.whiteSpace = "pre-wrap";
        mirror.style.pointerEvents = "none";
        document.body.appendChild(mirror);
        mirrorRef.current = mirror;

        const span = document.createElement("span");
        span.textContent = "|";
        cursorSpanRef.current = span;
      }

      const mirror = mirrorRef.current;
      const cursorSpan = cursorSpanRef.current;
      if (!cursorSpan) {
        return { top: 0, left: 0 };
      }

      // Sync styles from the textarea so measurement is accurate
      const styles = window.getComputedStyle(textarea);
      const stylesToCopy = [
        "fontFamily",
        "fontSize",
        "fontWeight",
        "letterSpacing",
        "lineHeight",
        "padding",
        "border",
        "boxSizing",
        "whiteSpace",
        "wordWrap",
      ] as const;

      stylesToCopy.forEach((prop) => {
        const value = styles.getPropertyValue(prop);
        if (value) {
          mirror.style.setProperty(prop, value);
        }
      });

      mirror.style.width = `${String(textarea.clientWidth)}px`;

      // Update content: text before cursor + cursor marker span
      const textBeforeCursor = textarea.value.substring(0, cursorIndex);
      mirror.textContent = textBeforeCursor;
      mirror.appendChild(cursorSpan);

      const textareaRect = textarea.getBoundingClientRect();
      const cursorSpanRect = cursorSpan.getBoundingClientRect();

      const top = cursorSpanRect.top - textareaRect.top + textarea.scrollTop + 20;
      const left = cursorSpanRect.left - textareaRect.left + textarea.scrollLeft;

      return { top, left };
    },
    []
  );

  /**
   * Handle input changes in the textarea
   */
  const handleInput = useCallback((): void => {
    const textarea = textareaRef.current;
    if (!textarea) return;

    const cursorPos = textarea.selectionStart;
    const text = textarea.value;

    // Look for `[[` before cursor
    const textBeforeCursor = text.substring(0, cursorPos);
    const lastTrigger = textBeforeCursor.lastIndexOf("[[");

    // Check if we're in an autocomplete context
    if (lastTrigger !== -1) {
      const textAfterTrigger = textBeforeCursor.substring(lastTrigger + 2);

      // Check if there's a closing `]]` between trigger and cursor
      const hasClosing = textAfterTrigger.includes("]]");

      if (!hasClosing) {
        // We're in autocomplete mode
        const query = textAfterTrigger;
        const position = calculateDropdownPosition(textarea, cursorPos);

        setState({
          isOpen: true,
          query,
          position,
          triggerIndex: lastTrigger,
        });

        debouncedSearch(query);
        return;
      }
    }

    // Not in autocomplete mode
    if (state.isOpen) {
      setState({
        isOpen: false,
        query: "",
        position: { top: 0, left: 0 },
        triggerIndex: -1,
      });
      setResults([]);
    }
  }, [textareaRef, state.isOpen, calculateDropdownPosition, debouncedSearch]);

  /**
   * Handle keyboard navigation in the dropdown
   */
  const handleKeyDown = useCallback(
    (e: KeyboardEvent): void => {
      if (!state.isOpen) return;

      switch (e.key) {
        case "ArrowDown":
          e.preventDefault();
          setSelectedIndex((prev) => (prev + 1) % results.length);
          break;

        case "ArrowUp":
          e.preventDefault();
          setSelectedIndex((prev) => (prev - 1 + results.length) % results.length);
          break;

        case "Enter":
          e.preventDefault();
          if (results.length > 0 && selectedIndex >= 0) {
            const selected = results[selectedIndex];
            if (selected) {
              insertLink(selected);
            }
          }
          break;

        case "Escape":
          e.preventDefault();
          setState({
            isOpen: false,
            query: "",
            position: { top: 0, left: 0 },
            triggerIndex: -1,
          });
          setResults([]);
          break;
      }
    },
    [state.isOpen, results, selectedIndex]
  );

  /**
   * Insert the selected link into the textarea
   */
  const insertLink = useCallback(
    (result: SearchResult): void => {
      const textarea = textareaRef.current;
      if (!textarea) return;

      const linkText = `[[${result.slug}|${result.title}]]`;
      const text = textarea.value;
      const before = text.substring(0, state.triggerIndex);
      const after = text.substring(textarea.selectionStart);
      const newText = before + linkText + after;

      onInsert(newText);

      // Close autocomplete
      setState({
        isOpen: false,
        query: "",
        position: { top: 0, left: 0 },
        triggerIndex: -1,
      });
      setResults([]);

      // Move cursor after the inserted link
      const newCursorPos = state.triggerIndex + linkText.length;
      setTimeout(() => {
        textarea.focus();
        textarea.setSelectionRange(newCursorPos, newCursorPos);
      }, 0);
    },
    [textareaRef, state.triggerIndex, onInsert]
  );

  /**
   * Handle click on a result
   */
  const handleResultClick = useCallback(
    (result: SearchResult): void => {
      insertLink(result);
    },
    [insertLink]
  );

  /**
   * Setup event listeners
   */
  useEffect(() => {
    const textarea = textareaRef.current;
    if (!textarea) return;

    textarea.addEventListener("input", handleInput);
    textarea.addEventListener("keydown", handleKeyDown as unknown as EventListener);

    return (): void => {
      textarea.removeEventListener("input", handleInput);
      textarea.removeEventListener("keydown", handleKeyDown as unknown as EventListener);
    };
  }, [textareaRef, handleInput, handleKeyDown]);

  /**
   * Cleanup timeout, abort in-flight requests, and remove the
   * persistent mirror element on unmount
   */
  useEffect(() => {
    return (): void => {
      if (searchTimeoutRef.current) {
        clearTimeout(searchTimeoutRef.current);
      }
      if (abortControllerRef.current) {
        abortControllerRef.current.abort();
      }
      if (mirrorRef.current) {
        document.body.removeChild(mirrorRef.current);
        mirrorRef.current = null;
        cursorSpanRef.current = null;
      }
    };
  }, []);

  if (!state.isOpen) {
    return <></>;
  }

  return (
    <div
      ref={dropdownRef}
      className="absolute z-50 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-md shadow-lg max-h-64 overflow-y-auto min-w-[300px] max-w-[500px]"
      style={{
        top: `${String(state.position.top)}px`,
        left: `${String(state.position.left)}px`,
      }}
    >
      {isLoading ? (
        <div className="p-3 text-sm text-gray-500 dark:text-gray-400">Searching...</div>
      ) : searchError ? (
        <div className="p-3 text-sm text-amber-600 dark:text-amber-400">{searchError}</div>
      ) : results.length === 0 ? (
        <div className="p-3 text-sm text-gray-500 dark:text-gray-400">
          {state.query ? "No entries found" : "Start typing to search..."}
        </div>
      ) : (
        <ul className="py-1">
          {results.map((result, index) => (
            <li
              key={result.id}
              className={`px-3 py-2 cursor-pointer transition-colors ${
                index === selectedIndex
                  ? "bg-blue-50 dark:bg-blue-900/30"
                  : "hover:bg-gray-50 dark:hover:bg-gray-700"
              }`}
              onClick={() => {
                handleResultClick(result);
              }}
              onMouseEnter={() => {
                setSelectedIndex(index);
              }}
            >
              <div className="font-medium text-sm text-gray-900 dark:text-gray-100">
                {result.title}
              </div>
              {result.summary && (
                <div className="text-xs text-gray-500 dark:text-gray-400 mt-1 line-clamp-2">
                  {result.summary}
                </div>
              )}
              <div className="text-xs text-gray-400 dark:text-gray-500 mt-1">{result.slug}</div>
            </li>
          ))}
        </ul>
      )}
      <div className="px-3 py-2 text-xs text-gray-400 dark:text-gray-500 border-t border-gray-200 dark:border-gray-700">
        ↑↓ Navigate • Enter Select • Esc Cancel
      </div>
    </div>
  );
}
