/**
 * NPC-Initiated Messages
 *
 * NPCs can text the player first based on realistic triggers:
 * - Birthday: close NPCs wish happy birthday
 * - Time gap: high-affinity NPCs check in after days without contact
 * - Morning text: romantic partners send morning messages
 *
 * Constraints:
 * - Max 1 NPC-initiated message per game day
 * - Only during appropriate hours (7am-10pm)
 * - Per-character cooldown of 7 days between initiatives
 * - Skip if the character has unanswered messages (player hasn't replied)
 */

import type { Player } from '../../models/Player.js';
import type { Person } from '../../models/Person.js';
import type { PlayerSession } from '../../game/PlayerSession.js';
import { ConversationObj } from './types.js';
import { getOpenAIResponse } from './ai_response.js';
import { saveConversation } from '../../database/players.js';
import { queueRealtimeNotification, notifyRealtimeEvent, notifyRelationshipAtRisk } from '../../services/notifications/notificationManager.js';

export interface NPCTrigger {
  type: 'birthday' | 'time_gap' | 'morning_text';
  character: Person;
  prompt: string;
  priority: number;
}

interface NPCInitiativeState {
  messagesThisDay: number;
  lastDay: string;
  lastInitiatedByCharacter: Map<string, string>;
}

const playerNPCState = new Map<string, NPCInitiativeState>();

function getState(playerId: string, currentDay: string): NPCInitiativeState {
  let state = playerNPCState.get(playerId);
  if (!state || state.lastDay !== currentDay) {
    state = {
      messagesThisDay: 0,
      lastDay: currentDay,
      lastInitiatedByCharacter: state?.lastInitiatedByCharacter ?? new Map(),
    };
    playerNPCState.set(playerId, state);
  }
  return state;
}

/**
 * Extract MM-DD from a date string that may be in MM-DD or YYYY-MM-DD format.
 */
function extractMonthDay(dateStr: string): string {
  if (!dateStr) return '';
  // YYYY-MM-DD format (10 chars, has dashes at positions 4 and 7)
  if (dateStr.length >= 10 && dateStr[4] === '-') {
    return dateStr.slice(5, 10);
  }
  // Already MM-DD format (5 chars)
  return dateStr;
}

/**
 * Check if an hour is appropriate for NPC-initiated messages.
 * NPCs should only text during reasonable hours: 7am to 10pm.
 */
export function isAppropriateHour(hour: number): boolean {
  return hour >= 7 && hour <= 22;
}

/** Relationship types considered "close" regardless of affinity */
const CLOSE_RELATIONSHIP_TYPES = [
  'spouse', 'girlfriend', 'boyfriend', 'partner',
  'mother', 'father', 'sibling', 'best_friend',
];

/** Relationship types considered "romantic partner" */
const PARTNER_RELATIONSHIP_TYPES = [
  'spouse', 'girlfriend', 'boyfriend', 'partner',
];

/**
 * Detect triggers that would cause an NPC to initiate a message.
 * Returns an array of triggers sorted by priority (highest first).
 */
export function detectNPCTriggers(player: Player, character: Person): NPCTrigger[] {
  // Dead characters don't send messages
  if (character.status === 'dead') return [];

  const triggers: NPCTrigger[] = [];
  const affinity = character.affinity ?? 0;
  const relationships = character.relationships ?? [];

  const isClose = affinity > 40 ||
    CLOSE_RELATIONSHIP_TYPES.some(r => relationships.includes(r));
  const isPartner = PARTNER_RELATIONSHIP_TYPES.some(r => relationships.includes(r));

  // --- Birthday trigger ---
  // Close NPCs wish the player happy birthday
  if (player.c.birthday && player.date && isClose) {
    const playerBirthdayMD = extractMonthDay(player.c.birthday);
    const currentDateMD = extractMonthDay(player.date);
    if (playerBirthdayMD && currentDateMD && playerBirthdayMD === currentDateMD) {
      triggers.push({
        type: 'birthday',
        character,
        prompt: `It's the player's birthday today! Send them a birthday message. Be warm and genuine. Your relationship: ${relationships.join(', ')}. Affinity: ${affinity}/100.`,
        priority: 10,
      });
    }
  }

  // --- Time gap trigger ---
  // High-affinity NPCs check in after days without contact
  if (affinity > 60 && (character as any).lastConversationDate) {
    const lastDateStr = (character as any).lastConversationDate as string;
    const lastDate = new Date(lastDateStr);
    const currentDate = new Date(player.date);
    // Only compute gap if both dates parse correctly
    if (!isNaN(lastDate.getTime()) && !isNaN(currentDate.getTime())) {
      const daysDiff = Math.floor(
        (currentDate.getTime() - lastDate.getTime()) / (1000 * 60 * 60 * 24)
      );
      if (daysDiff >= 3) {
        triggers.push({
          type: 'time_gap',
          character,
          prompt: `You haven't heard from the player in ${daysDiff} days. Check in on them naturally. Don't be clingy. Your relationship: ${relationships.join(', ')}. Affinity: ${affinity}/100.`,
          priority: 5,
        });
      }
    }
  }

  // --- Morning text trigger ---
  // Romantic partners send sweet morning texts
  if (isPartner && affinity > 70 && player.hourOfDay >= 7 && player.hourOfDay <= 9) {
    triggers.push({
      type: 'morning_text',
      character,
      prompt: `Send a sweet morning text to your partner. Keep it natural and short. Affinity: ${affinity}/100.`,
      priority: 3,
    });
  }

  return triggers;
}

