import type { EventEffects, ResolvedRelationshipEffect } from '../types.js';
import { STAT_BOUNDS, clamp } from '../../../utils/statUtils.js';

/** A relationship/NPC record on the player (player.r entries). */
interface RelationshipRecord {
  id?: string;
  firstname?: string;
  lastname?: string;
  affinity?: number;
  [key: string]: unknown;
}

interface StatPlayer {
  c: Record<string, unknown>;
  /** Full person objects for the player's relationships (Player.r). */
  r?: RelationshipRecord[];
}

function asNumber(value: unknown, fallback = 0): number {
  return typeof value === 'number' ? value : fallback;
}

/** Human-readable name for an NPC record, e.g. "Mom" or "Jane Doe". */
function relationshipDisplayName(record: RelationshipRecord): string {
  const first = typeof record.firstname === 'string' ? record.firstname.trim() : '';
  const last = typeof record.lastname === 'string' ? record.lastname.trim() : '';
  const full = [first, last].filter(Boolean).join(' ');
  return full || 'Someone';
}

/**
 * Apply a choice's effects to the player and return the affinity changes that
 * were actually applied, attributed to specific named characters. The returned
 * array is surfaced in the event_resolved payload so the client can show
 * "Mom +5 affinity" on the decision-confirmation screen.
 */
export function applyEventEffects(
  player: StatPlayer,
  effects?: EventEffects
): ResolvedRelationshipEffect[] {
  if (!effects?.resources && !effects?.stats && !effects?.relationships) {
    return [];
  }

  if (effects.resources) {
    const c = player.c;
    if (effects.resources.energy !== undefined) {
      c.energy = Math.max(0, Math.min(100, asNumber(c.energy, 0) + effects.resources.energy));
    }
    if (effects.resources.money !== undefined) {
      c.money = asNumber(c.money, 0) + effects.resources.money;
    }
    if (effects.resources.diamonds !== undefined) {
      // Diamonds are the premium/IAP currency and must NEVER go negative.
      // A negative delta is a SPEND (e.g. choice.diamondCost): gate it by
      // affordability so an unaffordable choice no-ops the diamond portion
      // instead of driving the balance below zero. (The choice's non-diamond
      // effects — stats, energy, money, relationships — still apply; only the
      // diamond spend is gated.) A positive delta is a grant/reward and is
      // always applied. Either way the result is clamped at >= 0 to mirror the
      // never-negative invariant enforced by diamondEconomy.writeBalance.
      const currentDiamonds = asNumber(c.diamonds, 0);
      const delta = effects.resources.diamonds;
      const isUnaffordableSpend = delta < 0 && currentDiamonds + delta < 0;
      if (!isUnaffordableSpend) {
        c.diamonds = Math.max(0, currentDiamonds + delta);
      }
      // else: no-op the diamond spend — player can't afford it, balance unchanged.
    }
  }

  if (effects.stats) {
    for (const [key, delta] of Object.entries(effects.stats)) {
      const current = asNumber(player.c[key], 0);
      const next = current + delta;
      // Clamp known stats to their centralized STAT_BOUNDS range so generic
      // effects can't push e.g. happiness above 100 or below 0. Unknown keys
      // (not in STAT_BOUNDS) retain the prior unclamped behavior.
      const bounds = (STAT_BOUNDS as Record<string, { min: number; max: number } | undefined>)[key];
      player.c[key] = bounds ? clamp(next, bounds.min, bounds.max) : next;
    }
  }

  const applied: ResolvedRelationshipEffect[] = [];
  if (effects.relationships && effects.relationships.length > 0) {
    const relationships = Array.isArray(player.r) ? player.r : [];
    for (const change of effects.relationships) {
      if (!change.personId || !Number.isFinite(change.affinityDelta) || change.affinityDelta === 0) {
        continue;
      }
      const target = relationships.find((person) => person?.id === change.personId);
      if (!target) {
        continue;
      }
      // Affinity is clamped to -100..100 (matches the relationship system).
      const current = asNumber(target.affinity, 50);
      target.affinity = Math.max(-100, Math.min(100, current + change.affinityDelta));
      applied.push({
        personId: change.personId,
        name: relationshipDisplayName(target),
        affinityDelta: change.affinityDelta,
      });
    }
  }

  return applied;
}
