/**
 * Romance Handlers
 * Handles romantic relationships, dating, and relationship management
 */

import type { PlayerSession } from '../game/PlayerSession.js';
import type { Person } from '../models/Person.js';
import { resolvePayloadId } from './payloadHelpers.js';
import {
  romance,
  establishMatchRelationship,
  dateNight,
  breakUp,
  divorce,
  partnerGift,
  getDateIdeas,
  propose,
  marry,
  getActiveRelationship,
} from '../services/relationships/index.js';
import { createCharacter } from '../services/character/character_manager.js';
import { calculateCompatibility } from '../services/dating/compatibility.js';
import { updateQuestProgress, sendQuestProgress } from '../services/retention/index.js';
import { getAllEventDefinitions, processResolution } from '../services/dating/relationshipEvents.js';
import { deductDiamonds } from '../monetization/diamondEconomy.js';

// ============================================================================
// Payload Normalization
// ============================================================================

/**
 * Extract partnerId from payload.
 * iOS sends payloads in multiple formats:
 *   - DatingView: {"type": "breakUp", "message": "partner-id"} → payload = "partner-id" (string)
 *   - RelationshipDetailView: {"type": "breakUp", "partnerId": "partner-id"} → payload = {partnerId: "partner-id"}
 */
function extractPartnerId(payload: unknown): string | undefined {
  return resolvePayloadId(payload, 'partnerId', 'message');
}

/**
 * Extract date activity name from payload.
 * iOS sends: {"type": "dateNight", "message": "activity-name"} → payload = "activity-name"
 * Or: {"type": "startDate", "activityId": "id", "partnerId": "pid"} → payload = full object
 */
function extractDateActivity(payload: unknown): { ideaName?: string; activityId?: string } {
  if (typeof payload === 'string') {
    return { ideaName: payload };
  }
  if (payload && typeof payload === 'object') {
    const obj = payload as Record<string, unknown>;
    return {
      ideaName: typeof obj.ideaName === 'string' ? obj.ideaName : typeof obj.message === 'string' ? obj.message : undefined,
      activityId: typeof obj.activityId === 'string' ? obj.activityId : undefined,
    };
  }
  return {};
}

/**
 * Resolve the romantic partner's age for the adults-only gate.
 *
 * Prefers an explicit partnerId (gift path) and falls back to the player's active
 * romantic relationship's partner (date-night path). Returns undefined when no
 * partner can be resolved (the underlying service call will then reject anyway).
 */
function resolvePartnerAge(player: PlayerSession['player'], partnerId?: string): number | undefined {
  if (partnerId) {
    const byId = player.r.find((p) => p.id === partnerId);
    if (byId) return byId.ageYears;
  }
  // Defensive: getActiveRelationship may be unavailable in some call contexts;
  // a failure here must never throw (it would block a legitimate adult action).
  // When no partner is resolvable we return undefined and let the underlying
  // service handle the "no partner" case.
  try {
    if (typeof getActiveRelationship !== 'function') return undefined;
    const relationship = getActiveRelationship(player);
    if (relationship) {
      const otherId =
        relationship.person1 === player.c.id ? relationship.person2 : relationship.person1;
      const partner = player.r.find((p) => p.id === otherId);
      if (partner) return partner.ageYears;
    }
  } catch {
    return undefined;
  }
  return undefined;
}

/**
 * Adults-only (18+) content gate for romance actions that escalate a relationship
 * (date night, gifts). Blocks when the player OR the resolved partner is under 18.
 * Mirrors the swipe gate message style. Returns true when the action is blocked.
 */
function blockedByAgeGate(
  session: PlayerSession,
  title: string,
  partnerId?: string
): boolean {
  const player = session.player;
  const partnerAge = resolvePartnerAge(player, partnerId);
  if (player.c.ageYears < 18 || (partnerAge !== undefined && partnerAge < 18)) {
    session.send({
      type: 'error',
      title,
      message: 'Dating unlocks at age 18.',
    });
    return true;
  }
  return false;
}

export async function handleRomance(payload: unknown, session: PlayerSession): Promise<void> {
  const partnerId = extractPartnerId(payload);
  const player = session.player;

  if (!partnerId) {
    session.send({ type: 'error', message: 'Missing partnerId.' });
    return;
  }

  const success = romance(player, partnerId);

  if (success) {
    session.sendPlayerObject();
    session.send({
      type: 'romanceStarted',
      success: true,
      partnerId,
    });
  } else {
    session.send({
      type: 'romanceFailed',
      success: false,
      message: 'Romance attempt failed. The person may not be interested enough.',
    });
  }
}

