/**
 * Friendship V2 — a real friendship SYSTEM (tiers, neglect, payoffs, beats).
 *
 * Romance has a legible progression (Prospect→Dating→Engaged→Married, stored in
 * player.relData). Friendship was just an affinity number that drifted. This
 * manager gives friend-tagged NPCs (player.r) a stored tier with hysteresis,
 * interaction-gated neglect drift/fade, tier-gated payoffs (close-friend
 * happiness buffer, best-friend bailout), and the tier-aware weekly beat that
 * supersedes the friend half of T9's processWeeklyFriendEvents.
 *
 * Runs from BOTH weekly ticks: online PlayerSession.processWeekTick and offline
 * GameEngine.handleWeeklyUpdates — keep the two call sites mirrored.
 *
 * Day basis: player.c.ageDays (monotonic; same basis as v2 event cooldowns).
 */
import { Player } from '../../models/Player.js';
import { Person } from '../../models/Person.js';
import { createMessageEvent, EventResult } from '../../events/base.js';
import { getActiveRelationship } from './relationship_manager.js';
import { clamp } from '../../utils/statUtils.js';

export type FriendshipTier = 'acquaintance' | 'friend' | 'close' | 'best';

/** Tags that mark an NPC as a (non-family) friend. Family beats stay in T9. */
export const FRIENDSHIP_TAGS = new Set(['friend', 'best friend', 'bestfriend', 'bff']);

// Promote thresholds (affinity >=) and demote thresholds (affinity <) per tier.
const PROMOTE = { friend: 30, close: 60, best: 85 } as const;
const DEMOTE = { friend: 20, close: 50, best: 75 } as const;

export const NEGLECT_DRIFT_AFTER_DAYS = 28;
export const NEGLECT_DRIFT_PER_WEEK = 2;
export const FADE_AFTER_DAYS = 84;
export const BAILOUT_COOLDOWN_DAYS = 365;
export const BAILOUT_MAX = 600;

export function isFriendNpc(person: Person): boolean {
  return (
    person.status === 'alive' &&
    (person.relationships ?? []).some((t) => FRIENDSHIP_TAGS.has(t.toLowerCase()))
  );
}

/**
 * Tier from affinity with hysteresis: promote at PROMOTE thresholds, but once
 * in a tier, only demote when affinity falls below the (lower) DEMOTE
 * threshold. `bestSlotTaken` enforces the one-best-friend cap: a candidate
 * can't be PROMOTED into 'best' while another living friend holds it (an NPC
 * already 'best' keeps it subject to its own demote threshold).
 */
export function computeTier(
  affinity: number,
  current: FriendshipTier | undefined,
  bestSlotTaken: boolean
): FriendshipTier {
  if (current === 'best') {
    if (affinity >= DEMOTE.best) return 'best';
    return affinity >= DEMOTE.close ? 'close' : affinity >= DEMOTE.friend ? 'friend' : 'acquaintance';
  }
  if (affinity >= PROMOTE.best && !bestSlotTaken) return 'best';
  if (current === 'close') {
    if (affinity >= DEMOTE.close) return 'close';
    return affinity >= DEMOTE.friend ? 'friend' : 'acquaintance';
  }
  if (affinity >= PROMOTE.close) return 'close';
  if (current === 'friend') {
    return affinity >= DEMOTE.friend ? 'friend' : 'acquaintance';
  }
  return affinity >= PROMOTE.friend ? 'friend' : 'acquaintance';
}

const TIER_LABEL: Record<FriendshipTier, string> = {
  acquaintance: 'acquaintances',
  friend: 'friends',
  close: 'close friends',
  best: 'best friends',
};

const TIER_RANK: Record<FriendshipTier, number> = {
  acquaintance: 0,
  friend: 1,
  close: 2,
  best: 3,
};

/** Stamp a positive interaction (resets neglect). Exported for hangOut + v2 effects. */
export function recordFriendInteraction(player: Player, person: Person): void {
  person.lastFriendInteractionDay = Math.floor(player.c.ageDays ?? 0);
}

