/**
 * DC-081: Plain-English log insights + dispose endpoint
 *
 * GET /api/v1/log-insights — Plain English summary of who's doing what
 * POST /api/v1/log-insights/dispose — Preview then confirm cleanup
 *
 * DC-081 hardening (paired with the deploy path fix):
 *   - AUDIT_LOG_FILE / SECURITY_EVENT_LOG_FILE were HARDCODED to
 *     `/opt/dashcaddy/dashcaddy-api/data/...` which DOES NOT EXIST in the
 *     production container — files live at `/app/data/...`. The dispose
 *     endpoint silently no-op'd (read empty arrays, wrote empty arrays
 *     back) and the GET endpoint dropped the storage-size block. Both
 *     paths now use the same canonical resolution as the audit-logger
 *     itself: `process.env.AUDIT_LOG_FILE || path.join(platformPaths.dataDir, 'audit-log.json')`.
 *   - keepDays was unbounded — `parseInt(req.body.keepDays) || 30` accepted
 *     negative numbers (e.g. -1000 → cutoff = +3 years in the future,
 *     deleting 100% of forensic context) and non-integers (Infinity,
 *     floats). Now validated to an integer in [1, 3650] (1 day .. 10 years)
 *     before any file read.
 *   - confirm gate added: must send { confirm: true, keepDays: N } — the
 *     preview pass is read-only, the confirm pass writes. Matches the
 *     audit-logs/DELETE confirm=CLEAR pattern.
 *   - The dispose handler now uses a single shared `_resolvePaths()` helper
 *     to keep GET and POST in lockstep (and so a future path-config change
 *     touches one site, not four).
 *
 * Pre-DC-081 verification: from inside the running container, both
 * `/app/data/audit-log.json` (318 KB) and `/app/data/security-events.jsonl`
 * (15 MB) exist, but the old hardcoded `/opt/dashcaddy/dashcaddy-api/data/...`
 * paths resolve to ENOENT. The dispose endpoint therefore did nothing;
 * this fix wires it back to the actual files.
 */

const express = require('express');
const path = require('path');
const fs = require('fs').promises;
const platformPaths = require('../platform-paths');

/**
 * Resolve the canonical paths for the audit log + security event log.
 *
 * Both store the file path in their own module-level constants, so any
 * environment override (e.g. AUDIT_LOG_FILE=...) is honoured here too —
 * exactly the same behaviour as src/security/audit-logger.js and
 * src/security/event-store.js. Without this, a container with
 * AUDIT_LOG_FILE set would see the dispose handler read from one file
 * and the audit-logger write to a different one.
 *
 * @returns {{auditPath: string, secPath: string, auditPathFrom: string, secPathFrom: string}}
 *   paths + the source ("env" or "default") so tests can verify.
 */
function _resolvePaths() {
  const auditPath = process.env.AUDIT_LOG_FILE
    || path.join(platformPaths.dataDir, 'audit-log.json');
  const secPath = process.env.SECURITY_EVENT_LOG_FILE
    || path.join(platformPaths.dataDir, 'security-events.jsonl');
  return {
    auditPath,
    secPath,
    auditPathFrom: process.env.AUDIT_LOG_FILE ? 'env' : 'default',
    secPathFrom: process.env.SECURITY_EVENT_LOG_FILE ? 'env' : 'default',
  };
}

/**
 * Validate the keepDays input. Coerces + bounds-checks BEFORE any file
 * read so a malicious or mistyped client can't:
 *   - pass a negative number (cutoff = far future → wipe 100%)
 *   - pass Infinity (parseInt(Infinity, 10) === NaN, currently falls
 *     through `|| 30` — fixed to fail-fast instead)
 *   - pass a non-integer (e.g. 1.5 → cutoff mid-day, off-by-half-day)
 *   - pass 0 (no-op-but-lies) or 10000 (way past retention policy)
 *
 * @param {unknown} raw - value from req.body.keepDays
 * @returns {number} validated integer in [1, 3650]
 * @throws {Error} when out of range / wrong type
 */
function _validateKeepDays(raw) {
  if (raw === undefined || raw === null) {
    throw new Error('keepDays is required (integer in [1, 3650])');
  }
  const n = Number(raw);
  if (!Number.isFinite(n)) {
    throw new Error(`keepDays must be a finite number (received ${JSON.stringify(raw)})`);
  }
  if (!Number.isInteger(n)) {
    throw new Error(`keepDays must be an integer (received ${raw})`);
  }
  if (n < 1 || n > 3650) {
    throw new Error(`keepDays must be between 1 and 3650 (received ${n})`);
  }
  return n;
}

