import type BetterSqlite3 from "better-sqlite3";

export const auditRetentionMilliseconds = 180 * 24 * 60 * 60 * 1_000;

const auditRefPattern = /^[A-Za-z0-9][A-Za-z0-9_.:-]{0,159}$/;
const forbiddenSummaryKeys = new Set([
  "absolute_path", "api_key", "body", "code_hmac", "content", "credential",
  "email", "image", "image_content", "password", "path", "prompt", "secret",
  "session_token", "verification_code", "whitelist",
]);
const forbiddenSummaryKeyFragments = [
  "content", "credential", "email", "image", "password", "path", "prompt", "secret", "token",
];
const safeStringPattern = /^[A-Za-z0-9_.:@-]{1,160}$/;

function isSafeSummaryValue(value: unknown, depth: number): boolean {
  if (depth > 3) return false;
  if (value === null || typeof value === "boolean") return true;
  if (typeof value === "number") return Number.isSafeInteger(value);
  if (typeof value === "string") return safeStringPattern.test(value) && !value.includes("@");
  if (Array.isArray(value)) return value.length <= 20 && value.every((entry) => isSafeSummaryValue(entry, depth + 1));
  if (!value || typeof value !== "object") return false;
  const entries = Object.entries(value);
  return entries.length <= 32 && entries.every(([key, entry]) => (
    auditRefPattern.test(key)
    && !forbiddenSummaryKeys.has(key.toLowerCase())
    && !forbiddenSummaryKeyFragments.some((fragment) => key.toLowerCase().includes(fragment))
    && isSafeSummaryValue(entry, depth + 1)
  ));
}

export function isSafeAuditRef(value: unknown) {
  return typeof value === "string" && auditRefPattern.test(value) ? 1 : 0;
}

export function isSafeAuditSummaryJson(value: unknown) {
  if (typeof value !== "string" || Buffer.byteLength(value, "utf8") > 2_048) return 0;
  try {
    return isSafeSummaryValue(JSON.parse(value), 0) ? 1 : 0;
  } catch {
    return 0;
  }
}

export function serializeAuditSummary(value: Record<string, unknown> | null) {
  if (value === null) return null;
  const serialized = JSON.stringify(value);
  if (isSafeAuditSummaryJson(serialized) !== 1) throw new Error("audit_summary_sensitive_or_invalid");
  return serialized;
}

function toMilliseconds(value: string | number | null | undefined, fallback: number) {
  if (typeof value === "number" && Number.isSafeInteger(value)) return value;
  const parsed = Date.parse(String(value));
  return Number.isFinite(parsed) ? parsed : fallback;
}

function createAdminOperationTable(database: BetterSqlite3.Database) {
  database.exec(`
    CREATE TABLE admin_operation_logs (
      log_id TEXT PRIMARY KEY,
      actor_type TEXT NOT NULL CHECK (actor_type IN ('system', 'super_admin')),
      actor_ref TEXT NOT NULL CHECK (dada_audit_ref_is_safe(actor_ref) = 1),
      operation_type TEXT NOT NULL CHECK (dada_audit_ref_is_safe(operation_type) = 1),
      target_type TEXT NOT NULL CHECK (dada_audit_ref_is_safe(target_type) = 1),
      target_ref TEXT NOT NULL CHECK (dada_audit_ref_is_safe(target_ref) = 1),
      result TEXT NOT NULL CHECK (result IN ('succeeded', 'failed')),
      before_summary TEXT CHECK (before_summary IS NULL OR dada_audit_summary_is_safe(before_summary) = 1),
      after_summary TEXT CHECK (after_summary IS NULL OR dada_audit_summary_is_safe(after_summary) = 1),
      occurred_at INTEGER NOT NULL,
      expires_at INTEGER NOT NULL CHECK (expires_at = occurred_at + ${auditRetentionMilliseconds})
    );
  `);
}