/**
 * Check and fire NPC-initiated messages during the hour tick.
 *
 * Constraints:
 * - Max 1 NPC-initiated message per game day
 * - Only during appropriate hours (7am-10pm)
 * - Per-character cooldown: at least 7 days between initiatives from same NPC
 * - Skip if the character's last message is unanswered by the player
 * - Dead characters are skipped
 */
export async function checkNPCInitiatedMessages(
  player: Player,
  session: PlayerSession,
): Promise<void> {
  if (!isAppropriateHour(player.hourOfDay)) return;

  const state = getState(player.userId, player.date);
  if (state.messagesThisDay >= 1) return; // Max 1 NPC-initiated message per game day

  const allTriggers: NPCTrigger[] = [];

  for (const character of (player.r ?? [])) {
    if (!character.id || character.status === 'dead') continue;

    // Per-character cooldown: skip if this NPC initiated within the last 7 game days
    const lastInitiated = state.lastInitiatedByCharacter.get(character.id);
    if (lastInitiated) {
      const lastDate = new Date(lastInitiated);
      const currentDate = new Date(player.date);
      if (!isNaN(lastDate.getTime()) && !isNaN(currentDate.getTime())) {
        const daysSince = Math.floor(
          (currentDate.getTime() - lastDate.getTime()) / (1000 * 60 * 60 * 24)
        );
        if (daysSince < 7) continue;
      }
    }

    // Skip if this character has unanswered messages (don't spam without a reply)
    const existingConvo = player.conversations.find(
      (c) => c.character === character.id || (c.character as any)?.id === character.id
    );
    if (existingConvo) {
      const messages = Array.isArray(existingConvo.conversation) ? existingConvo.conversation : [];
      if (messages.length > 0) {
        const lastMsg = messages[messages.length - 1];
        // If the last message was from the NPC (sender === character.id), player hasn't replied
        const lastSender = (lastMsg as any)?.sender;
        if (lastSender && lastSender === character.id) {
          continue; // Skip — player hasn't responded to the last message yet
        }
      }
    }

    const triggers = detectNPCTriggers(player, character);
    allTriggers.push(...triggers);
  }

  if (allTriggers.length === 0) return;

  // Pick the highest-priority trigger
  allTriggers.sort((a, b) => b.priority - a.priority);
  const trigger = allTriggers[0];

  try {
    if (process.env.NODE_ENV !== 'test') {
      console.log(
        `[NPC Initiative] ${trigger.character.firstname} triggered: ${trigger.type} (priority ${trigger.priority})`
      );
    }

    const character = trigger.character;

    // Find or create conversation for this character
    let convoData = player.conversations.find(
      (c) => c.character === character.id || (c.character as any)?.id === character.id
    );

    let convo: ConversationObj;
    if (!convoData) {
      convo = new ConversationObj(character, 'chat');
      player.conversations.push(convo);
    } else if (convoData instanceof ConversationObj) {
      convo = convoData;
    } else {
      convo = ConversationObj.fromData(convoData as unknown as Record<string, unknown>, character);
      const idx = player.conversations.indexOf(convoData);
      if (idx >= 0) player.conversations[idx] = convo;
    }

    // Build context about conversation state for the AI
    const existingMessages = Array.isArray(convo.conversation) ? convo.conversation : [];
    let convoContext = '';
    if (existingMessages.length > 0) {
      const lastMsg = existingMessages[existingMessages.length - 1];
      const lastSender = (lastMsg as any)?.sender;
      const lastText = (lastMsg as any)?.message ?? '';
      if (lastSender === character.id) {
        convoContext = `Your last message to them was: "${lastText.substring(0, 100)}". They haven't replied yet. Don't repeat yourself or send the same kind of message — acknowledge the gap naturally or bring up something new.`;
      } else {
        convoContext = `The last thing they said to you was: "${lastText.substring(0, 100)}". It's been a while since that exchange.`;
      }
    } else {
      convoContext = 'This is the very first message in your conversation with them.';
    }

    // Generate AI response with trigger context as custom prompt
    const npcPrompt = `IMPORTANT: You are initiating this conversation — the player did NOT message you first. ${trigger.prompt} ${convoContext} Send ONE natural message. Do not reference that you are "reaching out" or "checking in" explicitly — just be natural.`;
    await getOpenAIResponse(convo, character, player, npcPrompt);

    // Mark as unread and send to client
    convo.unread = true;
    session.send(convo.toJSON());

    // Push notification if app is backgrounded
    const npcName = character.firstname ?? 'Someone';
    const lastMsg = Array.isArray(convo.conversation) && convo.conversation.length > 0
      ? (convo.conversation[convo.conversation.length - 1] as any)?.message ?? ''
      : '';
    const preview = lastMsg.length > 80 ? lastMsg.substring(0, 80) + '...' : lastMsg;
    const pushBody = preview || 'sent you a message';

    // Primary path: queueRealtimeNotification checks player.connection === 'disconnected'
    const result = await queueRealtimeNotification(player, {
      title: npcName,
      body: pushBody,
      type: 'relationship',
      id: convo.id,
    }).catch(err => {
      if (process.env.NODE_ENV !== 'test') {
        console.error('[NPC Initiative] Push notification error:', err);
      }
      return { sent: false, reason: 'error' };
    });

    // Fallback: if WS isn't fully open but player wasn't flagged as disconnected yet,
    // send push directly (matches v2Prompt pattern in PlayerSession)
    if (!result?.sent && !session.isWSOpen && player.deviceToken) {
      notifyRealtimeEvent(
        player.userId, player.deviceToken, true,
        npcName, pushBody, 'relationship', convo.id
      ).catch(err => {
        if (process.env.NODE_ENV !== 'test') {
          console.error('[NPC Initiative] Fallback push error:', err);
        }
      });
    }

    // Persist to database
    const messages = Array.isArray(convo.conversation) ? convo.conversation : [];
    await saveConversation(
      player.userId,
      character.id,
      convo.id,
      messages,
      convo.unread
    ).catch(err => {
      if (process.env.NODE_ENV !== 'test') {
        console.error('[NPC Initiative] Save error:', err);
      }
    });

    state.messagesThisDay++;
    state.lastInitiatedByCharacter.set(character.id, player.date);
  } catch (error) {
    if (process.env.NODE_ENV !== 'test') {
      console.log(`[NPC Initiative] Failed for ${trigger.character.firstname}: ${error}`);
    }
  }
}

