/**
 * routes/takedown.js
 *
 * Async-by-default: the HTTP POST enqueues a takedown_job and returns 202
 * with a job_id. The worker (jobs/takedownWorker.js) processes the job
 * in the background and updates the job's status. The UI polls
 * GET /takedown/job/:id to render progress.
 *
 * Why async:
 *  - Contact discovery can take 3-10 seconds (security.txt + sitemaps +
 *    Cloudflare header probe + RDAP for registrar).
 *  - The user shouldn't wait on a single request to fire off 50 takedowns.
 *  - Failed dispatches can be retried with backoff without re-prompting
 *    the user.
 */

'use strict';

const path = require('path');
const express = require('express');
const validator = require('validator');
const { requireLogin } = require('../middleware/auth');
const { sendUserError } = require('../middleware/userErrors');
const alertBus = require('../services/alertBus');
const planGate = require('../middleware/planGate');
const Case     = require('../models/case');
const Finding  = require('../models/finding');
const Takedown = require('../models/takedown');
const Audit    = require('../models/auditLog');
const platformContacts = require('../services/platformContacts');
const discovery        = require('../services/contactDiscovery');
const rdap             = require('../services/rdap');
const email    = require('../services/email');
const { db }   = require('../config/db');
const escalationCfg = require('../config/escalation');
const worker  = require('../jobs/takedownWorker');

const router = express.Router();

function fromFor(user) {
  return {
    fromName:  user.name || 'Shield Operator',
    fromEmail: user.email,
  };
}

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

(async function ensureTakedownColumns() {
  try {
    const cols = db.prepare('PRAGMA table_info(takedowns)').all().map(c => c.name);
    if (!cols.includes('discovery_json'))  db.exec('ALTER TABLE takedowns ADD COLUMN discovery_json TEXT');
    if (!cols.includes('escalation_json')) db.exec('ALTER TABLE takedowns ADD COLUMN escalation_json TEXT');
    if (!cols.includes('escalated_at'))    db.exec('ALTER TABLE takedowns ADD COLUMN escalated_at INTEGER');
    if (!cols.includes('next_escalate_at'))db.exec('ALTER TABLE takedowns ADD COLUMN next_escalate_at INTEGER');
  } catch (e) { console.warn('[takedown] column check failed:', e.message); }
})();

// List historical takedowns (resolved jobs)
router.get('/', requireLogin, (req, res) => {
  const list = (req.user.role === 'superadmin')
    ? Takedown.listAllTakedowns(100, 0)
    : Takedown.listForUser(req.user.id, 100, 0);
  res.json({ takedowns: list });
});

router.get('/jobs', requireLogin, (req, res) => {
  const list = (req.user.role === 'superadmin')
    ? db.prepare(`SELECT * FROM takedown_jobs ORDER BY created_at DESC LIMIT 200`).all()
    : Takedown.listJobsForUser(req.user.id, 200);
  res.json({ jobs: list });
});

router.get('/job/:id', requireLogin, (req, res) => {
  const job = Takedown.getJob(parseInt(req.params.id, 10));
  if (!job) return res.status(404).json({ error: 'NOT_FOUND' });
  if (req.user.role !== 'superadmin' && job.user_id !== req.user.id) {
    return res.status(403).json({ error: 'FORBIDDEN' });
  }
  let discovery = null, escalation = null, result = null;
  try { discovery = job.discovery_json ? JSON.parse(job.discovery_json) : null; } catch {}
  try { escalation = job.escalation_json ? JSON.parse(job.escalation_json) : null; } catch {}
  try { result = job.result_json ? JSON.parse(job.result_json) : null; } catch {}
  res.json({ job, discovery, escalation, result });
});