export async function handleDateNight(payload: unknown, session: PlayerSession): Promise<void> {
  const { ideaName, activityId } = extractDateActivity(payload);
  const player = session.player;

  // Content safety: dating/romance is adults only (18+). Block if either the
  // player or the partner is under 18.
  if (blockedByAgeGate(session, 'Date Night Failed')) {
    return;
  }

  // Look up by name first, then by activityId matching date idea name
  const lookupName = ideaName ?? activityId;
  if (!lookupName) {
    session.send({ type: 'error', title: 'Date Night Failed', message: 'No activity specified.' });
    return;
  }

  const result = dateNight(player, lookupName);

  if (result) {
    session.sendPlayerObject();
    session.send({
      ...result,
      type: 'messageEvent',
    });

    // Update quest progress for going on a date
    const quest = await updateQuestProgress(player.userId, 'go_on_date', 1, player);
    if (quest) {
      sendQuestProgress(session, quest);
    }

    // A date raises the partner's affinity (recordPositiveInteraction). Fire the
    // increase_affinity daily quest here at the handler level (session present),
    // ONCE per successful date. No loop tick advances this type.
    const affinityQuest = await updateQuestProgress(player.userId, 'increase_affinity', 1, player);
    if (affinityQuest) {
      sendQuestProgress(session, affinityQuest);
    }
  } else {
    session.send({
      type: 'error',
      title: 'Date Night Failed',
      message: 'Not enough energy or money for the date night.',
    });
  }
}

export async function handleBreakUp(payload: unknown, session: PlayerSession): Promise<void> {
  const partnerId = extractPartnerId(payload);
  const player = session.player;

  if (!partnerId) {
    session.send({ type: 'error', message: 'Missing partnerId.' });
    return;
  }

  const success = breakUp(player, partnerId);

  if (success) {
    session.sendPlayerObject();
    session.send({
      type: 'relationshipEnded',
      success: true,
      message: 'You have broken up with your partner.',
    });
  } else {
    session.send({
      type: 'error',
      message: 'Failed to break up.',
    });
  }
}

export async function handleDivorce(payload: unknown, session: PlayerSession): Promise<void> {
  const partnerId = extractPartnerId(payload);
  const player = session.player;

  if (!partnerId) {
    session.send({ type: 'error', message: 'Missing partnerId.' });
    return;
  }

  const success = divorce(player, partnerId);

  if (success) {
    session.sendPlayerObject();
    session.send({
      type: 'relationshipEnded',
      success: true,
      message: 'You have filed for divorce.',
    });
  } else {
    session.send({
      type: 'error',
      message: 'Failed to file for divorce.',
    });
  }
}

export async function handlePropose(payload: unknown, session: PlayerSession): Promise<void> {
  const partnerId = extractPartnerId(payload);
  const player = session.player;

  if (!partnerId) {
    session.send({ type: 'error', message: 'Missing partnerId.' });
    return;
  }

  const success = propose(player, partnerId);

  if (success) {
    session.sendPlayerObject();
    session.send({
      type: 'messageEvent',
      id: 'proposal',
      message: 'You proposed and they said yes! You are now engaged.',
      title: 'Engaged!',
    });
  } else {
    session.send({
      type: 'error',
      message: 'Proposal failed. You must be dating someone first.',
    });
  }
}

export async function handleMarry(payload: unknown, session: PlayerSession): Promise<void> {
  const partnerId = extractPartnerId(payload);
  const player = session.player;

  if (!partnerId) {
    session.send({ type: 'error', message: 'Missing partnerId.' });
    return;
  }

  const success = marry(player, partnerId);

  if (success) {
    session.sendPlayerObject();
    session.send({
      type: 'messageEvent',
      id: 'marriage',
      message: 'Congratulations! You are now married.',
      title: 'Married!',
    });
  } else {
    session.send({
      type: 'error',
      message: 'Marriage failed. You must be dating or engaged first.',
    });
  }
}

export async function handlePartnerGift(payload: unknown, session: PlayerSession): Promise<void> {
  const partnerId = extractPartnerId(payload);
  const player = session.player;

  if (!partnerId) {
    session.send({ type: 'error', message: 'Missing partnerId.' });
    return;
  }

  // Content safety: dating/romance is adults only (18+). Block if either the
  // player or the resolved partner is under 18.
  if (blockedByAgeGate(session, 'Gift Failed', partnerId)) {
    return;
  }

  const result = partnerGift(player, partnerId);

  if (result) {
    session.sendPlayerObject();
    session.send({
      ...result,
      type: 'messageEvent',
    });

    // A gift raises the partner's affinity. Fire the increase_affinity daily
    // quest here at the handler level (session present), ONCE per gift. No loop
    // tick advances this type.
    const affinityQuest = await updateQuestProgress(player.userId, 'increase_affinity', 1, player);
    if (affinityQuest) {
      sendQuestProgress(session, affinityQuest);
    }
  } else {
    session.send({
      type: 'error',
      title: 'Gift Failed',
      message: 'Not enough money for the gift.',
    });
  }
}