// ============================================================================
// Relationship-at-risk neglect nudge (T010d)
// ============================================================================

/** Affinity at/under which a once-high relationship is "at risk" and warrants a nudge. */
export const RELATIONSHIP_AT_RISK_AFFINITY = 40;

/**
 * Decide whether an NPC warrants a "relationship at risk" nudge. Fires ONLY for
 * a character the player once felt strongly about (affinityWasHigh) whose
 * affinity has since decayed at/under the at-risk threshold and who has not yet
 * been nudged for this decline (neglectAtRiskNotified not set). Dead characters
 * never qualify.
 */
export function shouldNudgeAtRisk(character: Person): boolean {
  if (character.status === 'dead') return false;
  if (!character.affinityWasHigh) return false;
  if (character.neglectAtRiskNotified) return false;
  return (character.affinity ?? 0) <= RELATIONSHIP_AT_RISK_AFFINITY;
}

/**
 * Scan the player's relationships for once-high NPCs that have decayed past the
 * at-risk threshold and fire a single "relationship at risk" nudge each. Marks
 * neglectAtRiskNotified so the nudge fires only once per decline (it is reset by
 * recordPositiveInteraction once affinity recovers). Returns the list of NPC ids
 * that were nudged (for testing / telemetry).
 *
 * Safe to call once per in-game day from the loop; needs no real APNs (the
 * notification layer is stubbed/gated).
 */
export async function checkRelationshipAtRiskNudges(
  player: Player,
  session?: PlayerSession,
): Promise<string[]> {
  const nudged: string[] = [];

  for (const character of (player.r ?? [])) {
    if (!character.id) continue;
    if (!shouldNudgeAtRisk(character)) continue;

    // Mark first so a mid-loop throw can't re-nudge the same NPC repeatedly.
    character.neglectAtRiskNotified = true;
    nudged.push(character.id);

    const npcName = character.firstname ?? 'Someone';

    // Optionally surface in-app via the session so the player sees it even when
    // foregrounded.
    if (session) {
      try {
        session.send({
          type: 'messageEvent',
          id: `relationship_at_risk_${character.id}`,
          message: `You and ${npcName} have been drifting apart. It might be time to reach out.`,
          positive: false,
          title: 'Drifting apart',
          characters: [character.id],
        });
      } catch {
        // non-critical
      }
    }

    // Fire the push nudge (gated/stubbed — no real APNs needed for tests).
    await notifyRelationshipAtRisk(player, npcName, character.id).catch((err) => {
      if (process.env.NODE_ENV !== 'test') {
        console.error('[NPC Initiative] At-risk nudge error:', err);
      }
      return { sent: false, reason: 'error' as const };
    });
  }

  return nudged;
}

/**
 * Clear NPC initiative state for a player (e.g., on disconnect).
 */
export function clearNPCInitiativeState(playerId: string): void {
  playerNPCState.delete(playerId);
}