router.get('/preview', requireLogin, async (req, res) => {
  try {
    const caseId = parseInt(req.query.case_id, 10);
    const url    = req.query.url;
    if (!url || !validator.isURL(url, { require_protocol: true })) {
      return res.status(400).json({ error: 'INVALID_URL' });
    }
    const c = Case.getById(caseId);
    if (!c) return res.status(404).json({ error: 'CASE_NOT_FOUND' });
    if (req.user.role !== 'superadmin' && c.user_id !== req.user.id && c.org_id !== req.user.org_id) {
      return res.status(403).json({ error: 'FORBIDDEN' });
    }
    const domain = Finding.domainOf(url);
    const known  = platformContacts.getKnown(domain);
    const from   = fromFor(req.user);
    let discoveryResult = null, chain = [];
    try { discoveryResult = await discovery.discover(domain); chain = discoveryResult.escalation_chain || []; } catch (e) {}
    // Enrich with RDAP
    try {
      const rdapData = await rdap.lookup(domain);
      if (rdapData && rdapData.registrar_abuse_email) {
        const filtered = chain.filter(s => !(s.type === 'registrar' && s.address.startsWith('WHOIS-lookup')));
        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 || 'Registrar'} (per RDAP) — RFC 7485 abuse contact.`,
        };
        if (idx >= 0) filtered[idx] = { ...filtered[idx], ...registrarStep, step: idx + 1 };
        else filtered.push({ step: filtered.length + 1, ...registrarStep });
        chain = filtered.map((s, i) => ({ ...s, step: i + 1 }));
        if (discoveryResult) discoveryResult.escalation_chain = chain;
      }
    } catch (e) {}
    const contact = (known && known.contact) || (discoveryResult && discoveryResult.primary && discoveryResult.primary.contact);
    if (!contact) {
      return res.json({
        ok: true, contact: null, escalation_chain: chain, discovery: discoveryResult,
        reason: 'No direct email found; manual action required.',
        security: known && known.security, form_url: known && known.form_url,
      });
    }
    const { subject, body } = platformContacts.buildTemplate({
      to: contact, platformName: known ? known.name : domain,
      findingUrl: url, victimName: c.victim_name, referenceUrl: c.reference_url,
      escalationSteps: chainToText(chain),
      hosting: discoveryResult && discoveryResult.hosting,
      caseId: c.id, ...from,
    });
    res.json({ ok: true, contact, subject, body, escalation_chain: chain, discovery: discoveryResult });
  } catch (err) {
    console.error('[takedown] preview error:', err);
    res.status(500).json({ error: 'INTERNAL', message: err.message });
  }
});

/**
 * POST /takedown/
 * body: { case_id, url, finding_id?, draft_method? }
 * Returns 202 with a job_id. The worker processes it in the background.
 */
router.post('/', requireLogin, planGate('takedown'), async (req, res) => {
  try {
    const { case_id, finding_id, url, draft_method = 'auto' } = req.body || {};
    if (!url || !validator.isURL(url, { require_protocol: true })) {
      return sendUserError(res, 'INVALID_INPUT', {
        publicMessage: "That doesn't look like a valid URL. Please check and try again.",
        adminDetail: { url, field: 'url' },
      });
    }
    const c = Case.getById(parseInt(case_id, 10));
    if (!c) return sendUserError(res, 'NOT_FOUND', {
      publicMessage: "We couldn't find that case.",
      adminDetail: { case_id },
    });
    if (req.user.role !== 'superadmin' && c.user_id !== req.user.id && c.org_id !== req.user.org_id) {
      return sendUserError(res, 'FORBIDDEN');
    }
    const domain = Finding.domainOf(url);
    const jobId = Takedown.enqueueJob({
      case_id: c.id, user_id: req.user.id, url, domain,
      finding_id: finding_id ? parseInt(finding_id, 10) : null,
      draft_method,
    });
    Audit.log(req.user, 'takedown.enqueue', `job:${jobId}`, { domain, url });
    res.status(202).json({
      ok: true, job_id: jobId, status: 'queued',
      poll_url: `/takedown/job/${jobId}`,
      message: `Queued. Watch the "Recent jobs" panel for live progress.`,
    });
  } catch (err) {
    console.error('[takedown] enqueue error:', err);
    return sendUserError(res, 'INTERNAL', {
      adminDetail: { message: err.message, body: req.body },
      alertTitle:  `takedown enqueue error`,
    });
  }
});

/**
 * POST /takedown/batch
 * body: { case_id, urls: [string, ...], draft_method? }
 * Enqueues all of them as separate jobs. Returns 202 with array of job_ids.
 */
router.post('/batch', requireLogin, planGate('takedown'), async (req, res) => {
  try {
    const { case_id, urls, draft_method = 'auto' } = req.body || {};
    if (!Array.isArray(urls) || urls.length === 0) {
      return sendUserError(res, 'INVALID_INPUT', { publicMessage: 'Please provide at least one URL.' });
    }
    if (urls.length > 100) {
      return sendUserError(res, 'INVALID_INPUT', {
        publicMessage: 'You can submit up to 100 takedowns at a time. Please split into smaller batches.',
        adminDetail: { submitted: urls.length, max: 100 },
      });
    }
    const c = Case.getById(parseInt(case_id, 10));
    if (!c) return sendUserError(res, 'NOT_FOUND', { adminDetail: { case_id } });
    if (req.user.role !== 'superadmin' && c.user_id !== req.user.id && c.org_id !== req.user.org_id) {
      return sendUserError(res, 'FORBIDDEN');
    }
    const ids = [];
    for (const u of urls) {
      if (!validator.isURL(u, { require_protocol: true })) continue;
      const domain = Finding.domainOf(u);
      const id = Takedown.enqueueJob({
        case_id: c.id, user_id: req.user.id, url: u, domain,
        draft_method,
      });
      ids.push({ job_id: id, url: u, domain });
    }
    Audit.log(req.user, 'takedown.batch', `case:${c.id}`, { count: ids.length });
    res.status(202).json({
      ok: true, queued: ids,
      message: `Queued ${ids.length} takedown(s). They'll process in the background.`,
    });
  } catch (err) {
    console.error('[takedown] batch error:', err);
    return sendUserError(res, 'INTERNAL', { adminDetail: { message: err.message } });
  }
});

