/**
 * Tool Processor for AI Conversation Tools
 *
 * Processes tool calls from AI responses and converts them into:
 * - Immediate effects (affinity changes, mood updates)
 * - Pending game events (activities, dates, emotional moments)
 * - Stat changes for player/character
 *
 * Extensibility:
 * - Add new tool handlers by creating a process* function
 * - Register in the processToolCall switch statement
 * - Define effects in ToolCallResult interface
 */

import { Player, Person, PendingConversationEvent, PendingEventType } from '../../models/index.js';
import {
  toolMetadata,
  recordToolUse,
  MoodType,
  FeelingType,
  ActivityType,
  DateType,
  NewsType,
} from './tools.js';
import { evaluateFavor } from '../../services/relationships/favors.js';
import { romance, updateAffinity } from '../../services/relationships/relationship_manager.js';
import { recordPlayerChoiceMemory } from './character_memory.js';

// Re-export types for convenience
export type { PendingConversationEvent, PendingEventType };

/**
 * Announcement to show as an official game popup
 */
export interface ToolAnnouncement {
  /** Title for the announcement popup */
  title: string;
  /** Message body */
  message: string;
  /** Optional image URL */
  image?: string;
  /** Optional category for styling */
  category?: 'social' | 'romantic' | 'gift' | 'activity' | 'emotional';
}

/**
 * Result of processing a tool call
 */
export interface ToolCallResult {
  /** The message text to display */
  message: string;
  /** Sentiment for affinity calculation */
  sentiment: 'positive' | 'negative' | 'neutral';
  /** Character's mood during this message */
  mood?: MoodType;
  /** Direct affinity change (applied immediately) */
  affinityChange?: number;
  /** Direct familiarity change */
  familiarityChange?: number;
  /** Stat changes for player character */
  playerStatChanges?: Partial<{
    social: number;
    happiness: number;
    stress: number;
    energy: number;
    money: number;
  }>;
  /** Event to queue for game loop processing */
  pendingEvent?: PendingConversationEvent;
  /** Additional data to store on the message */
  messageData?: Record<string, unknown>;
  /** Whether this tool use should be recorded for cooldown */
  recordForCooldown?: boolean;
  /** Announcement to show as an official game popup/notification */
  announcement?: ToolAnnouncement;
}

/**
 * Context passed to tool handlers
 */
interface ToolContext {
  character: Person;
  player: Player;
  currentGameHour: number;
  isRomantic: boolean;
  relationshipTypes: string[];
}

// ============================================================
// Main Processor
// ============================================================

/**
 * Process a tool call from the AI and return the result
 */
export function processToolCall(
  toolName: string,
  args: Record<string, unknown>,
  character: Person,
  player: Player
): ToolCallResult {
  // Build context
  const relationshipTypes = character.relationships ?? [];
  const romanticRelationships = [
    'dating', 'dating_match', 'partner', 'boyfriend', 'girlfriend',
    'spouse', 'wife', 'husband', 'fiancé', 'fiancée', 'engaged'
  ];
  const isRomantic = relationshipTypes.some(rel =>
    romanticRelationships.includes(rel.toLowerCase())
  );

  const context: ToolContext = {
    character,
    player,
    currentGameHour: calculateGameHour(player),
    isRomantic,
    relationshipTypes,
  };

  // Route to appropriate handler
  switch (toolName) {
    case 'send_message':
      return processSendMessage(args, context);

    case 'suggest_activity':
      return processSuggestActivity(args, context);

    case 'express_feeling':
      return processExpressFeeling(args, context);

    case 'ask_for_date':
      return processAskForDate(args, context);

    case 'share_news':
      return processShareNews(args, context);

    case 'request_favor':
      return processRequestFavor(args, context);

    case 'ask_favor':
      return processAskFavor(args, context);

    case 'give_gift':
      return processGiveGift(args, context);

    case 'confess_feelings':
      return processConfessFeelings(args, context);

    case 'ask_to_be_official':
      return processAskToBeOfficial(args, context);

    case 'accept_relationship':
      return processAcceptRelationship(args, context);

    case 'accept_date':
      return processAcceptDate(args, context);

    case 'accept_activity':
      return processAcceptActivity(args, context);

    case 'accept_confession':
      return processAcceptConfession(args, context);

    case 'initiate_breakup':
      return processInitiateBreakup(args, context);

    case 'accept_breakup':
      return processAcceptBreakup(args, context);

    default:
      // Unknown tool - treat as basic message
      console.log(`Unknown tool: ${toolName}, treating as send_message`);
      return {
        message: (args.message as string) ?? '',
        sentiment: (args.sentiment as 'positive' | 'negative' | 'neutral') ?? 'neutral',
      };
  }
}

