const crypto = require("crypto");
const fs = require("fs");
const path = require("path");
const { performance } = require("perf_hooks");
const { db } = require("./db");
const { dependencyIssues } = require("./dependency-manager");
const { createLogger, listLogs } = require("./logger");
const { getPlugins, scanPluginDirectories } = require("./plugins");
const { readRecoveryMarker } = require("./recovery-mode");
const { getSetting, setSetting } = require("./settings");
const { readUpdateState } = require("./update-repository");

const repoRoot = path.join(__dirname, "..", "..");
const packageJson = require(path.join(repoRoot, "package.json"));
const TOKEN_PREFIX = "lumi_diag_";
const MAX_REQUESTS_PER_MINUTE = 20;
const requestWindows = new Map();
const diagnosticsLog = createLogger("core:diagnostics", { category: "security" });
const CHECKS = Object.freeze({
  system_health: "Runtime, database, dependency, disk-space, and recovery health.",
  update_state: "Latest local update state, recovery marker, and snapshot summary.",
  plugins: "Installed plugin manifests and registry versions without plugin data.",
  recent_errors: "Recent warning/error records with secrets and local paths redacted.",
  benchmark: "Bounded read-only database, plugin-scan, and serialization timing."
});

function diagnosticsAccessStatus() {
  return {
    enabled: getSetting("production_diagnostics_enabled", false) === true,
    configured: Boolean(getSetting("production_diagnostics_key_hash", "")),
    key_prefix: String(getSetting("production_diagnostics_key_prefix", "") || ""),
    created_at: getSetting("production_diagnostics_key_created_at", null),
    checks: Object.entries(CHECKS).map(([id, description]) => ({ id, description }))
  };
}

function issueDiagnosticsAccessKey() {
  const key = `${TOKEN_PREFIX}${crypto.randomBytes(32).toString("base64url")}`;
  setSetting("production_diagnostics_key_hash", hashAccessKey(key));
  setSetting("production_diagnostics_key_prefix", `${key.slice(0, TOKEN_PREFIX.length + 8)}…`);
  setSetting("production_diagnostics_key_created_at", new Date().toISOString());
  setSetting("production_diagnostics_enabled", true);
  diagnosticsLog.warn("Production diagnostics access key rotated", { key_prefix: `${key.slice(0, TOKEN_PREFIX.length + 8)}…` }, { event: "access_rotated" });
  return key;
}

function revokeDiagnosticsAccess() {
  setSetting("production_diagnostics_enabled", false);
  setSetting("production_diagnostics_key_hash", "");
  setSetting("production_diagnostics_key_prefix", "");
  setSetting("production_diagnostics_key_created_at", null);
  requestWindows.clear();
  diagnosticsLog.warn("Production diagnostics access revoked", null, { event: "access_revoked" });
}

function authenticateDiagnosticsRequest(req, now = Date.now()) {
  if (!diagnosticsAccessStatus().enabled) return { allowed: false, status: 404, reason: "disabled" };
  if (!isSecureDiagnosticRequest(req)) return { allowed: false, status: 404, reason: "secure_transport_required" };
  const authorization = String(req.get?.("authorization") || "");
  const match = authorization.match(/^Bearer\s+([^\s]+)$/i);
  const candidate = match?.[1] || "";
  const storedHash = String(getSetting("production_diagnostics_key_hash", "") || "");
  if (!verifyAccessKey(candidate, storedHash)) return { allowed: false, status: 404, reason: "invalid_key" };
  const fingerprint = hashAccessKey(candidate).slice(0, 20);
  const rate = consumeRateLimit(fingerprint, now);
  if (!rate.allowed) return { allowed: false, status: 429, reason: "rate_limited", retry_after_seconds: rate.retry_after_seconds };
  return { allowed: true, fingerprint, remaining: rate.remaining };
}

