import { useEffect, useMemo, useRef, useState } from 'react';
import type { CSSProperties, RefObject } from 'react';
import { createPortal } from 'react-dom';
import { cn } from '@/lib/utils';
import type { Skill } from '@/api/types';

interface Props {
  query: string;
  skills: Skill[];
  // v1.12 CP7.5: was `anchorRect: {top, left}` (snapshot at open time). Now a
  // live ref so the dropdown can re-stat the input on visualViewport events —
  // critical on iOS where the keyboard shifts the visual viewport and the
  // dropdown would otherwise sit in the wrong place (often hidden).
  inputRef: RefObject<HTMLElement | null>;
  onSelect: (skillName: string) => void;
  onClose: () => void;
}

// max-h-[320px] on the popover — use as the height budget for above/below
// fit decisions. Slightly under-estimates when the list is short, but the
// only consequence is we sometimes flip below when we'd fit above; no UX
// breakage either way.
const DROPDOWN_HEIGHT_BUDGET = 320;

// Batch 9.6: slash-command dropdown. Models FileMentionPopover's pattern —
// fixed-positioned popover, keyboard nav, click-outside-to-close. shadcn
// `Command` (cmdk) isn't installed in this project; per the addendum we use
// a plain div + Tailwind instead of pulling a new primitive autonomously.
//
// v1.12 CP7.5: portalled to document.body (escapes transformed/will-change
// ancestor stacking contexts that hid the popover inside ChatInput on iOS)
// + visualViewport-aware positioning (handles keyboard open/close + the iOS
// "shift layout to keep input visible" auto-scroll).

// Case-insensitive prefix match on `name` only. Description is display-only
// in v1 (substring search across description is deferred to a polish batch).
function filterByPrefix(skills: Skill[], query: string): Skill[] {
  const q = query.toLowerCase();
  const filtered = q
    ? skills.filter((s) => s.name.toLowerCase().startsWith(q))
    : skills;
  // Stable alphabetical ordering matches the server's cache order (skills.ts
  // sorts on name asc) but we re-sort here so a stale client cache doesn't
  // surprise the user.
  return [...filtered].sort((a, b) => a.name.localeCompare(b.name));
}

