/**
 * jobs/autoEscalate.js
 * Periodically scan takedowns that:
 *   - have been sent (status = 'sent')
 *   - have NOT been responded to / removed
 *   - are past their `next_escalate_at` deadline (default: 30 hours)
 * and forward the notice to the next step in the escalation chain.
 *
 * This is what "always escalate" means in practice: the system actually does it,
 * not just promises. Tunables in config/escalation.js.
 */

'use strict';

const { db }    = require('../config/db');
const Takedown  = require('../models/takedown');
const Case      = require('../models/case');
const platformContacts = require('../services/platformContacts');
const email     = require('../services/email');
const Audit     = require('../models/auditLog');
const escalationCfg = require('../config/escalation');
const { db: raw } = require('../config/db');

let timer = null;

function chainToText(chain) {
  if (!chain || !chain.length) return '(none)';
  return chain.map(s =>
    `  ${s.step}. [${s.type.toUpperCase()}] ${s.address}\n     why: ${s.why}\n     source: ${s.source}`
  ).join('\n');
}

async function tryEscalate(td) {
  if (!td.escalation_json) return;
  let chain;
  try { chain = JSON.parse(td.escalation_json); } catch { return; }
  if (!Array.isArray(chain) || !chain.length) return;
  if (td.status !== 'sent' && td.status !== 'queued' && td.status !== 'failed') return;
  if (!td.next_escalate_at || td.next_escalate_at > Date.now()) return;
  if (td.escalated_at && td.escalated_at > (td.sent_at || 0)) return; // already escalated
  // Skip test-data takedowns — they're noise. Operators flag rows via
  // scripts/flag-test-data.js (or the future admin UI) and the
  // autoEscalate cron should ignore them entirely.
  if (td.is_test_data) return;

  // Find the next step that hasn't been tried.
  const tried = new Set();
  // We don't have a per-step history, but we do know the current contact_email
  // has been tried. Also try "responded_at" presence as a stop.
  if (td.responded_at) return; // operator marked as responded
  if (td.contact_email) tried.add(td.contact_email);
  const next = chain.find(s => !tried.has(s.address));
  if (!next) return; // exhausted

  const c = Case.getById(td.case_id);
  if (!c) return;

  // Validate the escalation address is a real email before sending.
  // Some chain steps (e.g. HackerOne URLs, ICANN forms) are URLs, not
  // mailto: addresses. We don't skip them silently any more — that
  // would leave the operator in the dark. We raise an alert so the
  // dashboard shows a "manual escalation required" badge.
  const validator = require('validator');
  if (!validator.isEmail(next.address)) {
    try {
      const Alert = require('../services/alertBus');
      Alert.raise({
        severity: 'warn', category: 'escalation',
        title: `Auto-escalate blocked: ${td.domain} → ${next.type} step is a URL, not an email`,
        detail: {
          takedown_id: td.id,
          chain_step: next.step,
          chain_type: next.type,
          url: next.address,
          why: 'Some escalation steps (HackerOne, ICANN lookup, manual forms) are web forms, not email addresses. The operator must escalate manually to these.',
          contact_attempted: td.contact_email,
        },
        ref_url: `takedown:${td.id}`,
        dedupe_key: `auto_escalate_blocked:${td.id}:${next.step}`,
      });
    } catch (_) {}
    return;
  }

  const fromName  = 'Shield Auto-Escalation';
  const fromEmail = process.env.RESEND_FROM_EMAIL || 'noreply@yourdomain.com';
  const { subject, body } = platformContacts.buildTemplate({
    to: next.address,
    platformName: td.domain,
    findingUrl: td.url,
    victimName: c.victim_name,
    referenceUrl: c.reference_url,
    escalationSteps: chainToText(chain),
    hosting: null,
    caseId: c.id,
    fromName, fromEmail,
  });
  const escalatedBody = `==============================================================
AUTOMATIC ESCALATION — STEP ${next.step} of ${chain.length}
Takedown #${td.id}, escalated at ${new Date().toISOString()}
==============================================================
This is an AUTOMATIC 30-hour escalation from Shield.
The original notice was sent to ${td.contact_email || '(no direct email)'} on ${td.sent_at ? new Date(td.sent_at).toISOString() : new Date(td.created_at).toISOString()}. No response was received within 30 HOURS.

You are receiving this because the previous recipient did not respond.
We have also notified other parties in the chain in parallel.

Your role: [${next.type.toUpperCase()}]
Original case: ${td.url}
Original contact: ${td.contact_email || '(no direct email — manual form/HackerOne report filed)'}

==============================================================
BELOW IS THE ORIGINAL NOTICE
==============================================================

` + body;

  try {
    await email.sendTakedownEmail({ to: next.address, subject: `[ESCALATION] ${subject}`, body: escalatedBody });
    raw.prepare(`
      UPDATE takedowns
         SET escalated_at = ?, contact_email = ?, status = 'sent', sent_at = COALESCE(sent_at, ?)
       WHERE id = ?
    `).run(Date.now(), next.address, Date.now(), td.id);
    Audit.log(null, 'takedown.auto_escalate', `takedown:${td.id}`, { to: next.address, type: next.type, chain_step: next.step });
    // Webhook fanout — every configured endpoint hears the escalation
    try {
      const notifier = require('../services/notifier');
      notifier.notify('takedown.escalated', {
        takedown_id: td.id, case_id: td.case_id, domain: td.domain,
        from: td.contact_email, to: next.address,
        chain_step: next.step, chain_type: next.type,
        escalated_at: Date.now(),
      });
    } catch (_) { /* best-effort */ }
    console.log(`[autoEscalate] takedown #${td.id} → ${next.address} (${next.type})`);
  } catch (e) {
    console.warn(`[autoEscalate] takedown #${td.id} escalation failed: ${e.message}`);
    raw.prepare(`UPDATE takedowns SET error = ?, status = 'failed' WHERE id = ?`).run(e.message, td.id);
  }
}

async function tick() {
  // A row is eligible to escalate when:
  //   - its next_escalate_at has passed, AND
  //   - it has never been escalated, OR the last escalation was before
  //     the most recent sent_at.
  // The COALESCE-trap that bit us before: when sent_at is NULL,
  // COALESCE(sent_at, 0) returned 0, so `escalated_at < 0` was always
  // false → takedowns that were never marked sent (chain-only rows,
  // legacy data) would never escalate. Now we fall back to using
  // next_escalate_at - 1 as a sentinel, which is always < any real
  // escalated_at.
  const rows = db.prepare(`
    SELECT t.* FROM takedowns t
     WHERE t.status IN ('sent', 'queued', 'failed')
       AND t.next_escalate_at IS NOT NULL
       AND t.next_escalate_at < ?
       AND (t.escalated_at IS NULL
            OR t.escalated_at < COALESCE(t.sent_at, t.next_escalate_at - 1))
       AND COALESCE(t.is_test_data, 0) = 0
     LIMIT 20
  `).all(Date.now());
  for (const td of rows) {
    try { await tryEscalate(td); } catch (e) { console.error('[autoEscalate] tick error:', e.message); }
  }
}

function start() {
  if (timer) return;
  // Run immediately, then every AUTO_ESCALATE_TICK_MS
  tick();
  timer = setInterval(tick, escalationCfg.AUTO_ESCALATE_TICK_MS);
  console.log(`[autoEscalate] cron started (every ${escalationCfg.AUTO_ESCALATE_TICK_MS / 60000}m, escalation window ${escalationCfg.ESCALATION_WINDOW_MS / 3600000}h)`);
}

module.exports = { start, tick, tryEscalate };
