import { useEffect, useMemo, useState } from 'react';
import { useNavigate } from '@tanstack/react-router';
import { Lightbulb, RotateCcw, Star } from 'lucide-react';
import { type Campaign, type Character, type SpellEntry, newSpellEntry } from '@/lib/schemas';
import { charactersRepo } from '@/lib/db/repositories';
import {
  getSystem, ABILITY_ABBR, abilityModifier, buildCharacter, getClassDef, SYSTEM_OPTIONS,
  pf2eApplyBoosts, parseAncestryBoosts, parseBackgroundBoosts, parseFlaw,
  type AbilityKey, type AbilityScores, type ProficiencyRank, type SystemId,
} from '@/lib/rules';
import { STANDARD_ARRAY, rollAbilityScores, pointBuyRemaining, POINT_BUY_MIN, POINT_BUY_MAX } from '@/lib/rules/abilityGen';
import { loadClasses, loadRaces5e, loadBackgrounds5e, loadSpells, loadPf2e } from '@/lib/compendium';
import type { RulesetClass } from '@/lib/ruleset/normalize';
import { createRng } from '@/lib/rng';
import { newId } from '@/lib/ids';
import { formatModifier } from '@/lib/format';
import { getClassTip, raceSynergyNote } from '@/lib/assistant/builder';
import { briefOverview, dedupeByName } from './overview';
import { parseAsiBonuses, makeSkillResolver, parseBackgroundSkills, parseTraitSkills } from './origin';
import { PF2E_SUBCLASSES } from '@/lib/rules/pf2e/progression';
import { DND5E_SUBCLASSES } from '@/lib/rules/dnd5e/progression';
import { Modal } from '@/components/ui/Modal';
import { Button } from '@/components/ui/Button';
import { Badge } from '@/components/ui/Codex';
import { Field, Input, Select } from '@/components/ui/Input';
import { NumberField } from '@/components/ui/NumberField';
import { cn } from '@/lib/cn';

const ABILITIES: AbilityKey[] = ['str', 'dex', 'con', 'int', 'wis', 'cha'];
type AbilityMethod = 'standard' | 'pointbuy' | 'roll' | 'manual';

interface Origin {
  name: string; desc: string; meta?: string; hp?: number; speed?: number;
  /** 5e racial ability score increases, applied to the final scores. */
  asiBonuses?: Partial<AbilityScores>;
  /** skill keys this origin grants as trained (background skills, racial proficiencies). */
  skillGrants?: string[];
  /** pf2e ancestry boosts: specific abilities + count of free boosts. */
  ancestryBoosts?: { fixed: AbilityKey[]; free: number };
  /** pf2e legacy ancestry flaw (-2), if any. */
  ancestryFlaw?: AbilityKey;
  /** pf2e background boosts: choose-one options + free-boost count. */
  backgroundBoosts?: { options: AbilityKey[]; free: number };
}
interface SpellOpt { name: string; level: number; meta: string }

/** A class's "what it plays like" tag, to help newcomers pick. */
function playstyle(c: RulesetClass): string {
  if (c.caster !== 'none' && c.caster.length <= 3) return c.hitDie >= 8 && c.system === 'pf2e' ? 'Caster' : 'Spellcaster';
  if (c.hitDie >= 10 || c.hitDie >= 8) return 'Martial';
  return 'Skirmisher';
}

const TEMPLATES: Record<string, { label: string; hint: string; className: string; ability: AbilityScores }[]> = {
  '5e': [
    { label: 'Stalwart Fighter', hint: 'Tough front-liner. Easy to play.', className: 'Fighter', ability: { str: 15, dex: 13, con: 14, int: 8, wis: 12, cha: 10 } },
    { label: 'Clever Wizard', hint: 'Versatile spellcaster.', className: 'Wizard', ability: { str: 8, dex: 14, con: 13, int: 15, wis: 12, cha: 10 } },
    { label: 'Sneaky Rogue', hint: 'Skills + big sneak attacks.', className: 'Rogue', ability: { str: 10, dex: 15, con: 13, int: 12, wis: 14, cha: 8 } },
    { label: 'Helpful Cleric', hint: 'Heals and supports the party.', className: 'Cleric', ability: { str: 14, dex: 10, con: 13, int: 8, wis: 15, cha: 12 } },
  ],
  pf2e: [
    { label: 'Stalwart Fighter', hint: 'Best weapon proficiency in the game.', className: 'Fighter', ability: { str: 18, dex: 14, con: 14, int: 10, wis: 12, cha: 10 } },
    { label: 'Clever Wizard', hint: 'Prepared arcane spellcaster.', className: 'Wizard', ability: { str: 8, dex: 14, con: 12, int: 18, wis: 12, cha: 10 } },
    { label: 'Sneaky Rogue', hint: 'Skills, mobility, sneak attack.', className: 'Rogue', ability: { str: 10, dex: 18, con: 12, int: 12, wis: 14, cha: 10 } },
    { label: 'Healing Cleric', hint: 'Divine font of healing.', className: 'Cleric', ability: { str: 12, dex: 10, con: 12, int: 10, wis: 18, cha: 12 } },
  ],
};

