import type { Database } from 'bun:sqlite';
import { ensureUserExists } from '../db';
import { normalizeWhatsAppId, isGroupId } from '../utils/whatsapp';
import { IdentityService } from '../services/identity';
import { AllowedGroups } from '../services/allowed-groups';
import { GroupSyncService } from '../services/group-sync';
import { ResponseQueue } from '../services/response-queue';
import { AdminService } from '../services/admin';
import { CommandService } from '../services/command';
import { TaskService } from '../tasks/service';
import { RateLimiter } from '../services/rate-limit';
import { Metrics } from '../services/metrics';

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

const isTest = () => process.env.NODE_ENV === 'test';
const log = (msg: string, ...args: unknown[]) => { if (!isTest()) console.log(msg, ...args); };
const logErr = (msg: string, ...args: unknown[]) => { if (!isTest()) console.error(msg, ...args); };
const logWarn = (msg: string, ...args: unknown[]) => { if (!isTest()) console.warn(msg, ...args); };
const logDebug = (msg: string, ...args: unknown[]) => { if (!isTest()) console.debug(msg, ...args); };

export function getMessageText(message: any): string {
  if (!message || typeof message !== 'object') return '';
  const text =
    message.conversation ||
    message?.extendedTextMessage?.text ||
    message?.imageMessage?.caption ||
    message?.videoMessage?.caption ||
    '';
  return typeof text === 'string' ? text.trim() : '';
}

function isEnvTrue(key: string): boolean {
  return ['true', '1', 'yes', 'on'].includes(String(process.env[key] || 'false').toLowerCase());
}

function resolveRateLimitPerMin(): number {
  const v = Number(process.env.RATE_LIMIT_PER_MIN);
  return Number.isFinite(v) && v > 0 ? v : 15;
}

// ---------------------------------------------------------------------------
// Message validation
// ---------------------------------------------------------------------------

/** Returns false if the message payload is missing required fields. */
function isValidMessageShape(data: any): boolean {
  if (!data?.key?.remoteJid || !data.message) {
    log('⚠️ Invalid message format - missing required fields', data);
    return false;
  }
  return true;
}

// ---------------------------------------------------------------------------
// Sender resolution
// ---------------------------------------------------------------------------

/** Returns true when the message should be discarded (broadcast or self). */
function shouldSkipBroadcastOrSelf(data: any, remoteJid: string): boolean {
  if (remoteJid === 'status@broadcast' || (typeof remoteJid === 'string' && remoteJid.endsWith('@broadcast'))) {
    log('ℹ️ Ignoring broadcast/status message');
    return true;
  }
  if (data.key.fromMe) {
    log('ℹ️ Ignoring message sent by the bot (fromMe=true)');
    return true;
  }
  return false;
}

/** Prefers participantAlt (Baileys change) in groups, falls back to remoteJid. */
function resolveSenderJid(data: any, remoteJid: string): string {
  if (isGroupId(remoteJid)) {
    return data.key.participantAlt || data.key.participant || remoteJid;
  }
  return remoteJid;
}

/** Learns participant ↔ participantAlt alias when both differ. */
function learnAliasFromKey(data: any, remoteJid: string): void {
  if (!isGroupId(remoteJid)) return;
  const pAlt = typeof data.key.participantAlt === 'string' ? data.key.participantAlt : null;
  const p = typeof data.key.participant === 'string' ? data.key.participant : null;
  if (!pAlt || !p) return;
  try {
    const nAlt = normalizeWhatsAppId(pAlt);
    const n = normalizeWhatsAppId(p);
    if (nAlt && n && nAlt !== n) {
      log('[A0] message.key participants', {
        participant: p,
        participantAlt: pAlt,
        normalized_participant: n,
        normalized_participantAlt: nAlt,
        alias_upsert: true,
      });
      IdentityService.upsertAlias(p, pAlt, 'message.key');
    }
  } catch { /* best-effort */ }
}