// ============================================================
// Tool Handlers
// ============================================================

function processSendMessage(
  args: Record<string, unknown>,
  context: ToolContext
): ToolCallResult {
  const sentiment = args.sentiment as 'positive' | 'negative' | 'neutral';
  const mood = args.mood as MoodType | undefined;

  // Use AI-provided dynamic affinity delta, clamped to -50/+30 range.
  // Falls back to sentiment-based ±5 only if AI didn't provide a value.
  const rawDelta = typeof args.affinityDelta === 'number' ? args.affinityDelta
    : sentiment === 'positive' ? 5 : sentiment === 'negative' ? -5 : 0;
  const affinityChange = Math.max(-50, Math.min(30, rawDelta));

  if (typeof args.affinityDelta === 'number') {
    console.log(`Tool affinityDelta: AI scored ${args.affinityDelta}, clamped to ${affinityChange}`);
  }

  return {
    message: args.message as string,
    sentiment,
    mood,
    affinityChange,
    familiarityChange: 1,
    messageData: mood ? { mood } : undefined,
  };
}

function processSuggestActivity(
  args: Record<string, unknown>,
  context: ToolContext
): ToolCallResult {
  const activityType = args.activity_type as ActivityType;
  const urgency = (args.urgency as string) ?? 'sometime';
  const reason = args.reason as string | undefined;

  // Calculate expiration based on urgency
  const expirationHours = urgency === 'now' ? 4 :
                          urgency === 'soon' ? 24 : 72;

  // Higher affinity boost for closer relationships
  const baseAffinity = context.isRomantic ? 8 : 5;

  const eventId = `activity_${context.character.id}_${Date.now()}`;

  // Format activity type for display
  const activityDisplayName = activityType.replace(/_/g, ' ');

  return {
    message: args.message as string,
    sentiment: args.sentiment as 'positive' | 'negative' | 'neutral',
    mood: 'excited',
    affinityChange: baseAffinity,
    familiarityChange: 2,
    pendingEvent: {
      id: eventId,
      type: 'activity_invite',
      characterId: context.character.id,
      characterName: context.character.firstname,
      data: {
        activityType,
        urgency,
        reason,
        costs: getActivityCosts(activityType),
      },
      createdAt: new Date().toISOString(),
      expiresAt: calculateExpiration(context.player, expirationHours),
      priority: urgency === 'now' ? 'high' : 'medium',
    },
    announcement: {
      title: 'Activity Invite',
      message: `${context.character.firstname} wants to ${activityDisplayName} with you!`,
      category: 'activity',
    },
    recordForCooldown: true,
  };
}

function processExpressFeeling(
  args: Record<string, unknown>,
  context: ToolContext
): ToolCallResult {
  const feelingType = args.feeling_type as FeelingType;
  const intensity = (args.intensity as string) ?? 'moderate';
  const aboutPlayer = args.about_player as boolean ?? true;

  // Emotional moments have bigger affinity swings
  const intensityMultiplier = intensity === 'intense' ? 2 :
                              intensity === 'subtle' ? 0.5 : 1;

  // Positive feelings boost, negative may not (depends on context)
  const positiveFeelings = ['love', 'gratitude', 'excitement', 'pride'];
  const vulnerableFeelings = ['fear', 'hurt', 'longing', 'disappointment'];

  let baseAffinity = 5;
  if (positiveFeelings.includes(feelingType) && aboutPlayer) {
    baseAffinity = 10;
  } else if (vulnerableFeelings.includes(feelingType)) {
    // Vulnerability builds trust
    baseAffinity = 7;
  } else if (feelingType === 'jealousy') {
    baseAffinity = -3; // Jealousy can be negative
  }

  // Mood mapping
  const moodMap: Record<string, MoodType> = {
    love: 'flirty',
    gratitude: 'happy',
    excitement: 'excited',
    fear: 'worried',
    hurt: 'sad',
    jealousy: 'angry',
    longing: 'sad',
    concern: 'worried',
    pride: 'happy',
    disappointment: 'sad',
  };

  const eventId = `emotion_${context.character.id}_${Date.now()}`;

  // Format feeling for display
  const feelingDisplay = feelingType.charAt(0).toUpperCase() + feelingType.slice(1);
  const intensityPrefix = intensity === 'intense' ? 'deeply ' : intensity === 'subtle' ? 'quietly ' : '';

  return {
    message: args.message as string,
    sentiment: args.sentiment as 'positive' | 'negative' | 'neutral',
    mood: moodMap[feelingType] ?? 'neutral',
    affinityChange: Math.round(baseAffinity * intensityMultiplier),
    familiarityChange: 3, // Emotional moments build familiarity
    playerStatChanges: {
      social: 2, // Player gains social XP from emotional conversations
    },
    pendingEvent: {
      id: eventId,
      type: 'emotional_moment',
      characterId: context.character.id,
      characterName: context.character.firstname,
      data: {
        feelingType,
        intensity,
        aboutPlayer,
        // Better response options if player has high social stat
        bonusOptions: (context.player.c.social ?? 50) > 70,
      },
      createdAt: new Date().toISOString(),
      priority: intensity === 'intense' ? 'high' : 'medium',
    },
    announcement: {
      title: 'Emotional Moment',
      message: `${context.character.firstname} is ${intensityPrefix}sharing their ${feelingDisplay.toLowerCase()}...`,
      category: 'emotional',
    },
    recordForCooldown: true,
  };
}