function installAdminOperationTriggers(database: BetterSqlite3.Database) {
  database.exec(`
    DROP TRIGGER IF EXISTS admin_operation_logs_no_update;
    DROP TRIGGER IF EXISTS admin_operation_logs_no_delete;
    CREATE TRIGGER admin_operation_logs_no_update
      BEFORE UPDATE ON admin_operation_logs
      BEGIN SELECT RAISE(ABORT, 'admin_operation_logs_immutable'); END;
    CREATE TRIGGER admin_operation_logs_no_delete
      BEFORE DELETE ON admin_operation_logs
      WHEN dada_allow_retention_purge() <> 1 OR OLD.expires_at > dada_retention_purge_now()
      BEGIN SELECT RAISE(ABORT, 'admin_operation_logs_immutable'); END;
  `);
}

export function ensureAdminOperationAuditSchema(database: BetterSqlite3.Database, fallbackNow: number) {
  const table = database.prepare(`
    SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'admin_operation_logs'
  `).get() as { sql: string } | undefined;
  if (!table) {
    createAdminOperationTable(database);
    installAdminOperationTriggers(database);
    return;
  }
  if (table.sql.includes("dada_audit_summary_is_safe") && table.sql.includes(String(auditRetentionMilliseconds))) {
    installAdminOperationTriggers(database);
    return;
  }

  const columns = database.prepare("PRAGMA table_info(admin_operation_logs)").all() as Array<{ name: string }>;
  const current = columns.some((column) => column.name === "actor_type");
  const rows = current
    ? database.prepare("SELECT * FROM admin_operation_logs").all() as Array<Record<string, string | number | null>>
    : database.prepare("SELECT log_id, operation, outcome, target_ref, created_at FROM admin_operation_logs").all() as Array<Record<string, string | number | null>>;
  const migrate = database.transaction(() => {
    database.exec(`
      DROP TRIGGER IF EXISTS admin_operation_logs_no_update;
      DROP TRIGGER IF EXISTS admin_operation_logs_no_delete;
      ALTER TABLE admin_operation_logs RENAME TO admin_operation_logs_before_wp1_06;
    `);
    createAdminOperationTable(database);
    const insert = database.prepare(`
      INSERT INTO admin_operation_logs (
        log_id, actor_type, actor_ref, operation_type, target_type, target_ref,
        result, before_summary, after_summary, occurred_at, expires_at
      ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
    `);
    for (const row of rows) {
      const occurredAt = toMilliseconds(current ? row.occurred_at! : row.created_at!, fallbackNow);
      const expiresAt = current ? toMilliseconds(row.expires_at!, fallbackNow) : occurredAt + auditRetentionMilliseconds;
      if (expiresAt !== occurredAt + auditRetentionMilliseconds) throw new Error("audit_retention_invalid");
      const beforeSummary = current && typeof row.before_summary === "string"
        ? serializeAuditSummary(JSON.parse(row.before_summary) as Record<string, unknown>)
        : null;
      const afterSummary = current
        ? typeof row.after_summary === "string" ? serializeAuditSummary(JSON.parse(row.after_summary) as Record<string, unknown>) : null
        : serializeAuditSummary({ legacy_outcome: String(row.outcome) });
      insert.run(
        row.log_id,
        current ? row.actor_type : "system",
        current ? row.actor_ref : "managed_storage_migration",
        current ? row.operation_type : row.operation,
        current ? row.target_type : "legacy_operation",
        row.target_ref,
        current ? row.result : String(row.outcome).startsWith("denied") ? "failed" : "succeeded",
        beforeSummary,
        afterSummary,
        occurredAt,
        expiresAt,
      );
    }
    database.exec("DROP TABLE admin_operation_logs_before_wp1_06");
    installAdminOperationTriggers(database);
  });
  migrate();
}

