/**
 * services/webhook.js
 *
 * Pure-HTTP webhook delivery. No email involved — just POST a JSON
 * payload to the operator's endpoint (Slack incoming webhook,
 * Discord webhook, Teams, PagerDuty, custom, etc.).
 *
 * Each webhook can have:
 *   - a name (for the dashboard)
 *   - a URL
 *   - an HMAC secret (if the receiver verifies signatures — Slack/Discord
 *     style. We send a `X-Ncii-Signature: sha256=<hex>` header.)
 *   - an event filter (e.g. "takedown.removed,case.closed") so the
 *     operator can route only the high-signal events to a noisy
 *     channel
 *
 * Deliveries are recorded in webhook_deliveries. Failures increment
 * the webhook's `failures` counter; after 10 consecutive failures the
 * webhook is auto-disabled.
 */

'use strict';

const crypto = require('crypto');

const MAX_FAILURES_BEFORE_DISABLE = 10;
const DELIVERY_TIMEOUT_MS = 15000;
const MAX_BODY_LOG = 500;

let _db = null;
function db() { if (!_db) _db = require('../config/db').db; return _db; }

/**
 * Dispatch an event to all matching webhooks. Returns the number
 * of webhooks the event was delivered to (or attempted).
 */
async function dispatch(event, payload) {
  // Load all active webhooks, then filter in JS so we can support
  // both CSV and glob patterns (e.g. "takedown.*" matches
  // takedown.sent, takedown.removed, takedown.escalated).
  const webhooks = db().prepare(`SELECT * FROM webhooks WHERE active = 1`).all();
  const matched = webhooks.filter(w => eventMatchesFilter(event, w.event_filter));
  if (!matched.length) return { delivered: 0, failed: 0 };
  const body = JSON.stringify({
    event,
    delivered_at: new Date().toISOString(),
    payload,
  });
  let delivered = 0, failed = 0;
  for (const w of matched) {
    try {
      const r = await deliver(w, event, body);
      if (r) delivered++; else failed++;
    } catch (e) {
      failed++;
    }
  }
  return { delivered, failed };
}

/**
 * Match an event name against a webhook's event_filter.
 *   - null/empty filter → match anything
 *   - comma-separated list of patterns; each pattern supports
 *     trailing `*` as a wildcard (e.g. `takedown.*` matches
 *     takedown.sent, takedown.removed, etc.)
 *   - exact match otherwise
 */
function eventMatchesFilter(event, filter) {
  if (!filter || !filter.trim()) return true;
  for (const pat of filter.split(',').map(s => s.trim()).filter(Boolean)) {
    if (pat === event) return true;
    if (pat.endsWith('*') && event.startsWith(pat.slice(0, -1))) return true;
  }
  return false;
}

async function deliver(webhook, event, body) {
  const headers = { 'content-type': 'application/json', 'user-agent': 'NCII-Shield/1.5' };
  if (webhook.secret) {
    const sig = crypto.createHmac('sha256', webhook.secret).update(body).digest('hex');
    headers['x-ncii-signature'] = 'sha256=' + sig;
  }
  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort(), DELIVERY_TIMEOUT_MS);
  let status = null, responseText = null;
  try {
    const r = await fetch(webhook.url, { method: 'POST', body, headers, signal: controller.signal });
    status = r.status;
    responseText = (await r.text()).slice(0, MAX_BODY_LOG);
    if (r.ok) {
      // success — reset failure counter, record last status
      db().prepare(`UPDATE webhooks SET failures = 0, last_status = ?, last_fired_at = ?, last_error = NULL WHERE id = ?`)
        .run(status, Date.now(), webhook.id);
      db().prepare(`INSERT INTO webhook_deliveries (webhook_id, event, payload, status, response, attempt, delivered_at) VALUES (?, ?, ?, ?, ?, 1, ?)`)
        .run(webhook.id, event, body, status, responseText, Date.now());
      return true;
    }
    // non-2xx — treat as failure
    recordFailure(webhook, status, responseText);
    return false;
  } catch (e) {
    recordFailure(webhook, null, String(e.message || e).slice(0, 300));
    return false;
  } finally {
    clearTimeout(timer);
  }
}

function recordFailure(webhook, status, errMsg) {
  const newFailures = (webhook.failures || 0) + 1;
  const autoDisable = newFailures >= MAX_FAILURES_BEFORE_DISABLE ? 1 : 0;
  db().prepare(`UPDATE webhooks SET failures = ?, last_status = ?, last_fired_at = ?, last_error = ?, active = CASE WHEN ? = 1 THEN 0 ELSE active END WHERE id = ?`)
    .run(newFailures, status, Date.now(), errMsg, autoDisable, webhook.id);
  db().prepare(`INSERT INTO webhook_deliveries (webhook_id, event, payload, status, response, attempt, delivered_at) VALUES (?, ?, '{}', ?, ?, 1, ?)`)
    .run(webhook.id, 'failed-attempt', status, errMsg, Date.now());
}

function list() { return db().prepare('SELECT * FROM webhooks ORDER BY created_at DESC').all(); }
function listDeliveries(webhookId, limit = 50) {
  return db().prepare('SELECT * FROM webhook_deliveries WHERE webhook_id = ? ORDER BY delivered_at DESC LIMIT ?')
    .all(webhookId, limit);
}

function create({ name, url, secret = null, event_filter = null, owner_id = null }) {
  if (!name || !url) throw new Error('name and url are required');
  const info = db().prepare(`INSERT INTO webhooks (name, url, secret, event_filter, owner_id, created_at) VALUES (?, ?, ?, ?, ?, ?)`)
    .run(name, url, secret, event_filter, owner_id, Date.now());
  return db().prepare('SELECT * FROM webhooks WHERE id = ?').get(info.lastInsertRowid);
}

function update(id, patch) {
  const sets = [], vals = [];
  for (const k of ['name','url','secret','event_filter','active']) {
    if (patch[k] !== undefined) { sets.push(k + ' = ?'); vals.push(patch[k]); }
  }
  if (!sets.length) return null;
  vals.push(id);
  db().prepare(`UPDATE webhooks SET ${sets.join(', ')} WHERE id = ?`).run(...vals);
  return db().prepare('SELECT * FROM webhooks WHERE id = ?').get(id);
}
function remove(id) { db().prepare('DELETE FROM webhooks WHERE id = ?').run(id); }

module.exports = { dispatch, list, listDeliveries, create, update, remove, MAX_FAILURES_BEFORE_DISABLE };
