import type { AbilityKey, AbilityScores, ProficiencyRank, SystemId } from './types';
import { abilityModifier } from './abilities';
import { DND5E_CLASSES, dnd5eSlots, dnd5ePact, dnd5eHp, dnd5eAsiLevels } from './dnd5e/progression';
import { PF2E_CLASSES, pf2eHp, pf2eSlots, pf2eBoostLevels, pf2eSkillIncreaseLevels } from './pf2e/progression';

export type CasterKind = 'none' | 'full' | 'half' | 'pact';
/** Spellcasting abilities allowed by the character schema. */
export type SpellAbility = 'int' | 'wis' | 'cha';

export interface ClassDef {
  name: string;
  /** 5e: hit die size (6/8/10/12). PF2e: HP gained per level. */
  hitDie: number;
  keyAbilities: AbilityKey[];
  /** 5e: abilities whose saves become trained. */
  saveProficient: AbilityKey[];
  /** PF2e: initial save ranks keyed by the save's ability (con/dex/wis). */
  saveRanks?: Partial<Record<AbilityKey, ProficiencyRank>>;
  /** PF2e: initial perception rank. */
  perception?: ProficiencyRank;
  /** Skills to choose from (5e). Empty = choose from all (Bard / all PF2e). */
  skillKeys: string[];
  /** Number of trained skills to choose (PF2e adds INT mod on top at build time). */
  skillCount: number;
  caster: CasterKind;
  spellAbility?: SpellAbility;
  spellRank?: ProficiencyRank;
}

export interface SpellSlot { level: number; max: number; current: number }

export interface BuildChoices {
  className: string;
  level: number;
  /** Final ability scores (the caller folds in racial/ancestry bonuses before building). */
  abilities: AbilityScores;
  /** chosen skill keys → trained */
  skillChoices: string[];
  /**
   * Extra skill keys granted by background/race/heritage (trained), merged with the
   * class picks. A skill already chosen by the class stays trained (no downgrade).
   */
  grantedSkills?: string[];
  /** PF2e ancestry HP (from the ancestries data); ignored for 5e. */
  ancestryHp?: number;
  /** Used only when the class is unknown (custom): 5e hit die size. */
  hitDieOverride?: number;
}

/** Derived character fields produced by the builder — a Partial<Character> patch. */
export interface BuiltCharacter {
  abilities: AbilityScores;
  hp: { current: number; max: number; temp: number };
  skillRanks: Record<string, ProficiencyRank>;
  saveRanks: Record<string, ProficiencyRank>;
  perceptionRank: ProficiencyRank;
  spellcastingAbility?: SpellAbility;
  spellcastingRank?: ProficiencyRank;
  spellcasting: { slots: SpellSlot[]; pact?: SpellSlot; spells: never[] };
}

const TABLES: Record<SystemId, ClassDef[]> = { '5e': DND5E_CLASSES, pf2e: PF2E_CLASSES };

export function getClassDefs(system: SystemId): ClassDef[] {
  return TABLES[system];
}
export function getClassNames(system: SystemId): string[] {
  return TABLES[system].map((c) => c.name);
}
export function getClassDef(system: SystemId, className: string): ClassDef | undefined {
  const want = className.trim().toLowerCase();
  return TABLES[system].find((c) => c.name.toLowerCase() === want);
}

/** Skills the class may choose from (full list when unrestricted). */
export function classSkillChoices(system: SystemId, allSkillKeys: string[], className: string): string[] {
  const def = getClassDef(system, className);
  if (def && def.skillKeys.length > 0) return def.skillKeys;
  return allSkillKeys;
}

/** How many skills to pick (PF2e adds INT modifier, min 0). */
export function classSkillCount(system: SystemId, className: string, intMod: number): number {
  const def = getClassDef(system, className);
  const base = def?.skillCount ?? (system === 'pf2e' ? 3 : 2);
  return system === 'pf2e' ? base + Math.max(0, intMod) : base;
}

