const { AsyncLocalStorage } = require("async_hooks");
const util = require("util");
const { db } = require("./db");

const LEVELS = new Set(["debug", "info", "warn", "error"]);
const DEFAULT_MAX_AGE_DAYS = 30;
const DEFAULT_MAX_ENTRIES = 100000;
const MAX_MESSAGE_LENGTH = 1000;
const MAX_DETAILS_LENGTH = 64 * 1024;
const contextStorage = new AsyncLocalStorage();
let consoleHooked = false;

function log(level, ...args) {
  return writeLog(level, args, contextStorage.getStore() || {});
}

function createLogger(source, defaults = {}) {
  const base = normalizeMetadata({ ...defaults, source });
  const emit = (level, message, details, metadata = {}) => {
    const overrides = compactMetadata(normalizeMetadata(metadata));
    return writeLog(level, details === undefined ? [message] : [message, details], {
      ...base,
      ...overrides
    });
  };
  return Object.freeze({
    log: emit,
    debug: (message, details, metadata) => emit("debug", message, details, metadata),
    info: (message, details, metadata) => emit("info", message, details, metadata),
    warn: (message, details, metadata) => emit("warn", message, details, metadata),
    error: (message, details, metadata) => emit("error", message, details, metadata),
    child: (childSource, childDefaults = {}) => createLogger(
      [base.source, normalizeLabel(childSource, "")].filter(Boolean).join(":"),
      { ...base, ...childDefaults }
    ),
    run: (metadata, callback) => withLogContext({ ...base, ...metadata }, callback)
  });
}

function withLogContext(metadata, callback) {
  const parent = contextStorage.getStore() || {};
  return contextStorage.run({ ...parent, ...compactMetadata(normalizeMetadata(metadata)) }, callback);
}

function writeLog(level, args, metadata = {}) {
  const safeLevel = LEVELS.has(level) ? level : "info";
  const entry = normalizeArgs(args);
  const normalized = normalizeMetadata(metadata);
  const createdAt = Date.now();
  try {
    const result = db.prepare(
      "INSERT INTO logs (level, message, details, source, category, event, request_id, created_at) " +
        "VALUES (?, ?, ?, ?, ?, ?, ?, ?)"
    ).run(
      safeLevel,
      truncate(entry.message, MAX_MESSAGE_LENGTH),
      truncate(entry.details, MAX_DETAILS_LENGTH),
      normalized.source || "core",
      normalized.category || "general",
      normalized.event || null,
      normalized.requestId || null,
      createdAt
    );
    const stored = {
      id: Number(result.lastInsertRowid),
      level: safeLevel,
      message: truncate(entry.message, MAX_MESSAGE_LENGTH),
      details: truncate(entry.details, MAX_DETAILS_LENGTH),
      source: normalized.source || "core",
      category: normalized.category || "general",
      event: normalized.event || null,
      request_id: normalized.requestId || null,
      created_at: createdAt
    };
    publishLogEvent(stored);
    return stored;
  } catch {
    return null;
  }
}

function listLogs(options = {}) {
  const query = buildLogQuery(options);
  let sql =
    "SELECT id, level, message, details, source, category, event, request_id, created_at FROM logs";
  if (query.clauses.length) sql += ` WHERE ${query.clauses.join(" AND ")}`;
  sql += " ORDER BY created_at DESC, id DESC";
  if (query.limit) {
    sql += " LIMIT ?";
    query.params.push(query.limit);
  }
  return db.prepare(sql).all(...query.params);
}

function summarizeLogs(options = {}) {
  const query = buildLogQuery({ ...options, limit: null });
  let sql = "SELECT level, COUNT(*) AS count FROM logs";
  if (query.clauses.length) sql += ` WHERE ${query.clauses.join(" AND ")}`;
  sql += " GROUP BY level";
  const levels = { error: 0, warn: 0, info: 0, debug: 0 };
  let total = 0;
  for (const row of db.prepare(sql).all(...query.params)) {
    if (levels[row.level] !== undefined) levels[row.level] = Number(row.count) || 0;
    total += Number(row.count) || 0;
  }
  return { total, levels };
}