export function SkillSlashCommand({ query, skills, inputRef, onSelect, onClose }: Props) {
  const [highlightIndex, setHighlightIndex] = useState(0);
  const popoverRef = useRef<HTMLDivElement>(null);
  const filtered = useMemo(() => filterByPrefix(skills, query), [skills, query]);

  // Anchor + viewport tracking. `rect` is the input's bounding rect in layout
  // viewport coords. `vvTick` forces a re-render whenever visualViewport
  // changes even if the rect itself didn't (e.g. user scrolled the visual
  // viewport without the input moving in layout space).
  const [rect, setRect] = useState<DOMRect | null>(
    () => inputRef.current?.getBoundingClientRect() ?? null,
  );
  const [vvTick, setVvTick] = useState(0);

  useEffect(() => { setHighlightIndex(0); }, [query]);

  // v1.12 CP7.5: recalc on viewport changes. iOS Safari fires
  // visualViewport.resize when the soft keyboard opens/closes; .scroll fires
  // when the page is shifted to keep the focused input visible above the
  // keyboard. Both events should trigger a position recompute.
  useEffect(() => {
    function recalc() {
      setRect(inputRef.current?.getBoundingClientRect() ?? null);
      setVvTick((t) => t + 1);
    }
    recalc();
    const vv = window.visualViewport;
    vv?.addEventListener('resize', recalc);
    vv?.addEventListener('scroll', recalc);
    window.addEventListener('resize', recalc);
    return () => {
      vv?.removeEventListener('resize', recalc);
      vv?.removeEventListener('scroll', recalc);
      window.removeEventListener('resize', recalc);
    };
  }, [inputRef]);

  // Arrow / Enter / Tab / Escape. Bound on document so keystrokes from the
  // textarea reach the popover even though focus stays in the textarea.
  useEffect(() => {
    function handleKeyDown(e: KeyboardEvent) {
      if (e.key === 'ArrowDown') {
        e.preventDefault();
        setHighlightIndex((prev) => (prev < filtered.length - 1 ? prev + 1 : 0));
      } else if (e.key === 'ArrowUp') {
        e.preventDefault();
        setHighlightIndex((prev) => (prev > 0 ? prev - 1 : filtered.length - 1));
      } else if (e.key === 'Enter' || e.key === 'Tab') {
        if (filtered.length === 0) return;
        e.preventDefault();
        const target = filtered[highlightIndex] ?? filtered[0];
        if (target) onSelect(target.name);
      } else if (e.key === 'Escape') {
        e.preventDefault();
        onClose();
      }
    }
    document.addEventListener('keydown', handleKeyDown);
    return () => document.removeEventListener('keydown', handleKeyDown);
  }, [filtered, highlightIndex, onSelect, onClose]);

  useEffect(() => {
    function handleMouseDown(e: MouseEvent) {
      if (popoverRef.current && !popoverRef.current.contains(e.target as Node)) {
        onClose();
      }
    }
    document.addEventListener('mousedown', handleMouseDown);
    return () => document.removeEventListener('mousedown', handleMouseDown);
  }, [onClose]);

  useEffect(() => {
    const el = popoverRef.current?.querySelector('[data-highlighted="true"]');
    if (el) el.scrollIntoView({ block: 'nearest' });
  }, [highlightIndex]);

  // v1.12 CP7.5: visualViewport-corrected positioning. getBoundingClientRect
  // returns layout-viewport coords; iOS Safari's `position: fixed` positions
  // relative to the layout viewport too — but the visible area can be offset
  // (vv.offsetTop/offsetLeft) when iOS scrolls the input above the keyboard.
  // Subtracting the vv offsets keeps the dropdown locked to the input's
  // visual position. vvTick is in the dep list to force recompute on
  // visualViewport events even when the rect itself didn't change.
  //
  // Default: position above the input (matches original UX). Flip below if
  // above doesn't fit (input too close to top of visible viewport). When
  // below would overlap the keyboard, cap top so the dropdown stays visible.
  const style = useMemo<CSSProperties>(() => {
    if (!rect) return { display: 'none' };
    const vv = window.visualViewport;
    const vvOffsetTop = vv?.offsetTop ?? 0;
    const vvOffsetLeft = vv?.offsetLeft ?? 0;
    const vvHeight = vv?.height ?? window.innerHeight;

    const anchorTop = rect.top - vvOffsetTop;
    const anchorBottom = rect.bottom - vvOffsetTop;
    const left = rect.left - vvOffsetLeft;

    const fitsAbove = anchorTop >= DROPDOWN_HEIGHT_BUDGET;
    if (fitsAbove) {
      // translate(-100%) on Y so the dropdown grows upward from anchorTop.
      return {
        position: 'fixed',
        top: anchorTop,
        left,
        transform: 'translateY(-100%)',
      };
    }
    // Render below; clamp so the bottom edge stays inside the visible viewport.
    const maxTop = Math.max(0, vvHeight - DROPDOWN_HEIGHT_BUDGET);
    return {
      position: 'fixed',
      top: Math.min(anchorBottom, maxTop),
      left,
    };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [rect, vvTick]);

  const popover = filtered.length === 0 ? (
    <div
      ref={popoverRef}
      className="z-50 bg-popover border border-border rounded-md shadow min-w-[320px] p-2"
      style={style}
    >
      <div className="text-xs text-muted-foreground px-2 py-1">
        {query ? `No skill starts with "/${query}"` : 'No skills available'}
      </div>
    </div>
  ) : (
    <div
      ref={popoverRef}
      className="z-50 bg-popover border border-border rounded-md shadow min-w-[320px] max-w-[420px] max-h-[320px] overflow-y-auto"
      style={style}
    >
      {filtered.map((skill, i) => (
        <button
          key={skill.name}
          type="button"
          data-highlighted={i === highlightIndex}
          className={cn(
            'w-full text-left px-2.5 py-2 cursor-pointer block',
            i === highlightIndex && 'bg-muted',
          )}
          onMouseEnter={() => setHighlightIndex(i)}
          onMouseDown={(e) => {
            // mousedown not click — click runs after blur/focus shuffles which
            // can race with the textarea's onBlur close path.
            e.preventDefault();
            onSelect(skill.name);
          }}
        >
          <div className="font-mono text-xs font-bold text-foreground">/{skill.name}</div>
          <div
            className="text-xs text-muted-foreground overflow-hidden"
            style={{
              display: '-webkit-box',
              WebkitLineClamp: 2,
              WebkitBoxOrient: 'vertical',
            }}
          >
            {skill.description}
          </div>
        </button>
      ))}
    </div>
  );

  // v1.12 CP7.5: portal to document.body to escape ChatInput's stacking
  // context. The original render-in-place rendered the dropdown inside the
  // composer's transformed/will-change ancestor tree, which on iOS Safari +
  // Vivaldi caused the popover to either disappear or sit at z-index 0
  // behind the autofill toolbar. document.body has no transform ancestor.
  return createPortal(popover, document.body);
}