function processAskForDate(
  args: Record<string, unknown>,
  context: ToolContext
): ToolCallResult {
  const dateType = args.date_type as DateType;
  const venue = args.suggested_venue as string | undefined;

  // Date costs vary by type
  const dateCosts: Record<string, { energy: number; money: number }> = {
    casual: { energy: 20, money: 20 },
    romantic: { energy: 25, money: 50 },
    adventurous: { energy: 35, money: 35 },
    cozy: { energy: 15, money: 15 },
    fancy: { energy: 30, money: 100 },
  };

  const costs = dateCosts[dateType] ?? { energy: 25, money: 30 };
  const eventId = `date_${context.character.id}_${Date.now()}`;

  // Format date type for display
  const dateDisplayName = dateType.charAt(0).toUpperCase() + dateType.slice(1);

  return {
    message: args.message as string,
    sentiment: 'positive',
    mood: 'flirty',
    affinityChange: 10,
    familiarityChange: 2,
    pendingEvent: {
      id: eventId,
      type: 'date_request',
      characterId: context.character.id,
      characterName: context.character.firstname,
      data: {
        dateType,
        venue,
        costs,
      },
      createdAt: new Date().toISOString(),
      expiresAt: calculateExpiration(context.player, 48),
      priority: 'high',
    },
    announcement: {
      title: 'Date Request',
      message: `${context.character.firstname} is asking you on a ${dateDisplayName.toLowerCase()} date!`,
      category: 'romantic',
    },
    recordForCooldown: true,
  };
}

function processShareNews(
  args: Record<string, unknown>,
  context: ToolContext
): ToolCallResult {
  const newsType = args.news_type as NewsType;
  const wantsAdvice = args.wants_advice as boolean ?? false;
  const isSecret = args.is_secret as boolean ?? false;

  // Mood based on news type
  const moodMap: Record<string, MoodType> = {
    achievement: 'excited',
    problem: 'worried',
    change: 'nervous',
    gossip: 'playful',
    random: 'neutral',
    career: 'excited',
    health: 'worried',
  };

  // Affinity based on news type and sharing context
  let affinityChange = 3;
  if (isSecret) {
    affinityChange = 8; // Trusting with secrets builds relationship
  } else if (newsType === 'achievement') {
    affinityChange = 5; // Sharing good news
  } else if (newsType === 'problem' && wantsAdvice) {
    affinityChange = 6; // Seeking support
  }

  return {
    message: args.message as string,
    sentiment: args.sentiment as 'positive' | 'negative' | 'neutral',
    mood: moodMap[newsType] ?? 'neutral',
    affinityChange,
    familiarityChange: isSecret ? 4 : 2,
    playerStatChanges: wantsAdvice ? { social: 2 } : undefined,
    messageData: {
      newsType,
      wantsAdvice,
      isSecret,
    },
    recordForCooldown: true,
  };
}