function listLogFacets(options = {}) {
  const sinceMs = Number.isFinite(options.sinceMs) && options.sinceMs > 0 ? options.sinceMs : null;
  const where = sinceMs ? " WHERE created_at >= ?" : "";
  const params = sinceMs ? [sinceMs] : [];
  const facetRows = (column) => {
    const connector = sinceMs ? " AND" : " WHERE";
    return db.prepare(
      `SELECT ${column} AS value, COUNT(*) AS count FROM logs${where}${connector} ${column} IS NOT NULL AND ${column} != '' ` +
        `GROUP BY ${column} ORDER BY count DESC, ${column} ASC`
    ).all(...params).map((row) => ({ value: row.value, count: Number(row.count) || 0 }));
  };
  return { sources: facetRows("source"), categories: facetRows("category") };
}

function cleanupLogs(options = {}) {
  const maxAgeDays = clampNumber(options.maxAgeDays, 1, 3650, DEFAULT_MAX_AGE_DAYS);
  const maxEntries = clampNumber(options.maxEntries, 1000, 1000000, DEFAULT_MAX_ENTRIES);
  const cutoff = Date.now() - maxAgeDays * 24 * 60 * 60 * 1000;
  const expired = db.prepare("DELETE FROM logs WHERE created_at < ?").run(cutoff).changes;
  const overflow = db.prepare(
    "DELETE FROM logs WHERE id NOT IN (SELECT id FROM logs ORDER BY created_at DESC, id DESC LIMIT ?)"
  ).run(maxEntries).changes;
  return { removed: expired + overflow, expired, overflow, maxAgeDays, maxEntries };
}

function hookConsole() {
  if (consoleHooked) return;
  consoleHooked = true;

  const original = {
    debug: console.debug || console.log,
    log: console.log,
    info: console.info || console.log,
    warn: console.warn || console.log,
    error: console.error || console.log
  };
  const capture = (level, method, args) => {
    const context = contextStorage.getStore() || {};
    writeLog(level, args, {
      source: context.source || "console",
      category: context.category || "runtime",
      event: context.event,
      requestId: context.requestId
    });
    original[method].apply(console, args);
  };
  console.debug = (...args) => capture("debug", "debug", args);
  console.log = (...args) => capture("info", "log", args);
  console.info = (...args) => capture("info", "info", args);
  console.warn = (...args) => capture("warn", "warn", args);
  console.error = (...args) => capture("error", "error", args);
}

function buildLogQuery(options = {}) {
  const limit = Number.isFinite(options.limit) && options.limit !== null
    ? Math.max(1, Math.floor(options.limit))
    : null;
  const sinceMs = Number.isFinite(options.sinceMs) && options.sinceMs > 0 ? options.sinceMs : null;
  const levels = normalizeList(options.levels, LEVELS);
  const sources = normalizeList(options.sources);
  const categories = normalizeList(options.categories);
  const search = String(options.search || "").trim().slice(0, 200);
  const clauses = [];
  const params = [];
  if (sinceMs) {
    clauses.push("created_at >= ?");
    params.push(sinceMs);
  }
  appendInFilter(clauses, params, "level", levels);
  appendInFilter(clauses, params, "source", sources);
  appendInFilter(clauses, params, "category", categories);
  if (search) {
    const needle = `%${escapeLike(search)}%`;
    clauses.push(
      "(message LIKE ? ESCAPE '\\' OR details LIKE ? ESCAPE '\\' OR source LIKE ? ESCAPE '\\' " +
        "OR category LIKE ? ESCAPE '\\' OR event LIKE ? ESCAPE '\\' OR request_id LIKE ? ESCAPE '\\')"
    );
    params.push(needle, needle, needle, needle, needle, needle);
  }
  return { clauses, params, limit };
}

function appendInFilter(clauses, params, column, values) {
  if (!values.length) return;
  clauses.push(`${column} IN (${values.map(() => "?").join(",")})`);
  params.push(...values);
}

function normalizeList(values, allowed = null) {
  const list = Array.isArray(values) ? values : values ? [values] : [];
  return [...new Set(list.map((value) => normalizeLabel(value, "")).filter((value) =>
    value && (!allowed || allowed.has(value))
  ))];
}