export async function handleGetSwipeCharacter(
  _payload: unknown,
  session: PlayerSession
): Promise<void> {
  const player = session.player;

  // Content safety: dating/romance is adults only (18+). Do not serve a swipe
  // candidate to an under-18 player (candidate ages are already floored at 18,
  // but a minor player must never be matched with an adult).
  if (player.c.ageYears < 18) {
    session.send({
      type: 'getSwipeCharacter',
      swipeCharacter: false,
      message: 'Dating unlocks at age 18.',
    });
    return;
  }

  // Determine opposite sex for dating
  const sex = player.c.sex === 'Female' ? 'Male' : 'Female';

  // Create a new character for swiping
  const character = createCharacter(player, sex, 'random_adults', 'none');

  // Generate age within 5 years of player
  const ageRangeWidth = 5;
  const lowerBound = Math.max(18, player.c.ageYears - ageRangeWidth);
  const upperBound = player.c.ageYears + ageRangeWidth;
  const randomAge = Math.floor(Math.random() * (upperBound - lowerBound + 1)) + lowerBound;

  character.ageYears = randomAge;

  // Calculate compatibility score between player and swipe character
  // Note: educationLevel is optional - compatibility uses defaults if not set
  const compatibilityScore = calculateCompatibility(
    {
      likes: player.c.likes,
      ageYears: player.c.ageYears,
      prestige: player.c.prestige,
    },
    {
      likes: character.likes,
      ageYears: character.ageYears,
      prestige: character.prestige,
    }
  );
  character.compatibilityScore = compatibilityScore;

  // Store for swipe match
  (player as unknown as { swipeCharacter: typeof character }).swipeCharacter = character;

  console.log(`Sending swipe character: ${character.firstname} ${character.lastname}, compatibility: ${compatibilityScore}%`);

  // Python: {"type": event['type'], 'swipeCharacter': character}
  session.send({
    type: 'getSwipeCharacter',
    swipeCharacter: typeof character.toJSON === 'function' ? character.toJSON() : character,
  });
}

/**
 * Handle swipe match - when the player swipes right on a dating app character
 * Matches Python: handle_swipe_match() in command_dispatcher.py
 */
export async function handleSwipeMatch(_payload: unknown, session: PlayerSession): Promise<void> {
  const player = session.player;

  // Content safety: dating/romance is adults only (18+). A minor player can never
  // form a romantic match, even if a stale swipe character is present.
  if (player.c.ageYears < 18) {
    session.send({ type: 'error', message: 'Dating unlocks at age 18.' });
    return;
  }

  // Get the stored swipe character
  const swipeCharacter = (player as unknown as { swipeCharacter: typeof player.c | false })
    .swipeCharacter;

  if (!swipeCharacter) {
    session.send({ type: 'error', message: 'No character to match with' });
    return;
  }

  // A swipe match means mutual interest. Tag the character and seed affinity so
  // the relationship starts on solid footing.
  swipeCharacter.relationships.push('dating_match');
  (swipeCharacter as unknown as { title: string }).title = 'Dating Match';
  swipeCharacter.affinity += 40;

  // Add the matched character to the player's relationships list.
  player.r.push(swipeCharacter);

  // Promote the match to an active 'Dating' relationship so the client shows the
  // relationship UI (instead of the empty "your love story awaits" state) and
  // stops offering swipes. setRelationshipReferences() (inside this call) swaps the
  // 'dating_match' tag for 'partner' and wires up the scalar `relationship`/`partner`
  // references the client reads. Without this, a match never produced a relData
  // entry, leaving the Dating page stuck in the single state. (Bug 1 root cause.)
  const relationship = establishMatchRelationship(
    player,
    swipeCharacter as unknown as Person
  );

  if (relationship) {
    player.messageQueue.push(
      `You matched with ${swipeCharacter.firstname} ${swipeCharacter.lastname}! You're now dating.`
    );
  } else {
    // Player already has a different active partner — keep them as a match only.
    player.messageQueue.push(
      `You matched with ${swipeCharacter.firstname} ${swipeCharacter.lastname}!`
    );
  }

  // Clear the pending swipe character.
  (player as unknown as { swipeCharacter: false }).swipeCharacter = false;

  // Send updated player state
  session.sendPlayerObject();
}