/**
 * GET /takedown/:id — return a single takedown row with full discovery +
 * chain (for the UI to render the chain visualization).
 */
router.get('/:id', requireLogin, (req, res) => {
  const td = Takedown.getById(parseInt(req.params.id, 10));
  if (!td) return res.status(404).json({ error: 'NOT_FOUND' });
  if (req.user.role !== 'superadmin' && td.user_id !== req.user.id) {
    return res.status(403).json({ error: 'FORBIDDEN' });
  }
  let discovery = null, chain = null;
  try { discovery = td.discovery_json ? JSON.parse(td.discovery_json) : null; } catch {}
  try { chain = td.escalation_json ? JSON.parse(td.escalation_json) : null; } catch {}
  res.json({ takedown: td, discovery, escalation_chain: chain });
});

/**
 * Manually re-run a failed job (retry now).
 */
router.post('/job/:id/retry', requireLogin, (req, res) => {
  const job = Takedown.getJob(parseInt(req.params.id, 10));
  if (!job) return res.status(404).json({ error: 'NOT_FOUND' });
  if (req.user.role !== 'superadmin' && job.user_id !== req.user.id) {
    return res.status(403).json({ error: 'FORBIDDEN' });
  }
  Takedown.reschedule(job.id, 0, 'queued', null);
  Audit.log(req.user, 'takedown.retry', `job:${job.id}`);
  res.json({ ok: true, job_id: job.id, status: 'queued' });
});

/**
 * Cancel a queued/active job (best effort).
 */
router.post('/job/:id/cancel', requireLogin, (req, res) => {
  const job = Takedown.getJob(parseInt(req.params.id, 10));
  if (!job) return res.status(404).json({ error: 'NOT_FOUND' });
  if (req.user.role !== 'superadmin' && job.user_id !== req.user.id) {
    return res.status(403).json({ error: 'FORBIDDEN' });
  }
  if (!['queued', 'failed', 'discovering', 'drafting', 'dispatching'].includes(job.status)) {
    return res.status(400).json({ error: 'CANNOT_CANCEL', status: job.status });
  }
  Takedown.setJobStatus(job.id, 'cancelled', { progress: 'Cancelled by operator' });
  Audit.log(req.user, 'takedown.cancel', `job:${job.id}`);
  res.json({ ok: true, job_id: job.id, status: 'cancelled' });
});

/**
 * Legacy /:id/status — operator-driven status update (responded, removed, etc.)
 * Kept for the existing UI's status dropdown.
 */