function createPrivateAccessTable(database: BetterSqlite3.Database) {
  database.exec(`
    CREATE TABLE private_content_access_logs (
      log_id TEXT PRIMARY KEY,
      actor_ref TEXT NOT NULL CHECK (length(actor_ref) = 36 AND dada_audit_ref_is_safe(actor_ref) = 1),
      subject_ref TEXT NOT NULL CHECK (length(subject_ref) = 36 AND dada_audit_ref_is_safe(subject_ref) = 1),
      target_ref TEXT NOT NULL CHECK (length(target_ref) = 36 AND dada_audit_ref_is_safe(target_ref) = 1),
      content_type TEXT NOT NULL CHECK (content_type IN ('image', 'prompt')),
      occurred_at INTEGER NOT NULL,
      expires_at INTEGER NOT NULL CHECK (expires_at = occurred_at + ${auditRetentionMilliseconds})
    );
  `);
}

function installPrivateAccessTriggers(database: BetterSqlite3.Database) {
  database.exec(`
    DROP TRIGGER IF EXISTS private_content_access_logs_no_update;
    DROP TRIGGER IF EXISTS private_content_access_logs_no_delete;
    CREATE TRIGGER private_content_access_logs_no_update
      BEFORE UPDATE ON private_content_access_logs
      WHEN dada_allow_privacy_purge() <> 1
        OR OLD.subject_ref <> dada_privacy_purge_subject()
        OR NEW.actor_ref <> OLD.actor_ref
        OR NEW.content_type <> OLD.content_type
        OR NEW.occurred_at <> OLD.occurred_at
        OR NEW.expires_at <> OLD.expires_at
        OR NEW.subject_ref = OLD.subject_ref
        OR NEW.target_ref = OLD.target_ref
      BEGIN SELECT RAISE(ABORT, 'private_content_access_logs_immutable'); END;
    CREATE TRIGGER private_content_access_logs_no_delete
      BEFORE DELETE ON private_content_access_logs
      WHEN dada_allow_retention_purge() <> 1 OR OLD.expires_at > dada_retention_purge_now()
      BEGIN SELECT RAISE(ABORT, 'private_content_access_logs_immutable'); END;
  `);
}

export function ensurePrivateAccessAuditSchema(database: BetterSqlite3.Database) {
  const table = database.prepare(`
    SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'private_content_access_logs'
  `).get() as { sql: string } | undefined;
  if (!table) {
    createPrivateAccessTable(database);
    installPrivateAccessTriggers(database);
    return;
  }
  if (table.sql.includes(String(auditRetentionMilliseconds))) {
    installPrivateAccessTriggers(database);
    return;
  }
  const rows = database.prepare("SELECT * FROM private_content_access_logs").all() as Array<Record<string, string | number>>;
  const migrate = database.transaction(() => {
    database.exec(`
      DROP TRIGGER IF EXISTS private_content_access_logs_no_update;
      DROP TRIGGER IF EXISTS private_content_access_logs_no_delete;
      ALTER TABLE private_content_access_logs RENAME TO private_content_access_logs_before_wp1_06;
    `);
    createPrivateAccessTable(database);
    const insert = database.prepare(`
      INSERT INTO private_content_access_logs (
        log_id, actor_ref, subject_ref, target_ref, content_type, occurred_at, expires_at
      ) VALUES (?, ?, ?, ?, ?, ?, ?)
    `);
    for (const row of rows) {
      const occurredAt = toMilliseconds(row.occurred_at, 0);
      const expiresAt = toMilliseconds(row.expires_at, 0);
      if (expiresAt !== occurredAt + auditRetentionMilliseconds) throw new Error("audit_retention_invalid");
      insert.run(row.log_id, row.actor_ref, row.subject_ref, row.target_ref, row.content_type, occurredAt, expiresAt);
    }
    database.exec("DROP TABLE private_content_access_logs_before_wp1_06");
    installPrivateAccessTriggers(database);
  });
  migrate();
}
