import { useEffect, useMemo, useRef, useState } from 'react';
import type { CSSProperties, ReactNode, RefObject } from 'react';
import { createPortal } from 'react-dom';
import { ChevronRight } from 'lucide-react';
import { cn } from '@/lib/utils';

export interface SlashCommandItem {
  name: string;
  // One-liner shown always; also the fallback body when the row is expanded.
  description?: string;
  // Optional fuller explanation revealed when the row's chevron is expanded.
  // When absent, the expanded panel falls back to `description` (un-truncated)
  // — we never fabricate detail for commands that don't carry it (e.g. raw
  // agent `/help` passthrough rows).
  details?: string;
}

export interface SlashCommandGroup {
  label: string;
  items: SlashCommandItem[];
  icon?: ReactNode;
}

interface Props {
  query: string;
  items: SlashCommandItem[];
  // Optional segmented rendering. When provided, items are shown under labeled
  // group headers (in order). `items` is ignored. BooChat passes only `items`
  // (flat) so its menu is unchanged — grouping is opt-in.
  groups?: SlashCommandGroup[];
  inputRef: RefObject<HTMLElement | null>;
  onSelect: (name: string) => void;
  onClose: () => void;
  emptyLabel?: string;
}

const DROPDOWN_HEIGHT_BUDGET = 320;

function filterByPrefix(items: SlashCommandItem[], query: string): SlashCommandItem[] {
  const q = query.toLowerCase();
  const filtered = q ? items.filter((s) => s.name.toLowerCase().startsWith(q)) : items;
  return [...filtered].sort((a, b) => a.name.localeCompare(b.name));
}

