import type { AuthzScope } from "./types.ts";

export interface AuthzAuditEvent {
  subjectId?: string;
  scope?: AuthzScope;
  permission: string;
  allowed: boolean;
  reason?: string;
  policy?: string;
  /** Epoch milliseconds. */
  at: number;
}

export interface AuthzAuditSink {
  record(event: AuthzAuditEvent): void | Promise<void>;
}

export interface MemoryAuditSink extends AuthzAuditSink {
  events: AuthzAuditEvent[];
  clear(): void;
}

export function memoryAuditSink(): MemoryAuditSink {
  const events: AuthzAuditEvent[] = [];
  return {
    events,
    record: (event) => void events.push(event),
    clear: () => void events.splice(0, events.length),
  };
}

/**
 * Subject ids, tenant ids, and denial reasons trace back to request input, so
 * a newline in one would forge a second audit line indistinguishable from a
 * real entry. Strip control characters before interpolating.
 */
function logSafe(value: string): string {
  let out = "";
  for (const character of value) {
    const code = character.codePointAt(0)!;
    // C0 + DEL, plus NEL and the Unicode line/paragraph separators, which some
    // log shippers and JSON consumers also treat as line terminators.
    const isLineBreaking =
      code < 0x20 || code === 0x7f || code === 0x85 || code === 0x2028 || code === 0x2029;
    out += isLineBreaking ? " " : character;
  }
  return out;
}

export function consoleAuditSink(): AuthzAuditSink {
  return {
    record(event) {
      const verdict = event.allowed ? "allow" : "deny";
      console.info(
        `[wrnexus:authz] ${verdict} ${logSafe(event.permission)} ` +
          `subject=${logSafe(event.subjectId ?? "anonymous")}` +
          `${event.scope?.tenantId ? ` tenant=${logSafe(event.scope.tenantId)}` : ""}` +
          `${event.reason ? ` reason=${logSafe(event.reason)}` : ""}` +
          `${event.policy ? ` policy=${logSafe(event.policy)}` : ""}`,
      );
    },
  };
}

/** Record without ever letting a sink failure escape into the request path. */
export function safeRecord(sink: AuthzAuditSink | undefined, event: AuthzAuditEvent): void {
  if (!sink) return;
  try {
    const result = sink.record(event);
    if (result instanceof Promise) {
      result.catch((error) => console.warn("[wrnexus:authz] audit sink failed", error));
    }
  } catch (error) {
    console.warn("[wrnexus:authz] audit sink failed", error);
  }
}