export function CreationWizard({ campaign, onClose }: { campaign: Campaign; onClose: () => void }) {
  const navigate = useNavigate();
  // Characters are campaign-agnostic: the wizard asks which system to build for,
  // defaulting to the campaign's. Changing it resets all dependent selections.
  const [system, setSystem] = useState<SystemId>(campaign.system);
  const sys = getSystem(system);
  const allSkillKeys = sys.skills.map((s) => s.key);
  const skillLabel = (key: string) => sys.skills.find((s) => s.key === key)?.label ?? key;

  // ---- loaded data ----
  const [classes, setClasses] = useState<RulesetClass[]>([]);
  const [origins, setOrigins] = useState<Origin[]>([]);
  const [backgrounds, setBackgrounds] = useState<Origin[]>([]);
  const [allSpells, setAllSpells] = useState<SpellOpt[]>([]);

  useEffect(() => {
    let on = true;
    void loadClasses(system).then((c) => {
      if (!on) return;
      // The pf2e class data loads with caster:'none' and no subclasses; merge the
      // curated caster ability (so pf2e spellcasters get the Spells step + "Caster"
      // tag) and the defining 1st-level choice list (instinct/doctrine/bloodline…).
      const enriched = system === 'pf2e'
        ? c.map((rc) => {
            const def = getClassDef('pf2e', rc.name);
            const subclasses = PF2E_SUBCLASSES[rc.name] ?? rc.subclasses;
            const caster = def && def.caster !== 'none' && def.spellAbility ? def.spellAbility : rc.caster;
            return { ...rc, caster, subclasses };
          })
        : c.map((rc) => {
            // Merge curated official subclasses (classes.json is missing most PHB/XGE
            // archetypes) ahead of the on-disk list, deduped by name.
            const extra = DND5E_SUBCLASSES[rc.name] ?? [];
            if (!extra.length) return rc;
            const have = new Set(rc.subclasses.map((s) => s.name.toLowerCase()));
            return { ...rc, subclasses: [...extra.filter((s) => !have.has(s.name.toLowerCase())), ...rc.subclasses] };
          });
      setClasses([...enriched].sort((a, b) => a.name.localeCompare(b.name)));
    });
    return () => { on = false; };
  }, [system]);
  useEffect(() => {
    let on = true;
    if (system === '5e') {
      const resolve = makeSkillResolver(sys.skills);
      void loadRaces5e().then((rs) => {
        if (!on) return;
        const strip = (s: string) => s.replace(/\*{2,3}[^*]+\*{2,3}\s*/g, '').trim();
        setOrigins(rs.map((r) => {
          const desc = [r.asi, r.vision, r.traits].filter(Boolean).map(strip).join('\n\n');
          const speedFt = /(\d+) feet/.exec(r.speed)?.[1];
          const asiBonuses = parseAsiBonuses(r.asi ?? '');
          const asiSummary = ABILITIES.filter((a) => asiBonuses[a]).map((a) => `+${asiBonuses[a]} ${ABILITY_ABBR[a]}`).join(', ');
          const meta = [asiSummary, speedFt ? `${speedFt} ft` : ''].filter(Boolean).join(' · ');
          const skillGrants = parseTraitSkills(r.traits ?? '', resolve);
          return { name: r.name, desc, meta, asiBonuses, ...(skillGrants.length ? { skillGrants } : {}) };
        }));
      });
      void loadBackgrounds5e().then((bs) => on && setBackgrounds(bs.map((b) => {
        const skillGrants = parseBackgroundSkills(b.skills ?? '', resolve);
        return { name: b.name, desc: b.desc, meta: b.skills, ...(skillGrants.length ? { skillGrants } : {}) };
      })));
    } else {
      void loadPf2e('ancestries').then((rs) => {
        if (!on) return;
        setOrigins(dedupeByName(rs.map((r) => {
          const hp = typeof r.hp === 'number' ? r.hp : undefined;
          const speed = (r.speed as { land?: number } | undefined)?.land;
          const ancestryBoosts = parseAncestryBoosts(r.attribute);
          const ancestryFlaw = parseFlaw((r as Record<string, unknown>).attribute_flaw ?? (r as Record<string, unknown>).flaw);
          const fixedSummary = ancestryBoosts.fixed.map((a) => `+${ABILITY_ABBR[a]}`).join('/');
          const freeSummary = ancestryBoosts.free ? `+${ancestryBoosts.free} free` : '';
          const meta = [fixedSummary, freeSummary, ancestryFlaw ? `−${ABILITY_ABBR[ancestryFlaw]}` : '', hp !== undefined ? `${hp} HP` : ''].filter(Boolean).join(' · ');
          return {
            name: String(r.name),
            desc: briefOverview(String((r.summary ?? r.text) ?? '')),
            ...(hp !== undefined ? { hp } : {}),
            ...(speed ? { speed } : {}),
            ancestryBoosts,
            ...(ancestryFlaw ? { ancestryFlaw } : {}),
            meta,
          };
        })));
      });
      void loadPf2e('backgrounds').then((bs) => on && setBackgrounds(dedupeByName(bs.map((b) => ({
        name: String(b.name),
        desc: briefOverview(String((b.description ?? b.text) ?? '')),
        backgroundBoosts: parseBackgroundBoosts(b.attribute),
      })))));
    }
    return () => { on = false; };
    // sys.skills is a stable singleton keyed by `system`, so `system` covers it.
  }, [system, sys.skills]);

  // ---- selections ----
  const [step, setStep] = useState(0);
  const [name, setName] = useState('');
  const [kind, setKind] = useState<Character['kind']>('pc');
  const [level, setLevel] = useState(1);
  const [classSlug, setClassSlug] = useState('');
  const [subclass, setSubclass] = useState('');
  const [ancestry, setAncestry] = useState('');
  const [background, setBackground] = useState('');

  const selectedClass = classes.find((c) => c.slug === classSlug) ?? null;
  const selectedOrigin = origins.find((o) => o.name === ancestry) ?? null;
  const selectedBackground = backgrounds.find((b) => b.name === background) ?? null;
  const classDef = selectedClass ? getClassDef(system, selectedClass.name) : undefined;
  const isCaster = !!selectedClass && selectedClass.caster !== 'none';

  // ---- abilities ----
  const [method, setMethod] = useState<AbilityMethod>('standard');
  const [pool, setPool] = useState<number[]>([...STANDARD_ARRAY]);
  const [assignment, setAssignment] = useState<number[]>(ABILITIES.map((_, i) => i));
  const [pb, setPb] = useState<number[]>([8, 8, 8, 8, 8, 8]);
  const usesPool = method === 'standard' || method === 'roll';
  const chooseMethod = (m: AbilityMethod) => {
    setMethod(m);
    if (m === 'standard') { setPool([...STANDARD_ARRAY]); setAssignment(ABILITIES.map((_, i) => i)); }
    if (m === 'roll') { setPool(rollAbilityScores(createRng())); setAssignment(ABILITIES.map((_, i) => i)); }
  };

  // ---- pf2e ability boosts (start at 10; ancestry/background/class/free boosts) ----
  const boostSlots = useMemo(() => {
    if (system !== 'pf2e') return null;
    const anc = selectedOrigin?.ancestryBoosts ?? { fixed: [], free: 0 };
    const bg = selectedBackground?.backgroundBoosts ?? { options: [], free: 0 };
    const keyOpts = (classDef?.keyAbilities ?? selectedClass?.keyAbilities ?? []) as AbilityKey[];
    const slots: { id: string; label: string; options: AbilityKey[] }[] = [];
    for (let i = 0; i < anc.free; i++) slots.push({ id: `anc-free-${i}`, label: 'Ancestry free boost', options: ABILITIES });
    if (bg.options.length) slots.push({ id: 'bg-choice', label: 'Background boost', options: bg.options });
    for (let i = 0; i < bg.free; i++) slots.push({ id: `bg-free-${i}`, label: 'Background free boost', options: ABILITIES });
    if (keyOpts.length) slots.push({ id: 'class', label: 'Class key ability', options: keyOpts });
    for (let i = 0; i < 4; i++) slots.push({ id: `free-${i}`, label: 'Free boost', options: ABILITIES });
    return { fixed: anc.fixed, flaw: selectedOrigin?.ancestryFlaw, slots };
  }, [system, selectedOrigin, selectedBackground, classDef, selectedClass]);

  const [boostPicks, setBoostPicks] = useState<Record<string, AbilityKey>>({});
  // A boost slot's "source" — boosts within one source must target different abilities.
  const boostSource = (id: string) => (id.startsWith('anc') ? 'ancestry' : id.startsWith('bg') ? 'background' : id === 'class' ? 'class' : 'free');
  // Seed a legal default assignment (distinct within each source) when slots change.
  useEffect(() => {
    if (!boostSlots) return;
    const keyOpts = (classDef?.keyAbilities ?? []) as AbilityKey[];
    const favored = keyOpts[0] ?? 'str';
    const order: AbilityKey[] = [favored, ...ABILITIES.filter((a) => a !== favored)];
    const used: Record<string, Set<AbilityKey>> = { ancestry: new Set(boostSlots.fixed), background: new Set(), class: new Set(), free: new Set() };
    const next: Record<string, AbilityKey> = {};
    for (const s of boostSlots.slots) {
      const u = used[boostSource(s.id)]!;
      const pick = (s.options.length < 6 ? s.options : order).find((o) => !u.has(o))
        ?? order.find((o) => !u.has(o)) ?? s.options[0] ?? favored;
      next[s.id] = pick;
      u.add(pick);
    }
    setBoostPicks(next);
  }, [boostSlots, classDef]);

  const pf2eAbilities = useMemo(() => {
    if (!boostSlots) return null;
    const chosen = boostSlots.slots.map((s) => boostPicks[s.id]).filter((b): b is AbilityKey => !!b);
    return pf2eApplyBoosts([...boostSlots.fixed, ...chosen], boostSlots.flaw);
  }, [boostSlots, boostPicks]);

  const abilities = useMemo<AbilityScores>(() => {
    if (system === 'pf2e') return pf2eAbilities ?? { str: 10, dex: 10, con: 10, int: 10, wis: 10, cha: 10 };
    const out = {} as AbilityScores;
    ABILITIES.forEach((a, i) => { out[a] = usesPool ? (pool[assignment[i]!] ?? 10) : pb[i]!; });
    // 5e: fold the chosen race's ability score increases into the final scores.
    if (selectedOrigin?.asiBonuses) {
      for (const a of ABILITIES) out[a] += selectedOrigin.asiBonuses[a] ?? 0;
    }
    return out;
  }, [system, pf2eAbilities, usesPool, pool, assignment, pb, selectedOrigin]);
  const poolValid = !usesPool || new Set(assignment).size === ABILITIES.length;
  const pbValid = method !== 'pointbuy' || pointBuyRemaining(pb) >= 0;

  // ---- skills ----
  const [skills, setSkills] = useState<string[]>([]);
  const intMod = abilityModifier(abilities.int);
  // Prefer the curated class table (canonical free-choice count) over the parsed
  // data file, which over-counts pf2e classes and mangles some 5e skill names.
  const baseSkillCount = classDef?.skillCount ?? selectedClass?.skillCount ?? 2;
  const skillCount = selectedClass
    ? baseSkillCount + (system === 'pf2e' ? Math.max(0, intMod) : 0)
    : 2;
  const skillOptions = useMemo(() => {
    if (!selectedClass) return allSkillKeys;
    // Curated class skill list (canonical for 5e); empty curated list = choose from all.
    if (classDef && classDef.skillKeys.length > 0) return classDef.skillKeys;
    if (selectedClass.skillChoices.length === 0) return allSkillKeys;
    // Fallback to the parsed data, tolerating Oxford "and X" remnants.
    const wanted = new Set(selectedClass.skillChoices.map((s) => s.toLowerCase().replace(/^and\s+/, '')));
    const labelOf = (k: string) => sys.skills.find((s) => s.key === k)?.label.toLowerCase() ?? k.toLowerCase();
    const matched = allSkillKeys.filter((k) => wanted.has(labelOf(k)) || wanted.has(k.toLowerCase()));
    return matched.length ? matched : allSkillKeys;
  }, [selectedClass, classDef, allSkillKeys, sys]);
  const toggleSkill = (key: string) =>
    setSkills((prev) => (prev.includes(key) ? prev.filter((k) => k !== key) : prev.length < skillCount ? [...prev, key] : prev));

  // 5e skill proficiencies granted by background + race (applied as trained, on top of class picks).
  const grantedSkills = useMemo(
    () => (system === '5e' ? [...(selectedOrigin?.skillGrants ?? []), ...(selectedBackground?.skillGrants ?? [])] : []),
    [system, selectedOrigin, selectedBackground],
  );

  // ---- spells ----
  const [spellPicks, setSpellPicks] = useState<SpellOpt[]>([]);
  const [spellQuery, setSpellQuery] = useState('');
  useEffect(() => {
    if (!isCaster || allSpells.length) return;
    let on = true;
    if (system === '5e') void loadSpells().then((ss) => on && setAllSpells((ss as unknown as { name: string; level_int: number; school: string }[]).map((s) => ({ name: s.name, level: s.level_int ?? 0, meta: s.school ?? '' }))));
    else void loadPf2e('spells').then((ss) => on && setAllSpells(ss.map((s) => ({ name: String(s.name), level: Number(s.level) || 0, meta: Array.isArray(s.trait) ? (s.trait as string[]).slice(0, 2).join(', ') : '' }))));
    return () => { on = false; };
  }, [isCaster, system, allSpells.length]);
  const spellResults = useMemo(() => {
    const q = spellQuery.trim().toLowerCase();
    const maxLevel = Math.min(9, Math.ceil(level / 2));
    return allSpells.filter((s) => s.level <= maxLevel && (!q || s.name.toLowerCase().includes(q))).slice(0, 60);
  }, [allSpells, spellQuery, level]);
  const toggleSpell = (s: SpellOpt) =>
    setSpellPicks((prev) => (prev.some((x) => x.name === s.name) ? prev.filter((x) => x.name !== s.name) : [...prev, s]));

  // ---- derived build ----
  const built = useMemo(() => buildCharacter(system, {
    className: selectedClass?.name ?? '', level, abilities, skillChoices: skills,
    ...(grantedSkills.length ? { grantedSkills } : {}),
    ...(selectedOrigin?.hp ? { ancestryHp: selectedOrigin.hp } : {}),
    ...(selectedClass ? { hitDieOverride: selectedClass.hitDie } : {}),
  }), [system, selectedClass, level, abilities, skills, grantedSkills, selectedOrigin]);

  // Rough "how many should I pick" suggestion for the Spells step. Not enforced —
  // just guidance: a few cantrips plus a handful of starting leveled spells.
  const leveledSlots = useMemo(() => built.spellcasting.slots.reduce((n, s) => n + (s.level > 0 ? s.max : 0), 0), [built]);
  const suggestedSpells = Math.max(4, leveledSlots + 2);

  const STEPS = useMemo(() => ['Class', 'Origin', 'Abilities', 'Skills', ...(isCaster ? ['Spells'] : []), 'Review'], [isCaster]);
  const stepName = STEPS[Math.min(step, STEPS.length - 1)]!;

  const changeSystem = (next: SystemId) => {
    if (next === system) return;
    setSystem(next);
    // Reset everything keyed on the system so stale picks don't leak across systems.
    // The class/origin/background/spell lists reload from the loaders above.
    setClassSlug('');
    setSubclass('');
    setSkills([]);
    setSpellPicks([]);
    setSpellQuery('');
    setAllSpells([]); // force the spell loader (guarded on length) to refetch for the new system
    setAncestry('');
    setBackground('');
    setStep(0);
  };

  const applyTemplate = (t: { label: string; className: string; ability: AbilityScores }) => {
    const c = classes.find((x) => x.name === t.className);
    if (c) { setClassSlug(c.slug); setSubclass(''); setSkills([]); }
    // Templates can use scores above the point-buy cap (PF2e starts a key stat at 18),
    // which would softlock the point-buy step ("-Infinity / 27"). Use manual entry then.
    const fitsPointBuy = ABILITIES.every((a) => t.ability[a] >= POINT_BUY_MIN && t.ability[a] <= POINT_BUY_MAX);
    setMethod(fitsPointBuy ? 'pointbuy' : 'manual');
    setPb(ABILITIES.map((a) => t.ability[a]));
    if (!name) setName(t.label.replace(/^[^A-Za-z]+/, ''));
  };

  const stepValid: Record<string, boolean> = {
    Class: !!selectedClass,
    Origin: true,
    Abilities: system === 'pf2e' ? true : poolValid && pbValid,
    Skills: skills.length === Math.min(skillCount, skillOptions.length),
    Spells: true,
    Review: true,
  };

  const finish = async () => {
    if (!selectedClass) return;
    const created = await charactersRepo.create(campaign.id, {
      system, name: name.trim() || selectedClass.name, kind,
      ancestry: ancestry.trim(), className: selectedClass.name, level,
    });
    // For classes outside the curated tables (esp. PF2e), fill saves from data.
    const saveRanks: Record<string, ProficiencyRank> = { ...built.saveRanks };
    if (Object.keys(saveRanks).length === 0 && selectedClass.saveRanks) {
      for (const [k, v] of Object.entries(selectedClass.saveRanks)) saveRanks[k] = v.toLowerCase() as ProficiencyRank;
    }
    const spells: SpellEntry[] = spellPicks.map((s) => newSpellEntry({ id: newId(), name: s.name, level: Math.min(10, Math.max(0, s.level)) }));
    const notes = [subclass ? `Subclass: ${subclass}` : '', background ? `Background: ${background}` : ''].filter(Boolean).join('\n');
    await charactersRepo.update(created.id, {
      ...built,
      ...(Object.keys(saveRanks).length ? { saveRanks } : {}),
      spellcasting: { ...built.spellcasting, spells },
      ...(notes ? { notes } : {}),
      ...(selectedOrigin?.speed ? { speed: selectedOrigin.speed } : {}),
    });
    onClose();
    void navigate({ to: '/characters/$characterId', params: { characterId: created.id } });
  };

  const isLast = stepName === 'Review';
  const go = (d: number) => setStep((s) => Math.max(0, Math.min(STEPS.length - 1, s + d)));

  return (
    <Modal
      open onClose={onClose}
      title={`New character — ${stepName}`}
      className="max-w-3xl"
      footer={
        <>
          <Button variant="ghost" onClick={onClose}>Cancel</Button>
          {step > 0 && <Button variant="secondary" onClick={() => go(-1)}>Back</Button>}
          {isLast
            ? <Button variant="primary" disabled={!selectedClass} onClick={finish}>Create character</Button>
            : <Button variant="primary" disabled={!stepValid[stepName]} onClick={() => go(1)}>Next</Button>}
        </>
      }
    >
      <ol className="mb-4 flex flex-wrap gap-1 text-xs">
        {STEPS.map((s, i) => (
          <li key={s} className={cn('rounded-md px-2 py-1', i === step ? 'bg-accent text-accent-ink' : i < step ? 'bg-elevated text-ink' : 'bg-surface text-muted')}>{i + 1}. {s}</li>
        ))}
      </ol>

      {stepName === 'Class' && (
        <div className="space-y-3">
          <Field label="Name"><Input value={name} onChange={(e) => setName(e.target.value)} placeholder="Your hero's name" /></Field>
          <div className="grid grid-cols-2 gap-3">
            <Field label="Type"><Select value={kind} onChange={(e) => setKind(e.target.value as Character['kind'])}><option value="pc">Player character</option><option value="npc">NPC</option></Select></Field>
            <Field label="Level"><NumberField value={level} min={1} max={20} onChange={setLevel} aria-label="Level" /></Field>
          </div>

          <div>
            <div className="mb-1 smallcaps">Game system</div>
            <div className="flex gap-1" role="group" aria-label="Game system">
              {SYSTEM_OPTIONS.map((opt) => (
                <button key={opt.id} type="button" onClick={() => changeSystem(opt.id)} aria-pressed={system === opt.id}
                  className={cn('rounded-md px-3 py-1.5 text-sm', system === opt.id ? 'bg-accent text-accent-ink' : 'bg-elevated text-muted hover:text-ink')}>
                  {opt.label}
                </button>
              ))}
            </div>
          </div>

          <div className="rounded-lg border border-accent/30 bg-accent/5 p-2">
            <div className="mb-1 smallcaps">New here? Start from a ready-made hero</div>
            <div className="flex flex-wrap gap-1">
              {(TEMPLATES[system] ?? []).map((t) => (
                <button key={t.label} onClick={() => applyTemplate(t)} title={t.hint} className="rounded-md border border-line bg-surface px-2 py-1 text-xs text-ink hover:border-accent">{t.label}</button>
              ))}
            </div>
          </div>

          <div>
            <div className="mb-1 smallcaps">Choose a class</div>
            <div className="grid max-h-72 grid-cols-1 gap-2 overflow-y-auto pr-1 sm:grid-cols-2">
              {classes.map((c) => (
                <button key={c.slug} data-testid="class-card" onClick={() => { setClassSlug(c.slug); setSubclass(''); setSkills([]); }}
                  className={cn('rounded-lg border p-2 text-left', classSlug === c.slug ? 'border-accent bg-accent/5' : 'border-line bg-surface hover:border-accent/60')}>
                  <div className="flex items-center justify-between">
                    <span className="font-display font-semibold text-ink">{c.name}</span>
                    <span className="rounded-full bg-elevated px-1.5 py-0.5 text-[10px] uppercase text-muted">{playstyle(c)}</span>
                  </div>
                  <div className="mt-0.5 text-[11px] text-muted">{system === 'pf2e' ? `${c.hitDie} HP/lvl` : `d${c.hitDie}`}{c.keyAbilities.length ? ` · ${c.keyAbilities.map((k) => ABILITY_ABBR[k as AbilityKey]).join('/')}` : ''}{c.caster !== 'none' ? ' · caster' : ''}</div>
                  {c.description && <p className="mt-1 line-clamp-2 text-xs text-muted">{briefOverview(c.description)}</p>}
                </button>
              ))}
              {classes.length === 0 && <p className="text-sm text-muted">Loading classes…</p>}
            </div>
          </div>

          {selectedClass && (() => {
            const tip = getClassTip(system, selectedClass.slug);
            if (!tip) return null;
            return (
              <div className="rounded-lg border border-accent/30 bg-accent/5 p-3 text-sm">
                <div className="flex items-start gap-2">
                  <Lightbulb size={15} className="mt-0.5 shrink-0 text-accent" aria-hidden />
                  <div>
                    <div className="font-medium text-ink">{selectedClass.name}</div>
                    <p className="mt-0.5 text-muted">{tip.role}</p>
                    <div className="mt-1.5 text-xs text-muted">
                      <span className="font-medium text-ink">Key abilities:</span> {tip.primaryAbilities}
                    </div>
                    {tip.beginner && <Badge tone="verdigris" className="mt-1.5">Good for beginners</Badge>}
                  </div>
                </div>
              </div>
            );
          })()}

          {selectedClass && selectedClass.subclasses.length > 0 && (() => {
            const sc = selectedClass.subclasses.find((s) => s.name === subclass);
            return (
              <Field label={system === 'pf2e' ? 'Subclass / focus (optional)' : 'Subclass (optional)'}>
                <Select value={subclass} onChange={(e) => setSubclass(e.target.value)}>
                  <option value="">— decide later —</option>
                  {selectedClass.subclasses.map((s) => <option key={s.name} value={s.name}>{s.name}</option>)}
                </Select>
                {sc?.desc && (
                  <p className="mt-1 max-h-28 overflow-y-auto rounded-md border border-line bg-surface-2 p-2 text-xs text-muted">
                    {sc.desc}
                  </p>
                )}
              </Field>
            );
          })()}
        </div>
      )}

      {stepName === 'Origin' && (
        <div className="space-y-3">
          <div className="grid gap-4 sm:grid-cols-2">
            <OriginPicker title={system === 'pf2e' ? 'Ancestry' : 'Race'} options={origins} value={ancestry} onPick={setAncestry} />
            <OriginPicker title="Background" options={backgrounds} value={background} onPick={setBackground} />
          </div>
          {selectedClass && selectedOrigin && (() => {
            const synergy = raceSynergyNote(selectedOrigin.meta ?? '', selectedClass.keyAbilities);
            if (!synergy) return null;
            return (
              <p className="flex items-center gap-1.5 text-xs text-accent">
                <Lightbulb size={12} className="shrink-0" aria-hidden />
                {synergy}
              </p>
            );
          })()}
        </div>
      )}

      {stepName === 'Abilities' && (
        <div>
          {system === 'pf2e' && boostSlots && (
            <div className="space-y-3">
              <p className="text-sm text-muted">PF2e scores are built from <span className="text-ink">boosts</span>: each starts at 10, and a boost adds +2 (or +1 once a score reaches 18). Assign your free boosts below.</p>
              {(boostSlots.fixed.length > 0 || boostSlots.flaw) && (
                <div className="flex flex-wrap items-center gap-1 text-xs">
                  <span className="text-muted">{selectedOrigin?.name ?? 'Ancestry'}:</span>
                  {boostSlots.fixed.map((a, i) => <span key={i} className="rounded bg-accent/10 px-1.5 py-0.5 font-medium text-accent">+{ABILITY_ABBR[a]}</span>)}
                  {boostSlots.flaw && <span className="rounded bg-danger/10 px-1.5 py-0.5 font-medium text-danger">−{ABILITY_ABBR[boostSlots.flaw]}</span>}
                  <span className="text-faint">(fixed)</span>
                </div>
              )}
              {boostSlots.slots.length > 0 && (
                <div className="grid grid-cols-2 gap-2 sm:grid-cols-3">
                  {boostSlots.slots.map((s) => {
                    // Abilities already taken by OTHER slots in the same source (can't double-boost).
                    const taken = new Set<AbilityKey>(boostSource(s.id) === 'ancestry' ? boostSlots.fixed : []);
                    for (const o of boostSlots.slots) {
                      if (o.id !== s.id && boostSource(o.id) === boostSource(s.id) && boostPicks[o.id]) taken.add(boostPicks[o.id]!);
                    }
                    return (
                      <label key={s.id} className="text-xs text-muted">
                        {s.label}
                        <Select className="mt-0.5" aria-label={s.label} value={boostPicks[s.id] ?? s.options[0]} onChange={(e) => setBoostPicks((p) => ({ ...p, [s.id]: e.target.value as AbilityKey }))}>
                          {s.options.map((o) => <option key={o} value={o} disabled={taken.has(o) && boostPicks[s.id] !== o}>{ABILITY_ABBR[o]}{taken.has(o) ? ' ·' : ''}</option>)}
                        </Select>
                      </label>
                    );
                  })}
                </div>
              )}
              <div className="grid grid-cols-3 gap-2 sm:grid-cols-6">
                {ABILITIES.map((a) => {
                  const isKey = selectedClass?.keyAbilities.includes(a);
                  return (
                    <div key={a} className={cn('rounded-lg border bg-surface p-2 text-center', isKey ? 'border-accent/60' : 'border-line')}>
                      <div className="flex items-center justify-center gap-0.5 smallcaps">{ABILITY_ABBR[a]}{isKey && <Star size={10} className="text-accent" aria-hidden />}</div>
                      <div className="font-display text-lg font-semibold text-ink">{abilities[a]}</div>
                      <div className="text-xs text-accent">{formatModifier(abilityModifier(abilities[a]))}</div>
                    </div>
                  );
                })}
              </div>
            </div>
          )}
          {system !== 'pf2e' && (<>
          <div className="mb-3 flex flex-wrap gap-1">
            {([['standard', 'Standard array'], ['pointbuy', 'Point buy'], ['roll', '4d6 drop lowest'], ['manual', 'Manual']] as const).map(([m, label]) => (
              <button key={m} onClick={() => chooseMethod(m)} className={cn('rounded-md px-3 py-1.5 text-sm', method === m ? 'bg-accent text-accent-ink' : 'bg-elevated text-muted hover:text-ink')}>{label}</button>
            ))}
          </div>
          {method === 'roll' && <Button size="sm" variant="secondary" className="mb-3" onClick={() => chooseMethod('roll')}><RotateCcw size={13} aria-hidden /> Reroll</Button>}
          {method === 'pointbuy' && <p className={cn('mb-3 text-sm', pointBuyRemaining(pb) < 0 ? 'text-danger' : 'text-muted')}>Points remaining: <span className="font-semibold text-ink">{pointBuyRemaining(pb)}</span> / 27</p>}
          {method === 'manual' && <p className="mb-3 text-sm text-muted">Enter each score directly (1–30).</p>}
          {system === '5e' && selectedOrigin && Object.values(selectedOrigin.asiBonuses ?? {}).some(Boolean) && (
            <p className="mb-2 text-xs text-accent">
              {selectedOrigin.name} racial bonus ({ABILITIES.filter((a) => selectedOrigin.asiBonuses?.[a]).map((a) => `+${selectedOrigin.asiBonuses![a]} ${ABILITY_ABBR[a]}`).join(', ')}) is added to your final scores.
            </p>
          )}
          {selectedClass && (() => {
            const tip = getClassTip(system, selectedClass.slug);
            const text = tip?.abilityTip ?? (selectedClass.keyAbilities.length > 0
              ? `Prioritise ${selectedClass.keyAbilities.map((k) => ABILITY_ABBR[k as AbilityKey]).join(' / ')} for a ${selectedClass.name}.`
              : null);
            return text ? (
              <p className="mb-3 flex items-start gap-1.5 text-xs text-muted">
                <Lightbulb size={12} className="mt-0.5 shrink-0 text-accent" aria-hidden />
                {text}
              </p>
            ) : null;
          })()}
          {usesPool && !poolValid && <p className="mb-2 text-sm text-warning">Assign each value to a different ability.</p>}
          <div className="grid grid-cols-2 gap-3 sm:grid-cols-3">
            {ABILITIES.map((a, i) => {
              const racial = (system === '5e' && selectedOrigin?.asiBonuses?.[a]) || 0;
              const base = usesPool ? (pool[assignment[i]!] ?? 0) : pb[i]!;
              const value = base + racial;
              const isKey = selectedClass?.keyAbilities.includes(a);
              const fMin = method === 'pointbuy' ? POINT_BUY_MIN : 1;
              const fMax = method === 'pointbuy' ? POINT_BUY_MAX : 30;
              return (
                <div key={a} className={cn('rounded-lg border bg-surface p-3 text-center', isKey ? 'border-accent/60' : 'border-line')}>
                  <div className="flex items-center justify-center gap-0.5 smallcaps">{ABILITY_ABBR[a]}{isKey && <Star size={10} className="text-accent" aria-hidden />}</div>
                  {usesPool ? (
                    <Select className="mt-1" aria-label={`${ABILITY_ABBR[a]} value`} value={assignment[i]} onChange={(e) => {
                      const v = Number(e.target.value);
                      // Swap on collision so a value can be moved between abilities directly.
                      setAssignment((prev) => {
                        const next = [...prev];
                        const j = next.findIndex((x, k) => x === v && k !== i);
                        if (j !== -1) next[j] = next[i]!;
                        next[i] = v;
                        return next;
                      });
                    }}>
                      {pool.map((p, idx) => <option key={idx} value={idx}>{p}</option>)}
                    </Select>
                  ) : (
                    <NumberField className="mt-1" value={pb[i]!} min={fMin} max={fMax} aria-label={`${ABILITY_ABBR[a]} score`} onChange={(v) => setPb((prev) => prev.map((x, j) => (j === i ? Math.max(fMin, Math.min(fMax, v)) : x)))} />
                  )}
                  <div className="mt-1 text-xs text-accent">
                    {racial ? <span className="text-muted">{base}+{racial} · </span> : null}{formatModifier(abilityModifier(value))}
                  </div>
                </div>
              );
            })}
          </div>
          </>)}
        </div>
      )}

      {stepName === 'Skills' && (
        <div>
          <p className="mb-1 text-sm text-muted">Choose <span className="font-semibold text-ink">{Math.min(skillCount, skillOptions.length)}</span> {system === 'pf2e' ? 'Trained' : 'proficient'} {skillCount === 1 ? 'skill' : 'skills'} ({skills.length} selected).</p>
          <p className="mb-2 text-xs text-muted">{system === 'pf2e' ? 'These start at the Trained proficiency rank; you can raise ranks as you level up.' : 'You gain proficiency in these skills, adding your proficiency bonus to checks.'}</p>
          {selectedClass && (() => {
            const tip = getClassTip(system, selectedClass.slug);
            return tip?.skillSuggestions ? (
              <p className="mb-3 flex items-center gap-1.5 text-xs text-muted">
                <Lightbulb size={12} className="shrink-0 text-accent" aria-hidden />
                {selectedClass.name}s often pick: <span className="ml-1 text-ink">{tip.skillSuggestions}</span>
              </p>
            ) : null;
          })()}
          <div className="grid grid-cols-2 gap-2 sm:grid-cols-3">
            {skillOptions.map((key) => {
              const checked = skills.includes(key);
              const disabled = !checked && skills.length >= skillCount;
              return (
                <label key={key} className={cn('flex items-center gap-2 rounded-md border px-3 py-2 text-sm', checked ? 'border-accent/60 bg-accent/5 text-ink' : disabled ? 'border-line text-muted opacity-50' : 'border-line text-ink')}>
                  <input type="checkbox" checked={checked} disabled={disabled} onChange={() => toggleSkill(key)} />
                  {skillLabel(key)}
                </label>
              );
            })}
          </div>
        </div>
      )}

      {stepName === 'Spells' && (
        <div>
          <p className="mb-1 text-sm text-muted">Pick your cantrips and a few starting spells — about <span className="font-semibold text-ink">{suggestedSpells}</span> is a good start at level {level}. Search by name; you can always adjust on the sheet later.</p>
          <p className="mb-1 text-xs text-muted">These are the spells you <span className="text-ink">{system === 'pf2e' ? 'know (your repertoire)' : 'know (the spells you’ve learned)'}</span>. You prepare or cast them from slots on the character sheet.</p>
          <p className={cn('mb-2 text-xs', spellPicks.length > suggestedSpells + 4 ? 'text-warning' : 'text-muted')}>
            {spellPicks.length} selected{spellPicks.length > suggestedSpells + 4 ? ' — that’s quite a few; you can trim some later if you like.' : ''}
          </p>
          <Input value={spellQuery} onChange={(e) => setSpellQuery(e.target.value)} placeholder="Search spells…" className="mb-2" aria-label="Search spells" />
          <div className="grid max-h-64 grid-cols-1 gap-1 overflow-y-auto pr-1 sm:grid-cols-2">
            {spellResults.map((s) => {
              const checked = spellPicks.some((x) => x.name === s.name);
              return (
                <label key={s.name} className={cn('flex items-center gap-2 rounded-md border px-2 py-1 text-sm', checked ? 'border-accent/60 bg-accent/5' : 'border-line')}>
                  <input type="checkbox" checked={checked} onChange={() => toggleSpell(s)} />
                  <span className="flex-1 truncate text-ink">{s.name}</span>
                  <span className="text-[10px] text-muted">{s.level === 0 ? 'cantrip' : `lv ${s.level}`}</span>
                </label>
              );
            })}
            {allSpells.length === 0 && <p className="text-sm text-muted">Loading spells…</p>}
          </div>
        </div>
      )}

      {stepName === 'Review' && selectedClass && (
        <div className="space-y-3 text-sm">
          <div className="rounded-lg border border-line bg-surface p-3">
            <div className="font-medium text-ink">{name || selectedClass.name} — level {level} {[ancestry, selectedClass.name].filter(Boolean).join(' ')}{subclass ? ` (${subclass})` : ''}</div>
            <div className="mt-2 grid grid-cols-2 gap-2 text-center">
              <Stat label="Max HP" value={built.hp.max} />
              <Stat label="AC" value={sys.baseArmorClass({ level, abilities, armorBonus: 0 })} />
            </div>
          </div>
          <Review label="Abilities" value={ABILITIES.map((a) => `${ABILITY_ABBR[a]} ${abilities[a]}`).join('  ')} />
          {background && <Review label="Background" value={background} />}
          <Review label="Trained skills" value={skills.map(skillLabel).join(', ') || '—'} />
          {built.spellcasting.slots.length > 0 && <Review label="Spell slots" value={built.spellcasting.slots.map((s) => `${s.max}×L${s.level}`).join('  ')} />}
          {spellPicks.length > 0 && <Review label="Spells" value={spellPicks.map((s) => s.name).join(', ')} />}
          <p className="text-xs text-muted">You can fine-tune equipment, feats, and more on the sheet next.</p>
        </div>
      )}
    </Modal>
  );
}

