import {
  useEffect,
  useMemo,
  useRef,
  useState,
  type CSSProperties,
  type KeyboardEvent,
  type ReactNode,
} from 'react';
import { useCommands } from './registry.js';
import { scoreCommand } from './fuzzy.js';
import type { Command, CommandMode } from './types.js';

export interface CommandPaletteProps {
  open: boolean;
  onClose: () => void;
  /** Initial mode tab. Default: 'actions'. */
  initialMode?: CommandMode;
  /** Hide the mode tabs (only render the active mode). */
  hideModeTabs?: boolean;
  /** Render a custom Ask-AI panel (replaces the default suggestion). */
  askAiPanel?: (query: string) => ReactNode;
  /** Called when the user picks a navigate-mode command. */
  onNavigate?: (href: string) => void;
  /** Force-merge external commands without registering them. */
  extraCommands?: Command[];
  /** localStorage key for recents. Default: 'bl-cmdk-recents'. */
  recentsKey?: string;
  /** How many recents to remember. Default: 8. */
  recentsLimit?: number;
  /** ARIA label for the dialog. */
  ariaLabel?: string;
  className?: string;
  style?: CSSProperties;
}

/**
 * `<CommandPalette>` — Cmd-K dialog with three modes:
 *   - **actions**  — invoke a registered command (calls its `run`)
 *   - **navigate** — jump to a registered route (uses `onNavigate` or
 *                    `window.location.assign` as fallback)
 *   - **ask-ai**   — delegate the query to an AI panel (host-supplied)
 *
 * Commands are sourced from `useCommands()` (provider context) plus any
 * `extraCommands` prop. Recents persist to `localStorage` under
 * `recentsKey` and float to the top of the actions list when the query
 * is empty.
 *
 * Keyboard:
 *   ↑ ↓     — move selection
 *   Enter   — run selection
 *   Tab     — cycle mode tabs
 *   Esc     — close
 */
