/**
 * session.ts — Baileys session factory
 *
 * Creates a Baileys WhatsApp session for a given channelId.
 * - Emits QR/connection events back to the API via /internal/ws-event (POST)
 * - Forwards inbound messages to the API via /internal/messages (POST)
 * - The API's Socket.IO broadcasts these to connected browsers
 */
import makeWASocket, {
  DisconnectReason,
  downloadMediaMessage,
  extractMessageContent,
  fetchLatestBaileysVersion,
  isJidGroup,
  isLidUser,
  useMultiFileAuthState,
  type WAMessage,
  type WASocket,
} from '@whiskeysockets/baileys';
import { Boom } from '@hapi/boom';
import QRCode from 'qrcode';
import fs from 'fs';
import path from 'path';
import os from 'os';

export interface ActiveSession {
  socket: WASocket;
  channelId: string;
}

export interface SessionOptions {
  channelId: string;
  apiUrl: string;
  onReconnect?: (session: ActiveSession) => void;
  /** Fired when Baileys closes with loggedOut (auth wiped; caller should drop the Map entry). */
  onLoggedOut?: (channelId: string) => void;
}

/** Per-channel Baileys multi-file auth (persisted via wa_auth volume in Docker). */
export function authFolderFor(channelId: string): string {
  return path.join(os.tmpdir(), 'wa_ticketing_auth', channelId);
}

/**
 * Remove stored WhatsApp credentials for a channel.
 * Required before reconnect when the session was logged out — otherwise Baileys
 * tries to restore stale creds, never emits a QR, and the UI stays on "Generating…".
 */
export function clearAuthState(channelId: string): void {
  const folder = authFolderFor(channelId);
  try {
    if (fs.existsSync(folder)) {
      fs.rmSync(folder, { recursive: true, force: true });
      console.log(`[Session] Cleared auth state for channel ${channelId}`);
    }
  } catch (err) {
    console.error(`[Session] Failed to clear auth for ${channelId}:`, err);
  }
}

/**
 * Digit user id for @s.whatsapp.net (no +). Modern chats often use @lid on remoteJid;
 * Baileys then fills senderPn / participantPn with the real phone JID.
 */
function extractInboundCustomerPhone(msg: WAMessage): string {
  const k = msg.key;

  if (isJidGroup(k.remoteJid ?? undefined)) {
    const p = k.participantPn || k.participant;
    if (p?.endsWith('@s.whatsapp.net')) {
      return p.split('@')[0].split(':')[0];
    }
    return '';
  }

  const pn = k.senderPn || k.participantPn;
  if (pn?.endsWith('@s.whatsapp.net')) {
    return pn.split('@')[0].split(':')[0];
  }

  const rj = k.remoteJid ?? '';
  if (rj.endsWith('@s.whatsapp.net')) {
    return rj.split('@')[0].split(':')[0];
  }

  return '';
}

/** When PN fields are missing, resolve @lid → phone JID via WhatsApp. */
async function resolvePhoneWithSocket(msg: WAMessage, sock: WASocket): Promise<string> {
  const direct = extractInboundCustomerPhone(msg);
  if (direct) return direct;
  const rj = msg.key.remoteJid;
  if (rj && isLidUser(rj)) {
    try {
      const results = await sock.onWhatsApp(rj);
      const jid = results?.[0]?.jid;
      if (jid?.endsWith('@s.whatsapp.net')) {
        return jid.split('@')[0].split(':')[0];
      }
    } catch (e) {
      console.error('[Session] onWhatsApp(LID) failed:', e);
    }
  }
  return '';
}

function extractInboundText(msg: WAMessage): string {
  const content = extractMessageContent(msg.message);
  return (
    content?.conversation ||
    content?.extendedTextMessage?.text ||
    '[unsupported message type]'
  );
}

