import type { Combatant, Encounter } from '@/lib/schemas/encounter';
import type { SystemId } from '@/lib/rules';
import { tickConditionsEndOfTurn } from '@/lib/mechanics/creatureState';

/**
 * Pure combat-state transitions. Every function returns a NEW encounter and
 * never mutates its input. The turn pointer is tracked by combatant *identity*,
 * so adding/removing/reordering combatants mid-fight never silently changes
 * whose turn it is — the bug cluster (C16–C19) that the old app never fixed.
 */

export function currentCombatant(enc: Encounter): Combatant | undefined {
  return enc.combatants[enc.turnIndex];
}

/** Sort by initiative (desc). JS sort is stable, so ties keep insertion order. */
export function sortByInitiative(combatants: Combatant[]): Combatant[] {
  return [...combatants].sort((a, b) => b.initiative - a.initiative);
}

function indexOfId(combatants: Combatant[], id: string | undefined): number {
  if (!id) return -1;
  return combatants.findIndex((c) => c.id === id);
}

/** Append an event to the encounter log (immutably). */
export function logEvent(enc: Encounter, text: string): Encounter {
  return { ...enc, log: [...(enc.log ?? []), { round: enc.round, text }] };
}

/**
 * Tick a combatant's timed conditions down by one round and drop any that hit 0.
 * Returns the updated combatant and the names of conditions that expired.
 */
export function tickConditions(c: Combatant): { combatant: Combatant; expired: string[] } {
  const expired: string[] = [];
  const conditions = c.conditions.flatMap((cond) => {
    if (cond.duration === undefined) return [cond];
    const remaining = cond.duration - 1;
    if (remaining <= 0) {
      expired.push(cond.name);
      return [];
    }
    return [{ ...cond, duration: remaining }];
  });
  return { combatant: { ...c, conditions }, expired };
}

/** Reorder by initiative and start at the top of the order, round 1. */
export function startEncounter(enc: Encounter): Encounter {
  const started: Encounter = {
    ...enc,
    status: 'active',
    round: enc.combatants.length > 0 ? 1 : 0,
    turnIndex: 0,
    combatants: sortByInitiative(enc.combatants),
  };
  const first = currentCombatant(started);
  return logEvent(logEvent(started, '⚔ Combat started — Round 1'), first ? `${first.name}'s turn` : '');
}

export function endEncounter(enc: Encounter): Encounter {
  return logEvent({ ...enc, status: 'ended' }, '⚑ Combat ended');
}

/**
 * Advance to the next combatant; wrapping past the end increments the round.
 * Ticks the newly-active combatant's timed conditions and logs expirations.
 *
 * When `system` is provided, the combatant whose turn is *ending* also has its
 * end-of-turn condition decay applied (PF2e Frightened drops by 1).
 */
export function nextTurn(enc: Encounter, system?: SystemId): Encounter {
  if (enc.combatants.length === 0) return { ...enc, turnIndex: 0 };

  // End-of-turn auto-decay for the combatant whose turn is ending (pf2e Frightened).
  let from: Encounter = enc;
  if (system) {
    const outgoing = enc.combatants[enc.turnIndex];
    if (outgoing) {
      const decayed = tickConditionsEndOfTurn(system, outgoing.conditions);
      if (decayed !== outgoing.conditions) {
        from = { ...enc, combatants: enc.combatants.map((c, i) => (i === enc.turnIndex ? { ...c, conditions: decayed } : c)) };
      }
    }
  }

  const atEnd = from.turnIndex >= from.combatants.length - 1;
  const turnIndex = atEnd ? 0 : from.turnIndex + 1;
  const round = atEnd ? from.round + 1 : from.round;

  let next: Encounter = { ...from, turnIndex, round };
  if (atEnd) next = logEvent(next, `— Round ${round} —`);

  // Tick the combatant whose turn is now beginning.
  const active = next.combatants[turnIndex];
  if (active) {
    const { combatant, expired } = tickConditions(active);
    const combatants = next.combatants.map((c, i) => (i === turnIndex ? combatant : c));
    next = { ...next, combatants };
    next = logEvent(next, `${combatant.name}'s turn`);
    for (const name of expired) next = logEvent(next, `  ${combatant.name}: ${name} wore off`);
  }
  return next;
}