function processRequestFavor(
  args: Record<string, unknown>,
  context: ToolContext
): ToolCallResult {
  const favorType = args.favor_type as string;
  const importance = (args.importance as string) ?? 'medium';

  // Favor costs to player
  const favorCosts: Record<string, Partial<{ energy: number; money: number; time: number }>> = {
    advice: { energy: 5 },
    help_with_task: { energy: 20 },
    emotional_support: { energy: 15 },
    money: { money: 50 },
    introduction: { energy: 10 },
    time: { energy: 25 },
  };

  const costs = favorCosts[favorType] ?? { energy: 15 };
  const eventId = `favor_${context.character.id}_${Date.now()}`;

  // Format favor type for display
  const favorDisplayName = favorType.replace(/_/g, ' ');

  // The affinity reward for HELPING is now scaled by how much the NPC already
  // likes the player (affinity-as-gameplay-gate, T008): helping a beloved
  // relation deepens the bond more than helping a near-stranger. A small
  // importance bump is layered on top. We reuse evaluateFavor's affinityDelta as
  // a normalized "bond strength" signal and combine it with importance.
  const importanceBase = importance === 'big' ? 15 : importance === 'medium' ? 10 : 5;
  const affinity = context.character.affinity ?? 50;
  // 0 at affinity<=0, up to +50% reward at affinity 100.
  const affinityMultiplier = 1 + Math.max(0, Math.min(100, affinity)) / 200;
  const affinityReward = Math.round(importanceBase * affinityMultiplier);

  return {
    message: args.message as string,
    sentiment: args.sentiment as 'positive' | 'negative' | 'neutral',
    mood: 'nervous',
    affinityChange: 3,
    familiarityChange: 2,
    pendingEvent: {
      id: eventId,
      type: 'favor_request',
      characterId: context.character.id,
      characterName: context.character.firstname,
      data: {
        favorType,
        importance,
        costs,
        // Affinity reward for helping, scaled by current affinity (see above).
        affinityReward,
      },
      createdAt: new Date().toISOString(),
      expiresAt: calculateExpiration(context.player, importance === 'big' ? 72 : 48),
      priority: importance === 'big' ? 'high' : 'medium',
    },
    announcement: {
      title: 'Favor Request',
      message: `${context.character.firstname} is asking for your help with ${favorDisplayName}`,
      category: 'social',
    },
    recordForCooldown: true,
  };
}

/**
 * Player asked the NPC for a favor (loan / job referral / childcare / help).
 * Unlike `request_favor` (NPC asks PLAYER), the outcome here is decided by the
 * NPC's affinity via evaluateFavor — this is the core "affinity is a gate on
 * real outcomes" mechanic. A granted loan immediately credits the player's
 * money so the payoff is concrete and observable.
 */
function processAskFavor(
  args: Record<string, unknown>,
  context: ToolContext
): ToolCallResult {
  const favorKind = (args.favor_kind as 'loan' | 'job_referral' | 'childcare' | 'practical_help')
    ?? 'practical_help';
  const requestedAmount =
    typeof args.requested_amount === 'number' ? args.requested_amount : undefined;

  const result = evaluateFavor(context.character, favorKind, requestedAmount);

  // Apply the real, concrete effect of a granted loan: credit the player
  // DIRECTLY here. We intentionally do NOT route this through
  // `playerStatChanges`, because the ai_response applier clamps every stat to
  // 0..100 (correct for happiness/social, but it would silently cap an $800
  // loan at 100). Money has no 0..100 ceiling, so we mutate it on the player.
  if (result.granted && result.moneyGranted > 0) {
    const c = context.player.c as { money?: number };
    c.money = (c.money ?? 0) + result.moneyGranted;
  }

  // Surface the outcome so the client / downstream job logic can read it
  // (referralStrength can boost hiring odds when the player applies for a job).
  const messageData: Record<string, unknown> = {
    favorKind,
    granted: result.granted,
    moneyGranted: result.moneyGranted,
    referralStrength: result.referralStrength,
  };

  return {
    message: (args.message as string) || result.message,
    sentiment: result.granted ? 'positive' : 'negative',
    mood: result.granted ? 'happy' : 'neutral',
    affinityChange: result.affinityDelta,
    familiarityChange: 1,
    messageData,
    announcement: {
      title: result.granted ? 'Favor Granted' : 'Favor Declined',
      message: result.message,
      category: 'social',
    },
    recordForCooldown: result.granted,
  };
}

function processGiveGift(
  args: Record<string, unknown>,
  context: ToolContext
): ToolCallResult {
  const giftType = args.gift_type as string;
  const occasion = args.occasion as string | undefined;

  // Gift values (what player receives)
  const giftValues: Record<string, { happiness: number; description: string }> = {
    flowers: { happiness: 10, description: 'a beautiful bouquet of flowers' },
    food: { happiness: 8, description: 'some delicious homemade food' },
    handmade: { happiness: 15, description: 'something they made just for you' },
    thoughtful_item: { happiness: 12, description: 'a thoughtful gift' },
    experience: { happiness: 20, description: 'tickets to something special' },
    money: { happiness: 5, description: 'some money to help out' },
  };

  const gift = giftValues[giftType] ?? { happiness: 10, description: 'a nice gift' };
  const eventId = `gift_${context.character.id}_${Date.now()}`;

  return {
    message: args.message as string,
    sentiment: 'positive',
    mood: 'happy',
    affinityChange: 12,
    familiarityChange: 3,
    playerStatChanges: {
      happiness: gift.happiness,
    },
    pendingEvent: {
      id: eventId,
      type: 'gift_received',
      characterId: context.character.id,
      characterName: context.character.firstname,
      data: {
        giftType,
        occasion,
        description: gift.description,
        happinessBoost: gift.happiness,
      },
      createdAt: new Date().toISOString(),
      priority: 'medium',
    },
    announcement: {
      title: 'Gift Received',
      message: `${context.character.firstname} gave you ${gift.description}!`,
      category: 'gift',
    },
    recordForCooldown: true,
  };
}

