/**
 * services/alertBus.js
 *
 * Operator-only alert pipeline. NEVER expose alerts/alert_prefs to end users.
 *
 * Design:
 *  - raise({ severity, category, service, title, detail, ref_url, actor_id, dedupe_key })
 *      → inserts an `alerts` row, attempts to email the configured recipient,
 *        and dedupes by (category, service, ref_url) within a 30-minute window.
 *
 *  - get/ack/list for /admin/alerts
 *  - getPrefs / setPref for /admin/alert-prefs
 *
 * Severities:
 *  - info     → log only, no email
 *  - warn     → email only if "warn_email_enabled" pref is true
 *  - error    → email if "error_email_enabled" pref is true (default true)
 *  - critical → ALWAYS email; this is the "production is on fire" tier
 *
 * Recipients:
 *  - alert_prefs['email_to']   — comma-separated list (defaults to RESEND_ALERT_TO env)
 *  - alert_prefs['warn_email_enabled'] / ['error_email_enabled'] — '1' / '0'
 */

'use strict';

const crypto = require('crypto');
const { db }  = require('../config/db');
const email  = require('./email');
const Audit  = require('../models/auditLog');

const DEFAULTS = {
  email_to:              process.env.RESEND_ALERT_TO || process.env.SEED_ADMIN_EMAIL || '',
  warn_email_enabled:    '0',
  error_email_enabled:   '1',
  critical_email_enabled:'1',   // always
  // When the same dedupe_key appears within this window, suppress
  // the duplicate alert (but always keep the row in the DB).
  dedupe_window_ms:      '1800000', // 30 min
};

// Per-category dedup window overrides. Persistent-state alerts
// (e.g. "chain step 1 is a URL not an email", "Scrape.do quota
// exhausted", "health probe blocked") don't change between cron
// ticks, so the default 30-min window means we spam a new alert
// every 30-60 min until the operator acknowledges. Longer windows
// keep the noise down while still letting the dashboard reflect
// the current state.
const CATEGORY_DEDUPE_MS = {
  // 'auto-escalate blocked: registrar step is a URL' is a permanent
  // property of the takedown until the operator fixes the chain.
  // Fire ONCE, then never again until the takedown is resolved.
  'auto_escalate_blocked': 7 * 24 * 60 * 60 * 1000,    // 7 days
  // 'health probe blocked' — same takedown, same target, same block.
  // Once is enough; 24h lets the operator re-check after fixing keys.
  'health_blocked':        24 * 60 * 60 * 1000,        // 24h
  // Scrape.do / MiniMax / Resend outages often span hours. 6h
  // dedup means at most 4 alerts per day for a sustained outage,
  // instead of one every 30 min.
  'api_error':             6 * 60 * 60 * 1000,         // 6h
  // Default — keep at 30 min for the rest.
};

function getPref(key) {
  const row = db.prepare('SELECT value FROM alert_prefs WHERE key = ?').get(key);
  if (row) return row.value;
  return Object.prototype.hasOwnProperty.call(DEFAULTS, key) ? DEFAULTS[key] : null;
}
function setPref(key, value) {
  db.prepare(`
    INSERT INTO alert_prefs (key, value, updated_at) VALUES (?, ?, ?)
    ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at
  `).run(key, value, Date.now());
}
function getAllPrefs() {
  const rows = db.prepare('SELECT key, value FROM alert_prefs').all();
  const out = { ...DEFAULTS };
  for (const r of rows) out[r.key] = r.value;
  return out;
}

function makeRef() {
  return crypto.randomBytes(4).toString('hex'); // 8 hex chars
}

/**
 * Suppression: if an alert with the same dedupe_key was raised within the
 * last `dedupe_window_ms` ms, return the existing row instead of inserting.
 */