export function CommandPalette({
  open,
  onClose,
  initialMode = 'actions',
  hideModeTabs = false,
  askAiPanel,
  onNavigate,
  extraCommands = [],
  recentsKey = 'bl-cmdk-recents',
  recentsLimit = 8,
  ariaLabel = 'Command palette',
  className,
  style,
}: CommandPaletteProps) {
  const registryCommands = useCommands();
  const allCommands = useMemo(
    () => [...registryCommands, ...extraCommands],
    [registryCommands, extraCommands],
  );

  const [mode, setMode] = useState<CommandMode>(initialMode);
  const [query, setQuery] = useState('');
  const [selected, setSelected] = useState(0);
  const inputRef = useRef<HTMLInputElement>(null);

  // Reset state every time the palette opens.
  useEffect(() => {
    if (open) {
      setQuery('');
      setSelected(0);
      setMode(initialMode);
      // Defer focus until after the dialog mounts.
      queueMicrotask(() => inputRef.current?.focus());
    }
  }, [open, initialMode]);

  // Esc to close — handled at the document level so clicks outside the
  // dialog don't need to bubble.
  useEffect(() => {
    if (!open) return;
    const handler = (e: globalThis.KeyboardEvent) => {
      if (e.key === 'Escape') {
        e.preventDefault();
        onClose();
      }
    };
    document.addEventListener('keydown', handler);
    return () => document.removeEventListener('keydown', handler);
  }, [open, onClose]);

  // ── Filter + score ──────────────────────────────────────────────────────
  const recents = useRecents(recentsKey, recentsLimit);

  const filtered = useMemo(() => {
    const bucketed = allCommands.filter(c => {
      if (c.requires && !c.requires()) return false;
      // ask-ai bucket is virtual — no commands live in it.
      if (mode === 'navigate') return c.mode === 'navigate';
      if (mode === 'actions') return c.mode !== 'navigate';
      return false;
    });

    if (!query) {
      // No query: float recents to the top in actions mode.
      if (mode === 'actions' && recents.length) {
        const recentSet = new Set(recents);
        const recentItems = bucketed.filter(c => recentSet.has(c.id));
        const rest = bucketed.filter(c => !recentSet.has(c.id));
        return recentItems.concat(rest).map(command => ({ command, score: 0 }));
      }
      return bucketed.map(command => ({ command, score: 0 }));
    }

    return bucketed
      .map(command => ({ command, score: scoreCommand(command, query) }))
      .filter((row): row is { command: Command; score: number } => row.score !== null)
      .sort((a, b) => b.score - a.score);
  }, [allCommands, mode, query, recents]);

  useEffect(() => {
    if (selected >= filtered.length) setSelected(0);
  }, [filtered.length, selected]);

  // ── Activation ──────────────────────────────────────────────────────────
  const activate = (cmd: Command) => {
    if (cmd.enabled === false) return;
    recents.push(cmd.id);
    onClose();
    if (cmd.run) {
      void cmd.run();
      return;
    }
    if (cmd.href) {
      if (onNavigate) onNavigate(cmd.href);
      else if (typeof window !== 'undefined') window.location.assign(cmd.href);
    }
  };

  const handleKeyDown = (e: KeyboardEvent<HTMLDivElement>) => {
    if (e.key === 'ArrowDown') {
      e.preventDefault();
      setSelected(s => Math.min(s + 1, Math.max(filtered.length - 1, 0)));
    } else if (e.key === 'ArrowUp') {
      e.preventDefault();
      setSelected(s => Math.max(s - 1, 0));
    } else if (e.key === 'Enter') {
      e.preventDefault();
      const row = filtered[selected];
      if (row) activate(row.command);
    } else if (e.key === 'Tab') {
      e.preventDefault();
      setMode(m => nextMode(m, e.shiftKey ? -1 : 1));
    }
  };

  if (!open) return null;

  return (
    <div
      role="dialog"
      aria-modal="true"
      aria-label={ariaLabel}
      data-testid="bl-cmdk"
      className={className}
      onKeyDown={handleKeyDown}
      style={{
        position: 'fixed',
        inset: 0,
        zIndex: 1000,
        background: 'var(--bl-overlay-scrim, rgba(0,0,0,0.55))',
        display: 'flex',
        alignItems: 'flex-start',
        justifyContent: 'center',
        paddingTop: '12vh',
        ...style,
      }}
      onMouseDown={e => {
        // Close on backdrop click — but never when the inner panel is clicked.
        if (e.target === e.currentTarget) onClose();
      }}
    >
      <div
        data-testid="bl-cmdk-panel"
        style={{
          width: 'min(640px, 92vw)',
          maxHeight: '70vh',
          display: 'flex',
          flexDirection: 'column',
          background: 'var(--bl-surface-card, #fff)',
          color: 'var(--bl-text-primary, inherit)',
          border: '1px solid var(--bl-border, rgba(0,0,0,0.12))',
          borderRadius: 'var(--bl-radius-card, 12px)',
          boxShadow: '0 20px 60px rgba(0,0,0,0.30)',
          overflow: 'hidden',
        }}
      >
        {/* Search input */}
        <div
          style={{
            display: 'flex',
            alignItems: 'center',
            gap: 'var(--bl-space-2, 8px)',
            padding: 'var(--bl-space-3, 12px) var(--bl-space-4, 16px)',
            borderBottom: '1px solid var(--bl-border-subtle, rgba(0,0,0,0.06))',
          }}
        >
          <span
            aria-hidden
            style={{ fontSize: 14, color: 'var(--bl-text-tertiary, #888)' }}
          >
            ⌘
          </span>
          <input
            ref={inputRef}
            data-testid="bl-cmdk-input"
            type="text"
            value={query}
            onChange={e => {
              setQuery(e.target.value);
              setSelected(0);
            }}
            placeholder={
              mode === 'navigate'
                ? 'Jump to…'
                : mode === 'ask-ai'
                  ? 'Ask AI anything…'
                  : 'Type a command, or search…'
            }
            aria-label="Search commands"
            style={{
              flex: 1,
              border: 'none',
              outline: 'none',
              background: 'transparent',
              color: 'inherit',
              fontSize: '1rem',
            }}
          />
          {!hideModeTabs && <ModeTabs mode={mode} onChange={setMode} />}
        </div>

        {/* Body */}
        <div
          data-testid="bl-cmdk-body"
          style={{
            flex: 1,
            overflowY: 'auto',
            padding: 'var(--bl-space-1, 4px)',
          }}
        >
          {mode === 'ask-ai' ? (
            askAiPanel ? (
              askAiPanel(query)
            ) : (
              <DefaultAskAiPanel query={query} />
            )
          ) : filtered.length === 0 ? (
            <EmptyState query={query} mode={mode} />
          ) : (
            <ul
              role="listbox"
              aria-label={mode === 'navigate' ? 'Navigate targets' : 'Actions'}
              style={{ listStyle: 'none', margin: 0, padding: 0 }}
            >
              {filtered.map((row, i) => (
                <CommandRow
                  key={row.command.id}
                  command={row.command}
                  selected={i === selected}
                  onSelect={() => activate(row.command)}
                  onHover={() => setSelected(i)}
                />
              ))}
            </ul>
          )}
        </div>

        <Footer mode={mode} hits={filtered.length} />
      </div>
    </div>
  );
}

