// src/lib/admin/audit.ts — God-Mode audit trail.
//
// recordAdminAction() captures WHO (the current super-admin) did WHAT to WHICH
// target, with optional structured context, and appends a row to
// admin_audit_log. It is intentionally fail-soft: a logging failure must never
// break the privileged action it accompanies, so insert errors are swallowed
// (console.error only). Callers already assert super-admin BEFORE acting —
// this helper does NOT re-gate.
import "server-only";
import {
  and,
  count,
  desc,
  eq,
  gte,
  ilike,
  lte,
  type SQL,
  sql,
} from "drizzle-orm";
import { headers } from "next/headers";
import { auth } from "@/lib/auth";
import { assertSysPermission } from "@/lib/auth/sys-permissions";
import { adminAuditLog } from "@/lib/db/admin-schema";
import { db } from "@/lib/db/client";

export type AdminAuditTargetType =
  | "user"
  | "organization"
  | "subscription"
  | "session"
  | "system";

export type AdminAuditLog = typeof adminAuditLog.$inferSelect;

export interface RecordAdminActionInput {
  action: string;
  targetType: AdminAuditTargetType;
  targetId?: string | null;
  targetLabel?: string | null;
  metadata?: Record<string, unknown> | null;
}

/** First IP from the proxy headers we trust (mirror auth.ts ipAddressHeaders). */
function readClientIp(h: Headers): string | null {
  const realIp = h.get("x-real-ip");
  if (realIp) return realIp.trim();
  const fwd = h.get("x-forwarded-for");
  if (fwd) return fwd.split(",")[0]?.trim() || null;
  return null;
}

/** Executor accepted by `recordAdminAction` for the transactional path — a
 *  Drizzle transaction handle satisfies this (it only needs `.insert`). */
export type AuditExecutor = Pick<typeof db, "insert">;

type AuditSession = Awaited<ReturnType<typeof auth.api.getSession>>;

/** Build the row payload shared by both the transactional and fail-soft paths. */
function buildAuditRow(
  input: RecordAdminActionInput,
  h: Headers,
  session: AuditSession,
) {
  return {
    actorId: session?.user?.id ?? null,
    actorEmail: session?.user?.email ?? "unknown",
    action: input.action,
    targetType: input.targetType,
    targetId: input.targetId ?? null,
    targetLabel: input.targetLabel ?? null,
    metadata: input.metadata ?? null,
    ipAddress: readClientIp(h),
  };
}

/**
 * Append an entry to the admin audit log. Reads the current session to snapshot
 * the actor (id + email) and the client IP.
 *
 * Two modes:
 * - With `executor` (spec §8.9): the insert runs through the caller-supplied
 *   executor (typically a Drizzle transaction handle) and RETHROWS on
 *   failure, so the enclosing transaction rolls back — a mutation must never
 *   be committed without its audit row.
 * - Without `executor`: historical fail-soft behavior — a logging failure
 *   must never break the privileged action it accompanies, so insert errors
 *   are swallowed (console.error only).
 */
export async function recordAdminAction(
  input: RecordAdminActionInput,
  executor?: AuditExecutor,
): Promise<void> {
  if (executor) {
    const h = await headers();
    const session = await auth.api.getSession({ headers: h });
    await executor
      .insert(adminAuditLog)
      .values(buildAuditRow(input, h, session));
    return;
  }
  try {
    const h = await headers();
    const session = await auth.api.getSession({ headers: h });
    await db.insert(adminAuditLog).values(buildAuditRow(input, h, session));
  } catch (err) {
    console.error("[audit] failed to record admin action", input.action, err);
  }
}

export interface ListAuditLogOptions {
  limit?: number;
  offset?: number;
  actorId?: string;
  /** Case-insensitive exact match on the actor's email snapshot. */
  actorEmail?: string;
  targetType?: string;
  targetId?: string;
  /** Case-insensitive substring match on the dotted action name. */
  action?: string;
  /** Inclusive lower bound on createdAt. */
  since?: Date;
  /** Inclusive upper bound on createdAt. */
  until?: Date;
}

/** Escape LIKE/ILIKE wildcards so user input matches literally (\ is the
 *  default Postgres escape char). */
export function escapeLike(value: string): string {
  return value.replace(/[\\%_]/g, (c) => `\\${c}`);
}

export interface ListAuditLogResult {
  entries: AdminAuditLog[];
  total: number;
}

const DEFAULT_LIMIT = 50;

/** Most-recent-first audit entries with a total count for pagination. Gated on
 *  `audit:read` (Task 9 — this loader had NO gate before; the /admin/audit
 *  page relied entirely on the (superadmin) layout's super-admin check, which
 *  a Staff SYS with a valid `audit:read` grant would never pass). */
export async function listAuditLog(
  opts: ListAuditLogOptions = {},
): Promise<ListAuditLogResult> {
  await assertSysPermission("audit:read");
  const limit = Math.min(Math.max(opts.limit ?? DEFAULT_LIMIT, 1), 200);
  const offset = Math.max(opts.offset ?? 0, 0);

  const filters: SQL[] = [];
  if (opts.actorId) filters.push(eq(adminAuditLog.actorId, opts.actorId));
  if (opts.actorEmail)
    filters.push(
      sql`lower(${adminAuditLog.actorEmail}) = ${opts.actorEmail.trim().toLowerCase()}`,
    );
  if (opts.targetType)
    filters.push(eq(adminAuditLog.targetType, opts.targetType));
  if (opts.targetId) filters.push(eq(adminAuditLog.targetId, opts.targetId));
  if (opts.action)
    filters.push(
      ilike(adminAuditLog.action, `%${escapeLike(opts.action.trim())}%`),
    );
  if (opts.since) filters.push(gte(adminAuditLog.createdAt, opts.since));
  if (opts.until) filters.push(lte(adminAuditLog.createdAt, opts.until));
  const where = filters.length ? and(...filters) : undefined;

  const [entries, totalRows] = await Promise.all([
    db
      .select()
      .from(adminAuditLog)
      .where(where)
      .orderBy(desc(adminAuditLog.createdAt))
      .limit(limit)
      .offset(offset),
    db.select({ value: count() }).from(adminAuditLog).where(where),
  ]);

  return { entries, total: totalRows[0]?.value ?? 0 };
}
