/**
 * jobs/takedownWorker.js
 *
 * Background worker that processes takedown_jobs. Polls the queue every
 * few seconds, runs discovery + drafting + dispatch, persists progress,
 * and handles retries with exponential backoff.
 *
 * This is what makes the system feel instant: the HTTP /takedown endpoint
 * just enqueues a row and returns 202 in <50ms. The worker does the slow
 * stuff in the background.
 *
 * Backoff: 30s, 1m, 5m, 15m, 60m, then give up (max_attempts).
 */

'use strict';

const Takedown        = require('../models/takedown');
const Finding         = require('../models/finding');
const Case            = require('../models/case');
const platformContacts = require('../services/platformContacts');
const discovery        = require('../services/contactDiscovery');
const rdap             = require('../services/rdap');
const email            = require('../services/email');
const alertBus         = require('../services/alertBus');
const { db }           = require('../config/db');
const Audit            = require('../models/auditLog');
const escalationCfg    = require('../config/escalation');

const POLL_MS = parseInt(process.env.TAKEDOWN_WORKER_POLL_MS, 10) || 3000;
let timer = null;
let running = false;

function backoffMs(attempts) {
  // 30s, 1m, 5m, 15m, 60m, 2h, 4h
  const stages = [30_000, 60_000, 5 * 60_000, 15 * 60_000, 60 * 60_000, 2 * 3_600_000, 4 * 3_600_000];
  return stages[Math.min(attempts - 1, stages.length - 1)];
}