// ─── Sub-components ──────────────────────────────────────────────────────

function ModeTabs({
  mode,
  onChange,
}: {
  mode: CommandMode;
  onChange: (m: CommandMode) => void;
}) {
  const tabs: Array<{ id: CommandMode; label: string }> = [
    { id: 'actions', label: 'Actions' },
    { id: 'navigate', label: 'Navigate' },
    { id: 'ask-ai', label: 'Ask AI' },
  ];
  return (
    <div
      role="tablist"
      style={{
        display: 'inline-flex',
        gap: 2,
        padding: 2,
        background: 'var(--bl-surface-muted, rgba(0,0,0,0.04))',
        borderRadius: 'var(--bl-radius-pill, 999px)',
      }}
    >
      {tabs.map(t => {
        const active = t.id === mode;
        return (
          <button
            key={t.id}
            type="button"
            role="tab"
            aria-selected={active}
            data-testid={`bl-cmdk-tab-${t.id}`}
            onClick={() => onChange(t.id)}
            style={{
              padding: '4px 10px',
              fontSize: '0.75rem',
              border: 'none',
              borderRadius: 'var(--bl-radius-pill, 999px)',
              background: active ? 'var(--bl-surface-card, #fff)' : 'transparent',
              color: active
                ? 'var(--bl-text-primary, inherit)'
                : 'var(--bl-text-tertiary, #888)',
              fontWeight: active ? 600 : 500,
              cursor: 'pointer',
            }}
          >
            {t.label}
          </button>
        );
      })}
    </div>
  );
}

function CommandRow({
  command,
  selected,
  onSelect,
  onHover,
}: {
  command: Command;
  selected: boolean;
  onSelect: () => void;
  onHover: () => void;
}) {
  const disabled = command.enabled === false;
  return (
    <li
      role="option"
      aria-selected={selected}
      aria-disabled={disabled}
      data-testid={`bl-cmdk-item-${command.id}`}
      onMouseMove={onHover}
      onClick={() => {
        if (!disabled) onSelect();
      }}
      style={{
        display: 'flex',
        alignItems: 'center',
        gap: 'var(--bl-space-3, 12px)',
        padding: 'var(--bl-space-2, 8px) var(--bl-space-3, 12px)',
        background: selected
          ? 'var(--bl-accent-muted, rgba(99,102,241,0.12))'
          : 'transparent',
        borderRadius: 'var(--bl-radius-control, 6px)',
        cursor: disabled ? 'not-allowed' : 'pointer',
        opacity: disabled ? 0.5 : 1,
      }}
    >
      {command.icon && (
        <span
          aria-hidden
          style={{
            display: 'inline-flex',
            alignItems: 'center',
            justifyContent: 'center',
            width: 22,
            color: 'var(--bl-accent, #6366f1)',
          }}
        >
          {command.icon}
        </span>
      )}
      <span style={{ flex: 1, minWidth: 0 }}>
        <div style={{ fontSize: '0.9rem', fontWeight: 600 }}>{command.label}</div>
        {command.description && (
          <div
            style={{
              fontSize: '0.75rem',
              color: 'var(--bl-text-tertiary, #888)',
              marginTop: 1,
            }}
          >
            {command.description}
          </div>
        )}
      </span>
      {command.shortcut && (
        <span style={{ display: 'inline-flex', gap: 2 }}>
          {command.shortcut.map(k => (
            <kbd
              key={k}
              style={{
                fontFamily: 'inherit',
                fontSize: '0.7rem',
                padding: '1px 5px',
                background: 'var(--bl-surface-muted, rgba(0,0,0,0.06))',
                color: 'var(--bl-text-secondary, #555)',
                border: '1px solid var(--bl-border-subtle, rgba(0,0,0,0.08))',
                borderRadius: 4,
              }}
            >
              {k}
            </kbd>
          ))}
        </span>
      )}
    </li>
  );
}