router.post('/:id/status', requireLogin, (req, res) => {
  const td = Takedown.getById(parseInt(req.params.id, 10));
  if (!td) return res.status(404).json({ error: 'NOT_FOUND' });
  if (req.user.role !== 'superadmin' && td.user_id !== req.user.id) {
    return res.status(403).json({ error: 'FORBIDDEN' });
  }
  const { status, error } = req.body || {};
  if (!['opened','responded','removed','rejected','failed'].includes(status)) {
    return res.status(400).json({ error: 'INVALID_STATUS' });
  }
  const now = Date.now();
  const extras = { error: error || null };
  if (status === 'opened')     extras.opened_at     = now;
  if (status === 'responded')  extras.responded_at  = now;
  if (status === 'removed')    extras.resolved_at   = now;
  Takedown.updateStatus(td.id, status, extras);
  Audit.log(req.user, 'takedown.status', `takedown:${td.id}`, { status });

  // Auto-learner: when the operator marks a takedown as removed or
  // responded, that contact address is PROVEN to work. Promote it in
  // the learned contact map so the next case for the same domain
  // uses it at the highest confidence.
  if ((status === 'removed' || status === 'responded') && td.domain && td.contact_email) {
    try {
      const contactsRepo = require('../services/platformContactsRepo');
      contactsRepo.recordObservedSuccess({ domain: td.domain, contact: td.contact_email });
    } catch (_) { /* don't fail the operator action if the repo errors */ }
  }
  // When the operator marks it as failed/rejected, downgrade the
  // contact so the system prefers alternatives next time.
  if ((status === 'failed' || status === 'rejected') && td.domain && td.contact_email) {
    try {
      const contactsRepo = require('../services/platformContactsRepo');
      contactsRepo.recordFailure({ domain: td.domain, contact: td.contact_email, reason: error || status });
    } catch (_) {}
  }

  res.json({ ok: true });
});

/**
 * Edit a takedown row's contact_email / subject / body before sending.
 * Any change to the contact_email counts as an admin_override in the
 * learned contact map (confidence=1.0).
 */
router.patch('/:id', requireLogin, (req, res) => {
  const td = Takedown.getById(parseInt(req.params.id, 10));
  if (!td) return res.status(404).json({ error: 'NOT_FOUND' });
  if (req.user.role !== 'superadmin' && td.user_id !== req.user.id) {
    return res.status(403).json({ error: 'FORBIDDEN' });
  }
  if (td.status !== 'production_pending' && td.status !== 'queued' && td.status !== 'chain_only') {
    return res.status(400).json({ error: 'NOT_EDITABLE', message: 'Can only edit takedowns that have not been sent yet.' });
  }
  const { contact_email, email_subject, email_body } = req.body || {};
  const validator = require('validator');
  const updates = {};
  if (contact_email !== undefined) {
    if (contact_email && !validator.isEmail(contact_email)) {
      return res.status(400).json({ error: 'INVALID_EMAIL' });
    }
    updates.contact_email = contact_email || null;
  }
  if (email_subject !== undefined) updates.email_subject = email_subject;
  if (email_body   !== undefined) updates.email_body   = email_body;
  if (Object.keys(updates).length === 0) return res.json({ ok: true, takedown: td });
  const setSql = Object.keys(updates).map(k => `${k} = ?`).join(', ');
  const { db } = require('../config/db');
  db.prepare(`UPDATE takedowns SET ${setSql} WHERE id = ?`).run(...Object.values(updates), td.id);
  Audit.log(req.user, 'takedown.edit', `takedown:${td.id}`, { fields: Object.keys(updates) });

  // If the operator changed the contact, record the new one in the
  // learned contact map as an admin_override — it's now the verified
  // address for this domain.
  if (updates.contact_email && updates.contact_email !== td.contact_email) {
    try {
      const contactsRepo = require('../services/platformContactsRepo');
      contactsRepo.recordAdminOverride({
        domain: td.domain,
        contact: updates.contact_email,
        notes: `Set by operator ${req.user.email} on takedown ${td.id} (was: ${td.contact_email || 'none'})`,
      });
    } catch (_) {}
  }

  res.json({ ok: true, takedown: { ...td, ...updates } });
});

module.exports = router;