function processConfessFeelings(
  args: Record<string, unknown>,
  context: ToolContext
): ToolCallResult {
  const confessionStyle = args.confession_style as string;
  const hasBeenBuilding = args.has_been_building as boolean ?? true;

  const eventId = `confession_${context.character.id}_${Date.now()}`;

  return {
    message: args.message as string,
    sentiment: 'positive',
    mood: confessionStyle === 'shy' || confessionStyle === 'nervous' ? 'nervous' : 'flirty',
    affinityChange: 15,
    familiarityChange: 5,
    pendingEvent: {
      id: eventId,
      type: 'confession',
      characterId: context.character.id,
      characterName: context.character.firstname,
      data: {
        confessionStyle,
        hasBeenBuilding,
        // This is a major relationship moment
        canStartDating: true,
      },
      createdAt: new Date().toISOString(),
      priority: 'high',
    },
    announcement: {
      title: 'Confession',
      message: `${context.character.firstname} has something important to tell you...`,
      category: 'romantic',
    },
    recordForCooldown: true,
  };
}

function processAskToBeOfficial(
  args: Record<string, unknown>,
  context: ToolContext
): ToolCallResult {
  const approachStyle = args.approach_style as string;
  const reason = args.reason as string | undefined;

  const eventId = `official_${context.character.id}_${Date.now()}`;

  // Determine the new relationship type based on character's sex
  const characterSex = context.character.sex?.toLowerCase();
  const newRelationshipType = characterSex === 'female' ? 'girlfriend' :
                              characterSex === 'male' ? 'boyfriend' : 'partner';

  return {
    message: args.message as string,
    sentiment: 'positive',
    mood: approachStyle === 'nervous' ? 'nervous' : 'flirty',
    affinityChange: 20, // Big affinity boost for this milestone
    familiarityChange: 5,
    playerStatChanges: {
      happiness: 15, // Player feels happy about being asked
    },
    pendingEvent: {
      id: eventId,
      type: 'relationship_upgrade',
      characterId: context.character.id,
      characterName: context.character.firstname,
      data: {
        approachStyle,
        reason,
        currentRelationship: context.relationshipTypes,
        proposedRelationship: newRelationshipType,
        // This upgrades the relationship if player accepts
        upgradesTo: newRelationshipType,
      },
      createdAt: new Date().toISOString(),
      priority: 'high',
    },
    announcement: {
      title: 'Relationship Moment',
      message: `${context.character.firstname} wants to make things official!`,
      category: 'romantic',
    },
    recordForCooldown: true,
  };
}

function processAcceptRelationship(
  args: Record<string, unknown>,
  context: ToolContext
): ToolCallResult {
  const reactionStyle = args.reaction_style as string;
  const newRelationship = args.new_relationship as string;

  const eventId = `accept_official_${context.character.id}_${Date.now()}`;

  return {
    message: args.message as string,
    sentiment: 'positive',
    mood: reactionStyle === 'shy' ? 'nervous' : reactionStyle === 'excited' ? 'excited' : 'happy',
    affinityChange: 25, // Big boost - they said yes!
    familiarityChange: 5,
    playerStatChanges: {
      happiness: 20,
    },
    pendingEvent: {
      id: eventId,
      type: 'relationship_accepted',
      characterId: context.character.id,
      characterName: context.character.firstname,
      data: {
        reactionStyle,
        previousRelationship: context.relationshipTypes,
        newRelationship,
        // Immediately upgrade the relationship
        upgradesTo: newRelationship,
        immediate: true, // Apply immediately, no confirmation needed
      },
      createdAt: new Date().toISOString(),
      priority: 'high',
    },
    recordForCooldown: false, // No cooldown for acceptance
  };
}

