import path from 'node:path';
import fs from 'node:fs';
import baileys, {
  DisconnectReason,
  useMultiFileAuthState,
  fetchLatestBaileysVersion,
  makeCacheableSignalKeyStore,
} from '@whiskeysockets/baileys';
import express from 'express';
import pino from 'pino';
import qrTerminal from 'qrcode-terminal';
import { normalizeChatId } from './allowlist.js';

const makeWASocket = baileys.default || baileys.makeWASocket || baileys;

const SESSION_DIR = process.env.WHATSAPP_SESSION_DIR
  ? path.resolve(process.env.WHATSAPP_SESSION_DIR)
  : path.resolve(process.cwd(), 'data', 'whatsapp', 'session');
const PORT = Number(process.env.WHATSAPP_BRIDGE_PORT || 3210);
const HOST = process.env.WHATSAPP_BRIDGE_HOST || '127.0.0.1';
const QUEUE_CAP = Number(process.env.WHATSAPP_BRIDGE_QUEUE_CAP || 100);

fs.mkdirSync(SESSION_DIR, { recursive: true });

// Known-benign Baileys log messages from optional bootstrap queries (server
// properties, presence updates without push name, etc). They time out because
// WhatsApp's server doesn't always reply to unofficial Web clients on these
// optional queries — but the socket stays fully functional. Suppress them so
// the daemon log isn't noisy.
const SUPPRESSED_LOG_PATTERNS = [
  /unexpected error in 'init queries'/i,
  /no name present, ignoring presence update/i,
  /failed to fetch ADV/i,
  /^stream errored out$/i,
];

const basePino = pino({ level: process.env.WHATSAPP_BRIDGE_LOG_LEVEL || 'warn' });

function logMessageText(arg) {
  if (typeof arg === 'string') return arg;
  if (arg && typeof arg === 'object' && typeof arg.msg === 'string') return arg.msg;
  return '';
}

function isSuppressed(args) {
  for (const arg of args) {
    const text = logMessageText(arg);
    if (text && SUPPRESSED_LOG_PATTERNS.some(re => re.test(text))) return true;
  }
  return false;
}

function wrapLogger(target) {
  const wrap = (level) => (...args) => {
    if (isSuppressed(args)) return;
    return target[level](...args);
  };
  return {
    level: target.level,
    fatal: wrap('fatal'),
    error: wrap('error'),
    warn: wrap('warn'),
    info: wrap('info'),
    debug: wrap('debug'),
    trace: wrap('trace'),
    child: (bindings) => wrapLogger(target.child(bindings)),
  };
}

const logger = wrapLogger(basePino);

let sock = null;
let latestQr = null;
let pairingState = 'starting';
let lastError = null;
let lastConnectedAt = null;

const queue = [];

function enqueue(entry) {
  queue.push(entry);
  while (queue.length > QUEUE_CAP) queue.shift();
}

function nowIso() {
  return new Date().toISOString();
}