/** Re-roll/assign initiative for combatants and re-sort, anchoring the current turn. */
export function applyInitiatives(enc: Encounter, rolls: Record<string, number>): Encounter {
  const currentId = currentCombatant(enc)?.id;
  const updated = enc.combatants.map((c) => (c.id in rolls ? { ...c, initiative: rolls[c.id]! } : c));
  const combatants = sortByInitiative(updated);
  const turnIndex = enc.status === 'active' ? Math.max(0, indexOfId(combatants, currentId)) : 0;
  return { ...enc, combatants, turnIndex };
}

/** Step back a turn; wrapping before the start decrements the round (min 1). */
export function previousTurn(enc: Encounter): Encounter {
  if (enc.combatants.length === 0) return { ...enc, turnIndex: 0 };
  const atStart = enc.turnIndex <= 0;
  return {
    ...enc,
    turnIndex: atStart ? enc.combatants.length - 1 : enc.turnIndex - 1,
    round: atStart ? Math.max(1, enc.round - 1) : enc.round,
  };
}

/** Strip a trailing " 3" style number so "Goblin 2" and "Goblin" share a base. */
function baseName(name: string): string {
  return name.replace(/\s+\d+$/, '').trim();
}

/**
 * Disambiguate a new combatant's name against existing ones: the first duplicate
 * renames the original to "Name 1" and the newcomer becomes "Name 2", etc.
 * Returns the (possibly renamed) existing list and the new combatant's name.
 */
function disambiguate(existing: Combatant[], name: string): { combatants: Combatant[]; name: string } {
  const base = baseName(name);
  const sameBase = existing.filter((c) => baseName(c.name) === base);
  if (sameBase.length === 0) return { combatants: existing, name };

  let combatants = existing;
  // If there's exactly one and it's unnumbered, number it "1" so the group reads 1,2,…
  if (sameBase.length === 1 && !/\s+\d+$/.test(sameBase[0]!.name)) {
    const id = sameBase[0]!.id;
    combatants = existing.map((c) => (c.id === id ? { ...c, name: `${base} 1` } : c));
  }
  const maxNum = Math.max(
    1,
    ...sameBase.map((c) => {
      const m = /\s+(\d+)$/.exec(c.name);
      return m ? Number(m[1]) : 0;
    }),
  );
  return { combatants, name: `${base} ${maxNum + 1}` };
}

/**
 * Insert a combatant. Auto-numbers duplicate names (Goblin 1, Goblin 2, …).
 * When the encounter is active, the new combatant is placed by initiative and
 * the turn pointer is corrected so it still points at whose turn it is.
 */
export function addCombatant(enc: Encounter, combatant: Combatant): Encounter {
  const { combatants: renamed, name } = disambiguate(enc.combatants, combatant.name);
  const incoming = name === combatant.name ? combatant : { ...combatant, name };

  if (enc.status !== 'active') {
    return { ...enc, combatants: [...renamed, incoming] };
  }
  const currentId = currentCombatant(enc)?.id;
  const combatants = sortByInitiative([...renamed, incoming]);
  const turnIndex = Math.max(0, indexOfId(combatants, currentId));
  return { ...enc, combatants, turnIndex };
}

/** Remove a combatant, keeping the turn on the correct next combatant. */
export function removeCombatant(enc: Encounter, id: string): Encounter {
  const removedIndex = indexOfId(enc.combatants, id);
  if (removedIndex === -1) return enc;

  const combatants = enc.combatants.filter((c) => c.id !== id);
  if (combatants.length === 0) return { ...enc, combatants, turnIndex: 0 };

  let turnIndex = enc.turnIndex;
  let round = enc.round;
  if (removedIndex < enc.turnIndex) {
    // someone before the current turn left: shift pointer left
    turnIndex = enc.turnIndex - 1;
  } else if (removedIndex === enc.turnIndex) {
    // current combatant left: turn passes to whoever now sits at this index
    if (turnIndex >= combatants.length) {
      turnIndex = 0;
      if (enc.status === 'active') round += 1;
    }
  }
  return { ...enc, combatants, turnIndex, round };
}

