/**
 * services/mhtmlBuilder.js
 *
 * Build a self-contained .mhtml file from a remote URL.
 *
 * MHTML (RFC 2557, "MIME Encapsulation of Aggregate HTML Documents")
 * is what Chrome/Edge produce when you "Save page as > Webpage, single file"
 * or "MHTML". One file, all images embedded as base64. Opens in
 * Chrome, Edge, IE11, and any modern mail client.
 *
 *   const m = await mhtmlBuilder.buildFromUrl('https://example.com/leaked');
 *   res.setHeader('Content-Type', 'multipart/related');
 *   res.setHeader('Content-Disposition', 'attachment; filename="finding-42.mhtml"');
 *   res.send(Buffer.from(m.body, 'binary'));
 *
 * CRITICAL: we DO NOT persist the page content. The build is a pure
 * function (URL → MHTML bytes) and the operator downloads directly.
 * Nothing is written to disk or to the database.
 */

'use strict';

const keyRotator = require('./keyRotator');
const URL_ENC    = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' };
const escapeAttr = (s) => String(s || '').replace(/[&<>"']/g, c => URL_ENC[c]);

// Whitelist of resource types we'll inline. Anything else stays as a
// remote URL inside the HTML. This keeps the file small and limits
// outbound fetches to what the operator actually needs to see.
const INLINEABLE = /\.(png|jpg|jpeg|gif|webp|svg|ico|bmp|avif)(\?.*)?$/i;

async function fetchPage(url) {
  // 1) Try a direct fetch (cheap).
  let text = null, finalUrl = url, contentType = 'text/html';
  try {
    const direct = await fetch(url, {
      redirect: 'follow',
      headers: { 'user-agent': 'Mozilla/5.0 (compatible; NCII-Shield-Evidence/1.0; +https://takedowns.vesamuni.com)' },
    });
    if (direct.ok) {
      text = await direct.text();
      finalUrl = direct.url || url;
      contentType = direct.headers.get('content-type') || 'text/html';
    }
  } catch (_) { /* fall through to scrape.do */ }

  // 2) Fall back to Scrape.do (handles Cloudflare-fronted sites).
  if (!text) {
    text = await keyRotator.runWithRotation('scrape_do', async (k) => {
      const u = new URL('https://api.scrape.do/');
      u.searchParams.set('token', k.key_value);
      u.searchParams.set('url', url);
      u.searchParams.set('render', 'true');
      u.searchParams.set('super', 'true');        // Cloudflare-friendly
      const r = await fetch(u.toString());
      if (!r.ok) throw new Error(`scrape ${r.status}`);
      return r.text();
    }).then(r => r.result);
  }

  return { html: text || '', finalUrl, contentType };
}

/**
 * Parse the HTML, collect <img>, <link rel=stylesheet>, <script src=>
 * references, fetch each, base64-encode the binary ones, swap the
 * URL to a content-id reference. CSS is also inlined so the page
 * renders without network. Scripts are NOT inlined (heavy + security
 * risk) — we leave them as external URLs.
 */
async function inlineResources(html, baseUrl) {
  const base = new URL(baseUrl);

  // Collect every <img src>
  const imgTags = [...html.matchAll(/<img\b[^>]*?\bsrc\s*=\s*(['"])(.*?)\1/gi)];
  const imgSrcs = new Set();
  for (const m of imgTags) imgSrcs.add(m[2]);

  // Collect every <link rel="stylesheet" href>
  const linkTags = [...html.matchAll(/<link\b[^>]*?\bhref\s*=\s*(['"])(.*?)\1[^>]*?>/gi)];
  const cssHrefs = new Set();
  for (const m of linkTags) {
    if (/rel\s*=\s*(['"])[^'"]*stylesheet/i.test(m[0])) cssHrefs.add(m[2]);
  }

  const out = { html, parts: [] };  // parts = the inlined resource bodies
  let partIndex = 0;
  const cidFor = () => 'part.' + (++partIndex) + '@ncii-shield.local';

  // ---- inline images ----
  for (const src of imgSrcs) {
    if (!INLINEABLE.test(src)) continue;
    let abs;
    try { abs = new URL(src, base).toString(); } catch { continue; }
    try {
      const r = await fetch(abs, { redirect: 'follow' });
      if (!r.ok) continue;
      const buf = Buffer.from(await r.arrayBuffer());
      const ct  = r.headers.get('content-type') || 'image/png';
      const b64 = buf.toString('base64');
      const cid = cidFor();
      out.parts.push({
        cid,
        contentType: ct,
        encoding: 'base64',
        body: b64,
        original: abs,
      });
      // Swap the src to a cid: reference
      out.html = out.html.replace(
        new RegExp(`(<img\\b[^>]*?\\bsrc\\s*=\\s*)(['"])(${escapeRegex(src)})(\\2)`, 'i'),
        `$1$2cid:${cid}$4`
      );
    } catch (_) { /* leave the remote URL */ }
  }

  // ---- inline CSS ----
  for (const href of cssHrefs) {
    let abs;
    try { abs = new URL(href, base).toString(); } catch { continue; }
    try {
      const r = await fetch(abs, { redirect: 'follow' });
      if (!r.ok) continue;
      const cssText = await r.text();
      // CSS often references background images — rebase those to the
      // base URL so the inlined rules still resolve. We don't try to
      // inline nested images here; browsers can still fetch them
      // unless the operator views the file offline.
      const cid = cidFor();
      out.parts.push({
        cid,
        contentType: 'text/css',
        encoding: 'quoted-printable',  // safe for css; we just put as-is below
        body: cssText,
        original: abs,
      });
      out.html = out.html.replace(
        new RegExp(`<link\\b[^>]*?\\bhref\\s*=\\s*(['"])(${escapeRegex(href)})(\\1)[^>]*?\\s*/?>`, 'i'),
        `<style><!-- inlined from ${escapeAttr(abs)} -->\n${cssText}\n</style>`
      );
    } catch (_) { /* leave the link tag */ }
  }

  return out;
}

function escapeRegex(s) {
  return String(s || '').replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}

/**
 * Build the MHTML byte stream. Returns { body, contentType, boundary }
 * where `body` is a Buffer ready to send.
 *
 * The format is:
 *   From: <saved by Shield>
 *   Subject: <url>
 *   Date: <RFC 2822>
 *   MIME-Version: 1.0
 *   Content-Type: multipart/related; boundary="<boundary>"
 *
 *   --<boundary>
 *   Content-Type: text/html; charset="utf-8"
 *   Content-Transfer-Encoding: quoted-printable
 *   Content-Location: <url>
 *
 *   <html…>
 *   --<boundary>
 *   Content-Type: image/png
 *   Content-Transfer-Encoding: base64
 *   Content-ID: <part-N@ncii-shield.local>
 *   Content-Location: <original image url>
 *
 *   <base64 bytes>
 *   --<boundary>--
 */
function buildMhtml({ html, finalUrl, parts, subject = 'Shield evidence' }) {
  const boundary = '----=_NextPart_NCII_Shield_' + Math.random().toString(36).slice(2, 10);
  const lines = [];

  // ---- envelope header (RFC 2557 + RFC 2822) ----
  lines.push(`From: <saved by Shield>`);
  lines.push(`Subject: ${subject}`);
  lines.push(`Date: ${new Date().toUTCString()}`);
  lines.push(`MIME-Version: 1.0`);
  lines.push(`Content-Type: multipart/related; type="text/html"; boundary="${boundary}"`);
  lines.push('');

  // ---- HTML root part ----
  lines.push(`--${boundary}`);
  lines.push(`Content-Type: text/html; charset="utf-8"`);
  lines.push(`Content-Transfer-Encoding: quoted-printable`);
  lines.push(`Content-Location: ${finalUrl}`);
  // Patch all cid: references in the HTML so the <img src="cid:.."> survives QP.
  const safeHtml = html.replace(/cid:([^\s"']+)/g, 'cid:$1');
  lines.push('');
  lines.push(qpEncode(safeHtml));
  lines.push('');

  // ---- resource parts ----
  for (const p of parts || []) {
    lines.push(`--${boundary}`);
    lines.push(`Content-Type: ${p.contentType}`);
    lines.push(`Content-Transfer-Encoding: ${p.encoding || 'base64'}`);
    lines.push(`Content-ID: <${p.cid}>`);
    if (p.original) lines.push(`Content-Location: ${p.original}`);
    lines.push('');
    if (p.encoding === 'base64') {
      // Re-wrap base64 into 76-char lines per RFC 2045.
      const wrapped = (p.body || '').match(/.{1,76}/g)?.join('\r\n') || '';
      lines.push(wrapped);
    } else {
      lines.push(p.body);
    }
    lines.push('');
  }

  lines.push(`--${boundary}--`);

  return {
    body: Buffer.from(lines.join('\r\n'), 'utf8'),
    contentType: `multipart/related; type="text/html"; boundary="${boundary}"`,
  };
}

// Minimal quoted-printable encoder. We encode everything outside the
// base64 parts — the bulk of the file is base64 (images) so the QP
// overhead is just for the HTML root.
function qpEncode(input) {
  // Split on \r?\n and preserve line endings.
  const out = [];
  for (const line of String(input).split(/\r?\n/)) {
    let buf = '';
    for (const ch of line) {
      const code = ch.charCodeAt(0);
      if (code === 9 || (code >= 32 && code <= 126 && code !== 61)) {
        buf += ch;
      } else {
        buf += '=' + code.toString(16).toUpperCase().padStart(2, '0');
      }
      if (buf.length >= 72) {
        // soft line break
        out.push(buf.slice(0, 72) + '=');
        buf = buf.slice(72);
      }
    }
    out.push(buf);
  }
  return out.join('\r\n');
}

/**
 * One-shot builder: URL → MHTML bytes. Never writes to disk.
 */
async function buildFromUrl(url) {
  const page = await fetchPage(url);
  if (!page.html) {
    throw new Error('MHTML_BUILD_FAILED: no content fetched from ' + url);
  }
  const inlined = await inlineResources(page.html, page.finalUrl);
  const m = buildMhtml({
    html: inlined.html,
    finalUrl: page.finalUrl,
    parts: inlined.parts,
    subject: `Shield evidence — ${page.finalUrl}`,
  });
  return { body: m.body, contentType: m.contentType, finalUrl: page.finalUrl, parts: inlined.parts.length };
}

module.exports = { buildFromUrl, buildMhtml, inlineResources, fetchPage };