async function startSocket() {
  const { state, saveCreds } = await useMultiFileAuthState(SESSION_DIR);
  const { version } = await fetchLatestBaileysVersion().catch(() => ({ version: undefined }));

  sock = makeWASocket({
    version,
    logger,
    auth: {
      creds: state.creds,
      keys: makeCacheableSignalKeyStore(state.keys, logger),
    },
    printQRInTerminal: false,
    browser: ['madarX', 'Chrome', '1.0.0'],
    markOnlineOnConnect: false,
    syncFullHistory: false,
  });

  sock.ev.on('creds.update', saveCreds);

  sock.ev.on('connection.update', (update) => {
    const { connection, lastDisconnect, qr } = update;
    if (qr) {
      latestQr = qr;
      pairingState = 'qr_required';
      try { qrTerminal.generate(qr, { small: true }); } catch {}
      console.log('[whatsapp-bridge] QR available. Scan from WhatsApp → Linked Devices.');
    }
    if (connection === 'open') {
      latestQr = null;
      pairingState = 'paired';
      lastConnectedAt = nowIso();
      lastError = null;
      console.log('[whatsapp-bridge] connected', sock.user?.id || '');
      // Announce presence so WhatsApp server starts pushing inbound messages
      // to this linked device. Without this, some accounts silently drop
      // pushes until the device sends an initial presence update.
      sock.sendPresenceUpdate('available').catch((err) => {
        console.error('[whatsapp-bridge] presence update failed:', err.message);
      });
    }
    if (connection === 'close') {
      const reason = lastDisconnect?.error?.output?.statusCode;
      const loggedOut = reason === DisconnectReason.loggedOut;
      lastError = lastDisconnect?.error?.message || `disconnected:${reason || 'unknown'}`;
      pairingState = loggedOut ? 'logged_out' : 'reconnecting';
      console.error('[whatsapp-bridge] connection closed:', lastError);
      if (loggedOut) {
        console.error('[whatsapp-bridge] logged out. Wipe session dir and re-pair to recover.');
        return;
      }
      setTimeout(() => {
        startSocket().catch((err) => {
          console.error('[whatsapp-bridge] reconnect failed:', err.message);
        });
      }, 2000);
    }
  });

  sock.ev.on('messages.upsert', ({ messages, type }) => {
    if (type !== 'notify' && type !== 'append') return;
    for (const msg of messages) {
      if (!msg.message || msg.key?.fromMe) continue;
      const chatId = msg.key.remoteJid;
      if (!chatId) continue;
      const text = extractText(msg.message);
      if (!text) continue;
      // Allowlist enforcement is delegated to lib/whatsapp-router.js,
      // which has DB + dashboard access. Bridge forwards everything.
      console.log(`[whatsapp-bridge] queued inbound chat_id=${chatId} sender=${msg.key.participant || chatId}`);
      enqueue({
        id: msg.key.id,
        message_id: msg.key.id,
        chat_id: chatId,
        sender: msg.key.participant || msg.key.remoteJid,
        text,
        timestamp: typeof msg.messageTimestamp === 'number'
          ? new Date(msg.messageTimestamp * 1000).toISOString()
          : nowIso(),
      });
    }
  });
}

function extractText(message) {
  if (!message) return null;
  if (typeof message.conversation === 'string' && message.conversation.length) return message.conversation;
  if (typeof message.extendedTextMessage?.text === 'string' && message.extendedTextMessage.text.length) return message.extendedTextMessage.text;
  if (message.ephemeralMessage?.message) return extractText(message.ephemeralMessage.message);
  if (message.viewOnceMessage?.message) return extractText(message.viewOnceMessage.message);
  if (message.viewOnceMessageV2?.message) return extractText(message.viewOnceMessageV2.message);
  if (typeof message.imageMessage?.caption === 'string') return message.imageMessage.caption;
  if (typeof message.videoMessage?.caption === 'string') return message.videoMessage.caption;
  if (typeof message.buttonsResponseMessage?.selectedDisplayText === 'string') {
    return message.buttonsResponseMessage.selectedDisplayText;
  }
  if (typeof message.listResponseMessage?.title === 'string') {
    return message.listResponseMessage.title;
  }
  return null;
}

function requireReady(res) {
  if (!sock || pairingState !== 'paired') {
    res.status(409).json({ ok: false, error: `bridge not paired (state=${pairingState})` });
    return false;
  }
  return true;
}

const app = express();
// Bumped to fit document payloads (base64-encoded HTML artifacts) for /send-document.
// Bridge listens on 127.0.0.1 only, so the cap is just a safety net, not a DoS surface.
app.use(express.json({ limit: '32mb' }));

app.get('/health', (_req, res) => {
  res.json({
    ok: true,
    pairing_state: pairingState,
    has_qr: Boolean(latestQr),
    queue_size: queue.length,
    last_connected_at: lastConnectedAt,
    last_error: lastError,
    session_dir: SESSION_DIR,
    user: sock?.user?.id || null,
  });
});

app.get('/qr', (_req, res) => {
  if (pairingState === 'paired') {
    return res.json({ ok: true, paired: true, qr: null });
  }
  if (!latestQr) {
    return res.json({ ok: true, paired: false, qr: null, pairing_state: pairingState });
  }
  res.json({ ok: true, paired: false, qr: latestQr, pairing_state: pairingState });
});

app.get('/messages', (_req, res) => {
  const drained = queue.splice(0, queue.length);
  res.json({ ok: true, messages: drained });
});