/** Build the derived fields for a brand-new character from the wizard's choices. */
export function buildCharacter(system: SystemId, choices: BuildChoices): BuiltCharacter {
  const def = getClassDef(system, choices.className);
  const conMod = abilityModifier(choices.abilities.con);
  const die = def?.hitDie ?? choices.hitDieOverride ?? (system === 'pf2e' ? 8 : 8);

  const hpMax = system === 'pf2e'
    ? pf2eHp(die, choices.level, conMod, choices.ancestryHp ?? 8)
    : dnd5eHp(die, choices.level, conMod);

  const skillRanks: Record<string, ProficiencyRank> = {};
  for (const key of choices.skillChoices) skillRanks[key] = 'trained';
  // Background / racial skill proficiencies stack on top of the class picks.
  for (const key of choices.grantedSkills ?? []) if (!skillRanks[key]) skillRanks[key] = 'trained';

  const saveRanks: Record<string, ProficiencyRank> = {};
  if (system === 'pf2e') {
    const ranks: Partial<Record<AbilityKey, ProficiencyRank>> = def?.saveRanks ?? { con: 'trained', dex: 'trained', wis: 'trained' };
    for (const [ability, rank] of Object.entries(ranks)) {
      if (rank) saveRanks[ability] = rank;
    }
  } else {
    for (const ability of def?.saveProficient ?? []) saveRanks[ability] = 'trained';
  }

  const caster = def?.caster ?? 'none';
  const slots = system === 'pf2e' ? pf2eSlots(caster, choices.level) : dnd5eSlots(caster, choices.level);
  const pact = system === '5e' && caster === 'pact' ? dnd5ePact(choices.level) : undefined;

  return {
    abilities: choices.abilities,
    hp: { current: hpMax, max: hpMax, temp: 0 },
    skillRanks,
    saveRanks,
    perceptionRank: system === 'pf2e' ? def?.perception ?? 'trained' : 'trained',
    ...(def?.spellAbility ? { spellcastingAbility: def.spellAbility, spellcastingRank: def.spellRank ?? 'trained' } : {}),
    spellcasting: { slots, ...(pact ? { pact } : {}), spells: [] },
  };
}

// ---------------- Level-up planning ----------------

export type LevelChoice =
  | { kind: 'asi'; label: string }
  | { kind: 'boosts'; count: number; label: string }
  | { kind: 'skill-increase'; label: string }
  | { kind: 'feat'; label: string };

export interface LevelUpPlan {
  nextLevel: number;
  hpGainAverage: number;
  hitDie: number;
  slots?: SpellSlot[];
  pact?: SpellSlot;
  notes: string[];
  choices: LevelChoice[];
}

export function planLevelUp(system: SystemId, className: string, currentLevel: number, conMod: number): LevelUpPlan {
  const nextLevel = Math.min(20, Math.floor(currentLevel) + 1);
  const def = getClassDef(system, className);
  const die = def?.hitDie ?? 8;
  const caster = def?.caster ?? 'none';
  const notes: string[] = [];
  const choices: LevelChoice[] = [];

  if (system === '5e') {
    const hpGainAverage = Math.max(1, Math.ceil(die / 2) + 1 + conMod);
    const slots = dnd5eSlots(caster, nextLevel);
    const pact = caster === 'pact' ? dnd5ePact(nextLevel) : undefined;
    if (dnd5eAsiLevels(className).includes(nextLevel)) {
      choices.push({ kind: 'asi', label: 'Ability Score Improvement — raise abilities by +2 total, or take a feat instead.' });
    }
    if (slots.length) notes.push('Spell slots updated for the new level.');
    return { nextLevel, hpGainAverage, hitDie: die, ...(slots.length ? { slots } : {}), ...(pact ? { pact } : {}), notes, choices };
  }

  // PF2e
  const hpGainAverage = Math.max(1, die + conMod);
  const slots = pf2eSlots(caster, nextLevel);
  if (pf2eBoostLevels().includes(nextLevel)) {
    choices.push({ kind: 'boosts', count: 4, label: 'Ability boosts — raise four different abilities (+2 if under 18, otherwise +1).' });
  }
  if (pf2eSkillIncreaseLevels().includes(nextLevel)) {
    choices.push({ kind: 'skill-increase', label: 'Skill increase — raise one skill to the next proficiency rank.' });
  }
  if (nextLevel % 2 === 0) choices.push({ kind: 'feat', label: 'Class feat — pick a new class feat for this level.' });
  if (slots.length) notes.push('Spell slots updated for the new level.');
  return { nextLevel, hpGainAverage, hitDie: die, ...(slots.length ? { slots } : {}), notes, choices };
}

// ---------------- Apply helpers (pure) ----------------

const RANK_ORDER: ProficiencyRank[] = ['untrained', 'trained', 'expert', 'master', 'legendary'];

export function bumpRank(rank: ProficiencyRank): ProficiencyRank {
  const i = RANK_ORDER.indexOf(rank);
  return RANK_ORDER[Math.min(RANK_ORDER.length - 1, i + 1)]!;
}

/** Apply ability increases. 5e: +1 per pick (pick the same twice for +2). PF2e: +2 if <18 else +1. */
export function applyIncreases(abilities: AbilityScores, picks: AbilityKey[], system: SystemId): AbilityScores {
  const next = { ...abilities };
  for (const k of picks) {
    next[k] = system === 'pf2e' ? (next[k] >= 18 ? next[k] + 1 : next[k] + 2) : next[k] + 1;
  }
  return next;
}
