'use client';

import { useState, useRef, useCallback, useEffect } from 'react';
import type { Location, HoverInfo } from '@cockpit/feature-explorer/server/lsp/types';
import { getLanguageForFile } from '@cockpit/feature-explorer/server/lsp/types';
import { Effect } from 'effect';
import { BrowserRuntime } from '@cockpit/effect-runtime';
import { lspDefinition, lspHover, lspReferences, lspWarmup } from './effect/lspClient';

// ============================================
// LSP Definition Hook
// ============================================

export function useLSPDefinition(cwd: string) {
  const [loading, setLoading] = useState(false);

  const goToDefinition = useCallback(async (
    filePath: string,
    line: number,
    column: number,
  ): Promise<Location[]> => {
    setLoading(true);
    const exit = await BrowserRuntime.runPromiseExit(
      lspDefinition({ cwd, filePath, line, column })
    );
    setLoading(false);
    if (exit._tag === 'Success') {
      return (exit.value.definitions ?? []) as Location[];
    }
    console.error('[useLSP] definition error:', exit.cause);
    return [];
  }, [cwd]);

  return { goToDefinition, loading };
}

// ============================================
// LSP Hover Hook
// ============================================

const HOVER_DELAY = 300; // ms

interface HoverData extends HoverInfo {
  x: number;
  y: number;
  filePath: string;
  line: number;
  column: number;
}

export function useLSPHover(cwd: string) {
  const [hoverInfo, setHoverInfo] = useState<HoverData | null>(null);
  const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
  const leaveTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
  const activeRequestRef = useRef<number>(0);
  const onCardRef = useRef(false); // Whether the mouse is over the card (with pointer-events-auto activated)

  // tooltip DOM ref + global mouse position (ref-only writes, zero re-renders)
  const tooltipElRef = useRef<HTMLDivElement | null>(null);
  const mousePosRef = useRef({ x: 0, y: 0 });

  // ref-callback exposed to consumers — keeps tooltipElRef internal so React
  // Compiler doesn't flag external mutation of the hook's return value.
  const setTooltipEl = useCallback((el: HTMLDivElement | null) => {
    tooltipElRef.current = el;
  }, []);

  useEffect(() => {
    const h = (e: MouseEvent) => { mousePosRef.current.x = e.clientX; mousePosRef.current.y = e.clientY; };
    document.addEventListener('mousemove', h);
    return () => document.removeEventListener('mousemove', h);
  }, []);

  // Imperatively activate tooltip interaction (pointer-events: auto + bind mouseleave)
  const activatedRef = useRef(false);
  const nativeLeaveRef = useRef<(() => void) | null>(null);

  const deactivateTooltip = useCallback(() => {
    const el = tooltipElRef.current;
    if (el && activatedRef.current) {
      el.style.pointerEvents = 'none';
    }
    activatedRef.current = false;
    if (nativeLeaveRef.current) {
      tooltipElRef.current?.removeEventListener('mouseleave', nativeLeaveRef.current);
      nativeLeaveRef.current = null;
    }
  }, []);

  const activateTooltip = useCallback(() => {
    const el = tooltipElRef.current;
    if (!el || activatedRef.current) return;
    activatedRef.current = true;
    onCardRef.current = true;
    el.style.pointerEvents = 'auto';

    // Bind native mouseleave (bypasses React, avoids re-render until the tooltip actually needs to hide)
    const handleLeave = () => {
      onCardRef.current = false;
      deactivateTooltip();
      activeRequestRef.current++;
      setHoverInfo(null);
    };
    nativeLeaveRef.current = handleLeave;
    el.addEventListener('mouseleave', handleLeave);
  }, [deactivateTooltip]);

  const onTokenMouseEnter = useCallback((
    filePath: string,
    line: number,
    column: number,
    rect: { x: number; y: number },
  ) => {
    if (timerRef.current) clearTimeout(timerRef.current);
    if (leaveTimerRef.current) clearTimeout(leaveTimerRef.current);

    const requestId = ++activeRequestRef.current;

    timerRef.current = setTimeout(async () => {
      const exit = await BrowserRuntime.runPromiseExit(
        lspHover({ cwd, filePath, line, column })
      );
      if (requestId !== activeRequestRef.current) return;
      if (exit._tag !== 'Success') return; // ignore errors (v1 .catch silently)
      const hover = exit.value.hover;
      if (hover && hover.displayString) {
        setHoverInfo({
          ...hover,
          x: rect.x,
          y: rect.y,
          filePath,
          line,
          column,
        });
      }
    }, HOVER_DELAY);
  }, [cwd]);

  const onTokenMouseLeave = useCallback(() => {
    if (timerRef.current) clearTimeout(timerRef.current);
    // Immediately invalidate in-flight requests to prevent a stale fetch from triggering setHoverInfo
    activeRequestRef.current++;
    // Delay hiding the card to give the user time to move the mouse onto it
    leaveTimerRef.current = setTimeout(function checkAndHide() {
      if (onCardRef.current) return; // Already on the activated card
      // Geometry check: is the mouse within the tooltip rect (even when pointer-events-none)
      const el = tooltipElRef.current;
      if (el) {
        const r = el.getBoundingClientRect();
        const { x, y } = mousePosRef.current;
        if (x >= r.left && x <= r.right && y >= r.top && y <= r.bottom) {
          // Mouse is over the tooltip → activate interaction, do not hide
          activateTooltip();
          return;
        }
      }
      setHoverInfo(null);
    }, 150);
  }, [activateTooltip]);

  // onCardMouseEnter / onCardMouseLeave are reserved for the button area's pointer-events-auto region
  const onCardMouseEnter = useCallback(() => {
    onCardRef.current = true;
    if (leaveTimerRef.current) clearTimeout(leaveTimerRef.current);
  }, []);

  const onCardMouseLeave = useCallback(() => {
    onCardRef.current = false;
    activeRequestRef.current++;
    setHoverInfo(null);
  }, []);

  const clearHover = useCallback(() => {
    if (timerRef.current) clearTimeout(timerRef.current);
    if (leaveTimerRef.current) clearTimeout(leaveTimerRef.current);
    onCardRef.current = false;
    deactivateTooltip();
    activeRequestRef.current++;
    setHoverInfo(null);
  }, [deactivateTooltip]);

  // When hoverInfo is cleared, reset the activation state
  useEffect(() => {
    if (!hoverInfo) {
      deactivateTooltip();
    }
  }, [hoverInfo, deactivateTooltip]);

  return { hoverInfo, onTokenMouseEnter, onTokenMouseLeave, onCardMouseEnter, onCardMouseLeave, clearHover, setTooltipEl };
}