function findDuplicate(dedupeKey, windowMs) {
  if (!dedupeKey) return null;
  const row = db.prepare(`
    SELECT * FROM alerts
     WHERE dedupe_key = ? AND created_at > ?
     ORDER BY created_at DESC LIMIT 1
  `).get(dedupeKey, Date.now() - windowMs);
  return row || null;
}

/**
 * Raise an alert. Returns the row (new or existing-duplicate).
 */
function raise(opts) {
  const {
    severity       = 'error',
    category       = 'other',
    service        = null,
    title,
    detail         = null,
    ref_url        = null,
    actor_id       = null,
    dedupe_key     = null,
    email_override = null,    // set to false to skip email; set to an address to force-send
  } = opts;

  if (!title) throw new Error('alertBus.raise: title required');
  if (!['info','warn','error','critical'].includes(severity)) {
    throw new Error(`alertBus.raise: bad severity ${severity}`);
  }

  const windowMs = (() => {
    // 1) Per-key override (passed via opts, e.g. emergency = 0)
    if (opts.dedupe_window_ms != null) return parseInt(opts.dedupe_window_ms, 10) || 0;
    // 2) Per-dedupe-key prefix (so 'auto_escalate_blocked:18:1' matches
    //    the CATEGORY_DEDUPE_MS['auto_escalate_blocked'] bucket).
    if (dedupe_key) {
      for (const prefix of Object.keys(CATEGORY_DEDUPE_MS)) {
        if (dedupe_key === prefix || dedupe_key.startsWith(prefix + ':')) {
          return CATEGORY_DEDUPE_MS[prefix];
        }
      }
    }
    // 3) Operator-configured default (alert_prefs.dedupe_window_ms)
    return parseInt(getPref('dedupe_window_ms') || DEFAULTS.dedupe_window_ms, 10);
  })();
  const existing = findDuplicate(dedupe_key, windowMs);
  if (existing) {
    // Already raised recently. Re-arm the email if this is a NEW occurrence
    // since (e.g. 6th quota failure in 30 min). We just bump a counter.
    db.prepare(`UPDATE alerts SET detail = ? WHERE id = ?`)
      .run(JSON.stringify({ count: ((JSON.parse(existing.detail||'{}').count || 1) + 1), last_detail: detail }), existing.id);
    return { alert: existing, deduped: true, emailed: false };
  }

  const ref = makeRef();
  const detailJson = detail ? (typeof detail === 'string' ? detail : JSON.stringify(detail)) : null;
  const info = db.prepare(`
    INSERT INTO alerts (ref, severity, category, service, actor_id, title, detail, ref_url, dedupe_key, created_at)
    VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
  `).run(ref, severity, category, service, actor_id, title, detailJson, ref_url, dedupe_key, Date.now());
  const alert = db.prepare('SELECT * FROM alerts WHERE id = ?').get(info.lastInsertRowid);

  // Email (best-effort, never throws to caller)
  if (email_override !== false) {
    maybeEmail(alert, email_override).catch(err => {
      console.warn('[alertBus] email send failed:', err.message);
    });
  }

  return { alert, deduped: false, emailed: false };
}

async function maybeEmail(alert, forceTo) {
  const prefs = getAllPrefs();
  // Per-severity gating
  const shouldEmail =
    alert.severity === 'critical' ||
    (alert.severity === 'error'   && prefs.error_email_enabled   === '1') ||
    (alert.severity === 'warn'    && prefs.warn_email_enabled    === '1') ||
    (alert.severity === 'info'    && prefs.info_email_enabled    === '1');
  if (!shouldEmail) return;

  const to = forceTo || prefs.email_to;
  if (!to) {
    console.warn('[alertBus] no alert recipient configured (alert_prefs.email_to)');
    return;
  }

  const subject = severityPrefix(alert.severity) + alert.title;
  const body = formatAlertEmail(alert);
  try {
    await email.sendRaw(to, subject, body, { tag: 'alert' });
    db.prepare('UPDATE alerts SET email_sent = 1 WHERE id = ?').run(alert.id);
  } catch (err) {
    // Don't re-raise — we don't want alert-email failure to break the caller.
    console.warn('[alertBus] failed to email alert', alert.ref, err.message);
  }
}

