/** Pure normalizers turning Open5e (5e) and Foundry PF2e raw records into our
 *  unified RulesetClass shape. Used by the scrapers (scripts/) and unit tests. */

export interface RulesetClass {
  system: '5e' | 'pf2e';
  slug: string;
  name: string;
  /** 5e: hit-die size (e.g. 12). PF2e: HP gained per level (e.g. 10). */
  hitDie: number;
  keyAbilities: string[];
  /** abilities that start proficient/trained (lowercase keys) */
  saves: string[];
  /** PF2e initial save ranks by ability */
  saveRanks?: Record<string, string>;
  perception?: string;
  skillCount: number;
  skillChoices: string[];
  /** spellcasting ability ('int'/'wis'/'cha') or 'none' */
  caster: string;
  subclasses: { name: string; desc?: string }[];
  armor: string[];
  weapons: string[];
  description: string;
  source: string;
  license?: string;
}

const ABBR: Record<string, string> = {
  strength: 'str', dexterity: 'dex', constitution: 'con',
  intelligence: 'int', wisdom: 'wis', charisma: 'cha',
};
const NUM_WORD: Record<string, number> = { one: 1, two: 2, three: 3, four: 4, five: 5, six: 6 };
const RANKS = ['Untrained', 'Trained', 'Expert', 'Master', 'Legendary'];

/** 5e classes don't expose a "key ability"; this is the conventional primary. */
const DND_KEY: Record<string, string> = {
  barbarian: 'str', bard: 'cha', cleric: 'wis', druid: 'wis', fighter: 'str', monk: 'dex',
  paladin: 'str', ranger: 'dex', rogue: 'dex', sorcerer: 'cha', warlock: 'cha', wizard: 'int',
};

function stripMd(s: string): string {
  return (s || '').replace(/[#*_`>]/g, '').replace(/\r/g, '').trim();
}
function firstParagraph(s: string): string {
  const p = stripMd(s).split('\n').map((x) => x.trim()).filter(Boolean)[0] ?? '';
  return p.length > 400 ? `${p.slice(0, 397)}…` : p;
}
function abilList(s: string | undefined): string[] {
  return (s ?? '').split(/[,&]| and /).map((x) => ABBR[x.trim().toLowerCase()]).filter(Boolean) as string[];
}

export interface Open5eClass {
  name: string; slug: string; desc?: string; hit_dice?: string;
  prof_armor?: string; prof_weapons?: string; prof_saving_throws?: string; prof_skills?: string;
  spellcasting_ability?: string; archetypes?: { name: string; desc?: string }[];
  document__title?: string; document__license_url?: string;
}

export function normalizeOpen5eClass(c: Open5eClass): RulesetClass {
  const hitDie = Number(/d(\d+)/.exec(c.hit_dice ?? '')?.[1]) || 8;
  // "Choose two from Acrobatics, Athletics, …"
  const skillsRaw = c.prof_skills ?? '';
  const countWord = /choose (\w+)/i.exec(skillsRaw)?.[1]?.toLowerCase() ?? '';
  const skillCount = NUM_WORD[countWord] ?? 2;
  const fromPart = /from (.+)$/is.exec(skillsRaw)?.[1] ?? '';
  const skillChoices = fromPart.split(',').map((s) => stripMd(s).replace(/\.$/, '').trim()).filter(Boolean).slice(0, 18);
  const caster = ABBR[(c.spellcasting_ability ?? '').trim().toLowerCase()] ?? 'none';
  return {
    system: '5e',
    slug: c.slug,
    name: c.name,
    hitDie,
    keyAbilities: DND_KEY[c.slug] ? [DND_KEY[c.slug]!] : [],
    saves: abilList(c.prof_saving_throws),
    skillCount,
    skillChoices,
    caster,
    subclasses: (c.archetypes ?? []).map((a) => ({ name: a.name, ...(a.desc ? { desc: firstParagraph(a.desc) } : {}) })),
    armor: (c.prof_armor ?? '').split(',').map((x) => x.trim()).filter(Boolean),
    weapons: (c.prof_weapons ?? '').split(',').map((x) => x.trim()).filter(Boolean),
    description: firstParagraph(c.desc ?? ''),
    source: c.document__title ?? 'Open5e',
    ...(c.document__license_url ? { license: c.document__license_url } : {}),
  };
}

export interface FoundryClass {
  name: string;
  system?: {
    hp?: number;
    keyAbility?: { value?: string[] };
    perception?: number;
    savingThrows?: { fortitude?: number; reflex?: number; will?: number };
    trainedSkills?: { value?: string[]; additional?: number };
    attacks?: Record<string, number | { rank?: number; name?: string }>;
    defenses?: Record<string, number>;
    description?: { value?: string };
    publication?: { title?: string; license?: string };
  };
}

const PF2E_SAVE_ABIL: Record<string, string> = { fortitude: 'con', reflex: 'dex', will: 'wis' };

export function normalizeFoundryClass(slug: string, c: FoundryClass): RulesetClass {
  const s = c.system ?? {};
  const saveRanks: Record<string, string> = {};
  const saves: string[] = [];
  for (const [k, abil] of Object.entries(PF2E_SAVE_ABIL)) {
    const n = (s.savingThrows as Record<string, number> | undefined)?.[k] ?? 0;
    if (n > 0) { saveRanks[abil] = RANKS[Math.min(4, n)]!; saves.push(abil); }
  }
  const weaponProfs = Object.entries(s.attacks ?? {})
    .filter(([k, v]) => k !== 'other' && (typeof v === 'number' ? v : v?.rank ?? 0) > 0)
    .map(([k]) => k);
  const armorProfs = Object.entries(s.defenses ?? {}).filter(([, v]) => (v ?? 0) > 0).map(([k]) => k);
  return {
    system: 'pf2e',
    slug,
    name: c.name,
    hitDie: s.hp ?? 8,
    keyAbilities: s.keyAbility?.value ?? [],
    saves,
    saveRanks,
    ...(s.perception !== undefined ? { perception: RANKS[Math.min(4, s.perception)]! } : {}),
    skillCount: (s.trainedSkills?.additional ?? 0) + (s.trainedSkills?.value?.length ?? 0),
    skillChoices: [],
    caster: 'none',
    subclasses: [],
    armor: armorProfs,
    weapons: weaponProfs,
    description: firstParagraph(stripHtml(s.description?.value ?? '')),
    source: s.publication?.title ?? 'Pathfinder 2e (Foundry VTT)',
    license: s.publication?.license ?? 'OGL/ORC',
  };
}

function stripHtml(s: string): string {
  return (s || '').replace(/<[^>]+>/g, ' ').replace(/&[a-z]+;/g, ' ').replace(/\s+/g, ' ').trim();
}