function currentDay(player: Player): number {
  return Math.floor(player.c.ageDays ?? 0);
}

/**
 * The weekly friendship tick. Order: normalize -> neglect/fade -> tier
 * transitions -> passives -> bailout -> beat. Returns message EventResults for
 * the caller to drain onto player.messageQueue (same contract as
 * processWeeklyRelationshipEvents / processWeeklyFriendEvents).
 */
export function processWeeklyFriendshipTick(player: Player): EventResult[] {
  const events: EventResult[] = [];
  const today = currentDay(player);

  const activeRel = getActiveRelationship(player);
  const partnerId = activeRel
    ? (activeRel.person1 === player.c.id ? activeRel.person2 : activeRel.person1)
    : undefined;

  const friends = (player.r ?? []).filter((p) => p.id !== partnerId && isFriendNpc(p));
  if (friends.length === 0) return events;

  // ── 1. Normalize old saves: derive tier silently, init interaction day. ──
  for (const f of friends) {
    if (f.lastFriendInteractionDay === undefined) {
      f.lastFriendInteractionDay = today;
    }
    if (f.friendshipTier === undefined) {
      const bestTaken = friends.some((o) => o !== f && o.friendshipTier === 'best');
      f.friendshipTier = computeTier(f.affinity ?? 50, undefined, bestTaken);
    }
  }

  // ── 2+3. Neglect drift and fade. ──
  for (const f of [...friends]) {
    const idle = today - (f.lastFriendInteractionDay ?? today);
    if (idle > NEGLECT_DRIFT_AFTER_DAYS) {
      f.affinity = clamp((f.affinity ?? 50) - NEGLECT_DRIFT_PER_WEEK, -100, 100);
    }
    if ((f.affinity ?? 50) <= 0 && idle > FADE_AFTER_DAYS) {
      f.relationships = (f.relationships ?? []).filter((t) => !FRIENDSHIP_TAGS.has(t.toLowerCase()));
      f.friendshipTier = undefined;
      const fade = createMessageEvent(
        `friendship_faded_${f.id}`,
        `You and ${f.firstname} have drifted apart for good. The friendship has faded.`,
        player,
        true,
        { title: 'Friendship' }
      );
      if (fade) events.push(fade);
    }
  }
  const remaining = friends.filter((f) => isFriendNpc(f));

  // ── 4. Tier transitions (with messages). ──
  for (const f of remaining) {
    const before = f.friendshipTier;
    const bestTaken = remaining.some((o) => o !== f && o.friendshipTier === 'best');
    const after = computeTier(f.affinity ?? 50, before, bestTaken);
    if (after !== before) {
      f.friendshipTier = after;
      const up = TIER_RANK[after] > TIER_RANK[before ?? 'acquaintance'];
      const message = up
        ? after === 'best'
          ? `You and ${f.firstname} are officially best friends. Some people just get you.`
          : `You and ${f.firstname} have become ${TIER_LABEL[after]}.`
        : `You and ${f.firstname} aren't as close as you used to be. You're more like ${TIER_LABEL[after]} now.`;
      if (up && after === 'best') {
        player.c.happiness = clamp((player.c.happiness ?? 50) + 5, 0, 100);
      }
      const evt = createMessageEvent(`friendship_tier_${after}_${f.id}`, message, player, true, {
        title: 'Friendship',
      });
      if (evt) events.push(evt);
    }
  }

  // ── 5. Tier passives: close friends buffer happiness; a best friend eases stress. ──
  const closeCount = remaining.filter((f) => f.friendshipTier === 'close').length;
  const best = remaining.find((f) => f.friendshipTier === 'best');
  const happinessBuffer = Math.min(closeCount, 2) + (best ? 1 : 0);
  if (happinessBuffer > 0) {
    player.c.happiness = clamp((player.c.happiness ?? 50) + happinessBuffer, 0, 100);
  }
  if (best) {
    player.c.stress = clamp((player.c.stress ?? 50) - 2, 0, 100);
  }

  // ── 6. Best-friend bailout: once per in-game year, when broke. ──
  const lastBailout = player.lastFriendBailoutDay;
  if (
    best &&
    (best.affinity ?? 50) >= DEMOTE.best &&
    (player.c.money ?? 0) < 0 &&
    (lastBailout === undefined || today - lastBailout >= BAILOUT_COOLDOWN_DAYS)
  ) {
    const gift = Math.min(BAILOUT_MAX, -(player.c.money ?? 0) + 200);
    player.c.money = (player.c.money ?? 0) + gift;
    player.c.happiness = clamp((player.c.happiness ?? 50) + 3, 0, 100);
    best.affinity = clamp((best.affinity ?? 50) + 3, -100, 100);
    recordFriendInteraction(player, best);
    player.lastFriendBailoutDay = today;
    const evt = createMessageEvent(
      `friendship_bailout_${best.id}_${today}`,
      `${best.firstname} noticed you were struggling and quietly covered you with $${gift}. "Pay me back in good company."`,
      player,
      true,
      { title: 'Best Friend', moneyCost: -gift }
    );
    if (evt) events.push(evt);
  }

  // ── 7. Tier-aware weekly beat (supersedes T9's friend half). ──
  const beat = rollWeeklyBeat(player, remaining);
  if (beat) events.push(beat);

  return events;
}