function normalizeMetadata(metadata = {}) {
  return {
    source: normalizeLabel(metadata.source, ""),
    category: normalizeLabel(metadata.category, ""),
    event: normalizeLabel(metadata.event, ""),
    requestId: normalizeLabel(metadata.requestId || metadata.request_id, "")
  };
}

function compactMetadata(metadata) {
  return Object.fromEntries(Object.entries(metadata).filter(([, value]) => value));
}

function normalizeLabel(value, fallback) {
  const normalized = String(value || "").trim().toLowerCase().replace(/[^a-z0-9_.:-]+/g, "-").replace(/^-+|-+$/g, "");
  return normalized.slice(0, 80) || fallback;
}

function normalizeArgs(args) {
  if (!args || args.length === 0) return { message: "Log entry", details: "" };
  let message = "";
  const detailParts = [];
  const first = args[0];
  if (first instanceof Error) {
    message = redactText(first.message || "Error");
    detailParts.push(redactText(first.stack || String(first)));
  } else {
    message = formatArg(first);
  }
  for (const arg of args.slice(1)) {
    if (arg instanceof Error) {
      detailParts.push(redactText(arg.stack || arg.message || String(arg)));
      if (!message) message = redactText(arg.message || "Error");
    } else {
      detailParts.push(formatArg(arg));
    }
  }
  return {
    message: message || "Log entry",
    details: detailParts.filter(Boolean).join("\n")
  };
}

function formatArg(value) {
  if (typeof value === "string") return redactText(value);
  if (value instanceof Error) return redactText(value.stack || value.message || String(value));
  return redactText(util.inspect(redactValue(value), {
    depth: 6,
    maxArrayLength: 100,
    maxStringLength: 4000,
    breakLength: 120
  }));
}

function redactValue(value, seen = new WeakSet()) {
  if (value === null || value === undefined) return value;
  if (typeof value === "string") return redactText(value);
  if (typeof value !== "object") return value;
  if (value instanceof Error) return redactText(value.stack || value.message || String(value));
  if (seen.has(value)) return "[Circular]";
  seen.add(value);
  if (Array.isArray(value)) return value.map((item) => redactValue(item, seen));
  const output = {};
  for (const [key, item] of Object.entries(value)) {
    output[key] = isSensitiveKey(key) ? "[REDACTED]" : redactValue(item, seen);
  }
  return output;
}

function isSensitiveKey(key) {
  return /(?:^|[_-])(authorization|cookie|password|passwd|secret|token|api[_-]?key|access[_-]?token|refresh[_-]?token|client[_-]?secret|private[_-]?key)(?:$|[_-])/i.test(String(key));
}

function redactText(value) {
  return String(value || "")
    .replace(/\b(Bearer|Basic)\s+[A-Za-z0-9._~+/=-]+/gi, "$1 [REDACTED]")
    .replace(/([?&](?:token|key|secret|password|authorization)=)[^&#\s]+/gi, "$1[REDACTED]")
    .replace(/\b((?:api[_-]?key|access[_-]?token|refresh[_-]?token|client[_-]?secret|password|passwd|secret|authorization)\s*[:=]\s*)[^\s,;}]+/gi, "$1[REDACTED]");
}

function truncate(value, maxLength) {
  const text = String(value || "");
  if (text.length <= maxLength) return text;
  return `${text.slice(0, maxLength)}\n[truncated]`;
}

function escapeLike(value) {
  return String(value).replace(/[\\%_]/g, (character) => `\\${character}`);
}

function clampNumber(value, minimum, maximum, fallback) {
  const parsed = Number(value);
  if (!Number.isFinite(parsed)) return fallback;
  return Math.min(maximum, Math.max(minimum, Math.floor(parsed)));
}

function publishLogEvent(entry) {
  try {
    const { publishWebEvent } = require("./web-events");
    publishWebEvent("log:created", { ...entry, details: truncate(entry.details, 4000) }, { role: "admin" });
  } catch {
    // Logging must never fail because live delivery is unavailable.
  }
}

module.exports = {
  DEFAULT_MAX_AGE_DAYS,
  DEFAULT_MAX_ENTRIES,
  cleanupLogs,
  createLogger,
  hookConsole,
  listLogFacets,
  listLogs,
  log,
  redactValue,
  summarizeLogs,
  withLogContext
};