app.post('/send', async (req, res) => {
  if (!requireReady(res)) return;
  const chatId = normalizeChatId(req.body?.chat_id || req.body?.chatId);
  const text = String(req.body?.text || '');
  if (!chatId || !text) {
    return res.status(400).json({ ok: false, error: 'chat_id and text are required' });
  }
  try {
    const result = await sock.sendMessage(chatId, { text });
    res.json({ ok: true, message_id: result?.key?.id || null });
  } catch (err) {
    res.status(500).json({ ok: false, error: err.message });
  }
});

app.post('/send-document', async (req, res) => {
  if (!requireReady(res)) return;
  const chatId = normalizeChatId(req.body?.chat_id || req.body?.chatId);
  const contentB64 = req.body?.content_base64;
  const filename = String(req.body?.filename || '').trim();
  const mimetype = String(req.body?.mimetype || 'application/octet-stream');
  const caption = req.body?.caption ? String(req.body.caption) : undefined;
  if (!chatId || !contentB64 || !filename) {
    return res.status(400).json({ ok: false, error: 'chat_id, content_base64, and filename are required' });
  }
  let buffer;
  try {
    buffer = Buffer.from(contentB64, 'base64');
  } catch (err) {
    return res.status(400).json({ ok: false, error: `invalid base64: ${err.message}` });
  }
  if (!buffer.length) return res.status(400).json({ ok: false, error: 'empty document body' });
  try {
    const result = await sock.sendMessage(chatId, {
      document: buffer,
      fileName: filename,
      mimetype,
      caption,
    });
    res.json({ ok: true, message_id: result?.key?.id || null });
  } catch (err) {
    res.status(500).json({ ok: false, error: err.message });
  }
});

app.post('/typing', async (req, res) => {
  if (!requireReady(res)) return;
  const chatId = normalizeChatId(req.body?.chat_id || req.body?.chatId);
  const state = req.body?.state === 'paused' ? 'paused' : 'composing';
  if (!chatId) return res.status(400).json({ ok: false, error: 'chat_id is required' });
  try {
    await sock.sendPresenceUpdate(state, chatId);
    res.json({ ok: true });
  } catch (err) {
    res.status(500).json({ ok: false, error: err.message });
  }
});

app.post('/logout', async (req, res) => {
  // True disconnect: unlink this device from WhatsApp server-side (so it
  // disappears from Linked Devices), wipe local creds, then restart the
  // socket so a fresh QR is offered for re-pairing.
  try {
    try {
      if (sock?.logout) await sock.logout();
    } catch (err) {
      // logout may throw if the socket is already down — still wipe + restart.
      console.error('[whatsapp-bridge] logout call failed (continuing):', err.message);
    }
    try { sock?.end?.(undefined); } catch {}
    sock = null;
    latestQr = null;
    lastConnectedAt = null;
    lastError = null;
    pairingState = 'starting';
    try {
      fs.rmSync(SESSION_DIR, { recursive: true, force: true });
    } catch (err) {
      return res.status(500).json({ ok: false, error: `failed to wipe session: ${err.message}` });
    }
    fs.mkdirSync(SESSION_DIR, { recursive: true });
    startSocket().catch((err) => {
      console.error('[whatsapp-bridge] restart after logout failed:', err.message);
    });
    console.log('[whatsapp-bridge] logged out and wiped session; awaiting re-pair.');
    res.json({ ok: true });
  } catch (err) {
    res.status(500).json({ ok: false, error: err.message });
  }
});

app.get('/chat/:id', async (req, res) => {
  if (!requireReady(res)) return;
  const chatId = normalizeChatId(req.params.id);
  if (!chatId) return res.status(400).json({ ok: false, error: 'invalid chat id' });
  res.json({ ok: true, chat_id: chatId });
});

const server = app.listen(PORT, HOST, () => {
  console.log(`[whatsapp-bridge] listening on http://${HOST}:${PORT}`);
});

function shutdown(signal) {
  console.log(`[whatsapp-bridge] ${signal} received, shutting down`);
  try { server.close(); } catch {}
  try { sock?.end?.(undefined); } catch {}
  setTimeout(() => process.exit(0), 250);
}
process.on('SIGINT', () => shutdown('SIGINT'));
process.on('SIGTERM', () => shutdown('SIGTERM'));

startSocket().catch((err) => {
  console.error('[whatsapp-bridge] failed to start socket:', err);
  process.exit(1);
});