/** Returns the normalized WhatsApp ID, or null if invalid. */
function normalizeSender(senderRaw: string, remoteJid: string, participant: string, fromMe: boolean): string | null {
  const id = normalizeWhatsAppId(senderRaw);
  if (!id) {
    logDebug('⚠️ Invalid sender ID, ignoring message', { remoteJid, participant, fromMe });
    return null;
  }
  return id;
}

/** True if the sender matches the configured bot phone number. */
function isOwnBotNumber(normalizedId: string): boolean {
  if (process.env.CHATBOT_PHONE_NUMBER && normalizedId === process.env.CHATBOT_PHONE_NUMBER) {
    log('ℹ️ Ignoring message from the bot number');
    return true;
  }
  return false;
}

// ---------------------------------------------------------------------------
// User / activation
// ---------------------------------------------------------------------------

/** Ensures the sender exists in the database. Returns the userId or null on failure. */
function ensureUserOrBail(senderRaw: string, db: Database): string | null {
  try {
    const userId = ensureUserExists(senderRaw, db);
    if (!userId) log('⚠️ Failed to ensure user exists, ignoring message');
    return userId || null;
  } catch (e) {
    logErr('⚠️ Error ensuring user exists, ignoring message:', e);
    return null;
  }
}

/** Handles the "activar" DM flow. Returns true if the message was consumed. */
async function handleActivationDM(
  remoteJid: string,
  senderId: string,
  text: string,
): Promise<boolean> {
  if (isGroupId(remoteJid) || text !== 'activar') return false;
  const base = (process.env.WEB_BASE_URL || '').trim();
  const msg = base
    ? "Listo, ya puedes reclamar/ser responsable y acceder a la web. Para acceder a la web, envía 't web' y abre el enlace."
    : "Listo, ya puedes reclamar/ser responsable.";
  try { await ResponseQueue.add([{ recipient: senderId, message: msg }]); } catch {}
  return true;
}

// ---------------------------------------------------------------------------
// Group gating
// ---------------------------------------------------------------------------

/**
 * Discover mode: when an unknown group receives a message, register it as
 * pending and optionally notify admins.
 *
 * Returns true if the message was consumed (not an admin command → bail out).
 */
async function handleGroupDiscovery(
  db: Database,
  groupId: string,
  senderId: string,
  isAdminCmd: boolean,
): Promise<boolean> {
  if (!isGroupId(groupId)) return false; // only applies to groups
  const mode = String(process.env.GROUP_GATING_MODE || 'off').toLowerCase();
  if (mode !== 'discover') return false;

  try {
    const exists = db.prepare('SELECT 1 FROM allowed_groups WHERE group_id = ? LIMIT 1').get(groupId);
    if (exists) return false; // already known — continue normally
  } catch {
    // Table may not exist yet — fall through to discovery
  }

  // Register unknown group
  return await registerDiscoveredGroup(groupId, senderId, isAdminCmd);
}

/**
 * Enforce mode: silently drop messages from groups that haven't been
 * approved by an admin (unless the message itself is an /admin command).
 *
 * Returns true if the message was blocked.
 */
function handleGroupEnforcement(groupId: string, isAdminCmd: boolean): boolean {
  if (!isGroupId(groupId)) return false; // only applies to groups
  const mode = String(process.env.GROUP_GATING_MODE || 'off').toLowerCase();
  if (mode !== 'enforce') return false;

  try {
    if (!AllowedGroups.isAllowed(groupId) && !isAdminCmd) {
      try { Metrics.inc('messages_blocked_group_total'); } catch {}
      return true;
    }
  } catch {
    // If the check fails, be permissive
  }
  return false;
}