function runDiagnosticCheck(checkId) {
  const check = String(checkId || "").trim();
  if (!Object.hasOwn(CHECKS, check)) throw new Error("Unknown diagnostic check.");
  const started = performance.now();
  let result;
  if (check === "system_health") result = systemHealth();
  else if (check === "update_state") result = updateState();
  else if (check === "plugins") result = pluginInventory();
  else if (check === "recent_errors") result = recentErrors();
  else result = boundedBenchmark();
  return redactDiagnosticValue({
    schema_version: 1,
    check,
    generated_at: new Date().toISOString(),
    duration_ms: round(performance.now() - started),
    result
  });
}

function auditDiagnosticRequest(values = {}) {
  diagnosticsLog.log(values.ok === false ? "warn" : "info", "Production diagnostics request", {
    request_id: values.request_id,
    check: values.check,
    ok: values.ok !== false,
    key_fingerprint: values.fingerprint || null,
    reason: values.reason || null,
    duration_ms: values.duration_ms || null
  }, { event: "diagnostic_request", requestId: values.request_id });
}

function recentDiagnosticAudit(limit = 30) {
  return listLogs({ limit: 250 })
    .filter((entry) => ["Production diagnostics request", "Production diagnostics access key rotated", "Production diagnostics access revoked"].includes(entry.message))
    .slice(0, Math.max(1, Math.min(100, Number(limit) || 30)))
    .map((entry) => redactDiagnosticValue(entry));
}

function systemHealth() {
  let database = { ok: false };
  try {
    database = { ok: db.prepare("SELECT 1 AS ok").get()?.ok === 1 };
  } catch (error) {
    database = { ok: false, error: error.message };
  }
  let disk = null;
  try {
    if (typeof fs.statfsSync !== "function") throw new Error("Disk statistics are unavailable on this Node.js version.");
    const stats = fs.statfsSync(repoRoot);
    disk = {
      available_bytes: Number(stats.bavail) * Number(stats.bsize),
      total_bytes: Number(stats.blocks) * Number(stats.bsize)
    };
  } catch {
    disk = { available: false };
  }
  const dependencies = dependencyIssues(repoRoot);
  return {
    core_version: packageJson.version,
    node_version: process.version,
    platform: process.platform,
    architecture: process.arch,
    uptime_seconds: Math.floor(process.uptime()),
    memory: process.memoryUsage(),
    database,
    disk,
    dependencies: {
      ready: dependencies.filter((item) => !item.optional).length === 0,
      issues: dependencies
    },
    recovery: summarizeRecovery(readRecoveryMarker())
  };
}

function updateState() {
  const state = readUpdateState();
  const index = readJson(path.join(repoRoot, "data", "snapshots", "index.json"), []);
  const snapshots = Array.isArray(index) ? index : [];
  return {
    core_version: packageJson.version,
    last_update: {
      status: state.last_update_status || null,
      target_kind: state.last_target_kind || null,
      target_id: state.last_target_id || null,
      target_version: state.last_target_version || null,
      stage: state.last_update_stage || null,
      error: state.last_error || null,
      updated_at: state.last_update_at || state.updated_at || null,
      source_ref: state.branch || null,
      commit: state.commit || null
    },
    recovery: summarizeRecovery(readRecoveryMarker()),
    snapshots: {
      available: snapshots.filter((entry) => entry.status === "available").length,
      latest: snapshots
        .filter((entry) => entry.status === "available")
        .sort((left, right) => Number(right.createdAt) - Number(left.createdAt))
        .slice(0, 10)
        .map((entry) => ({
          type: entry.type,
          plugin_id: entry.pluginId || null,
          from_version: entry.from_version || null,
          to_version: entry.to_version || null,
          created_at: entry.createdAt || null,
          storage_bytes: entry.storage_bytes || null
        }))
    }
  };
}