function processAcceptDate(
  args: Record<string, unknown>,
  context: ToolContext
): ToolCallResult {
  const dateType = args.date_type as string;
  const suggestedTime = (args.suggested_time as string) ?? 'soon';

  // Date costs vary by type
  const dateCosts: Record<string, { energy: number; money: number }> = {
    casual: { energy: 20, money: 20 },
    romantic: { energy: 25, money: 50 },
    adventurous: { energy: 35, money: 35 },
    cozy: { energy: 15, money: 15 },
    fancy: { energy: 30, money: 100 },
  };

  const costs = dateCosts[dateType] ?? { energy: 25, money: 30 };
  const eventId = `accept_date_${context.character.id}_${Date.now()}`;

  // Calculate when the date should trigger based on suggested time
  const currentHour = context.player.hourOfDay ?? 12;
  const hoursFromNow = suggestedTime === 'now' ? 1 :
                       suggestedTime === 'tonight' ? Math.max(1, 19 - currentHour) :
                       suggestedTime === 'tomorrow' ? 24 + 18 - currentHour :
                       suggestedTime === 'this_weekend' ? 48 : 6;

  // triggersAt is the game hour when this should fire
  // We calculate it as (dayOfYear * 24 + hour) for a unique game-time identifier
  const currentGameHour = (context.player.dayOfYear ?? 1) * 24 + currentHour;
  const triggersAt = currentGameHour + hoursFromNow;

  // Expiration is slightly after trigger time
  const expirationHours = hoursFromNow + 12;

  return {
    message: args.message as string,
    sentiment: 'positive',
    mood: 'flirty',
    affinityChange: 12,
    familiarityChange: 2,
    pendingEvent: {
      id: eventId,
      type: 'date_accepted',
      characterId: context.character.id,
      characterName: context.character.firstname,
      data: {
        dateType,
        suggestedTime,
        costs,
        scheduled: true,
      },
      createdAt: new Date().toISOString(),
      triggersAt,
      expiresAt: calculateExpiration(context.player, expirationHours),
      priority: 'high',
    },
    recordForCooldown: false,
  };
}

function processAcceptActivity(
  args: Record<string, unknown>,
  context: ToolContext
): ToolCallResult {
  const activityType = args.activity_type as string;
  const enthusiasm = (args.enthusiasm as string) ?? 'happy';

  const costs = getActivityCosts(activityType as any);
  const eventId = `accept_activity_${context.character.id}_${Date.now()}`;

  // Affinity based on enthusiasm
  const affinityChange = enthusiasm === 'very_excited' ? 10 :
                         enthusiasm === 'happy' ? 7 :
                         enthusiasm === 'casual' ? 5 : 3;

  // Schedule activity for 2 hours from now
  const currentHour = context.player.hourOfDay ?? 12;
  const currentGameHour = (context.player.dayOfYear ?? 1) * 24 + currentHour;
  const triggersAt = currentGameHour + 2;

  return {
    message: args.message as string,
    sentiment: args.sentiment as 'positive' | 'negative' | 'neutral',
    mood: enthusiasm === 'very_excited' ? 'excited' : enthusiasm === 'reluctant_but_yes' ? 'neutral' : 'happy',
    affinityChange,
    familiarityChange: 2,
    pendingEvent: {
      id: eventId,
      type: 'activity_accepted',
      characterId: context.character.id,
      characterName: context.character.firstname,
      data: {
        activityType,
        enthusiasm,
        costs,
        scheduled: true,
      },
      createdAt: new Date().toISOString(),
      triggersAt,
      expiresAt: calculateExpiration(context.player, 24),
      priority: 'medium',
    },
    recordForCooldown: false,
  };
}

function processAcceptConfession(
  args: Record<string, unknown>,
  context: ToolContext
): ToolCallResult {
  const reactionStyle = args.reaction_style as string;
  const reciprocateFeelings = args.reciprocate_feelings as boolean ?? true;

  const eventId = `accept_confession_${context.character.id}_${Date.now()}`;

  // Determine new relationship type
  const newRelationship = 'dating';

  // Ripple (T011b): the confession LANDED — the NPC reciprocates. Create a
  // romance prospect directly so the relationship begins to exist immediately,
  // not only after the player separately resolves the pending event. Gated by
  // romance()'s own affinity/exclusivity checks; the big affinityChange below
  // (applied by the caller) typically clears the gate.
  if (reciprocateFeelings) {
    applyConfessionRipple(context.player, context.character);
  }

  return {
    message: args.message as string,
    sentiment: 'positive',
    mood: reactionStyle === 'shy' ? 'nervous' : reactionStyle === 'overjoyed' ? 'excited' : 'flirty',
    affinityChange: 20,
    familiarityChange: 5,
    playerStatChanges: {
      happiness: 25, // Big happiness boost - they like you back!
    },
    pendingEvent: {
      id: eventId,
      type: 'confession_accepted',
      characterId: context.character.id,
      characterName: context.character.firstname,
      data: {
        reactionStyle,
        reciprocateFeelings,
        previousRelationship: context.relationshipTypes,
        // Start dating
        upgradesTo: newRelationship,
        immediate: true,
      },
      createdAt: new Date().toISOString(),
      priority: 'high',
    },
    recordForCooldown: false,
  };
}