// ============================================
// LSP References Hook
// ============================================

export function useLSPReferences(cwd: string) {
  const [references, setReferences] = useState<Location[]>([]);
  const [loading, setLoading] = useState(false);
  const [visible, setVisible] = useState(false);

  const findReferences = useCallback(async (
    filePath: string,
    line: number,
    column: number,
  ) => {
    setLoading(true);
    setVisible(true);
    const exit = await BrowserRuntime.runPromiseExit(
      lspReferences({ cwd, filePath, line, column })
    );
    if (exit._tag === 'Success') {
      setReferences((exit.value.references ?? []) as Location[]);
    } else {
      console.error('[useLSP] references error:', exit.cause);
      setReferences([]);
    }
    setLoading(false);
  }, [cwd]);

  const closeReferences = useCallback(() => {
    setVisible(false);
    setReferences([]);
  }, []);

  return { references, loading, visible, findReferences, closeReferences };
}

// ============================================
// LSP Warmup Hook - Pre-start the Language Server when a file is selected
// ============================================

export function useLSPWarmup(cwd: string, selectedPath: string | null) {
  const warmedRef = useRef<string | null>(null);

  useEffect(() => {
    if (!selectedPath || selectedPath === warmedRef.current) return;
    if (!getLanguageForFile(selectedPath)) return;

    warmedRef.current = selectedPath;

    // fire-and-forget, does not block the UI
    BrowserRuntime.runFork(
      lspWarmup({ cwd, filePath: selectedPath }).pipe(
        Effect.orElse(() => Effect.void)
      )
    );
  }, [cwd, selectedPath]);
}