function pluginInventory() {
  const registry = new Map(getPlugins().map((plugin) => [plugin.id, plugin]));
  return scanPluginDirectories().map((plugin) => ({
    id: plugin.id,
    name: plugin.name,
    manifest_version: plugin.version,
    registry_version: registry.get(plugin.id)?.version || null,
    enabled: Boolean(registry.get(plugin.id)?.enabled),
    version_matches_registry: !registry.has(plugin.id) || registry.get(plugin.id)?.version === plugin.version
  })).sort((left, right) => left.id.localeCompare(right.id));
}

function recentErrors() {
  return listLogs({ limit: 50, levels: ["warn", "error"] }).map((entry) => ({
    level: entry.level,
    message: entry.message,
    details: entry.details,
    created_at: entry.created_at
  }));
}

function boundedBenchmark() {
  const databaseStarted = performance.now();
  for (let index = 0; index < 100; index += 1) db.prepare("SELECT 1 AS ok").get();
  const databaseMs = performance.now() - databaseStarted;
  const pluginStarted = performance.now();
  let pluginCount = 0;
  for (let index = 0; index < 5; index += 1) pluginCount = scanPluginDirectories().length;
  const pluginMs = performance.now() - pluginStarted;
  const serializationStarted = performance.now();
  const sample = Array.from({ length: 250 }, (_, index) => ({ index, status: "ok", version: packageJson.version }));
  for (let index = 0; index < 20; index += 1) JSON.stringify(sample);
  const serializationMs = performance.now() - serializationStarted;
  return {
    fixed_workload: true,
    database_select_100_ms: round(databaseMs),
    plugin_scan_5_ms: round(pluginMs),
    plugin_count: pluginCount,
    json_serialize_20_ms: round(serializationMs)
  };
}

function redactDiagnosticValue(value, key = "", depth = 0) {
  if (depth > 8) return "[truncated]";
  if (/(?:token|secret|password|passwd|authorization|cookie|session|credential|private.?key|api.?key)/i.test(key)) return "[redacted]";
  if (typeof value === "string") return redactString(value).slice(0, 4000);
  if (typeof value === "bigint") return Number(value);
  if (Array.isArray(value)) return value.slice(0, 100).map((item) => redactDiagnosticValue(item, key, depth + 1));
  if (value && typeof value === "object") {
    return Object.fromEntries(Object.entries(value).slice(0, 100).map(([childKey, child]) => [childKey, redactDiagnosticValue(child, childKey, depth + 1)]));
  }
  return value;
}