/** Shared discovery registration & admin notification. */
async function registerDiscoveredGroup(
  groupId: string,
  senderId: string,
  isAdminCmd: boolean,
): Promise<boolean> {
  try { await GroupSyncService.ensureGroupLabelAndName(groupId); } catch {}
  try { AllowedGroups.upsertPending(groupId, GroupSyncService.activeGroupsCache.get(groupId) || null, senderId); } catch {}
  try { Metrics.inc('unknown_groups_discovered_total'); } catch {}

  if (isEnvTrue('NOTIFY_ADMINS_ON_DISCOVERY') && !isAdminCmd) {
    const admins = AdminService.getAdmins();
    if (admins.length > 0) {
      const msg = `🔎 Nuevo grupo detectado: ${groupId}\nEstado: pending.\nUsa /admin habilitar-aquí desde el grupo o /admin allow-group ${groupId}.`;
      try { await ResponseQueue.add(admins.map(a => ({ recipient: a, message: msg }))); } catch {}
    }
  }

  // Admin commands pass through; everything else stops here
  return !isAdminCmd;
}

// ---------------------------------------------------------------------------
// Admin commands
// ---------------------------------------------------------------------------

/** Routes /admin commands. Returns true if the message was an admin command. */
async function handleAdminCommand(
  senderId: string,
  groupId: string,
  messageText: string,
): Promise<boolean> {
  if (!messageText.trim().startsWith('/admin')) return false;

  const responses = await AdminService.handle({
    sender: senderId,
    groupId,
    message: messageText,
  });
  if (responses.length > 0) {
    await ResponseQueue.add(responses);
  }
  return true;
}

// ---------------------------------------------------------------------------
// Group cache
// ---------------------------------------------------------------------------

/**
 * Ensures the group is present in the active-groups cache (lazy registration).
 * In test mode, inactive groups cause an early bail; in production they are
 * registered on the fly.
 *
 * Returns true when the message should be dropped (tests only).
 */
function ensureGroupActive(db: Database, groupId: string, senderId: string): boolean {
  if (!isGroupId(groupId) || GroupSyncService.isGroupActive(groupId)) return false;

  if (isTest()) return true; // tests: bail on inactive groups

  log('ℹ️ Group not active in cache — ensuring group (no immediate members sync)');
  try {
    GroupSyncService.ensureGroupExists(groupId);
    try { GroupSyncService.upsertMemberSeen(groupId, senderId); } catch {}
  } catch (e) {
    logErr('⚠️ Failed to ensure group on-the-fly:', e);
  }
  return false;
}

// ---------------------------------------------------------------------------
// Task command routing (t, tarea)
// ---------------------------------------------------------------------------

/** Extracts mentioned JIDs across all known message contexts. */
function extractMentions(data: any): string[] {
  return data.message?.contextInfo?.mentionedJid
    || data.message?.extendedTextMessage?.contextInfo?.mentionedJid
    || data.message?.imageMessage?.contextInfo?.mentionedJid
    || data.message?.videoMessage?.contextInfo?.mentionedJid
    || [];
}

/** Rate-limits the sender. Returns false (and optionally notifies) if over the limit. */
async function applyRateLimit(senderId: string): Promise<boolean> {
  if (isTest()) return true;

  if (RateLimiter.checkAndConsume(senderId)) return true;

  if (RateLimiter.shouldNotify(senderId)) {
    await ResponseQueue.add([{
      recipient: senderId,
      message: `Has superado el límite de ${resolveRateLimitPerMin()} comandos por minuto. Inténtalo de nuevo en un momento.`,
    }]);
  }
  return false;
}

/**
 * Adds a success/error emoji reaction to the command message when reactions
 * are enabled and in scope.
 */
async function maybeReactToCommand(data: any, outcome: { ok: boolean }, messageId: string): Promise<void> {
  if (!isEnvTrue('REACTIONS_ENABLED')) return;

  const scope = String(process.env.REACTIONS_SCOPE || 'groups').toLowerCase();
  const isGroup = isGroupId(data.key.remoteJid);
  if (scope !== 'all' && !isGroup) return;

  // Respect enforce gating for reactions
  const mode = String(process.env.GROUP_GATING_MODE || 'off').toLowerCase();
  if (mode === 'enforce' && isGroup) {
    try { if (!AllowedGroups.isAllowed(data.key.remoteJid)) return; } catch {}
  }

  const emoji = outcome.ok ? '🤖' : '⚠️';
  const participant = typeof data?.key?.participantAlt === 'string'
    ? data.key.participantAlt
    : (typeof data?.key?.participant === 'string' ? data.key.participant : undefined);

  await ResponseQueue.enqueueReaction(data.key.remoteJid, messageId, emoji, {
    participant,
    fromMe: !!data?.key?.fromMe,
  });
}