/** Phone / LID digits from WhatsApp @mentions (group tagging). */
function extractMentionedPhones(msg: WAMessage): string[] {
  const ctx =
    msg.message?.extendedTextMessage?.contextInfo ||
    msg.message?.imageMessage?.contextInfo ||
    msg.message?.documentMessage?.contextInfo ||
    msg.message?.audioMessage?.contextInfo;
  const jids = ctx?.mentionedJid ?? [];
  const out: string[] = [];
  for (const jid of jids) {
    if (typeof jid !== 'string') continue;
    if (jid.endsWith('@s.whatsapp.net') || jid.endsWith('@lid')) {
      const digits = jid.split('@')[0].split(':')[0].replace(/\D/g, '');
      if (digits) out.push(digits);
    }
  }
  return out;
}

/** Channel account identity digits (phone + LID) used to detect @tags aimed at us. */
function channelIdentityDigits(sock: WASocket): string[] {
  const user = sock.user as { id?: string; lid?: string } | undefined;
  if (!user) return [];
  const out: string[] = [];
  for (const raw of [user.id, user.lid]) {
    if (!raw || typeof raw !== 'string') continue;
    const digits = raw.split('@')[0].split(':')[0].replace(/\D/g, '');
    if (digits && !out.includes(digits)) out.push(digits);
  }
  return out;
}

/** Build JSON body for /internal/messages — text or image / voice_note / document with base64 media */
async function buildInboundApiPayload(
  msg: WAMessage,
  sock: WASocket,
  channelId: string,
  phone: string,
  waId: string,
): Promise<Record<string, unknown>> {
  const im = msg.message?.imageMessage;
  const doc = msg.message?.documentMessage;
  const aud = msg.message?.audioMessage;

  const ctx = {
    reuploadRequest: (m: WAMessage) => sock.updateMediaMessage(m),
    logger: sock.logger,
  };

  if (im) {
    try {
      const buffer = await downloadMediaMessage(msg, 'buffer', {}, ctx);
      const mime = im.mimetype || 'image/jpeg';
      const caption = im.caption?.trim();
      return {
        channelId,
        phone,
        waId,
        messageType: 'image',
        text: caption,
        mediaBase64: buffer.toString('base64'),
        mediaMime: mime,
      };
    } catch (e) {
      console.error('[Session] downloadMediaMessage (image) failed:', e);
      return { channelId, phone, waId, text: '[Image — download failed]' };
    }
  }

  if (doc) {
    try {
      const buffer = await downloadMediaMessage(msg, 'buffer', {}, ctx);
      const mime = doc.mimetype || 'application/octet-stream';
      const fileName = doc.fileName?.trim() || 'document';
      const caption = doc.caption?.trim();
      return {
        channelId,
        phone,
        waId,
        messageType: 'document',
        mediaFileName: fileName,
        text: caption,
        mediaBase64: buffer.toString('base64'),
        mediaMime: mime,
      };
    } catch (e) {
      console.error('[Session] downloadMediaMessage (document) failed:', e);
      const name = doc.fileName?.trim() || 'document';
      return { channelId, phone, waId, text: `[Document: ${name} — download failed]` };
    }
  }

  if (aud) {
    try {
      const buffer = await downloadMediaMessage(msg, 'buffer', {}, ctx);
      const mime = aud.mimetype || 'audio/ogg; codecs=opus';
      const seconds = aud.seconds != null ? Number(aud.seconds) : undefined;
      return {
        channelId,
        phone,
        waId,
        messageType: 'voice_note',
        mediaBase64: buffer.toString('base64'),
        mediaMime: mime,
        ...(seconds != null && !Number.isNaN(seconds) ? { mediaDurationSec: seconds } : {}),
      };
    } catch (e) {
      console.error('[Session] downloadMediaMessage (audio) failed:', e);
      return { channelId, phone, waId, text: '[Voice note — download failed]' };
    }
  }

  return { channelId, phone, waId, text: extractInboundText(msg) };
}

