/**
 * T14 — immutable, ZERO-PAYLOAD audit writer (INV10). Single funnel that P5 also imports for
 * attach/manage/kill/revoke events. Append-only: no update/delete path is exposed. A payload
 * guard rejects any `meta` value that could smuggle terminal output (length cap + control/ANSI
 * bytes), so keystrokes/output/secrets can never land in `audit_log`.
 */
import { z } from 'zod'
import type { AuditStore, AuditRow } from '../store/ports.js'

export type AuditAction =
  | 'account.create'
  | 'account.suspend'
  | 'host.bind'
  | 'host.revoke'
  | 'pairing.issue'
  | 'pairing.redeem'
  | 'subdomain.assign'
  | 'node.drain'
  | 'attach'
  | 'manage'
  | 'kill'
  | 'revoke'

export interface AuditEntry {
  readonly action: AuditAction
  readonly principalId: string
  readonly accountId: string
  readonly hostId: string | null
  readonly ts: string
  readonly meta: Readonly<Record<string, string>>
}

/** Max length of a single metadata value — small enough that no terminal frame fits (INV10). */
export const MAX_META_VALUE_LEN = 256
/** Max number of metadata keys per entry. */
export const MAX_META_KEYS = 16

const AuditActionSchema = z.enum([
  'account.create',
  'account.suspend',
  'host.bind',
  'host.revoke',
  'pairing.issue',
  'pairing.redeem',
  'subdomain.assign',
  'node.drain',
  'attach',
  'manage',
  'kill',
  'revoke',
])

/** True if the string contains any C0 control byte (0x00–0x1f) — incl. ESC that begins ANSI. */
function hasControlBytes(v: string): boolean {
  for (let i = 0; i < v.length; i++) {
    if (v.charCodeAt(i) < 0x20) return true
  }
  return false
}

const MetaValue = z
  .string()
  .max(MAX_META_VALUE_LEN, 'meta value exceeds zero-payload cap')
  .refine((v) => !hasControlBytes(v), 'meta value contains control/ANSI bytes (possible terminal payload)')

const AuditEntrySchema = z
  .object({
    action: AuditActionSchema,
    principalId: z.string().min(1),
    accountId: z.string().min(1),
    hostId: z.string().nullable(),
    ts: z.string().datetime({ offset: true }),
    meta: z.record(z.string(), MetaValue).refine((m) => Object.keys(m).length <= MAX_META_KEYS, {
      message: 'too many meta keys',
    }),
  })
  .strict()

export interface AuditWriter {
  writeAuditEvent(entry: AuditEntry): Promise<void>
  queryAudit(accountId: string, from: string, to: string): Promise<readonly AuditEntry[]>
}

export function createAuditLog(store: AuditStore): AuditWriter {
  return {
    async writeAuditEvent(entry) {
      const parsed = AuditEntrySchema.safeParse(entry)
      if (!parsed.success) {
        throw new Error(`audit payload guard rejected entry: ${parsed.error.issues[0]?.message ?? 'invalid'}`)
      }
      const row: AuditRow = { ...parsed.data }
      await store.append(row) // append-only, immutable
    },
    async queryAudit(accountId, from, to) {
      const rows = await store.query(accountId, from, to)
      return rows.map((r) => ({
        action: r.action as AuditAction,
        principalId: r.principalId,
        accountId: r.accountId,
        hostId: r.hostId,
        ts: r.ts,
        meta: r.meta,
      }))
    },
  }
}

/** No-op writer for tasks that treat audit as an optional injected dependency in v0.9. */
export function noopAuditWriter(): AuditWriter {
  return {
    async writeAuditEvent() {
      /* intentionally does nothing */
    },
    async queryAudit() {
      return []
    },
  }
}