function OriginPicker({ title, options, value, onPick }: { title: string; options: Origin[]; value: string; onPick: (v: string) => void }) {
  const [q, setQ] = useState('');
  const filtered = options.filter((o) => !q || o.name.toLowerCase().includes(q.toLowerCase())).slice(0, 60);
  const sel = options.find((o) => o.name === value);
  return (
    <div>
      <div className="mb-1 flex items-center justify-between">
        <span className="smallcaps">{title}</span>
        {value && <button className="text-[11px] text-muted hover:text-ink" onClick={() => onPick('')}>clear</button>}
      </div>
      <Input value={q} onChange={(e) => setQ(e.target.value)} placeholder={`Search ${title.toLowerCase()}…`} className="mb-1" aria-label={`Search ${title}`} />
      <div className="max-h-44 space-y-1 overflow-y-auto pr-1">
        {filtered.map((o) => (
          <button key={o.name} onClick={() => onPick(o.name)} className={cn('block w-full rounded-md border px-2 py-1 text-left text-sm', value === o.name ? 'border-accent bg-accent/5 text-ink' : 'border-line text-ink hover:border-accent/60')}>
            {o.name}{o.meta ? <span className="ml-1 text-[10px] text-muted">{String(o.meta).slice(0, 28)}</span> : ''}
          </button>
        ))}
        {options.length === 0 && <p className="text-xs text-muted">Loading…</p>}
      </div>
      {sel?.desc && <p className="mt-1 max-h-32 overflow-y-auto whitespace-pre-wrap rounded-md border border-line bg-surface-2 p-2 text-xs text-muted">{sel.desc}</p>}
    </div>
  );
}

function Stat({ label, value }: { label: string; value: number | string }) {
  return <div className="rounded-md border border-line bg-panel p-2"><div className="font-display text-lg font-semibold text-accent">{value}</div><div className="text-xs uppercase tracking-wide text-muted">{label}</div></div>;
}
function Review({ label, value }: { label: string; value: string }) {
  return <div className="flex gap-2"><span className="w-28 shrink-0 text-xs uppercase tracking-wide text-muted">{label}</span><span className="text-ink">{value}</span></div>;
}