async function postToApi(apiUrl: string, p: string, body: unknown): Promise<void> {
  try {
    const res = await fetch(`${apiUrl}${p}`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(body),
    });
    if (!res.ok) {
      const snippet = await res.text().then(t => t.slice(0, 500));
      console.error(`[Session ${p}] API ${res.status}: ${snippet}`);
    }
  } catch (err) {
    console.error(`[Session ${p}] Failed to call API:`, err);
  }
}

export async function createBaileysSession(opts: SessionOptions): Promise<ActiveSession> {
  const { channelId, apiUrl } = opts;

  // Auth state stored per-channel in a temp folder (persists across worker restarts)
  const authFolder = authFolderFor(channelId);
  const { state, saveCreds } = await useMultiFileAuthState(authFolder);

  const { version } = await fetchLatestBaileysVersion();

  const sock = makeWASocket({
    version,
    auth: state,
    browser: ['WA Ticketing', 'Chrome', '126.0.0'],
    // Suppress deprecated printQRInTerminal warning
    printQRInTerminal: false,
  });

  // Persist creds whenever they update
  sock.ev.on('creds.update', saveCreds);

  // QR Code + connection state
  sock.ev.on('connection.update', async (update) => {
    const { connection, lastDisconnect, qr } = update;

    if (qr) {
      const qrDataUrl = await QRCode.toDataURL(qr);
      console.log(`[Session] QR generated for channel ${channelId}`);
      await postToApi(apiUrl, '/internal/ws-event', {
        event: 'qr:update',
        payload: { channelId, qrDataUrl },
      });
    }

    if (connection === 'open') {
      console.log(`[Session] Channel ${channelId} connected`);
      await postToApi(apiUrl, '/internal/channel-status', {
        channelId,
        status: 'connected',
      });
    }

    if (connection === 'close') {
      const statusCode = (lastDisconnect?.error as Boom)?.output?.statusCode;
      const shouldReconnect = statusCode !== DisconnectReason.loggedOut;
      const reason = shouldReconnect ? 'Connection closed — reconnecting' : 'Logged out';

      console.log(`[Session] Channel ${channelId} disconnected: ${reason}`);
      await postToApi(apiUrl, '/internal/channel-status', {
        channelId,
        status: 'disconnected',
        reason,
      });

      if (shouldReconnect) {
        console.log(`[Session] Reconnecting ${channelId} in 5s…`);
        setTimeout(async () => {
          const newSession = await createBaileysSession(opts);
          opts.onReconnect?.(newSession);
        }, 5000);
      } else {
        // Stale creds would block the next QR pairing — wipe on logout
        clearAuthState(channelId);
        opts.onLoggedOut?.(channelId);
      }
    }
  });

  // Inbound messages — Baileys uses both "notify" (live) and "append" (history / some clients)
  sock.ev.on('messages.upsert', async ({ messages, type }) => {
    if (type !== 'notify' && type !== 'append') return;

    for (const msg of messages) {
      if (!msg.message || msg.key.fromMe) continue;

      const phone = await resolvePhoneWithSocket(msg, sock);
      if (!phone) {
        console.warn(
          `[Session] Skip inbound (no phone): remoteJid=${msg.key.remoteJid} senderPn=${msg.key.senderPn ?? ''}`,
        );
        continue;
      }

      const waId = msg.key.id ?? '';
      const body = await buildInboundApiPayload(msg, sock, channelId, phone, waId);
      const remoteJid = msg.key.remoteJid ?? '';
      if (isJidGroup(remoteJid)) {
        body.groupJid = remoteJid;
        const mentioned = extractMentionedPhones(msg);
        if (mentioned.length) body.mentionedPhones = mentioned;
        const ids = channelIdentityDigits(sock);
        if (ids.length) body.channelMentionIds = ids;
      }
      await postToApi(apiUrl, '/internal/messages', body);
    }
  });

  return { socket: sock, channelId };
}
