/**
 * jobs/linkHealthWorker.js
 *
 * Periodically re-checks every sent takedown's URL to detect:
 *   - 'removed'   → mark takedown.removed, promote contact to
 *                   observed_success (locks it in for future cases)
 *   - 'live'      → still serving; if past 30h with no escalation,
 *                   trigger one. If past 60h, escalate faster.
 *   - 'blocked'   → leave alone, retry on next tick
 *   - 'dns_fail'  → could be host hiccup; retry on next tick
 *   - 'soft_404'  → treat as removed (the operator-visible badge
 *                   will say "removed" with reason="soft 404")
 *
 * Schedule: every 4h. Each tick checks at most 50 takedowns (so
 * we don't burn through Scrape.do credits on day 1 with 1000+ rows).
 * Round-robin: oldest check first.
 *
 * Each check = 1 Scrape.do credit. 6 checks/day × 100 takedowns
 * = 600 credits/day. Well under our 1000/day limit.
 */

'use strict';

const { db } = require('../config/db');
const checker = require('../services/linkHealthChecker');
const platformContactsRepo = require('../services/platformContactsRepo');
const Takedown = require('../models/takedown');
const Case = require('../models/case');
const Audit = require('../models/auditLog');
const alertBus = require('../services/alertBus');
const linkHealthRepo = require('../models/linkHealth');

let timer = null;
const MAX_PER_TICK = 50;
const TICK_MS = 4 * 60 * 60 * 1000;     // 4h

/**
 * Apply a single health check result to the takedown row + raise
 * alerts / update the auto-learner.
 */
async function applyResult(td, check) {
  // 1) Record the check
  linkHealthRepo.record({
    takedown_id: td.id, url: td.url,
    status: check.status, http_status: check.http_status,
    reason: check.reason, body_size: check.body_size,
    body_hash: check.body_hash, response_ms: check.response_ms,
  });

  // 2) Decide what to do
  if (check.status === 'removed' || check.status === 'soft_404') {
    // 2a. Mark takedown as removed
    const now = Date.now();
    db.prepare(`UPDATE takedowns SET status = 'removed', resolved_at = COALESCE(resolved_at, ?), updated_at = ? WHERE id = ?`)
      .run(now, now, td.id);
    Audit.log(null, 'takedown.removed', `takedown:${td.id}`, {
      check_status: check.status, reason: check.reason, http_status: check.http_status,
    });
    alertBus.raise({
      severity: 'info', category: 'takedown',
      title: `Content removed: ${td.domain}`,
      detail: {
        takedown_id: td.id, case_id: td.case_id, domain: td.domain,
        contact: td.contact_email, check_status: check.status, reason: check.reason,
        http_status: check.http_status,
      },
      ref_url: `takedown:${td.id}`, dedupe_key: `takedown_removed:${td.id}`,
    });
    // 2b. Auto-learner: promote the contact to observed_success
    // 2c. Webhook fanout — high-signal event, every configured
    //     Slack/Discord/Teams/JSON endpoint should hear it.
    try {
      const notifier = require('../services/notifier');
      notifier.notify('takedown.removed', {
        takedown_id: td.id, case_id: td.case_id, domain: td.domain,
        contact: td.contact_email, url: td.url,
        check_status: check.status, http_status: check.http_status,
        reason: check.reason,
        removed_at: Date.now(),
      });
    } catch (_) { /* best-effort */ }

    if (td.contact_email) {
      try {
        const promoted = platformContactsRepo.recordObservedSuccess({
          domain: td.domain, contact: td.contact_email,
          source: 'link_health',
          notes: `Content was removed (${check.status}) after takedown #${td.id} was sent on ${td.sent_at ? new Date(td.sent_at).toISOString() : 'unknown'}.`,
        });
        if (promoted) {
          alertBus.raise({
            severity: 'info', category: 'auto_learner',
            title: `Contact promoted to observed_success: ${td.domain} → ${td.contact_email}`,
            detail: { confidence: promoted.confidence, source: 'link_health', takedown_id: td.id },
            ref_url: `contact:${promoted.id}`, dedupe_key: `promote:${td.id}:${td.contact_email}`,
          });
        }
      } catch (_) { /* best-effort */ }
    }
    return 'removed';
  }

  if (check.status === 'live') {
    // Still serving. If we haven't escalated in >30h, do it now.
    // (autoEscalate handles its own cron; we just nudge the alert.)
    const hoursSinceSent = td.sent_at ? (Date.now() - td.sent_at) / 3600000 : 0;
    if (hoursSinceSent > 30 && !td.escalated_at) {
      alertBus.raise({
        severity: 'warn', category: 'takedown',
        title: `Takedown stale + content still live: ${td.domain}`,
        detail: {
          takedown_id: td.id, domain: td.domain, hours_since_sent: Math.floor(hoursSinceSent),
          contact: td.contact_email, check_reason: check.reason,
          why: 'autoEscalate cron will forward to the next chain step on its next tick.',
        },
        ref_url: `takedown:${td.id}`, dedupe_key: `takedown_stale_live:${td.id}`,
      });
    }
    return 'still_live';
  }

  if (check.status === 'blocked') {
    alertBus.raise({
      severity: 'info', category: 'takedown',
      title: `Health probe blocked: ${td.domain} (${check.status})`,
      detail: {
        takedown_id: td.id, domain: td.domain,
        contact: td.contact_email, http_status: check.http_status,
        why: 'Could not determine if content is removed. Will retry on the next health-check tick.',
      },
      ref_url: `takedown:${td.id}`, dedupe_key: `health_blocked:${td.id}`,
    });
    return 'blocked';
  }
  return check.status;
}

