import type {
  AbilityKey,
  CharacterRulesInput,
  DerivedSkill,
  ProficiencyRank,
  RestOption,
  RulesSystem,
} from '../types';
import { ABILITY_KEYS } from '../types';
import { ABILITY_LABELS, abilityModifier, defaultAbilityScores, formatDamage } from '../abilities';
import { PF2E_SKILLS } from './skills';

const PF2E_RESTS: readonly RestOption[] = [
  // Refocus restores exactly ONE Focus Point (recoverStep: 1), not the whole pool.
  { id: 'refocus', label: 'Refocus', restoresHp: false, restoresSlots: false, restoresPact: false, recovers: ['short'], recoverStep: 1 },
  // A full night's rest restores HP, spell slots, and daily resources (to max).
  { id: 'rest', label: 'Rest for the Night', restoresHp: true, restoresSlots: true, restoresPact: true, recovers: ['long', 'daily'] },
];

const RANK_BONUS: Record<ProficiencyRank, number> = {
  untrained: 0,
  trained: 2,
  expert: 4,
  master: 6,
  legendary: 8,
};

/**
 * PF2e proficiency adds your level only when trained or better, plus the rank
 * bonus. Untrained = 0 (no level). This is the core math difference from 5e.
 */
export function pf2eProficiency(level: number, rank: ProficiencyRank): number {
  if (rank === 'untrained') return 0;
  const lvl = Math.max(1, Math.floor(level) || 1);
  return lvl + RANK_BONUS[rank];
}

/**
 * Apply a striking rune to a weapon's damage dice by multiplying the dice COUNT
 * (striking = 2×, greater = 3×, major = 4×). "1d8" + striking(2) -> "2d8". Leaves
 * non-"NdM" expressions and the no-rune case untouched.
 */
export function expandStriking(dice: string | undefined, striking?: number): string | undefined {
  const mult = striking && striking > 1 ? Math.floor(striking) : 1;
  if (mult === 1 || !dice) return dice;
  return dice.replace(/(\d+)d(\d+)/gi, (_m, n: string, faces: string) => `${Number(n) * mult}d${faces}`);
}

export const pf2e: RulesSystem = {
  id: 'pf2e',
  label: 'Pathfinder 2e',
  abilities: ABILITY_KEYS,
  abilityLabels: ABILITY_LABELS,
  skills: PF2E_SKILLS,

  abilityModifier,
  defaultAbilityScores,

  proficiencyValue: pf2eProficiency,

  initiativeModifier(input) {
    // Initiative in PF2e is usually a Perception check.
    return abilityModifier(input.abilities.wis) + pf2eProficiency(input.level, input.perceptionRank ?? 'trained');
  },

  skillModifiers(input: CharacterRulesInput): DerivedSkill[] {
    return PF2E_SKILLS.map((s) => {
      const rank = input.skillRanks?.[s.key] ?? 'untrained';
      return {
        key: s.key,
        label: s.label,
        ability: s.ability,
        modifier: abilityModifier(input.abilities[s.ability]) + pf2eProficiency(input.level, rank),
      };
    });
  },

  saveModifiers(input): Record<AbilityKey, number> {
    const out = {} as Record<AbilityKey, number>;
    for (const a of ABILITY_KEYS) {
      // Only CON (Fort), DEX (Ref), WIS (Will) are real saves in PF2e.
      const isSave = a === 'con' || a === 'dex' || a === 'wis';
      const rank = input.saveRanks?.[a] ?? 'untrained';
      out[a] = abilityModifier(input.abilities[a]) + (isSave ? pf2eProficiency(input.level, rank) : 0);
    }
    return out;
  },

  baseArmorClass(input) {
    const dex = abilityModifier(input.abilities.dex);
    const misc = input.armorBonus ?? 0;
    // PF2e AC scales with level: 10 + Dex(capped) + proficiency(level + rank) + item.
    // `armorBonus` now carries only true item/circumstance bonuses (e.g. a shield).
    const prof = pf2eProficiency(input.level, input.acRank ?? 'trained');
    if (input.equippedArmor) {
      const { baseAc, dexCap } = input.equippedArmor;
      // baseAc embeds the armor's flat AC (10 + armor item bonus); add Dex + prof + misc.
      return baseAc + (dexCap === null ? dex : Math.min(dex, dexCap)) + prof + misc;
    }
    return 10 + dex + prof + misc;
  },

  spellSaveDc(input, ability) {
    return 10 + abilityModifier(input.abilities[ability]) + pf2eProficiency(input.level, input.spellcastingRank ?? 'trained');
  },

  spellAttackBonus(input, ability) {
    return abilityModifier(input.abilities[ability]) + pf2eProficiency(input.level, input.spellcastingRank ?? 'trained');
  },

  passivePerception(input) {
    return 10 + abilityModifier(input.abilities.wis) + pf2eProficiency(input.level, input.perceptionRank ?? 'trained');
  },

  weaponAttack(input, weapon) {
    const abilityMod = abilityModifier(input.abilities[weapon.ability]);
    const item = weapon.itemBonus ?? 0;
    // Potency (itemBonus) applies to the attack roll only — never a flat damage bonus.
    const toHit = abilityMod + pf2eProficiency(input.level, weapon.rank) + item;
    const dmgMod = weapon.addAbilityToDamage !== false ? abilityMod : 0;
    // Striking runes add weapon dice (2/3/4×), they do not add a flat bonus.
    const dice = expandStriking(weapon.damageDice, weapon.striking);
    // Multiple Attack Penalty: -5/-10, or -4/-8 with an agile weapon.
    const penalty = weapon.agile ? 4 : 5;
    return {
      toHit,
      damage: formatDamage(dice, dmgMod),
      map: { second: toHit - penalty, third: toHit - penalty * 2 },
    };
  },

  carryingCapacity(input) {
    // PF2e uses Bulk: encumbered at 5 + STR mod, max 10 + STR mod.
    const strMod = abilityModifier(input.abilities.str);
    return { encumbered: 5 + strMod, max: 10 + strMod, unit: 'Bulk' };
  },

  restOptions: PF2E_RESTS,
};
