import type { PrismaClient, Prisma } from "@capakraken/db";
import { logger } from "./logger.js";

type AuditAction = "CREATE" | "UPDATE" | "DELETE" | "SHIFT" | "IMPORT";
type AuditSource = "ui" | "api" | "ai" | "import" | "cron";

interface CreateAuditEntryParams {
  db: PrismaClient;
  entityType: string;
  entityId: string;
  entityName?: string;
  action: AuditAction;
  userId?: string;
  before?: Record<string, unknown>;
  after?: Record<string, unknown>;
  source?: AuditSource;
  summary?: string;
  metadata?: Record<string, unknown>;
}

const INTERNAL_FIELDS = new Set(["id", "createdAt", "updatedAt"]);

// Field names whose values are never safe to persist into the audit log.
// Matching is case-insensitive and applied at every level of the object graph.
const SENSITIVE_FIELD_NAMES = new Set([
  "password",
  "newpassword",
  "currentpassword",
  "oldpassword",
  "passwordhash",
  "passwordconfirmation",
  "confirmpassword",
  "token",
  "accesstoken",
  "refreshtoken",
  "sessiontoken",
  "apikey",
  "authorization",
  "cookie",
  "secret",
  "totpsecret",
  "backupcode",
  "backupcodes",
]);

const REDACTED_PLACEHOLDER = "[REDACTED]";
const MAX_REDACT_DEPTH = 8;

/**
 * Recursively strip values of fields whose names appear in SENSITIVE_FIELD_NAMES.
 * Used to prevent password/token leaks into the audit log JSONB column.
 *
 * The pino logger has its own redact config for stdout; this function is the
 * DB-write equivalent.
 */
function redactSensitive(value: unknown, depth: number = 0): unknown {
  if (depth > MAX_REDACT_DEPTH) return value;
  if (value === null || value === undefined) return value;
  if (Array.isArray(value)) {
    return value.map((v) => redactSensitive(v, depth + 1));
  }
  if (typeof value === "object") {
    const out: Record<string, unknown> = {};
    for (const [k, v] of Object.entries(value as Record<string, unknown>)) {
      if (SENSITIVE_FIELD_NAMES.has(k.toLowerCase())) {
        out[k] = REDACTED_PLACEHOLDER;
      } else {
        out[k] = redactSensitive(v, depth + 1);
      }
    }
    return out;
  }
  return value;
}

export const __test__ = { redactSensitive, SENSITIVE_FIELD_NAMES };

/**
 * Compare two snapshots and return only the changed fields.
 * Skips internal fields (id, createdAt, updatedAt).
 * Uses JSON.stringify for nested object comparison.
 */
export function computeDiff(
  before: Record<string, unknown>,
  after: Record<string, unknown>,
): Record<string, { old: unknown; new: unknown }> {
  const diff: Record<string, { old: unknown; new: unknown }> = {};

  const allKeys = new Set([...Object.keys(before), ...Object.keys(after)]);

  for (const key of allKeys) {
    if (INTERNAL_FIELDS.has(key)) continue;

    const oldVal = before[key];
    const newVal = after[key];

    // Compare by JSON serialization to handle nested objects/arrays
    const oldStr = JSON.stringify(oldVal) ?? "undefined";
    const newStr = JSON.stringify(newVal) ?? "undefined";

    if (oldStr !== newStr) {
      diff[key] = { old: oldVal, new: newVal };
    }
  }

  return diff;
}

/**
 * Auto-generate a human-readable summary from the action and diff.
 */
export function generateSummary(
  action: string,
  entityType: string,
  diff?: Record<string, { old: unknown; new: unknown }>,
): string {
  switch (action) {
    case "CREATE":
      return `Created ${entityType}`;
    case "DELETE":
      return `Deleted ${entityType}`;
    case "SHIFT":
      return `Shifted ${entityType}`;
    case "IMPORT":
      return `Imported ${entityType}`;
    case "UPDATE": {
      if (!diff || Object.keys(diff).length === 0) {
        return `Updated ${entityType}`;
      }
      const fields = Object.keys(diff);
      if (fields.length <= 3) {
        return `Updated ${fields.join(", ")}`;
      }
      return `Updated ${fields.slice(0, 3).join(", ")} and ${fields.length - 3} more`;
    }
    default:
      return `${action} ${entityType}`;
  }
}

/**
 * Create an audit log entry. Fire-and-forget — errors are logged but never thrown.
 *
 * If both `before` and `after` are provided, a diff is computed automatically.
 * If no `summary` is given, one is generated from the action and diff.
 */
export async function createAuditEntry(params: CreateAuditEntryParams): Promise<void> {
  try {
    const {
      db,
      entityType,
      entityId,
      entityName,
      action,
      userId,
      before,
      after,
      source,
      metadata,
    } = params;
    const auditLog = (db as Partial<PrismaClient>).auditLog;

    if (!auditLog || typeof auditLog.create !== "function") {
      return;
    }

    // Redact sensitive field values before anything else — diffs and summaries
    // must all be derived from already-sanitised snapshots.
    const safeBefore = before ? (redactSensitive(before) as Record<string, unknown>) : undefined;
    const safeAfter = after ? (redactSensitive(after) as Record<string, unknown>) : undefined;
    const safeMetadata = metadata
      ? (redactSensitive(metadata) as Record<string, unknown>)
      : undefined;

    // Compute diff if both snapshots are available
    const diff = safeBefore && safeAfter ? computeDiff(safeBefore, safeAfter) : undefined;

    // Skip UPDATE entries where nothing actually changed
    if (action === "UPDATE" && diff && Object.keys(diff).length === 0) {
      return;
    }

    // Auto-generate summary if not provided
    const summary = params.summary ?? generateSummary(action, entityType, diff);

    // Build the changes JSONB payload
    const changes: Record<string, unknown> = {};
    if (safeBefore) changes.before = safeBefore;
    if (safeAfter) changes.after = safeAfter;
    if (diff) changes.diff = diff;
    if (safeMetadata) changes.metadata = safeMetadata;

    await auditLog.create({
      data: {
        entityType,
        entityId,
        action,
        userId: userId ?? null,
        changes: changes as unknown as Prisma.InputJsonValue,
        source: source ?? null,
        entityName: entityName ?? null,
        summary,
      },
    });
  } catch (error) {
    // Fire-and-forget: log but never propagate
    logger.error(
      { err: error, entityType: params.entityType, entityId: params.entityId },
      "Failed to create audit entry",
    );
  }
}