function redactString(value) {
  return String(value)
    .replace(/\blumi_diag_[A-Za-z0-9_-]+\b/g, "[diagnostics key]")
    .replace(/(https?:\/\/)[^/@\s:]+:[^/@\s]+@/gi, "$1[credentials]@")
    .replace(/\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/g, "[signed token]")
    .replace(/\b([a-z0-9_-]*(?:token|secret|session|password|passwd|api[_-]?key|auth)[a-z0-9_-]*)\s*[:=]\s*[^,\s;]+/gi, "$1=[redacted]")
    .replace(/\b(?:bearer|basic)\s+[a-z0-9._~+/=-]+/gi, "[auth header]")
    .replace(/([?&](?:token|secret|session|password|api_key|apikey|auth|code)=)[^&#\s]+/gi, "$1[redacted]")
    .replace(/(?:[A-Za-z]:\\|\\\\)[^\s<>\"|?*]+/g, "[local path]")
    .replace(/\/(?:home|Users|mnt|var|tmp|opt|root)\/[^\s)\]}>]+/g, "[local path]")
    .replace(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/gi, "[email]")
    .replace(/[A-Za-z0-9+_=-]{40,}/g, (candidate) => /[A-Za-z]/.test(candidate) && /\d/.test(candidate) ? "[high-entropy value]" : candidate);
}

function hashAccessKey(value) {
  return crypto.createHash("sha256").update(String(value || "")).digest("hex");
}

function verifyAccessKey(candidate, expectedHash) {
  if (!String(candidate || "").startsWith(TOKEN_PREFIX) || !/^[a-f0-9]{64}$/i.test(String(expectedHash || ""))) return false;
  const actual = Buffer.from(hashAccessKey(candidate), "hex");
  const expected = Buffer.from(expectedHash, "hex");
  return actual.length === expected.length && crypto.timingSafeEqual(actual, expected);
}

function consumeRateLimit(fingerprint, now = Date.now()) {
  const windowStart = now - (now % 60000);
  const current = requestWindows.get(fingerprint);
  const state = !current || current.window_start !== windowStart ? { window_start: windowStart, count: 0 } : current;
  state.count += 1;
  requestWindows.set(fingerprint, state);
  if (requestWindows.size > 1000) {
    for (const [key, value] of requestWindows.entries()) if (value.window_start < windowStart) requestWindows.delete(key);
  }
  return {
    allowed: state.count <= MAX_REQUESTS_PER_MINUTE,
    remaining: Math.max(0, MAX_REQUESTS_PER_MINUTE - state.count),
    retry_after_seconds: Math.max(1, Math.ceil((windowStart + 60000 - now) / 1000))
  };
}

function isSecureDiagnosticRequest(req) {
  if (req.secure === true) return true;
  const address = String(req.socket?.remoteAddress || req.ip || "");
  if (isLoopbackAddress(address)) return true;
  return diagnosticsRequestProtocol(req) === "https";
}

function diagnosticsRequestProtocol(req) {
  if (req.secure === true || String(req.protocol || "").toLowerCase() === "https") return "https";
  const forwarded = String(req.get?.("x-forwarded-proto") || "")
    .split(",")[0]
    .trim()
    .toLowerCase();
  const proxyAddress = String(req.socket?.remoteAddress || "");
  if (forwarded === "https" && isTrustedPrivateProxyAddress(proxyAddress)) return "https";
  return "http";
}

function isTrustedPrivateProxyAddress(value) {
  const address = String(value || "").trim().toLowerCase().replace(/^::ffff:/, "");
  if (isLoopbackAddress(address)) return true;
  if (/^\d{1,3}(?:\.\d{1,3}){3}$/.test(address)) {
    const parts = address.split(".").map(Number);
    if (parts.some((part) => part < 0 || part > 255)) return false;
    return parts[0] === 10 ||
      (parts[0] === 172 && parts[1] >= 16 && parts[1] <= 31) ||
      (parts[0] === 192 && parts[1] === 168) ||
      (parts[0] === 169 && parts[1] === 254);
  }
  return address.startsWith("fc") || address.startsWith("fd") || /^fe[89ab]/.test(address);
}

function isLoopbackAddress(value) {
  const address = String(value || "").trim().toLowerCase();
  return address === "127.0.0.1" || address === "::1" || address === "::ffff:127.0.0.1";
}

function summarizeRecovery(marker) {
  if (!marker) return { active: false };
  return {
    active: ["pending", "applying", "verifying", "failed", "stale"].includes(marker.status),
    status: marker.status || null,
    target_kind: marker.target_kind || null,
    target_id: marker.target_id || null,
    from_version: marker.from_version || null,
    to_version: marker.to_version || null,
    error: marker.error || null,
    updated_at: marker.updated_at || null
  };
}

function readJson(filePath, fallback) {
  try { return JSON.parse(fs.readFileSync(filePath, "utf8")); } catch { return fallback; }
}

function round(value) {
  return Math.round(Number(value) * 100) / 100;
}

module.exports = {
  CHECKS,
  MAX_REQUESTS_PER_MINUTE,
  auditDiagnosticRequest,
  authenticateDiagnosticsRequest,
  consumeRateLimit,
  diagnosticsAccessStatus,
  diagnosticsRequestProtocol,
  hashAccessKey,
  issueDiagnosticsAccessKey,
  isSecureDiagnosticRequest,
  isTrustedPrivateProxyAddress,
  recentDiagnosticAudit,
  redactDiagnosticValue,
  revokeDiagnosticsAccess,
  runDiagnosticCheck,
  verifyAccessKey
};
