/**
 * Friendship V2 catalog — the first NPC-BOUND v2 events. Each event binds to a
 * specific eligible friend (selectTarget), names them in the text ({target}),
 * and moves THAT friend's affinity via effects.target. Replaces the dead
 * function-based events/social/friendships.ts (which only the HeadlessGame
 * harness ever parsed) with live, choiceful content.
 *
 * Stakes ported from the highest-value dead events: crisis support, betrayal,
 * lending money, helping a move, and the birthday-gift tradeoff.
 */
import type { BoundTarget, EventDefinition, EventPlayerContext } from '../types.js';
import { playerHasFlag } from '../engine/selector.js';

interface FriendRecord {
  id?: string;
  firstname?: string;
  status?: string;
  affinity?: number;
  relationships?: string[];
}

const FRIEND_TAGS = new Set(['friend', 'best friend', 'bestfriend', 'bff']);

/**
 * Pick a RANDOM living friend within an affinity band. Random (not first)
 * so repeatable events spread across the circle rather than always hitting
 * the same NPC.
 */
function pickFriend(
  player: EventPlayerContext,
  opts: { minAffinity?: number; maxAffinity?: number } = {}
): BoundTarget | null {
  const roster = (player as { r?: FriendRecord[] }).r;
  const candidates = (Array.isArray(roster) ? roster : []).filter((p) => {
    if (!p?.id || p.status !== 'alive') return false;
    if (!(p.relationships ?? []).some((t) => FRIEND_TAGS.has(String(t).toLowerCase()))) return false;
    const affinity = typeof p.affinity === 'number' ? p.affinity : 50;
    if (opts.minAffinity !== undefined && affinity < opts.minAffinity) return false;
    if (opts.maxAffinity !== undefined && affinity > opts.maxAffinity) return false;
    return true;
  });
  if (candidates.length === 0) return null;
  const pick = candidates[Math.floor(Math.random() * candidates.length)];
  return { personId: pick.id as string, name: pick.firstname || 'your friend' };
}

/**
 * Pick the LOWEST-affinity living friend in a band — the amends follow-up
 * targets whoever the confrontation damaged, and after the betrayal's -12
 * that friend is almost always the bottom of the band. (The flag doesn't
 * record who betrayed, so this is the closest faithful binding.)
 */
function pickEstrangedFriend(player: EventPlayerContext): BoundTarget | null {
  const roster = (player as { r?: FriendRecord[] }).r;
  const candidates = (Array.isArray(roster) ? roster : []).filter((p) => {
    if (!p?.id || p.status !== 'alive') return false;
    if (!(p.relationships ?? []).some((t) => FRIEND_TAGS.has(String(t).toLowerCase()))) return false;
    const affinity = typeof p.affinity === 'number' ? p.affinity : 50;
    return affinity >= 5 && affinity <= 60;
  });
  if (candidates.length === 0) return null;
  const pick = candidates.reduce((lowest, p) =>
    (p.affinity ?? 50) < (lowest.affinity ?? 50) ? p : lowest
  );
  return { personId: pick.id as string, name: pick.firstname || 'your friend' };
}

/** In-game days since the betrayal event fired, or undefined if unknown. */
function daysSinceBetrayal(player: EventPlayerContext): number | undefined {
  const lastFired = (player as { eventLastFired?: Record<string, number> }).eventLastFired;
  const fired = lastFired?.['friendship-betrayal'];
  if (typeof fired !== 'number') return undefined;
  const today = Math.floor((player.c as { ageDays?: number }).ageDays ?? 0);
  return today - fired;
}

