import { useEffect, useMemo, useRef, useState } from 'react';
import { useVirtualizer } from '@tanstack/react-virtual';
import { Search, BookOpenText } from 'lucide-react';
import Fuse from 'fuse.js';
import type { SystemId } from '@/lib/rules';
import { SYSTEM_OPTIONS } from '@/lib/rules';
import { newId } from '@/lib/ids';
import { createRng } from '@/lib/rng';
import { rollDice } from '@/lib/dice/notation';
import { addCombatant } from '@/lib/combat/engine';
import { charactersRepo, encountersRepo } from '@/lib/db/repositories';
import { type Character, type InventoryItem, type SpellEntry, newSpellEntry } from '@/lib/schemas';
import type { Spell, CompendiumEntry } from '@/lib/compendium/types';
import type { CombatantDamageDefenses } from '@/lib/schemas/encounter';
import { normalizeSpell5e, normalizeSpellPf2e } from '@/lib/mechanics';
import { useUiStore } from '@/stores/uiStore';
import { useActiveCampaign } from '@/features/campaigns/hooks';
import { useCharacters } from '@/features/characters/hooks';
import { useHomebrew } from '@/features/world/hooks';
import { homebrewToEntry, isHomebrew, homebrewCombatant, CATEGORY_HOMEBREW_KIND } from '@/features/world/homebrew';
import { HomebrewDetail } from './details';
import { Page, PageHeader } from '@/components/ui/Page';
import { Button } from '@/components/ui/Button';
import { Input, Select } from '@/components/ui/Input';
import { cn } from '@/lib/cn';
import { categoriesForSystem, filterMatches, type CategoryDef, type Entry } from './registry';

type SortKey = 'name-asc' | 'name-desc' | 'num-asc' | 'num-desc';