async function processJob(job) {
  const { id, case_id, user_id, url, domain, finding_id, draft_method, attempts, max_attempts } = job;
  const c = Case.getById(case_id);
  if (!c) {
    Takedown.setJobStatus(id, 'failed', { last_error: 'case not found', progress: 'case missing' });
    return;
  }

  // Plan-gate: re-check before dispatching (in case the user downgraded plans
  // after enqueuing).
  const usageLog = require('../models/usageLog');
  const { getPlan } = require('../config/plans');
  const u = db.prepare('SELECT * FROM users WHERE id = ?').get(user_id);
  if (!u) {
    Takedown.setJobStatus(id, 'failed', { last_error: 'user missing' });
    return;
  }
  const plan = getPlan(u.plan, u.plan_override);
  const tdUsed = usageLog.current(user_id, 'takedown');
  if (tdUsed >= plan.takedowns_limit) {
    Takedown.setJobStatus(id, 'failed', {
      last_error: `plan limit reached (${tdUsed}/${plan.takedowns_limit})`,
      progress: 'plan limit reached',
    });
    return;
  }

  // --- Step 1: discover ---
  Takedown.setJobStatus(id, 'discovering', { progress: 'Looking up contacts and hosting…' });
  let discoveryResult = null;
  try {
    discoveryResult = await discovery.discover(domain);
  } catch (e) {
    console.warn(`[worker] discovery failed for ${domain}:`, e.message);
    discoveryResult = { domain, primary: null, escalation_chain: [], errors: [e.message] };
  }

  // --- Step 2: enrich chain with RDAP-resolved registrar abuse email ---
  Takedown.setJobStatus(id, 'discovering', { progress: 'Resolving registrar (RDAP)…' });
  try {
    const rdapData = await rdap.lookup(domain);
    if (rdapData && rdapData.registrar_abuse_email) {
      // Replace the WHOIS-lookup placeholder step in the chain with a real
      // abuse email. If we already have a real abuse email from discovery,
      // add the registrar as a *prior* step (closer to operator than ICANN).
      const chain = (discoveryResult.escalation_chain || []).map(s => ({ ...s }));
      // Drop the old "WHOIS-lookup" placeholder if present
      const filtered = chain.filter(s => s.type !== 'registrar' || !s.address.startsWith('WHOIS-lookup'));
      // Find where registrar step currently sits and replace it
      const idx = filtered.findIndex(s => s.type === 'registrar');
      const registrarStep = {
        type: 'registrar',
        address: rdapData.registrar_abuse_email,
        source: `rdap:${rdapData.registrar || 'unknown'}`,
        why: `${rdapData.registrar || 'The registrar'} is the company that registered ${domain}. RFC 7485 requires them to maintain and respond to an abuse@ contact; they can suspend the domain if the operator ignores us.`,
      };
      if (idx >= 0) filtered[idx] = { ...filtered[idx], ...registrarStep, step: idx + 1 };
      else filtered.push({ step: filtered.length + 1, ...registrarStep });
      // Re-number
      discoveryResult.escalation_chain = filtered.map((s, i) => ({ ...s, step: i + 1 }));
    }
  } catch (e) {
    console.warn(`[worker] RDAP lookup failed for ${domain}:`, e.message);
  }

  const chain = discoveryResult.escalation_chain || [];
  Takedown.setJobStatus(id, 'discovering', {
    progress: `Chain built (${chain.length} step${chain.length === 1 ? '' : 's'})`,
    discovery_json: discoveryResult,
    escalation_json: chain,
  });

  // --- Step 3: pick the contact ---
  const known = platformContacts.getKnown(domain);
  let contact, platformName, curatedWins = false;

  if (known && known.contact) {
    contact = known.contact; platformName = known.name; curatedWins = true;
  } else if (discoveryResult.primary) {
    contact = discoveryResult.primary.contact; platformName = known ? known.name : domain;
  } else {
    // No direct contact. Create a takedown row that says "manual required"
    // and let the operator click the form / HackerOne / hosting chain.
    const disclosureStep = chain.find(s => s.type === 'platform-disclosure');
    const actionRequired = (known && known.form_url) ? { type: 'form', url: known.form_url }
                         : disclosureStep ? { type: 'hackerone', url: disclosureStep.address }
                         : { type: 'escalate', chain };
    const td = Takedown.create({
      case_id, finding_id, user_id, url, domain,
      contact_email: null, email_subject: null, email_body: null,
      draft_method: 'no-direct-email', status: 'manual-required',
    });
    // persist discovery + chain
    db.prepare('UPDATE takedowns SET discovery_json = ?, escalation_json = ? WHERE id = ?')
      .run(JSON.stringify(discoveryResult || {}), JSON.stringify(chain), td.id);
    Takedown.setJobStatus(id, 'manual_required', {
      progress: 'Manual action required',
      takedown_id: td.id,
      discovery_json: discoveryResult || {},
      escalation_json: chain,
      result_json: {
        takedown: td,
        action_required: actionRequired,
        hosting: discoveryResult.hosting || null,
        escalation_chain: chain,
      },
    });
    Audit.log(null, 'takedown.manual_required', `takedown:${td.id}`, {
      domain,
      form_url: (known && known.form_url) || discoveryResult.form_url || null,
      security: discoveryResult.security_txt || null,
    });
    // Informative alert (not an error) so the operator sees the user hit a
    // wall. info severity — just surfaces in the dashboard, doesn't email.
    alertBus.raise({
      severity: 'info',
      category: 'dispatch_fail',
      service: null,
      title: `Takedown needs manual action (no direct abuse email)`,
      detail: { takedown_id: td.id, case_id, url, domain, chain_steps: chain.length, hosting: discoveryResult.hosting },
      ref_url: `takedown:${td.id}`,
      dedupe_key: `manual_required:${td.id}`,
    });
    return {
      takedown: td,
      action_required: actionRequired,
    };
  }

  // If curated wins, fix the chain's first step.
  if (curatedWins && chain.length && chain[0].type === 'platform') {
    chain[0] = {
      ...chain[0],
      address: contact,
      source: 'platformContacts(curated)',
      why: 'Verified direct DMCA contact (curated database — last confirmed working).',
    };
  }

  // --- Step 4: build the email ---
  Takedown.setJobStatus(id, 'drafting', { progress: 'Drafting email…' });
  const from = { fromName: u.name || 'Shield Operator', fromEmail: u.email };
  const escalationText = chain.map(s =>
    `  ${s.step}. [${s.type.toUpperCase()}] ${s.address}\n     why: ${s.why}\n     source: ${s.source}`
  ).join('\n');

  let subject, body, method = draft_method === 'ai' ? 'ai' : (known ? 'template' : 'ai');
  if (method === 'template' || draft_method === 'template' || draft_method === 'auto') {
    ({ subject, body } = platformContacts.buildTemplate({
      to: contact,
      platformName,
      findingUrl: url,
      victimName: c.victim_name,
      referenceUrl: c.reference_url,
      escalationSteps: escalationText,
      hosting: discoveryResult.hosting,
      caseId: c.id, ...from,
    }));
    method = 'template';
  }
  if (method === 'ai' || draft_method === 'ai') {
    try {
      const draft = await aiDraftEmail({ url, victimName: c.victim_name, from, escalationText, discoveryResult, chain });
      subject = draft.subject; body = draft.body;
    } catch (e) {
      console.warn(`[worker] AI draft failed, falling back to template:`, e.message);
      ({ subject, body } = platformContacts.buildTemplate({
        to: contact, platformName, findingUrl: url,
        victimName: c.victim_name, referenceUrl: c.reference_url,
        escalationSteps: escalationText,
        hosting: discoveryResult.hosting, caseId: c.id, ...from,
      }));
      method = 'template';
    }
  }

  // --- Step 5: dispatch ---
  Takedown.setJobStatus(id, 'dispatching', { progress: `Sending to ${contact}…` });
  try {
    await email.sendTakedownEmail({ to: contact, subject, body });
  } catch (err) {
    console.warn(`[worker] dispatch failed (attempt ${attempts}/${max_attempts}):`, err.message);
    const delay = backoffMs(attempts);
    // First attempt failure → surface to operator now (not after exhaustion).
    if (attempts === 1) {
      alertBus.raise({
        severity: 'warn',
        category: 'dispatch_fail',
        service: 'resend',
        title: `Takedown dispatch failed (will retry)`,
        detail: { takedown_job_id: id, case_id, url, to: contact, error: err.message, retry_in_seconds: Math.round(delay/1000) },
        ref_url: `takedown_job:${id}`,
        dedupe_key: `dispatch:${id}`,
      });
    }
    if (attempts >= max_attempts) {
      Takedown.setJobStatus(id, 'failed', { last_error: `max attempts reached: ${err.message}` });
      alertBus.raise({
        severity: 'error',
        category: 'dispatch_fail',
        service: 'resend',
        title: `Takedown dispatch gave up after ${max_attempts} attempts`,
        detail: { takedown_job_id: id, case_id, url, to: contact, error: err.message },
        ref_url: `takedown_job:${id}`,
        dedupe_key: `dispatch_giveup:${id}`,
      });
      return;
    }
    Takedown.reschedule(id, delay, 'failed', `retry in ${Math.round(delay/1000)}s — ${err.message}`);
    return;
  }

  // --- Step 6: persist takedown row + job success ---
  const nextEscalateAt = Date.now() + escalationCfg.ESCALATION_WINDOW_MS;
  const td = Takedown.create({
    case_id, finding_id, user_id, url, domain,
    contact_email: contact, email_subject: subject, email_body: body,
    draft_method: method, status: 'sent',
  });
  // mark-as-sent
  Takedown.markAsSent(td.id);
  // Persist discovery + chain + escalation deadline on the takedown row
  db.prepare(`
    UPDATE takedowns
       SET discovery_json = ?, escalation_json = ?, next_escalate_at = ?
     WHERE id = ?
  `).run(JSON.stringify(discoveryResult), JSON.stringify(chain), nextEscalateAt, td.id);
  // Increment usage counter (do this at dispatch-success, not enqueue)
  usageLog.increment(user_id, 'takedown');

  Takedown.setJobStatus(id, 'sent', {
    progress: 'Done',
    takedown_id: td.id,
    result_json: {
      takedown: td,
      contact_email: contact,
      method,
      hosting: discoveryResult.hosting,
      escalation_chain: chain,
      next_escalate_at: nextEscalateAt,
      escalation_window_hours: escalationCfg.ESCALATION_WINDOW_MS / 3600000,
    },
  });
  Audit.log(u, 'takedown.sent', `takedown:${td.id}`, { domain, method, contact_kind: known ? 'curated' : 'discovered' });
}

