export type SystemId = '5e' | 'pf2e';

export const ABILITY_KEYS = ['str', 'dex', 'con', 'int', 'wis', 'cha'] as const;
export type AbilityKey = (typeof ABILITY_KEYS)[number];

export type AbilityScores = Record<AbilityKey, number>;

export interface SkillDef {
  key: string;
  label: string;
  ability: AbilityKey;
}

/** PF2e proficiency ranks; 5e collapses to trained/untrained semantics. */
export type ProficiencyRank = 'untrained' | 'trained' | 'expert' | 'master' | 'legendary';

export interface DerivedSkill {
  key: string;
  label: string;
  ability: AbilityKey;
  modifier: number;
}

/** Input describing a single weapon/attack for derived to-hit + damage. */
export interface WeaponInput {
  /** ability the attack uses (str for melee, dex for finesse/ranged) */
  ability: AbilityKey;
  /** proficiency in the weapon */
  rank: ProficiencyRank;
  /**
   * Flat magic/enhancement bonus. In 5e this applies to BOTH to-hit and damage.
   * In PF2e this is a weapon potency rune — it applies to the attack roll ONLY,
   * never to a flat damage bonus (extra magic-weapon damage comes from `striking`).
   */
  itemBonus?: number;
  /** damage dice, e.g. "1d8" */
  damageDice?: string;
  /** add the ability modifier to damage (off-hand/some ranged turn this off) */
  addAbilityToDamage?: boolean;
  /** PF2e: the agile trait lowers the multiple-attack penalty to -4/-8. */
  agile?: boolean;
  /** PF2e striking-rune tier as a damage-dice multiplier: 2=striking, 3=greater, 4=major. 1/undefined = none. */
  striking?: number;
}

export interface WeaponResult {
  toHit: number;
  /** ready-to-roll damage expression, e.g. "1d8+3" */
  damage: string;
  /**
   * PF2e multiple-attack penalty: the to-hit for the 2nd and 3rd+ attack actions
   * this turn (-5/-10, or -4/-8 with an agile weapon). Absent for 5e.
   */
  map?: { second: number; third: number };
}

export interface CarryingCapacity {
  /** at/over this, the character is encumbered */
  encumbered: number;
  /** absolute maximum carry */
  max: number;
  unit: 'lb' | 'Bulk';
}

/** A rest/recovery option and what it restores. */
export interface RestOption {
  id: string;
  label: string;
  restoresHp: boolean;
  restoresSlots: boolean;
  restoresPact: boolean;
  /** resource.recovery tags that refresh */
  recovers: readonly Recovery[];
  reduceExhaustion?: boolean;
  /**
   * How much of a matched resource this rest restores. Omitted/undefined = restore
   * to max (5e short/long rest). A number restores by that step, clamped to max —
   * PF2e Refocus uses `1` (one Focus Point), not the whole pool.
   */
  recoverStep?: number;
}

export type Recovery = 'short' | 'long' | 'daily' | 'none';

/**
 * The contract every supported game system implements. The UI and stores talk
 * to this interface only — no `if (system === '5e')` scattered through features.
 */
export interface RulesSystem {
  id: SystemId;
  label: string;
  abilities: readonly AbilityKey[];
  abilityLabels: Record<AbilityKey, string>;
  skills: readonly SkillDef[];

  /** Standard d20-family ability modifier: floor((score - 10) / 2). */
  abilityModifier(score: number): number;

  /** Default ability scores for a new character. */
  defaultAbilityScores(): AbilityScores;

  /** Initiative modifier for a character. */
  initiativeModifier(input: CharacterRulesInput): number;

  /** Per-skill modifiers given a character's stats. */
  skillModifiers(input: CharacterRulesInput): DerivedSkill[];

  /** Saving-throw / defense modifiers, keyed by ability. */
  saveModifiers(input: CharacterRulesInput): Record<AbilityKey, number>;

  /** Suggested unarmored defense/AC. */
  baseArmorClass(input: CharacterRulesInput): number;

  /** Proficiency contribution (5e: bonus by level; PF2e: rank+level). */
  proficiencyValue(level: number, rank: ProficiencyRank): number;

  /** Spellcasting save DC for the given casting ability, if any. */
  spellSaveDc?(input: CharacterRulesInput, ability: AbilityKey): number;

  /** Spell attack bonus for the given casting ability. */
  spellAttackBonus(input: CharacterRulesInput, ability: AbilityKey): number;

  /** Passive Perception (5e) / Perception DC basis (PF2e): 10 + perception mod. */
  passivePerception(input: CharacterRulesInput): number;

  /** To-hit + damage for a weapon attack. */
  weaponAttack(input: CharacterRulesInput, weapon: WeaponInput): WeaponResult;

  /** Carry limits, in the system's native unit. */
  carryingCapacity(input: CharacterRulesInput): CarryingCapacity;

  /** Rest/recovery options this system offers (short/long, refocus/rest, …). */
  restOptions: readonly RestOption[];
}

/** Minimal character shape the rules layer needs to compute derived values. */
export interface CharacterRulesInput {
  level: number;
  abilities: AbilityScores;
  /** rank per skill key; missing = untrained */
  skillRanks?: Record<string, ProficiencyRank>;
  /** rank per save ability; missing = untrained */
  saveRanks?: Partial<Record<AbilityKey, ProficiencyRank>>;
  /** flat AC bonus from shield / misc on top of armor. */
  armorBonus?: number;
  /**
   * Worn body armor mechanics. When set, AC = baseAc + (Dex capped by dexCap) +
   * armorBonus. When absent, the unarmored 10 + Dex + armorBonus formula applies.
   * `dexCap: null` = no cap (light); `0` = Dex ignored (heavy).
   */
  equippedArmor?: { baseAc: number; dexCap: number | null };
  /** PF2e: Perception proficiency (drives initiative). */
  perceptionRank?: ProficiencyRank;
  /** PF2e: spellcasting proficiency. */
  spellcastingRank?: ProficiencyRank;
  /**
   * PF2e: armor/unarmored-defense proficiency. PF2e AC scales with level via this
   * rank (10 + Dex + (level + rankBonus) + item). Missing = trained (the common
   * level-1 case). Ignored by 5e, whose AC has no proficiency term.
   */
  acRank?: ProficiencyRank;
}