export const friendshipCatalog: EventDefinition[] = [
  {
    id: 'friendship-friend-in-crisis',
    category: 'friendship',
    prompt:
      '{target} calls you late at night, voice shaking. Things have fallen apart for them — they need someone right now.',
    minAge: 14,
    repeatable: true,
    cooldownDays: 180,
    weight: 2,
    selectTarget: (player) => pickFriend(player, { minAffinity: 30 }),
    choices: [
      {
        choiceId: 'show-up',
        text: 'Drop everything and go over with food ($30)',
        resolutionText:
          'You showed up with takeout and stayed until sunrise. {target} will never forget that you came.',
        effects: {
          resources: { money: -30, energy: -20 },
          stats: { happiness: 3, stress: 5 },
          target: { affinityDelta: 15 },
        },
      },
      {
        choiceId: 'long-call',
        text: 'Stay on the phone as long as they need',
        resolutionText: 'You talked {target} through the worst of it. It mattered.',
        effects: {
          resources: { energy: -10 },
          target: { affinityDelta: 8 },
        },
      },
      {
        choiceId: 'text-support',
        text: 'Send a supportive text — you can talk tomorrow',
        resolutionText: 'You sent what comfort you could. {target} understood, mostly.',
        effects: { target: { affinityDelta: 3 } },
      },
      {
        choiceId: 'space',
        text: "You're exhausted. Let it go to voicemail",
        resolutionText:
          "{target} never brought it up again. But something between you cooled that night.",
        effects: {
          stats: { happiness: -2 },
          target: { affinityDelta: -8 },
        },
      },
    ],
  },
  {
    id: 'friendship-betrayal',
    category: 'friendship',
    prompt:
      'You find out {target} shared something you told them in confidence. People know things only {target} knew.',
    minAge: 14,
    repeatable: true,
    cooldownDays: 365,
    selectTarget: (player) => pickFriend(player, { minAffinity: 30, maxAffinity: 70 }),
    choices: [
      {
        choiceId: 'confront',
        text: 'Confront them directly',
        resolutionText:
          '{target} went pale, then owned it. The honesty hurt both of you — but at least it was honest.',
        effects: {
          stats: { stress: 10 },
          target: { affinityDelta: -12 },
        },
        setFlags: ['friendship_betrayal_confronted'],
      },
      {
        choiceId: 'forgive',
        text: 'Let it go — everyone slips',
        resolutionText:
          "You decided the friendship was worth more than the grudge. Still, you'll be careful what you tell {target} now.",
        effects: {
          stats: { happiness: -3 },
          target: { affinityDelta: -6 },
        },
      },
      {
        choiceId: 'distance',
        text: 'Say nothing, but quietly pull back',
        resolutionText:
          '{target} noticed the distance eventually. Neither of you ever said why.',
        effects: {
          stats: { happiness: -3 },
          target: { affinityDelta: -10 },
        },
      },
    ],
  },
  {
    // Follow-up arc to friendship-betrayal's "confront" branch. Fires once per
    // life, at least 30 in-game days after the betrayal, for players carrying
    // the friendship_betrayal_confronted flag.
    id: 'friendship-betrayal-amends',
    category: 'friendship',
    prompt:
      'Weeks after the confrontation, {target} shows up at your door with a long, hand-written apology. They want to make things right.',
    minAge: 14,
    weight: 2,
    isEligible: (player) => {
      if (!playerHasFlag(player, 'friendship_betrayal_confronted')) return false;
      const elapsed = daysSinceBetrayal(player);
      // Unknown stamp (older saves) -> allow; otherwise require the gap.
      return elapsed === undefined || elapsed >= 30;
    },
    selectTarget: pickEstrangedFriend,
    choices: [
      {
        choiceId: 'accept',
        text: 'Let them back in',
        resolutionText:
          'You talked for hours. The friendship that came out the other side is different — sturdier, like a broken bone healed.',
        effects: {
          stats: { happiness: 3 },
          target: { affinityDelta: 12 },
        },
      },
      {
        choiceId: 'cautious',
        text: 'Accept the apology, but keep your guard up',
        resolutionText:
          "You meant it when you said it was okay. You also meant it when you didn't tell {target} anything important for a while.",
        effects: { target: { affinityDelta: 5 } },
      },
      {
        choiceId: 'burn',
        text: "Tell them it's too late",
        resolutionText:
          '{target} nodded, left the letter, and walked away. Closing the door hurt less than you expected.',
        effects: {
          stats: { happiness: -2, stress: -5 },
          target: { affinityDelta: -20 },
        },
        setFlags: ['friendship_betrayal_bridge_burned'],
      },
    ],
  },
  {
    id: 'friendship-lend-money',
    category: 'friendship',
    prompt:
      "{target} asks to borrow money — they're short this month and out of options. They look embarrassed asking.",
    minAge: 16,
    repeatable: true,
    cooldownDays: 240,
    isEligible: (player) => {
      const money = (player.c as { money?: number }).money;
      return typeof money === 'number' && money >= 150;
    },
    selectTarget: (player) => pickFriend(player, { minAffinity: 40 }),
    choices: [
      {
        choiceId: 'lend-full',
        text: 'Lend them $200, no questions',
        resolutionText:
          '{target} took it with shaking hands. "I owe you." Some of it may come back; the trust already did.',
        effects: {
          resources: { money: -200 },
          target: { affinityDelta: 10 },
        },
      },
      {
        choiceId: 'lend-half',
        text: 'Lend $100 — what you can spare',
        resolutionText: '{target} was grateful for anything. It took the edge off their month.',
        effects: {
          resources: { money: -100 },
          target: { affinityDelta: 5 },
        },
      },
      {
        choiceId: 'decline',
        text: "Apologize — you can't right now",
        resolutionText:
          '{target} said it was fine. The next few hangouts felt a little stiff.',
        effects: { target: { affinityDelta: -6 } },
      },
    ],
  },
  {
    id: 'friendship-help-moving',
    category: 'friendship',
    prompt:
      "{target} is moving apartments this weekend and asks for your help hauling boxes. It's a big job.",
    minAge: 18,
    repeatable: true,
    cooldownDays: 240,
    isEligible: (player) => {
      const energy = (player.c as { energy?: number }).energy;
      return typeof energy === 'number' && energy >= 40;
    },
    selectTarget: (player) => pickFriend(player, { minAffinity: 40 }),
    choices: [
      {
        choiceId: 'help',
        text: 'Show up early with coffee and work all day',
        resolutionText:
          'Sore arms, pizza on moving boxes, and {target} declaring you a saint. Worth it.',
        effects: {
          resources: { energy: -30 },
          stats: { happiness: 2 },
          target: { affinityDelta: 10 },
        },
      },
      {
        choiceId: 'decline',
        text: 'Beg off — your weekend is packed',
        resolutionText:
          "{target} hired movers in the end. They didn't hold it against you. Mostly.",
        effects: { target: { affinityDelta: -6 } },
      },
    ],
  },
  {
    id: 'friendship-birthday-gift',
    category: 'friendship',
    prompt: "It's {target}'s birthday this week. What kind of friend are you going to be?",
    minAge: 10,
    repeatable: true,
    cooldownDays: 120,
    selectTarget: (player) => pickFriend(player),
    choices: [
      {
        choiceId: 'big-gift',
        text: 'Get them something they actually want ($60)',
        resolutionText:
          "{target} couldn't believe you remembered. Best gift they got all year.",
        effects: {
          resources: { money: -60 },
          stats: { happiness: 2 },
          target: { affinityDelta: 10 },
        },
      },
      {
        choiceId: 'small-gift',
        text: 'A small, thoughtful gift ($20)',
        resolutionText: 'Small but right. {target} loved that you got THEM, not just a gift.',
        effects: {
          resources: { money: -20 },
          target: { affinityDelta: 5 },
        },
      },
      {
        choiceId: 'just-call',
        text: 'Call and sing them happy birthday, badly',
        resolutionText: '{target} laughed for a full minute. Free, and somehow perfect.',
        effects: { target: { affinityDelta: 2 } },
      },
      {
        choiceId: 'forget',
        text: '(Do nothing — it slips your mind)',
        resolutionText: 'You remembered three days later. {target} said "no big deal" in the way that means it was.',
        effects: { target: { affinityDelta: -6 } },
      },
    ],
  },
];