export function CompendiumPage() {
  const activeCampaign = useActiveCampaign();
  const [system, setSystem] = useState<SystemId>(activeCampaign?.system ?? '5e');
  const categories = categoriesForSystem(system);
  const [categoryId, setCategoryId] = useState(categories[0]!.id);
  const category = categories.find((c) => c.id === categoryId) ?? categories[0]!;

  const [data, setData] = useState<Entry[]>([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);
  const [query, setQuery] = useState('');
  const [filters, setFilters] = useState<Record<string, string>>({});
  const [sort, setSort] = useState<SortKey>('name-asc');
  const [selected, setSelected] = useState<Entry | null>(null);

  // Load the active category's dataset.
  useEffect(() => {
    let cancelled = false;
    setLoading(true);
    setError(null);
    setSelected(null);
    setQuery('');
    setFilters({});
    setSort('name-asc');
    category.load().then(
      (d) => { if (!cancelled) { setData(d); setLoading(false); } },
      (e: unknown) => { if (!cancelled) { setError(e instanceof Error ? e.message : 'Failed to load'); setLoading(false); } },
    );
    return () => { cancelled = true; };
  }, [category]);

  // Merge the campaign's homebrew of this category's kind into the dataset.
  const homebrew = useHomebrew(activeCampaign?.id ?? '');
  const allData = useMemo(() => {
    const kind = CATEGORY_HOMEBREW_KIND[category.id];
    if (!kind) return data;
    const hb = homebrew.filter((h) => h.system === system && h.kind === kind).map(homebrewToEntry);
    return [...hb, ...data];
  }, [data, homebrew, category, system]);

  const filtered = useMemo(() => {
    const active = Object.entries(filters).filter(([, v]) => v !== '');
    if (active.length === 0) return allData;
    return allData.filter((e) =>
      active.every(([key, value]) => {
        const def = category.filters.find((f) => f.key === key);
        return def ? filterMatches(def, e, value) : true;
      }),
    );
  }, [allData, filters, category]);

  const fuse = useMemo(
    () => new Fuse(filtered, { keys: category.searchKeys, threshold: 0.34, ignoreLocation: true }),
    [filtered, category],
  );
  const results = useMemo(() => {
    const list = query.trim() ? fuse.search(query.trim()).map((r) => r.item) : [...filtered];
    const num = category.numericSort;
    const byName = (a: Entry, b: Entry) => a.name.localeCompare(b.name);
    const byNum = (a: Entry, b: Entry) => {
      const av = num ? num.get(a) : 0;
      const bv = num ? num.get(b) : 0;
      // entries without a numeric value sort to the end
      const an = Number.isFinite(av) ? av : Infinity;
      const bn = Number.isFinite(bv) ? bv : Infinity;
      return an - bn || byName(a, b);
    };
    switch (sort) {
      case 'name-asc': return list.sort(byName);
      case 'name-desc': return list.sort((a, b) => byName(b, a));
      case 'num-asc': return list.sort(byNum);
      case 'num-desc': return list.sort((a, b) => byNum(b, a));
    }
  }, [query, fuse, filtered, sort, category]);

  const changeSystem = (s: SystemId) => {
    setSystem(s);
    setCategoryId(categoriesForSystem(s)[0]!.id);
  };

  const sortOptions: { value: SortKey; label: string }[] = [
    { value: 'name-asc', label: 'Name A–Z' },
    { value: 'name-desc', label: 'Name Z–A' },
    ...(category.numericSort
      ? ([
          { value: 'num-asc', label: `${category.numericSort.label} ↑` },
          { value: 'num-desc', label: `${category.numericSort.label} ↓` },
        ] as const)
      : []),
  ];

  // Virtualize the results list so thousands of rows render smoothly.
  const parentRef = useRef<HTMLDivElement>(null);
  const rowVirtualizer = useVirtualizer({
    count: results.length,
    getScrollElement: () => parentRef.current,
    estimateSize: () => 46,
    overscan: 12,
  });

  return (
    <Page>
      <PageHeader
        eyebrow="The Codex"
        title="Compendium"
        subtitle="Searchable reference — bestiary, spells, items, feats, and more."
        actions={
          <div className="flex gap-0.5 rounded-lg border border-line bg-panel p-0.5">
            {SYSTEM_OPTIONS.map((o) => (
              <button
                key={o.id}
                onClick={() => changeSystem(o.id)}
                className={cn(
                  'rounded-md px-3 py-1 text-sm transition-colors',
                  system === o.id ? 'bg-accent text-accent-ink shadow-[inset_0_1px_0_rgba(255,255,255,0.3)]' : 'text-muted hover:text-ink',
                )}
              >
                {o.label}
              </button>
            ))}
          </div>
        }
      />

      {/* Category chips */}
      <div className="mb-4 flex flex-wrap gap-1">
        {categories.map((c) => (
          <button
            key={c.id}
            onClick={() => setCategoryId(c.id)}
            className={cn(
              'rounded-md border px-3 py-1.5 text-sm transition-colors',
              c.id === category.id
                ? 'border-accent bg-accent-glow font-medium text-accent-deep'
                : 'border-line bg-panel text-muted hover:text-ink',
            )}
          >
            {c.label}
          </button>
        ))}
      </div>

      <div className="grid gap-4 lg:grid-cols-[340px_1fr]">
        <div>
          <div className="relative">
            <Search
              aria-hidden
              size={16}
              className="pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 text-faint"
            />
            <Input
              value={query}
              onChange={(e) => setQuery(e.target.value)}
              placeholder={`Search ${category.label.toLowerCase()}…`}
              aria-label="Search compendium"
              className="pl-9"
            />
          </div>
          {category.filters.length > 0 && (
            <div className="mt-2 flex flex-wrap items-center gap-2">
              {category.filters.map((f) => {
                const opts = f.options(data);
                if (opts.length === 0) return null;
                return (
                  <Select
                    key={f.key}
                    className="w-auto py-1 text-xs"
                    aria-label={f.label}
                    value={filters[f.key] ?? ''}
                    onChange={(e) => setFilters((prev) => ({ ...prev, [f.key]: e.target.value }))}
                  >
                    <option value="">{f.label}</option>
                    {opts.map((o) => <option key={o.value} value={o.value}>{o.label}</option>)}
                  </Select>
                );
              })}
              {Object.values(filters).some((v) => v) && (
                <Button size="sm" variant="ghost" onClick={() => setFilters({})}>Clear</Button>
              )}
            </div>
          )}

          <div className="mt-2 flex items-center justify-between gap-2">
            <Select
              className="w-auto py-1 text-xs"
              aria-label="Sort"
              value={sort}
              onChange={(e) => setSort(e.target.value as SortKey)}
            >
              {sortOptions.map((o) => <option key={o.value} value={o.value}>{o.label}</option>)}
            </Select>
            <span className="font-mono text-xs text-faint">
              {loading ? 'Loading…' : error ? <span className="text-danger">{error}</span> : `${results.length.toLocaleString()} result${results.length === 1 ? '' : 's'}`}
            </span>
          </div>

          <div ref={parentRef} className="mt-2 max-h-[62vh] overflow-auto">
            <div style={{ height: `${rowVirtualizer.getTotalSize()}px`, position: 'relative' }}>
              {rowVirtualizer.getVirtualItems().map((vi) => {
                const entry = results[vi.index]!;
                return (
                  <div
                    key={(entry.slug as string) ?? `${entry.name}-${vi.index}`}
                    style={{ position: 'absolute', top: 0, left: 0, width: '100%', height: `${vi.size}px`, transform: `translateY(${vi.start}px)`, paddingBottom: 4 }}
                  >
                    <button
                      onClick={() => setSelected(entry)}
                      className={cn(
                        'flex h-full w-full items-center justify-between gap-2 rounded-md border px-3 text-left text-sm transition-colors',
                        selected === entry
                          ? 'border-accent bg-accent-glow'
                          : 'border-line bg-panel hover:border-accent/40',
                      )}
                    >
                      <span className="flex min-w-0 items-center gap-1.5">
                        {isHomebrew(entry) && <span className="shrink-0 rounded bg-accent-glow px-1 text-[10px] font-semibold uppercase text-accent-deep">HB</span>}
                        <span className="truncate font-display font-medium text-ink">{entry.name}</span>
                      </span>
                      <span className="shrink-0 font-mono text-xs text-faint">{isHomebrew(entry) ? 'Homebrew' : category.meta(entry)}</span>
                    </button>
                  </div>
                );
              })}
            </div>
          </div>
        </div>

        <div className="paper-grain overflow-hidden rounded-xl border border-line border-t-4 border-t-accent bg-panel p-6">
          {selected ? (
            <div>
              <DetailActions category={category} entry={selected} system={system} />
              {isHomebrew(selected) ? <HomebrewDetail entry={selected} /> : category.detail(selected)}
            </div>
          ) : (
            <div className="flex min-h-[40vh] flex-col items-center justify-center gap-3 text-center">
              <BookOpenText aria-hidden size={28} className="text-faint" />
              <p className="font-display italic text-muted">Select an entry to view details.</p>
            </div>
          )}
        </div>
      </div>
    </Page>
  );
}

function DetailActions({ category, entry, system }: { category: CategoryDef; entry: Entry; system: SystemId }) {
  // Homebrew entries derive their actions from their kind, not the category.
  if (isHomebrew(entry)) {
    const kind = entry.__kind;
    if (kind === 'monster') return <ActionBar><AddToCombat stats={homebrewCombatant(entry)} /></ActionBar>;
    if (kind === 'spell') return <ActionBar><AddToCharacter kind="spell" entry={entry} system={system} /></ActionBar>;
    if (kind === 'item') return <ActionBar><AddToCharacter kind="item" entry={entry} system={system} /></ActionBar>;
    return null;
  }
  if (!category.toCombatant && !category.linkAs) return null;
  return (
    <ActionBar>
      {category.toCombatant && <AddToCombat stats={category.toCombatant(entry)} />}
      {category.linkAs && <AddToCharacter kind={category.linkAs} entry={entry} system={system} />}
    </ActionBar>
  );
}

function ActionBar({ children }: { children: React.ReactNode }) {
  return (
    <div className="mb-4 flex flex-wrap items-center gap-2 border-b border-line/70 pb-4">{children}</div>
  );
}

function AddToCombat({ stats }: { stats: { name: string; ac: number; hp: number; initBonus: number; cr?: number; level?: number; damageDefenses?: CombatantDamageDefenses } }) {
  const activeEncounterId = useUiStore((s) => s.activeEncounterId);
  const [msg, setMsg] = useState<string | null>(null);

  const add = async () => {
    if (!activeEncounterId) return;
    const enc = await encountersRepo.get(activeEncounterId);
    if (!enc) { setMsg('Open an encounter in Combat first.'); return; }
    const initiative = rollDice('1d20', createRng()).total + stats.initBonus;
    await encountersRepo.save(
      addCombatant(enc, {
        id: newId(), name: stats.name, kind: 'monster',
        initiative, initBonus: stats.initBonus, ac: stats.ac,
        hp: { current: stats.hp, max: stats.hp, temp: 0 },
        conditions: [], notes: '',
        ...(stats.cr !== undefined ? { cr: stats.cr } : {}),
        ...(stats.level !== undefined ? { level: stats.level } : {}),
        ...(stats.damageDefenses ? { damageDefenses: stats.damageDefenses } : {}),
      }),
    );
    setMsg(`Added to "${enc.name}" (init ${initiative}).`);
  };

  return (
    <>
      <Button size="sm" variant="primary" disabled={!activeEncounterId} onClick={add} title={activeEncounterId ? 'Add to the open encounter' : 'Open an encounter in Combat first'}>
        + Add to combat
      </Button>
      {msg && <span className="text-xs text-success" aria-live="polite">{msg}</span>}
    </>
  );
}

function AddToCharacter({ kind, entry, system }: { kind: 'spell' | 'item'; entry: Entry; system: SystemId }) {
  const campaign = useActiveCampaign();
  const characters = useCharacters(campaign?.id ?? '');
  const [msg, setMsg] = useState<string | null>(null);

  const add = async (characterId: string) => {
    const c = await charactersRepo.get(characterId);
    if (!c) return;
    if (kind === 'spell') {
      // Snapshot the spell's mechanics (concentration / save / damage) so combat
      // enforcement never needs to re-load the compendium.
      const mech = system === '5e'
        ? normalizeSpell5e(entry as unknown as Spell)
        : normalizeSpellPf2e(entry as unknown as CompendiumEntry);
      const level = system === '5e' ? Number(entry.level_int ?? 0) : Number(entry.level ?? 0);
      const spell: SpellEntry = newSpellEntry({
        id: newId(), name: entry.name,
        level: mech?.level ?? (Number.isFinite(level) ? level : 0),
        concentration: mech?.concentration ?? false,
        save: mech?.save ?? null,
        damage: mech?.damage ?? [],
        ...(entry.slug ? { compendiumRef: String(entry.slug) } : {}),
      });
      await charactersRepo.update(c.id, { spellcasting: { ...c.spellcasting, spells: [...c.spellcasting.spells, spell] } });
    } else {
      const item: InventoryItem = {
        id: newId(), name: entry.name, quantity: 1, weight: 0, equipped: false, attuned: false,
        description: '', ...(entry.slug ? { compendiumRef: String(entry.slug) } : {}),
      };
      await charactersRepo.update(c.id, { inventory: [...c.inventory, item] });
    }
    setMsg(`Added to ${c.name}.`);
  };

  if (!campaign) return <span className="text-xs text-muted">Select a campaign to add to a character.</span>;
  if (characters.length === 0) return <span className="text-xs text-muted">No characters in this campaign yet.</span>;

  return (
    <>
      <Select
        className="w-auto py-1 text-sm"
        aria-label={`Add ${kind} to character`}
        value=""
        onChange={(e) => { if (e.target.value) { void add(e.target.value); e.target.value = ''; } }}
      >
        <option value="">+ Add {kind} to…</option>
        {characters.map((c: Character) => <option key={c.id} value={c.id}>{c.name}</option>)}
      </Select>
      {msg && <span className="text-xs text-success" aria-live="polite">{msg}</span>}
    </>
  );
}