interface FriendBeat {
  name: string;
  message: string;
  affinityChange: number;
}

const CASUAL_BEATS: FriendBeat[] = [
  { name: 'caught_up', message: 'You caught up with {name} over coffee. Good to reconnect.', affinityChange: 3 },
  { name: 'helped_out', message: 'You helped {name} through a busy week. They really appreciated it.', affinityChange: 4 },
  { name: 'hobby_together', message: 'You and {name} spent an afternoon on a shared hobby.', affinityChange: 3 },
  { name: 'lost_touch', message: "You realized you haven't talked to {name} in a while - you've drifted a little.", affinityChange: -2 },
  { name: 'small_disagreement', message: 'You and {name} had a small disagreement, but talked it through.', affinityChange: -1 },
];

const CLOSE_BEATS: FriendBeat[] = [
  { name: 'deep_talk', message: 'You and {name} stayed up late talking about everything and nothing. The kind of talk you only have with a close friend.', affinityChange: 5 },
  { name: 'showed_up', message: '{name} showed up for you this week without being asked. They just knew.', affinityChange: 5 },
  { name: 'inside_joke', message: 'Something happened that only {name} would find funny. You sent it to them immediately.', affinityChange: 4 },
  { name: 'tradition', message: 'You and {name} kept up your little tradition this week. These things add up.', affinityChange: 4 },
  { name: 'honest_callout', message: '{name} called you out on something, honestly and kindly. It stung, but they were right.', affinityChange: 3 },
];

/** One beat for one random friend per week, tier-aware in both odds and text. */
function rollWeeklyBeat(player: Player, friends: Person[]): EventResult | null {
  if (friends.length === 0) return null;
  const hasInner = friends.some((f) => f.friendshipTier === 'close' || f.friendshipTier === 'best');
  const chance = hasInner ? 0.18 : 0.1;
  if (Math.random() > chance) return null;

  const friend = friends[Math.floor(Math.random() * friends.length)];
  const inner = friend.friendshipTier === 'close' || friend.friendshipTier === 'best';
  const pool = inner ? CLOSE_BEATS : CASUAL_BEATS;
  const beat = pool[Math.floor(Math.random() * pool.length)];
  const bonus = inner && beat.affinityChange > 0 ? 1 : 0;
  const delta = beat.affinityChange + bonus;

  friend.affinity = clamp((friend.affinity ?? 50) + delta, -100, 100);
  if (delta > 0) recordFriendInteraction(player, friend);

  return createMessageEvent(
    `friend_${beat.name}_${friend.id}`,
    beat.message.replace('{name}', friend.firstname || 'a friend'),
    player,
    true,
    { title: 'Friendship', affinityChange: delta }
  );
}