function processInitiateBreakup(
  args: Record<string, unknown>,
  context: ToolContext
): ToolCallResult {
  const reason = args.reason as string;
  const tone = args.tone as string;

  const eventId = `breakup_${context.character.id}_${Date.now()}`;

  // Ripple (T011b): a breakup whose reason is 'betrayal' DISCLOSES that betrayal
  // into the wider circle. The drama does not stay between the two of them —
  // mutual NPCs who share a close (family/friend) tag with the breaking-up
  // partner hear about it and think a little less of the player. Bounded: small
  // per-NPC loss, only close-circle NPCs, partner themselves untouched here
  // (their hit is the primary affinityChange below).
  if (reason === 'betrayal') {
    applyBetrayalRipple(context.player, context.character.id);
  }

  // Mood based on tone
  const moodMap: Record<string, MoodType> = {
    sad: 'sad',
    angry: 'angry',
    cold: 'neutral',
    apologetic: 'sad',
    relieved: 'neutral',
  };

  return {
    message: args.message as string,
    sentiment: args.sentiment as 'positive' | 'negative' | 'neutral',
    mood: moodMap[tone] ?? 'sad',
    affinityChange: -30, // Big affinity drop
    familiarityChange: 0,
    playerStatChanges: {
      happiness: -20, // Player is sad about breakup
    },
    pendingEvent: {
      id: eventId,
      type: 'breakup_initiated',
      characterId: context.character.id,
      characterName: context.character.firstname,
      data: {
        reason,
        tone,
        initiatedBy: 'character',
        previousRelationship: context.relationshipTypes,
        // Downgrade to ex
        downgradesTo: 'ex',
        immediate: true,
      },
      createdAt: new Date().toISOString(),
      priority: 'high',
    },
    recordForCooldown: false,
  };
}

function processAcceptBreakup(
  args: Record<string, unknown>,
  context: ToolContext
): ToolCallResult {
  const reaction = args.reaction as string;

  const eventId = `accept_breakup_${context.character.id}_${Date.now()}`;

  // Mood based on reaction
  const moodMap: Record<string, MoodType> = {
    heartbroken: 'sad',
    understanding: 'neutral',
    angry: 'angry',
    relieved: 'neutral',
    begging: 'sad',
  };

  return {
    message: args.message as string,
    sentiment: args.sentiment as 'positive' | 'negative' | 'neutral',
    mood: moodMap[reaction] ?? 'sad',
    affinityChange: reaction === 'angry' ? -20 : reaction === 'understanding' ? -5 : -15,
    familiarityChange: 0,
    pendingEvent: {
      id: eventId,
      type: 'breakup_accepted',
      characterId: context.character.id,
      characterName: context.character.firstname,
      data: {
        reaction,
        initiatedBy: 'player',
        previousRelationship: context.relationshipTypes,
        // Downgrade to ex
        downgradesTo: 'ex',
        immediate: true,
      },
      createdAt: new Date().toISOString(),
      priority: 'high',
    },
    recordForCooldown: false,
  };
}

// ============================================================
// Helper Functions
// ============================================================

/**
 * Get costs for an activity type
 */
function getActivityCosts(activityType: ActivityType): { energy: number; money?: number } {
  const costs: Record<string, { energy: number; money?: number }> = {
    coffee: { energy: 10, money: 10 },
    movie: { energy: 15, money: 20 },
    walk: { energy: 20 },
    dinner: { energy: 20, money: 35 },
    study: { energy: 25 },
    workout: { energy: 35 },
    shopping: { energy: 25, money: 40 },
    gaming: { energy: 15 },
    cooking: { energy: 20, money: 20 },
    concert: { energy: 30, money: 60 },
    museum: { energy: 20, money: 15 },
    picnic: { energy: 25, money: 15 },
    beach: { energy: 30 },
    hiking: { energy: 40 },
    clubbing: { energy: 35, money: 50 },
  };

  return costs[activityType] ?? { energy: 20 };
}

/**
 * Calculate game hour for cooldown tracking
 */
