import { getDb, newId, nowIso, type DbClient } from "@/lib/db";
import type { SessionUser } from "@/lib/auth/session";

/**
 * Audit trail (SPEC 28).
 *
 * Academic records are sensitive administrative data, so every critical change
 * is recorded here with before/after snapshots. The table is append-only: the
 * application never issues an UPDATE or DELETE against `audit_logs`, and only
 * SUPER_ADMIN can read it.
 */

export const AUDIT_ACTIONS = {
  STUDENT_CREATED: "STUDENT_CREATED",
  STUDENT_UPDATED: "STUDENT_UPDATED",
  STUDENT_ARCHIVED: "STUDENT_ARCHIVED",
  STUDENT_RESTORED: "STUDENT_RESTORED",
  CURRICULUM_AUTO_ASSIGNED: "CURRICULUM_AUTO_ASSIGNED",
  CURRICULUM_MANUALLY_OVERRIDDEN: "CURRICULUM_MANUALLY_OVERRIDDEN",
  ASSESSMENT_CHANGED: "ASSESSMENT_CHANGED",
  ASSESSMENT_BULK_CHANGED: "ASSESSMENT_BULK_CHANGED",
  EQUIVALENCY_CHANGED: "EQUIVALENCY_CHANGED",
  NON_UNIT_PROGRESS_CHANGED: "NON_UNIT_PROGRESS_CHANGED",
  CURRICULUM_CREATED: "CURRICULUM_CREATED",
  CURRICULUM_IMPORTED: "CURRICULUM_IMPORTED",
  CURRICULUM_PUBLISHED: "CURRICULUM_PUBLISHED",
  CURRICULUM_ARCHIVED: "CURRICULUM_ARCHIVED",
  CURRICULUM_STRUCTURE_CHANGED: "CURRICULUM_STRUCTURE_CHANGED",
  CURRICULUM_TYPE_MAPPED: "CURRICULUM_TYPE_MAPPED",
  CURRICULUM_VALIDATION_ACKNOWLEDGED: "CURRICULUM_VALIDATION_ACKNOWLEDGED",
  RULE_CREATED: "RULE_CREATED",
  RULE_UPDATED: "RULE_UPDATED",
  RULE_DELETED: "RULE_DELETED",
  BULK_IMPORT_EXECUTED: "BULK_IMPORT_EXECUTED",
  USER_CREATED: "USER_CREATED",
  USER_UPDATED: "USER_UPDATED",
  SETTINGS_UPDATED: "SETTINGS_UPDATED",
  SIGN_IN: "SIGN_IN",
  SIGN_OUT: "SIGN_OUT",
} as const;

export type AuditAction = (typeof AUDIT_ACTIONS)[keyof typeof AUDIT_ACTIONS];

export interface AuditInput {
  actor: SessionUser | null;
  action: AuditAction | string;
  entityType: string;
  entityId?: string | null;
  before?: unknown;
  after?: unknown;
  reason?: string | null;
}

/** Fields that must never be written into an audit snapshot. */
const REDACTED_KEYS = new Set(["password", "password_hash", "password_salt", "token"]);

function serialize(value: unknown): string | null {
  if (value === undefined || value === null) return null;
  try {
    return JSON.stringify(value, (key, val) => (REDACTED_KEYS.has(key) ? "[redacted]" : val));
  } catch {
    return null;
  }
}

export async function recordAudit(input: AuditInput, client?: DbClient): Promise<void> {
  const db = client ?? (await getDb());
  await db.run(
    `INSERT INTO audit_logs
       (id, actor_user_id, actor_email, action, entity_type, entity_id, before_json, after_json,
        reason, program_id, created_at)
     VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
    [
      newId(),
      input.actor?.id ?? null,
      input.actor?.email ?? null,
      input.action,
      input.entityType,
      input.entityId ?? null,
      serialize(input.before),
      serialize(input.after),
      input.reason ?? null,
      // Stamped from the actor's scope so a scoped user's audit view can be
      // narrowed without leaking another program's records.
      input.actor?.program_id ?? null,
      nowIso(),
    ],
  );
}

/** Returns only the keys that actually changed, keeping audit rows small. */
export function diffRecords<T extends Record<string, unknown>>(
  before: T | null | undefined,
  after: T | null | undefined,
): { before: Partial<T>; after: Partial<T>; changedKeys: string[] } {
  const b: Partial<T> = {};
  const a: Partial<T> = {};
  const changedKeys: string[] = [];
  const keys = new Set([...Object.keys(before ?? {}), ...Object.keys(after ?? {})]);
  for (const key of keys) {
    const bv = before?.[key];
    const av = after?.[key];
    if (JSON.stringify(bv ?? null) !== JSON.stringify(av ?? null)) {
      changedKeys.push(key);
      (b as Record<string, unknown>)[key] = bv ?? null;
      (a as Record<string, unknown>)[key] = av ?? null;
    }
  }
  return { before: b, after: a, changedKeys };
}