function EmptyState({ query, mode }: { query: string; mode: CommandMode }) {
  return (
    <div
      data-testid="bl-cmdk-empty"
      style={{
        padding: 'var(--bl-space-4, 16px)',
        textAlign: 'center',
        color: 'var(--bl-text-tertiary, #888)',
        fontSize: '0.85rem',
      }}
    >
      {query
        ? `No ${mode === 'navigate' ? 'navigation targets' : 'commands'} match "${query}".`
        : `No ${mode === 'navigate' ? 'navigation targets' : 'commands'} registered.`}
    </div>
  );
}

function DefaultAskAiPanel({ query }: { query: string }) {
  return (
    <div
      data-testid="bl-cmdk-ask-ai"
      style={{
        padding: 'var(--bl-space-4, 16px)',
        textAlign: 'center',
        color: 'var(--bl-text-secondary, #444)',
      }}
    >
      <div style={{ fontSize: 28, marginBottom: 8 }} aria-hidden>
        ✨
      </div>
      <div style={{ fontSize: '0.9rem', fontWeight: 600 }}>
        {query ? `Ask AI: "${query}"` : 'Type a question to ask AI'}
      </div>
      <div
        style={{
          fontSize: '0.75rem',
          color: 'var(--bl-text-tertiary, #888)',
          marginTop: 4,
        }}
      >
        Provide an <code>askAiPanel</code> prop to wire this to{' '}
        <code>@bytelyst/ai-ui</code>.
      </div>
    </div>
  );
}

function Footer({ mode, hits }: { mode: CommandMode; hits: number }) {
  return (
    <div
      style={{
        display: 'flex',
        alignItems: 'center',
        gap: 'var(--bl-space-3, 12px)',
        padding: 'var(--bl-space-2, 8px) var(--bl-space-3, 12px)',
        borderTop: '1px solid var(--bl-border-subtle, rgba(0,0,0,0.06))',
        fontSize: '0.7rem',
        color: 'var(--bl-text-tertiary, #888)',
      }}
    >
      <span>
        <kbd style={kbdStyle}>↑↓</kbd> navigate
      </span>
      <span>
        <kbd style={kbdStyle}>↵</kbd> activate
      </span>
      <span>
        <kbd style={kbdStyle}>Tab</kbd> switch mode
      </span>
      <span>
        <kbd style={kbdStyle}>Esc</kbd> close
      </span>
      {mode !== 'ask-ai' && (
        <span style={{ marginLeft: 'auto' }}>{hits} result{hits === 1 ? '' : 's'}</span>
      )}
    </div>
  );
}

const kbdStyle: CSSProperties = {
  fontFamily: 'inherit',
  fontSize: '0.7rem',
  padding: '0 4px',
  background: 'var(--bl-surface-muted, rgba(0,0,0,0.06))',
  border: '1px solid var(--bl-border-subtle, rgba(0,0,0,0.08))',
  borderRadius: 3,
};

function nextMode(current: CommandMode, direction: 1 | -1): CommandMode {
  const order: CommandMode[] = ['actions', 'navigate', 'ask-ai'];
  const idx = order.indexOf(current);
  const next = (idx + direction + order.length) % order.length;
  return order[next]!;
}

// ─── localStorage-backed recents ─────────────────────────────────────────

function useRecents(key: string, limit: number) {
  const [recents, setRecents] = useState<string[]>(() => {
    if (typeof window === 'undefined') return [];
    try {
      const raw = window.localStorage.getItem(key);
      if (!raw) return [];
      const parsed = JSON.parse(raw);
      return Array.isArray(parsed) ? parsed.slice(0, limit) : [];
    } catch {
      return [];
    }
  });

  return useMemo(
    () => ({
      get list() {
        return recents;
      },
      // Iterable convenience.
      [Symbol.iterator]: () => recents[Symbol.iterator](),
      get length() {
        return recents.length;
      },
      includes: (id: string) => recents.includes(id),
      filter: (predicate: (id: string) => boolean) => recents.filter(predicate),
      push: (id: string) => {
        setRecents(prev => {
          const next = [id, ...prev.filter(x => x !== id)].slice(0, limit);
          try {
            window.localStorage.setItem(key, JSON.stringify(next));
          } catch {
            /* quota / private mode — best-effort */
          }
          return next;
        });
      },
    }),
    [recents, key, limit],
  );
}
