/**
 * 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,
  extractMessageContent,
  fetchLatestBaileysVersion,
  isJidGroup,
  isLidUser,
  useMultiFileAuthState,
  type WAMessage,
  type WASocket,
} from '@whiskeysockets/baileys';
import { Boom } from '@hapi/boom';
import QRCode from 'qrcode';
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;
}

/**
 * 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]'
  );
}

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 = path.join(os.tmpdir(), 'wa_ticketing_auth', 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);
      }
    }
  });

  // 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 text = extractInboundText(msg);

      await postToApi(apiUrl, '/internal/messages', { channelId, phone, waId, text });
    }
  });

  return { socket: sock, channelId };
}