function calculateGameHour(player: Player): number {
  // Convert game date + hour to a single number for comparison
  const dayOfYear = player.dayOfYear ?? 1;
  const hourOfDay = player.hourOfDay ?? 0;
  return dayOfYear * 24 + hourOfDay;
}

/**
 * Calculate expiration timestamp for pending events
 */
function calculateExpiration(player: Player, hoursFromNow: number): string {
  // Use real time for expiration (events expire even when game is paused)
  const expirationDate = new Date();
  expirationDate.setHours(expirationDate.getHours() + hoursFromNow);
  return expirationDate.toISOString();
}

// ============================================================
// Conversation Ripples (T011b)
// ============================================================
//
// Small, bounded side-effects that make in-conversation choices ripple into the
// wider relationship graph, so NPCs feel persistent and consequences carry. Each
// ripple reuses existing relationship-manager engine support (romance(),
// updateAffinity()) rather than introducing new mechanics, keeping the diff
// small and testable.

// Relationship-type tags that define a "circle" the betrayer belongs to. A
// betrayal disclosed in conversation propagates a (smaller) affinity loss to the
// betrayer's circle-mates who share one of these close categories.
const BETRAYAL_PROPAGATION_TAGS = [
  'family', 'parent', 'mother', 'father', 'sibling', 'brother', 'sister',
  'son', 'daughter', 'friend', 'best friend',
];

/**
 * Confession ripple: a confession that LANDS (the NPC accepts / reciprocates)
 * should be able to create a romance prospect directly, rather than only
 * queueing a pending event the player must separately resolve.
 *
 * Delegates to relationship_manager.romance(), which gates on the NPC's
 * affinity (>= 50) and exclusivity, and creates a 'Prospect' relData entry.
 * Because a successful confession just applied a large positive affinityChange,
 * the target's affinity is typically already past the gate by the time this
 * runs. Returns true if a prospect (or active romance) was created.
 */
export function applyConfessionRipple(player: Player, character: Person): boolean {
  if (!character?.id) {
    return false;
  }
  // Acceptance implies mutual interest. romance() gates on affinity >= 50; an
  // NPC who reciprocates a confession clearly meets that bar, so nudge their
  // affinity to the gate first if a not-yet-applied delta would otherwise leave
  // them just under it. Bounded: only ever raises to the threshold, never above
  // what the conversation already earned.
  const target = (player.r ?? []).find((p) => p.id === character.id);
  if (target && typeof target.affinity === 'number' && target.affinity < 50) {
    updateAffinity(player, character.id, 50 - target.affinity);
  }
  const created = romance(player, character.id);
  if (created) {
    // Recurring-NPC callback: persist that this confession happened so the NPC
    // can reference it in future conversations (fire-and-forget DB write).
    recordPlayerChoiceMemory(
      character.id,
      player.userId,
      `The player and I confessed our feelings and started seeing each other.`,
      9
    ).catch(() => {});
  }
  return created;
}

/**
 * Betrayal ripple: when a betrayal is disclosed in conversation (e.g. a breakup
 * whose reason is 'betrayal'), the fallout should not stay contained to the two
 * people in the chat. Mutual NPCs who share a close relationship category with
 * the betrayer (family/friends) hear about it and think a little less of the
 * betrayer — a propagated affinity loss.
 *
 * Bounded by design: only NPCs carrying a close-circle tag are touched, the per-
 * NPC loss is small and fixed, and the betrayer themselves is skipped (their own
 * affinity hit is handled by the breakup's primary affinityChange). Returns the
 * ids of the NPCs whose affinity was reduced.
 */
export function applyBetrayalRipple(
  player: Player,
  betrayerId: string,
  perNpcLoss = 8
): string[] {
  const affected: string[] = [];
  const loss = -Math.abs(perNpcLoss);

  for (const person of player.r ?? []) {
    if (!person?.id || person.id === betrayerId) {
      continue;
    }
    const tags = (person.relationships ?? []).map((t) => t.toLowerCase());
    const inBetrayerCircle = tags.some((t) => BETRAYAL_PROPAGATION_TAGS.includes(t));
    if (!inBetrayerCircle) {
      continue;
    }
    updateAffinity(player, person.id, loss);
    affected.push(person.id);
    // Recurring-NPC callback: the circle remembers the betrayal (fire-and-forget).
    recordPlayerChoiceMemory(
      person.id,
      player.userId,
      `I heard the player betrayed someone close to our circle.`,
      8
    ).catch(() => {});
  }

  return affected;
}

// ============================================================
// Exports for External Use
// ============================================================

export {
  calculateGameHour,
  getActivityCosts,
};