/** Patch a combatant by id. Re-sorts (and re-anchors the turn) if initiative changed. */
export function updateCombatant(enc: Encounter, id: string, patch: Partial<Combatant>): Encounter {
  const idx = indexOfId(enc.combatants, id);
  if (idx === -1) return enc;
  const updated = enc.combatants.map((c) => (c.id === id ? { ...c, ...patch } : c));

  if (patch.initiative !== undefined && enc.status === 'active') {
    const currentId = currentCombatant(enc)?.id;
    const combatants = sortByInitiative(updated);
    return { ...enc, combatants, turnIndex: Math.max(0, indexOfId(combatants, currentId)) };
  }
  return { ...enc, combatants: updated };
}

/** Manually nudge a combatant up/down the order, keeping the turn anchored. */
export function moveCombatant(enc: Encounter, id: string, direction: -1 | 1): Encounter {
  const idx = indexOfId(enc.combatants, id);
  if (idx === -1) return enc;
  const target = idx + direction;
  if (target < 0 || target >= enc.combatants.length) return enc;

  const currentId = currentCombatant(enc)?.id;
  const combatants = [...enc.combatants];
  [combatants[idx], combatants[target]] = [combatants[target]!, combatants[idx]!];
  return { ...enc, combatants, turnIndex: Math.max(0, indexOfId(combatants, currentId)) };
}

// ---------------- HP transitions ----------------

/**
 * Apply damage of an optional type. Resistances are consulted first (immune → 0,
 * resistant → halved and rounded down, vulnerable → doubled), then temporary HP
 * absorbs, then current HP (which may go negative). Passing no `type` reproduces
 * the original typeless behaviour exactly.
 */
export function applyDamage(c: Combatant, amount: number, type?: string): Combatant {
  if (!Number.isFinite(amount) || amount <= 0) return c;

  let dmg = amount;
  const def = c.damageDefenses;
  if (type && def) {
    if (def.immune.includes(type)) return c; // immune — no change
    // 5e: halve / double
    if (def.vulnerable.includes(type)) dmg = amount * 2;
    else if (def.resist.includes(type)) dmg = Math.floor(amount / 2);
    // pf2e: flat weakness adds, then flat resistance subtracts ('all' matches any type)
    const wk = def.weakness?.find((w) => w.type === type || w.type === 'all');
    if (wk) dmg += wk.amount;
    const rs = def.resistFlat?.find((r) => r.type === type || r.type === 'all');
    if (rs) dmg = Math.max(0, dmg - rs.amount);
  }
  if (dmg <= 0) return c; // fully resisted / immune — no change

  const absorbed = Math.min(c.hp.temp, dmg);
  const remaining = dmg - absorbed;
  return {
    ...c,
    hp: { ...c.hp, temp: c.hp.temp - absorbed, current: c.hp.current - remaining },
  };
}

/**
 * 5e instant death: a creature dropped to 0 HP dies outright if the leftover damage
 * equals or exceeds its HP maximum (no death saves). `currentAfter` is hp.current
 * after the hit (may be negative); the overflow below 0 is the leftover damage.
 */
export function isMassiveDamageDeath(hpMax: number, currentAfter: number): boolean {
  return currentAfter <= 0 && -currentAfter >= hpMax;
}

/** Heal up to max. Does not touch temporary HP. */
export function applyHealing(c: Combatant, amount: number): Combatant {
  if (!Number.isFinite(amount) || amount <= 0) return c;
  return { ...c, hp: { ...c.hp, current: Math.min(c.hp.max, c.hp.current + amount) } };
}

/** Set temporary HP — 5e/PF2e temp HP does not stack; take the higher value. */
export function setTempHp(c: Combatant, temp: number): Combatant {
  if (!Number.isFinite(temp) || temp < 0) return c;
  return { ...c, hp: { ...c.hp, temp: Math.max(c.hp.temp, temp) } };
}