export function SlashCommandPicker({
  query,
  items,
  groups,
  inputRef,
  onSelect,
  onClose,
  emptyLabel = 'No commands available',
}: Props) {
  const [highlightIndex, setHighlightIndex] = useState(0);
  // At most one row's detail panel is open at a time; null = all collapsed.
  // Indexed against the flat `filtered` list (same index space as highlight).
  const [expandedIndex, setExpandedIndex] = useState<number | null>(null);
  const popoverRef = useRef<HTMLDivElement>(null);
  // When grouped, filter each group and drop empties; otherwise the flat list.
  const filteredGroups = useMemo(
    () =>
      groups
        ? groups
            .map((g) => ({ label: g.label, icon: g.icon, items: filterByPrefix(g.items, query) }))
            .filter((g) => g.items.length > 0)
        : null,
    [groups, query],
  );
  // Flat list drives keyboard nav + Enter selection across all groups.
  const filtered = useMemo(
    () => (filteredGroups ? filteredGroups.flatMap((g) => g.items) : filterByPrefix(items, query)),
    [filteredGroups, items, query],
  );

  const [rect, setRect] = useState<DOMRect | null>(
    () => inputRef.current?.getBoundingClientRect() ?? null,
  );
  const [vvTick, setVvTick] = useState(0);

  // Filtering reshuffles indices, so any open detail panel is stale — collapse it.
  useEffect(() => { setHighlightIndex(0); setExpandedIndex(null); }, [query]);

  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]);

  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 === 'ArrowRight') {
        // Expand the highlighted row in place (separate affordance from select).
        // Only acts when the row has something to reveal, so caret-right still
        // works on non-expandable rows; Enter/Tab selection is untouched.
        const target = filtered[highlightIndex];
        if (!target || !(target.details ?? target.description)) return;
        e.preventDefault();
        setExpandedIndex(highlightIndex);
      } else if (e.key === 'ArrowLeft') {
        // Collapse the highlighted row if it's the open one; otherwise let the
        // caret move as usual.
        if (expandedIndex !== highlightIndex) return;
        e.preventDefault();
        setExpandedIndex(null);
      } else if (e.key === 'Escape') {
        e.preventDefault();
        onClose();
      }
    }
    document.addEventListener('keydown', handleKeyDown);
    return () => document.removeEventListener('keydown', handleKeyDown);
  }, [filtered, highlightIndex, expandedIndex, 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]);

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

  const style = useMemo<CSSProperties>(() => {
    if (!rect) return { display: 'none' };
    const vv = window.visualViewport;
    const vvOffsetTop = vv?.offsetTop ?? 0;
    const vvHeight = vv?.height ?? window.innerHeight;
    // Visible region in layout-viewport coords (what position:fixed uses)
    const visibleTop = vvOffsetTop;
    const visibleBottom = vvOffsetTop + vvHeight;

    const spaceAbove = rect.top - visibleTop;
    const spaceBelow = visibleBottom - rect.bottom;

    if (spaceAbove >= Math.min(DROPDOWN_HEIGHT_BUDGET, spaceBelow)) {
      // Place above: clamp to visible top
      const popupTop = Math.max(visibleTop, rect.top - DROPDOWN_HEIGHT_BUDGET);
      return {
        position: 'fixed',
        top: popupTop,
        left: rect.left,
        maxHeight: rect.top - popupTop,
      };
    }
    // Place below: clamp to visible bottom
    return {
      position: 'fixed',
      top: rect.bottom,
      left: rect.left,
      maxHeight: Math.min(DROPDOWN_HEIGHT_BUDGET, visibleBottom - rect.bottom),
    };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [rect, vvTick]);

  const renderItem = (item: SlashCommandItem, i: number) => {
    const expanded = i === expandedIndex;
    // Fuller text when provided, else the one-liner shown un-truncated. Never
    // invented — a row with neither shows an honest placeholder.
    const detailText = item.details ?? item.description ?? null;
    return (
      <div
        key={`${i}-${item.name}`}
        role="option"
        aria-selected={i === highlightIndex}
        data-highlighted={i === highlightIndex}
        data-expanded={expanded}
        className={cn(
          'w-full text-left px-2.5 py-2 cursor-pointer block',
          i === highlightIndex && 'bg-muted',
        )}
        onMouseEnter={() => setHighlightIndex(i)}
        onClick={() => onSelect(item.name)}
      >
        <div className="flex items-start gap-2">
          <div className="min-w-0 flex-1">
            <div className="font-mono text-xs font-bold text-foreground">/{item.name}</div>
            {item.description && (
              <div className="text-xs text-muted-foreground truncate">{item.description}</div>
            )}
          </div>
          {/* Chevron is a SEPARATE affordance: stopPropagation keeps the row's
              click (select) from firing; preventDefault on mousedown keeps focus
              on the composer so typing/arrow-nav continues uninterrupted. */}
          <button
            type="button"
            aria-label={expanded ? `Hide details for /${item.name}` : `Show details for /${item.name}`}
            aria-expanded={expanded}
            onMouseDown={(e) => e.preventDefault()}
            onClick={(e) => {
              e.stopPropagation();
              setHighlightIndex(i);
              setExpandedIndex((prev) => (prev === i ? null : i));
            }}
            className="-mr-1 -mt-0.5 flex shrink-0 items-center justify-center rounded-md border border-border bg-background p-1 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground max-md:min-h-[36px] max-md:min-w-[36px]"
          >
            <ChevronRight
              className={cn(
                'size-3.5 transition-transform duration-200 motion-reduce:transition-none',
                expanded && 'rotate-90',
              )}
            />
          </button>
        </div>
        {/* grid-rows 0fr→1fr animates height with no fixed value; motion-reduce
            disables the transition for users who ask for less motion. */}
        <div
          className={cn(
            'grid transition-[grid-template-rows] duration-200 ease-out motion-reduce:transition-none',
            expanded ? 'grid-rows-[1fr]' : 'grid-rows-[0fr]',
          )}
        >
          <div className="overflow-hidden">
            <div className="mt-1.5 border-t border-border/40 pt-1.5 text-xs leading-relaxed text-muted-foreground whitespace-pre-wrap break-words">
              {detailText ?? <span className="italic text-muted-foreground/70">No description provided.</span>}
            </div>
          </div>
        </div>
      </div>
    );
  };

  let runningIndex = -1;
  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 command starts with "/${query}"` : emptyLabel}
      </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 overscroll-contain touch-pan-y"
      style={style}
    >
      {filteredGroups
        ? filteredGroups.map((g) => (
            <div key={g.label}>
              <div className="px-2.5 pt-2 pb-1 text-[10px] font-semibold uppercase tracking-wide text-muted-foreground/70 flex items-center gap-1.5">
                {g.icon}
                {g.label}
              </div>
              {g.items.map((item) => renderItem(item, (runningIndex += 1)))}
            </div>
          ))
        : filtered.map((item, i) => renderItem(item, i))}
    </div>
  );

  return createPortal(popover, document.body);
}