module.exports = function({ asyncHandler, ok, auditLogger, securityEventStore }) {
  const router = express.Router();
  // Resolve once at module init so GET + POST both use the same files.
  // If the env vars change at runtime (rare — start.sh wires them at
  // container start), operators re-deploy rather than mutate env mid-flight.
  const { auditPath, secPath } = _resolvePaths();

  // GET /api/v1/log-insights — Plain English summary of who's doing what
  router.get('/log-insights', asyncHandler(async (req, res) => {
    const hours = parseInt(req.query.hours) || 24;
    const since = new Date(Date.now() - hours * 60 * 60 * 1000).toISOString();

    // --- Collect data ---
    const auditEntries = await auditLogger.query({ limit: 10000 });
    const recentAudit = auditEntries.filter(e => e.timestamp >= since);

    let securityEvents = [];
    try { securityEvents = securityEventStore.query({ since, limit: 10000 }); } catch {}

    // --- Analyze IPs ---
    const ipMap = {};
    recentAudit.forEach(e => {
      const ip = e.ip || 'unknown';
      if (!ipMap[ip]) ipMap[ip] = { count: 0, actions: {}, resources: new Set(), first: e.timestamp, last: e.timestamp, failures: 0 };
      const s = ipMap[ip];
      s.count++;
      const cat = (e.action || 'unknown').split('.')[0];
      s.actions[cat] = (s.actions[cat] || 0) + 1;
      if (e.resource) s.resources.add(e.resource);
      if (e.timestamp < s.first) s.first = e.timestamp;
      if (e.timestamp > s.last) s.last = e.timestamp;
      if (e.outcome === 'failure' || e.outcome === 'denied') s.failures++;
    });

    // --- Build plain-English insights ---
    const insights = [];
    const ipArray = Object.entries(ipMap).sort((a, b) => b[1].count - a[1].count);

    // Heavy users
    ipArray.slice(0, 3).forEach(([ip, s]) => {
      const topAction = Object.entries(s.actions).sort((a, b) => b[1] - a[1])[0];
      insights.push({
        severity: s.count > 500 ? 'warning' : 'info',
        title: ip + ' — ' + s.count + ' requests in ' + hours + 'h',
        plain: ip + ' made ' + s.count + ' requests (mostly ' + (topAction ? topAction[0] : 'unknown') + ')' +
          (s.failures > 0 ? ', ' + s.failures + ' failed' : '') + '.'
      });
    });

    // Auth failures
    const totalFailures = recentAudit.filter(e => e.outcome === 'failure' || e.outcome === 'denied').length;
    if (totalFailures > 5) {
      insights.push({
        severity: totalFailures > 50 ? 'warning' : 'info',
        title: totalFailures + ' failed actions',
        plain: totalFailures + ' requests were denied or failed in the last ' + hours + ' hours.' +
          (totalFailures > 50 ? ' This could indicate someone trying to brute-force access.' : '')
      });
    }

    // Security events
    const secBySev = {};
    securityEvents.forEach(e => { secBySev[e.severity] = (secBySev[e.severity] || 0) + 1; });
    if (secBySev.critical || secBySev.error) {
      insights.push({
        severity: 'warning',
        title: ((secBySev.critical || 0) + (secBySev.error || 0)) + ' security alerts',
        plain: (secBySev.critical || 0) + ' critical and ' + (secBySev.error || 0) + ' error-level security events were logged.'
      });
    }

    // Quiet / nothing
    if (insights.length === 0) {
      insights.push({ severity: 'ok', title: 'All quiet', plain: 'No notable activity in the last ' + hours + ' hours.' });
    }

    // --- Storage info ---
    // DC-081: read from the canonical resolved paths (NOT the hardcoded
    // /opt/... paths that don't exist in the container). Empty-object
    // fallback on ENOENT — the file may legitimately be absent on a
    // fresh install where the audit-logger hasn't written yet.
    let storage = {};
    try {
      const a = await fs.stat(auditPath);
      storage.auditLog = { sizeMB: +(a.size / 1048576).toFixed(2), entries: auditEntries.length, path: auditPath };
    } catch {}
    try {
      const s = await fs.stat(secPath);
      storage.securityEvents = { sizeMB: +(s.size / 1048576).toFixed(2), entries: securityEvents.length, path: secPath };
    } catch {}

    ok(res, {
      period: { hours, since, until: new Date().toISOString() },
      summary: {
        totalRequests: recentAudit.length,
        uniqueIPs: ipArray.length,
        securityEvents: securityEvents.length,
        failedActions: totalFailures
      },
      topIPs: ipArray.slice(0, 10).map(([ip, s]) => ({
        ip: ip,
        count: s.count,
        failures: s.failures,
        topActions: Object.entries(s.actions).sort((a, b) => b[1] - a[1]).slice(0, 3),
        activeFrom: s.first,
        lastSeen: s.last
      })),
      insights: insights,
      storage: storage
    });
  }));

  // POST /api/v1/log-insights/dispose — Preview then confirm cleanup
  //
  // Two-call pattern:
  //   1. { keepDays: 30 }                  → preview, no writes
  //   2. { keepDays: 30, confirm: true }   → actually delete
  //
  // DC-081 hardening:
  //   - keepDays is validated to integer [1, 3650] BEFORE any file read.
  //     A negative keepDays (e.g. -1000) would previously compute a
  //     cutoff +3 years in the future, then delete every entry older
  //     than that — i.e. 100% of the audit log. Now rejected at the gate.
  //   - auditPath / secPath come from the canonical _resolvePaths() helper
  //     so the container's actual /app/data files are read (the pre-fix
  //     hardcoded /opt/dashcaddy/dashcaddy-api/data/... paths resolved to
  //     ENOENT inside the container, so the endpoint silently did nothing).
  router.post('/log-insights/dispose', asyncHandler(async (req, res) => {
    // Validate keepDays first — fail-fast before any file IO so a bad
    // client never touches disk.
    let keepDays;
    try {
      keepDays = _validateKeepDays(req.body?.keepDays);
    } catch (e) {
      return res.status(400).json({ success: false, error: e.message, code: 'DC-081_INVALID_KEEP_DAYS' });
    }
    const confirm = req.body.confirm === true;
    const cutoff = new Date(Date.now() - keepDays * 86400000).toISOString();

    // Read both files via the canonical resolved paths (NOT the hardcoded
    // /opt/... paths from before — those don't exist in the container).
    const auditRaw = await fs.readFile(auditPath, 'utf8').catch(function () { return '[]'; });
    let auditData;
    try {
      auditData = JSON.parse(auditRaw);
    } catch (e) {
      return res.status(500).json({ success: false, error: `audit-log file is corrupt (${auditPath}): ${e.message}`, code: 'DC-081_AUDIT_PARSE_FAILED' });
    }
    if (!Array.isArray(auditData)) {
      return res.status(500).json({ success: false, error: `audit-log file is not an array (${auditPath})`, code: 'DC-081_AUDIT_SHAPE_INVALID' });
    }
    const oldAudit = auditData.filter(function (e) { return e.timestamp < cutoff; });

    const secRaw = await fs.readFile(secPath, 'utf8').catch(function () { return ''; });
    const secLines = secRaw.split('\n').filter(Boolean);
    const oldSec = secLines.filter(function (l) { try { return JSON.parse(l).timestamp < cutoff; } catch (e) { return false; } });

    if (!confirm) {
      ok(res, {
        preview: true,
        message: 'This will delete ' + oldAudit.length + ' audit entries and ' + oldSec.length + ' security events older than ' + keepDays + ' days. Send {confirm: true, keepDays: ' + keepDays + '} to proceed.',
        wouldDelete: { auditEntries: oldAudit.length, securityEvents: oldSec.length },
        cutoffDate: cutoff,
        paths: { auditPath, secPath },
      });
      return;
    }

    // Execute cleanup. Audit the wipe FIRST via the audit-logger so the
    // fact that a delete happened is itself preserved (matches the
    // audit-logs/DELETE + error-logs/DELETE pattern).
    try {
      if (auditLogger && typeof auditLogger.log === 'function') {
        await auditLogger.log({
          action: 'log-insights.dispose',
          resource: 'audit-log,security-events',
          outcome: 'success',
          details: {
            keepDays,
            cutoff,
            deleted: { auditEntries: oldAudit.length, securityEvents: oldSec.length },
          },
        });
      }
    } catch { /* don't fail the dispose on audit-side errors */ }

    // Rewrite audit-log.json atomically — write to tmp + rename so a
    // crash mid-write can't leave the file half-empty (the file is read
    // by state-manager on every container start; a corrupt file would
    // block the whole API).
    const keptAudit = auditData.filter(function (e) { return e.timestamp >= cutoff; });
    const tmpAudit = auditPath + '.tmp';
    await fs.writeFile(tmpAudit, JSON.stringify(keptAudit, null, 2));
    await fs.rename(tmpAudit, auditPath);

    const keptSec = secLines.filter(function (l) { try { return JSON.parse(l).timestamp >= cutoff; } catch (e) { return false; } });
    await fs.writeFile(secPath, keptSec.join('\n') + '\n');

    ok(res, {
      disposed: true,
      deleted: { auditEntries: oldAudit.length, securityEvents: oldSec.length },
      remaining: { auditEntries: keptAudit.length, securityEvents: keptSec.length },
      cutoffDate: cutoff,
    });
  }));

  return router;
};

// DC-081: export helpers for direct unit testing (the route handlers are
// otherwise unreachable from outside the factory closure).
module.exports.__test = {
  _resolvePaths,
  _validateKeepDays,
};