export async function handleGetDateIdeas(_payload: unknown, session: PlayerSession): Promise<void> {
  const dateIdeas = getDateIdeas();

  // Send dateIdeas array at root level like Python
  session.send({
    type: 'dateIdeas',
    dateIdeas,
  });
}

/**
 * Handle startDate command from iOS DateActivitySelectionView.
 * iOS sends: {"type": "startDate", "activityId": "id", "partnerId": "pid"}
 * Routes to the same dateNight logic.
 */
export async function handleStartDate(payload: unknown, session: PlayerSession): Promise<void> {
  return handleDateNight(payload, session);
}

interface RelationshipEventResponsePayload {
  eventId?: string;
  choiceId?: string;
}

function resolveRelationshipEventResponse(payload: unknown): RelationshipEventResponsePayload {
  if (payload && typeof payload === 'object') {
    return payload as RelationshipEventResponsePayload;
  }
  return {};
}

interface DateMiniGameResponsePayload {
  gameId?: string;
  round?: number;
  responseId?: string;
}

function resolveDateMiniGameResponse(payload: unknown): DateMiniGameResponsePayload {
  if (payload && typeof payload === 'object') {
    return payload as DateMiniGameResponsePayload;
  }
  return {};
}

export async function handleRelationshipEventResponse(
  payload: unknown,
  session: PlayerSession
): Promise<void> {
  const obj = resolveRelationshipEventResponse(payload);
  const eventId = typeof obj.eventId === 'string' ? obj.eventId : undefined;
  const choiceId = typeof obj.choiceId === 'string' ? obj.choiceId : undefined;

  if (!eventId || !choiceId) {
    session.send({
      type: 'error',
      message: 'Invalid relationship event response payload.',
    });
    return;
  }

  // Resolve the event server-side so the diamond cost is actually charged and the
  // affinity reward actually applied (previously this was acknowledge-only, so the
  // premium dating choices were inert — diamonds spent nothing, affinity unchanged).
  //
  // The client's eventId is `relationship_event_<type>_<npcId>` and choiceId is the
  // option's `choice` key (see createRelationshipQuestionEvent).
  const PREFIX = 'relationship_event_';
  if (!eventId.startsWith(PREFIX)) {
    session.send({ type: 'relationshipEventResponseAcknowledged', success: true, eventId, choiceId });
    return;
  }
  const rest = eventId.slice(PREFIX.length);
  const lastUnderscore = rest.lastIndexOf('_');
  const eventType = lastUnderscore >= 0 ? rest.slice(0, lastUnderscore) : rest;
  const npcId = lastUnderscore >= 0 ? rest.slice(lastUnderscore + 1) : '';

  const player = session.player;
  const npc = (player.r ?? []).find((p) => p.id === npcId);
  const def = getAllEventDefinitions().find((e) => e.type === eventType);
  const option = def?.options.find((o) => o.choice === choiceId);

  if (!npc || !def || !option) {
    // Unknown event/npc/choice — acknowledge without charging anything.
    session.send({ type: 'relationshipEventResponseAcknowledged', success: true, eventId, choiceId });
    return;
  }

  const currentDiamonds = player.c.diamonds ?? 0;
  const result = processResolution(
    player.userId,
    npc,
    eventType,
    choiceId,
    option.affinityChange,
    option.diamondCost,
    currentDiamonds
  );

  // processResolution validates affordability + applies affinity but does not
  // deduct; charge the premium cost here against the real balance.
  if (result.success && option.diamondCost > 0) {
    deductDiamonds(player as never, `relationship_${eventType}_${choiceId}`, option.diamondCost);
  }

  session.send({
    type: 'relationshipEventResponseAcknowledged',
    success: result.success,
    eventId,
    choiceId,
    message: result.message,
    affinityChange: result.success ? option.affinityChange : 0,
    diamondCost: result.success ? option.diamondCost : 0,
  });
  session.sendPlayerObject();
}

export async function handleDateMiniGameResponse(
  payload: unknown,
  session: PlayerSession
): Promise<void> {
  const obj = resolveDateMiniGameResponse(payload);
  const gameId = typeof obj.gameId === 'string' ? obj.gameId : undefined;
  const round = typeof obj.round === 'number' ? obj.round : undefined;
  const responseId = typeof obj.responseId === 'string' ? obj.responseId : undefined;

  if (!gameId || round === undefined || !responseId) {
    session.send({
      type: 'error',
      message: 'Invalid date mini-game response payload.',
    });
    return;
  }

  // Date mini-game scoring currently happens on client. Acknowledge to prevent unknown-command fallback.
  session.send({
    type: 'dateMiniGameResponseAcknowledged',
    success: true,
    gameId,
    round,
    responseId,
  });
}