/** Routes t and tarea commands (with trailing space) through the command service. */
async function handleTaskCommand(
  data: any,
  db: Database,
  senderId: string,
  messageText: string,
): Promise<void> {
  if (!await applyRateLimit(senderId)) return;

  const mentions = extractMentions(data);

  const messageId = typeof data?.key?.id === 'string' ? data.key.id : null;
  const participantForKey = typeof data?.key?.participantAlt === 'string'
    ? data.key.participantAlt
    : (typeof data?.key?.participant === 'string' ? data.key.participant : null);

  const outcome = await CommandService.handleWithOutcome({
    sender: senderId,
    groupId: data.key.remoteJid,
    message: messageText,
    mentions,
    messageId: messageId || undefined,
    participant: participantForKey || undefined,
    fromMe: !!data?.key?.fromMe,
  });

  if (outcome.responses.length > 0) {
    await ResponseQueue.add(outcome.responses);
  }

  if (messageId) {
    try {
      await maybeReactToCommand(data, outcome, messageId);
    } catch (e) {
      logWarn('⚠️ Reaction enqueue failed:', e);
    }
  }
}

/** True when the message should be forwarded to the command service. */
function isTaskCommand(text: string): boolean {
  const lower = text.toLowerCase();
  return lower.startsWith('tarea ') || lower.startsWith('t ');
}

// ---------------------------------------------------------------------------
// Main handler
// ---------------------------------------------------------------------------

export async function handleMessageUpsert(data: any, db: Database): Promise<void> {
  // 1. Validate message shape
  if (!isValidMessageShape(data)) return;

  const messageText = getMessageText(data.message);
  if (!messageText) { log('⚠️ Empty or unsupported message content'); return; }

  const remoteJid: string = data.key.remoteJid;

  // 2. Broadcast / self-message guard
  if (shouldSkipBroadcastOrSelf(data, remoteJid)) return;

  // 3. Resolve & normalize sender
  const senderRaw = resolveSenderJid(data, remoteJid);
  learnAliasFromKey(data, remoteJid);

  let normalizedSenderId = normalizeSender(senderRaw, remoteJid, data.key.participant, !!data.key.fromMe);
  if (!normalizedSenderId) return;

  // Canonicalise: resolve LID / alternative IDs to the real phone number
  try {
    const canonical = IdentityService.resolveAliasOrNull(normalizedSenderId);
    if (canonical && canonical !== normalizedSenderId) {
      log(`[canonical] resolved sender ${normalizedSenderId} → ${canonical}`);
      normalizedSenderId = canonical;
    }
  } catch {}

  if (isOwnBotNumber(normalizedSenderId)) return;

  // 4. Ensure user exists
  if (!ensureUserOrBail(senderRaw, db)) return;

  const text = messageText.trim();
  const isAdminCmd = text.startsWith('/admin');

  // 5. Activation DM
  if (await handleActivationDM(remoteJid, normalizedSenderId, text)) return;

  // 6. Group gating (discover → enforce)
  if (await handleGroupDiscovery(db, remoteJid, normalizedSenderId, isAdminCmd)) return;
  if (handleGroupEnforcement(remoteJid, isAdminCmd)) return;

  // 7. Admin commands (before any other group logic)
  if (await handleAdminCommand(normalizedSenderId, remoteJid, messageText)) return;

  // 8. Ensure group is active (lazy registration in prod, bail in tests)
  if (ensureGroupActive(db, remoteJid, normalizedSenderId)) return;

  // 9. Task commands (t, tarea)
  if (isTaskCommand(text)) {
    await handleTaskCommand(data, db, normalizedSenderId, messageText);
  }
}