function severityPrefix(s) {
  return ({
    critical: '🚨 [CRITICAL] ',
    error:    '❌ [ERROR] ',
    warn:     '⚠️  [WARN] ',
    info:     'ℹ️  [INFO] ',
  })[s] || '';
}

function formatAlertEmail(a) {
  const lines = [
    `Alert: ${a.title}`,
    `Ref:    ${a.ref}`,
    `When:   ${new Date(a.created_at).toISOString()}`,
    `Severity: ${a.severity}`,
    `Category: ${a.category}`,
    a.service ? `Service: ${a.service}` : null,
    a.ref_url  ? `Where:  ${a.ref_url}`   : null,
    '',
  ].filter(Boolean);
  if (a.detail) {
    lines.push('Detail:');
    lines.push(typeof a.detail === 'string' ? a.detail : JSON.stringify(a.detail, null, 2));
  }
  lines.push('');
  lines.push('Open the admin panel: /admin → Alerts to acknowledge or dig in.');
  lines.push('— Shield alert bus');
  return lines.join('\n');
}

// ----- Admin query helpers -----
function list({ severity = null, service = null, category = null, acknowledged = null, limit = 100, offset = 0 } = {}) {
  const where = [];
  const params = [];
  if (severity)    { where.push('severity = ?');    params.push(severity); }
  if (service)     { where.push('service = ?');     params.push(service); }
  if (category)    { where.push('category = ?');    params.push(category); }
  if (acknowledged !== null) { where.push('acknowledged = ?'); params.push(acknowledged ? 1 : 0); }
  const sql = `
    SELECT * FROM alerts
     ${where.length ? 'WHERE ' + where.join(' AND ') : ''}
     ORDER BY created_at DESC
     LIMIT ? OFFSET ?
  `;
  params.push(limit, offset);
  return db.prepare(sql).all(...params);
}

function getByRef(ref) { return db.prepare('SELECT * FROM alerts WHERE ref = ?').get(ref); }
function getById(id)   { return db.prepare('SELECT * FROM alerts WHERE id = ?').get(id); }
function acknowledge(id, userId) {
  db.prepare(`UPDATE alerts SET acknowledged = 1, ack_by = ?, ack_at = ? WHERE id = ?`)
    .run(userId, Date.now(), id);
  return getById(id);
}
function counts() {
  return db.prepare(`
    SELECT
      SUM(CASE WHEN acknowledged = 0 THEN 1 ELSE 0 END) AS unack,
      SUM(CASE WHEN severity = 'critical' AND acknowledged = 0 THEN 1 ELSE 0 END) AS unack_critical,
      SUM(CASE WHEN severity = 'error' AND acknowledged = 0 THEN 1 ELSE 0 END) AS unack_error,
      COUNT(*) AS total
      FROM alerts
  `).get();
}

module.exports = {
  raise,
  list, getByRef, getById, acknowledge, counts,
  getPref, setPref, getAllPrefs,
  // convenience: a ref → row lookup that user-facing support code can call
  // with the short ref the user was given. The detail field never leaves
  // the admin surface — if a regular user calls this we deliberately
  // return only the public-facing fields.
  publicView(ref) {
    const a = getByRef(ref);
    if (!a) return null;
    return {
      ref:       a.ref,
      severity:  severityPublicLabel(a.severity),
      category:  a.category,
      title:     a.title,
      created_at:a.created_at,
      acknowledged: !!a.acknowledged,
    };
  },
};

function severityPublicLabel(s) {
  // Map internal severity → customer-friendly bucket. Customer never sees
  // "critical" / "error" terminology — those are operator concerns.
  return ({
    critical: 'investigation_required',
    error:    'investigation_required',
    warn:     'in_progress',
    info:     'in_progress',
  })[s] || 'in_progress';
}