async function tick() {
  if (running) return;
  running = true;
  try {
    const job = Takedown.claimNextJob();
    if (!job) return;
    try { await processJob(job); }
    catch (err) {
      console.error(`[worker] job ${job.id} crashed:`, err);
      Takedown.setJobStatus(job.id, 'failed', { last_error: err.message });
    }
  } finally {
    running = false;
  }
}

function start() {
  if (timer) return;
  // Run on a short poll interval. We process at most one job per tick to
  // avoid hammering external services; if there's a backlog, the next
  // tick picks up the next one.
  timer = setInterval(tick, POLL_MS);
  console.log(`[takedownWorker] started (poll ${POLL_MS}ms, max_attempts default 5)`);
  // Also run one tick immediately so the first job doesn't wait
  setTimeout(tick, 500);
}

function stop() {
  if (timer) clearInterval(timer);
  timer = null;
}

// --- AI draft helper (also used by routes/takedown.js preview) ---
async function aiDraftEmail({ url, victimName, from, escalationText, discoveryResult, chain }) {
  const systemPrompt = 'You are a DMCA/NCII takedown email drafter. Be concise, formal, and legally correct. Output JSON: {"subject":"...","body":"..."}';
  const userPrompt = `Draft a DMCA/NCII takedown email for the URL below. Use the platform's abuse@/dmca@/legal@ address in the To: header line. Sign off with the operator's details.

URL: ${url}
Victim name (as it appears online): ${victimName}
Operator: ${from.fromName} <${from.fromEmail}>

Hosting: ${discoveryResult?.hosting || 'unknown'}
${escalationText ? `\nEscalation chain (mention this in the body so the recipient knows the consequences of ignoring):\n${escalationText}\n` : ''}
Output JSON: {"subject": "...", "body": "full email body"}`;
  const keyRotator = require('../services/keyRotator');
  const start = Date.now();
  const { result, key } = await keyRotator.runWithRotation('MiniMax', async (k) => {
    // MiniMax_API_URL is the BASE URL — we always append /chat/completions
    // ourselves. If the env var accidentally includes a path (e.g.
    // ".../text/chatcompletion_v2") we strip the trailing path so the
    // concatenated URL is correct. Operators who want full control can
    // set MiniMax_API_URL to just "https://api.minimax.io/v1".
    const baseUrl = (process.env.MiniMax_API_URL || 'https://api.minimax.io/v1')
      .replace(/\/+$/, '')
      .replace(/\/chat\/?(completions)?$/i, '');
    const res = await fetch(`${baseUrl}/chat/completions`, {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${k.key_value}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        model: process.env.MiniMax_MODEL || 'MiniMax-2.0',
        messages: [
          { role: 'system', content: systemPrompt },
          { role: 'user',   content: userPrompt },
        ],
        response_format: { type: 'json_object' },
        temperature: 0.2,
        max_tokens: 700,
      }),
    });
    if (!res.ok) {
      const t = await res.text();
      throw new Error(`MiniMax ${res.status}: ${t.slice(0, 200)}`);
    }
    return res.json();
  });
  const content = result.choices?.[0]?.message?.content || '{}';
  let parsed;
  try { parsed = JSON.parse(content); }
  catch { const m = content.match(/\{[\s\S]*\}/); parsed = m ? JSON.parse(m[0]) : { subject: 'DMCA Takedown Request', body: content }; }
  // Log
  const inT  = result.usage?.prompt_tokens     || 0;
  const outT = result.usage?.completion_tokens || 0;
  const cost = ((inT / 1000) * 0.005) + ((outT / 1000) * 0.015);
  db.prepare(`
    INSERT INTO ai_calls (service, key_id, endpoint, tokens_in, tokens_out, cost_estimate, tier3_reason, success, response_ms, created_at)
    VALUES ('MiniMax', ?, 'takedown/draft', ?, ?, ?, 'takedown-draft', 1, ?, ?)
  `).run(key.id, inT, outT, cost, Date.now() - start, Date.now());
  return { subject: parsed.subject || '[NCII / DMCA] Immediate Removal Required', body: parsed.body || '' };
}

module.exports = { start, stop, tick, processJob, aiDraftEmail };
