/*
 * whatsapp-bridge — a real WhatsApp Web session that only ever reads.
 *
 * Long-lived sidecar to digest-engine. It keeps a logged-in web.whatsapp.com
 * session open and appends every incoming message to /data/messages.jsonl, which
 * digest-engine/ingest/whatsapp_ingest.py drains once per digest run.
 *
 * There is no send path in this file, and there must never be one: no reply(),
 * no sendMessage(), no sendSeen(), no chat.markUnread(). Per docs/project-plan.md
 * Phase 12 step 8, digest-engine has no mutation path anywhere by construction.
 *
 * READ digest-engine/README.md BEFORE ENABLING THIS. Automating a personal
 * WhatsApp account carries a real ban risk, mitigated here but not eliminated.
 */

'use strict';

const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
const qrcode = require('qrcode-terminal');
const { Client, LocalAuth } = require('whatsapp-web.js');

const DATA_DIR = process.env.WHATSAPP_DATA_DIR || '/data';
const MESSAGES_PATH = path.join(DATA_DIR, 'messages.jsonl');
const AUTH_PATH = path.join(DATA_DIR, '.wwebjs_auth');
// Document attachments land here. digest-engine mounts the parent of this bridge's
// /data, so what is /data/documents here is /data/whatsapp-bridge/documents there —
// see ingest/whatsapp_ingest.py, which resolves exactly that.
const DOCUMENTS_DIR = path.join(DATA_DIR, 'documents');
const MAX_DOCUMENT_BYTES = Number(process.env.WHATSAPP_MAX_DOCUMENT_BYTES || 10 * 1024 * 1024);

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

const client = new Client({
  authStrategy: new LocalAuth({ dataPath: AUTH_PATH }),
  puppeteer: {
    /*
     * headless: false is deliberate — do NOT "fix" this to true.
     *
     * WhatsApp's automation detection fingerprints headless Chrome specifically
     * (navigator.webdriver, the HeadlessChrome UA token, missing GPU/permissions
     * surface). Running a genuinely headful Chromium under Xvfb inside the
     * container removes that signal entirely rather than trying to patch around
     * it. The Dockerfile's `xvfb-run -a node index.js` is what supplies the
     * virtual display that makes headful possible with no monitor attached.
     */
    headless: false,
    executablePath: process.env.CHROMIUM_PATH || '/usr/bin/chromium',
    args: [
      // Chromium's sandbox needs privileges this container deliberately does not
      // have; the container itself is the isolation boundary here.
      '--no-sandbox',
      '--disable-setuid-sandbox',
      // /dev/shm defaults to 64MB in Docker, which crashes Chromium's renderer.
      '--disable-dev-shm-usage'
    ]
  }
});

client.on('qr', (qr) => {
  console.log('--- Scan this QR code with WhatsApp -> Linked devices -> Link a device ---');
  qrcode.generate(qr, { small: true });
  console.log('--- (docker compose logs -f whatsapp-bridge) ---');
});

client.on('authenticated', () => {
  console.log(`Authenticated. Session persisted under ${AUTH_PATH}.`);
});

client.on('auth_failure', (message) => {
  console.error(`Authentication failed: ${message}`);
  console.error(`Delete ${AUTH_PATH} and restart to re-scan the QR code.`);
});

client.on('ready', () => {
  console.log(`Ready. Appending received messages to ${MESSAGES_PATH}.`);
});

client.on('disconnected', (reason) => {
  console.error(`Disconnected: ${reason}. Exiting so the container restart policy reconnects.`);
  process.exit(1);
});

// 'message' fires for incoming messages only ('message_create' would also fire
// for our own outgoing ones, which are not what the digest summarises).
client.on('message', async (message) => {
  try {
    const contact = await message.getContact();
    const chat = await message.getChat();
    const record = {
      id: message.id ? message.id._serialized : null,
      from: message.from,
      from_name: contact ? (contact.pushname || contact.name || contact.number) : message.from,
      chat: chat ? chat.name : null,
      is_group: chat ? Boolean(chat.isGroup) : false,
      timestamp: message.timestamp,
      type: message.type,
      body: message.body || '',
      has_media: Boolean(message.hasMedia),
      // whatsapp-web.js exposes the document's own filename on the raw payload. This
      // is not part of its documented API and may simply be undefined on some
      // message types or library versions — in which case the digest still gets the
      // caption and the type, and agenda matching falls back to those.
      filename: (message._data && message._data.filename) || null,
      mimetype: (message._data && message._data.mimetype) || null,
      document_file: null
    };

    // DOCUMENTS ONLY, and only documents: a meeting agenda arrives as a PDF, and
    // digest-engine reads its text to pull out the agenda points. Photos, video and
    // audio are never downloaded — they are the bulk of what a group chat carries,
    // this container has no use for them, and every download is one more request
    // through a session that is already the highest-risk part of this project.
    if (message.hasMedia && message.type === 'document') {
      try {
        const media = await message.downloadMedia();
        const size = media && media.data ? Buffer.byteLength(media.data, 'base64') : 0;
        if (!media || !media.data) {
          console.error('A document had no downloadable data; recording it by name only.');
        } else if (size > MAX_DOCUMENT_BYTES) {
          console.error(`Document is ${size} bytes, over the ${MAX_DOCUMENT_BYTES} cap; recording it by name only.`);
        } else {
          // Sanitised basename plus a hash, so a hostile or merely awkward filename
          // ("../../etc/passwd", or the fourth "Tagesordnung.pdf" this month) can
          // neither escape this directory nor overwrite an earlier file.
          const raw = media.filename || record.filename || `${record.id || Date.now()}.bin`;
          const safe = path.basename(raw).replace(/[^A-Za-z0-9._-]+/g, '_').slice(0, 80) || 'document.bin';
          const stamp = crypto.createHash('sha256').update(String(record.id || raw)).digest('hex').slice(0, 8);
          const target = path.join(DOCUMENTS_DIR, `${stamp}-${safe}`);
          fs.writeFileSync(target, Buffer.from(media.data, 'base64'));
          record.document_file = path.basename(target);
        }
      } catch (err) {
        // A failed download costs the document's text, not the message.
        console.error(`Could not save a document: ${err}`);
      }
    }
    // appendFileSync opens/appends/closes per message, so whatsapp_ingest.py can
    // rename the file out from under us mid-run without losing a partial write.
    fs.appendFileSync(MESSAGES_PATH, JSON.stringify(record) + '\n', 'utf8');
  } catch (err) {
    console.error(`Could not record a message: ${err}`);
  }
});

client.initialize();