async function tick() {
  // Round-robin: takedowns in 'sent' or 'escalated' state, oldest
  // last_checked first. Test-data takedowns are skipped — they're
  // never going to actually serve or remove content, and probing
  // them just burns Scrape.do credits + raises noise alerts.
  const rows = db.prepare(`
    SELECT t.*,
           COALESCE(lhc.last_checked, 0) as last_checked,
           lh.status as last_health_status
      FROM takedowns t
      LEFT JOIN (
        SELECT takedown_id, MAX(checked_at) as last_checked
          FROM link_health_checks GROUP BY takedown_id
      ) lhc ON lhc.takedown_id = t.id
      LEFT JOIN link_health_checks lh
        ON lh.takedown_id = t.id AND lh.checked_at = lhc.last_checked
     WHERE t.status IN ('sent', 'escalated', 'failed')
       AND t.url IS NOT NULL
       AND COALESCE(t.is_test_data, 0) = 0
     ORDER BY lhc.last_checked ASC
     LIMIT ?
  `).all(MAX_PER_TICK);
  if (!rows.length) return { checked: 0 };
  let removed = 0, still_live = 0, blocked = 0, soft = 0, err = 0;
  for (const td of rows) {
    // Skip rows checked in the last 30 min (anti-flap)
    if (td.last_checked && (Date.now() - td.last_checked) < 30 * 60 * 1000) continue;
    // If the last health check was a 'blocked' (Scrape.do quota etc),
    // skip this takedown until the quota resets. We don't want to
    // burn more Scrape.do credits on rows that already failed.
    if (td.last_health_status === 'blocked' && td.last_checked && (Date.now() - td.last_checked) < 6 * 3600 * 1000) continue;
    try {
      const c = Case.getById(td.case_id);
      const victim = c ? c.victim_name : null;
      // Look up the previous check's body size for the body-shrink
      // heuristic. If the body suddenly shrank by >80% AND no NCII
      // indicators match, we call it soft_404 even if the page 200s.
      const prev = linkHealthRepo.latestFor(td.id);
      const check = await checker.checkUrl(td.url, { victimName: victim, previousBodySize: prev ? prev.body_size : null });
      const outcome = await applyResult(td, check);
      if (outcome === 'removed' || outcome === 'soft_404') { soft++; removed++; }
      else if (outcome === 'still_live') still_live++;
      else if (outcome === 'blocked') blocked++;
      else err++;
    } catch (e) {
      console.warn('[linkHealth] takedown #' + td.id + ' check error:', e.message);
      err++;
    }
  }
  console.log('[linkHealth] tick: checked=' + rows.length + ' removed=' + removed + ' live=' + still_live + ' blocked=' + blocked + ' err=' + err);
  return { checked: rows.length, removed, still_live, blocked, err };
}

function start() {
  if (timer) return;
  // Run once on start (so we can show the operator fresh state)
  tick().catch(e => {
    if (global.__ncii_workers) global.__ncii_workers.linkHealth = 'failed';
    console.error('[linkHealth] initial tick error:', e.message);
  });
  timer = setInterval(() => {
    tick()
      .then(() => { if (global.__ncii_workers) global.__ncii_workers.linkHealth = 'running'; })
      .catch(e => {
        if (global.__ncii_workers) global.__ncii_workers.linkHealth = 'failed';
        console.error('[linkHealth] tick error:', e.message);
      });
  }, TICK_MS);
  console.log('[linkHealth] worker started (every ' + (TICK_MS / 3600000) + 'h, max ' + MAX_PER_TICK + ' checks/tick)');
}

module.exports = { start, tick, applyResult, MAX_PER_TICK, TICK_MS };
