// Canonical form for audit-chain hashing. Must be deterministic across
// writes and reads — anyone with the same data must compute the same hash.
// Stability rules:
//   - keys are sorted alphabetically at every level
//   - undefined fields are omitted (not null)
//   - dates serialized as ISO-8601 with millisecond precision (Date.toISOString())
//   - the `hash` field itself is never included (chicken-and-egg)
//   - new schema fields land in this canonicalizer at the same time the
//     schema field lands, otherwise the chain breaks at the upgrade boundary

import { createHash, createHmac, timingSafeEqual } from 'node:crypto'
import type { AuditEventDocument } from '../schemas/audit-event.schema.js'

// Subset of AuditEvent we hash. The order of keys here doesn't matter —
// canonicalJson() sorts before serializing — but listing them explicitly
// guarantees we don't accidentally hash Mongoose internals (_id, __v) or
// Date timestamps (createdAt/recordedAt) that vary across writes.
const HASHABLE_FIELDS = [
  'seq',
  'at',
  'actorType',
  'actorId',
  'actorEmail',
  'actorIp',
  'action',
  'outcome',
  'resourceType',
  'resourceId',
  'resourceName',
  'tenantSlug',
  'partnerSlug',
  'source',
  'externalId',
  'metadata',
  'prevHash',
] as const

export interface CanonicalEvent {
  seq: number
  at: string
  actorType: string
  actorId?: string
  actorEmail?: string
  actorIp?: string
  action: string
  outcome: string
  resourceType?: string
  resourceId?: string
  resourceName?: string
  tenantSlug?: string
  partnerSlug?: string
  source: string
  externalId?: string
  metadata?: unknown
  prevHash: string
}

export function eventToCanonical(evt: AuditEventDocument | Record<string, unknown>): CanonicalEvent {
  const e = evt as Record<string, unknown>
  const out: Record<string, unknown> = {}
  for (const k of HASHABLE_FIELDS) {
    const v = e[k]
    if (v === undefined || v === null) continue
    out[k] = k === 'at' && v instanceof Date ? v.toISOString() : v
  }
  return out as unknown as CanonicalEvent
}

// Stable JSON: sort keys recursively. We never emit `undefined`, and arrays
// preserve order (they're position-sensitive).
export function canonicalJson(value: unknown): string {
  return JSON.stringify(value, (_key, v) => {
    if (v === null || typeof v !== 'object' || Array.isArray(v)) return v
    if (v instanceof Date) return v.toISOString()
    const sorted: Record<string, unknown> = {}
    for (const k of Object.keys(v as Record<string, unknown>).sort()) {
      const val = (v as Record<string, unknown>)[k]
      if (val !== undefined) sorted[k] = val
    }
    return sorted
  })
}

export function hashCanonical(canonical: CanonicalEvent): string {
  return createHash('sha256').update(canonicalJson(canonical)).digest('hex')
}

// HMAC of a checkpoint's identifying triple. Signed with AUDIT_SIGNING_KEY.
// Verification: recompute on read, compare with timingSafeEqual.
export function checkpointSignature(
  key: string,
  headSeq: number,
  headHash: string,
  at: Date,
): string {
  const payload = `${headSeq}:${headHash}:${at.toISOString()}`
  return createHmac('sha256', key).update(payload).digest('hex')
}

export function verifyCheckpointSignature(
  key: string,
  headSeq: number,
  headHash: string,
  at: Date,
  signature: string,
): boolean {
  const expected = checkpointSignature(key, headSeq, headHash, at)
  if (expected.length !== signature.length) return false
  return timingSafeEqual(Buffer.from(expected, 'hex'), Buffer.from(signature, 'hex'))
}

export const GENESIS_HASH = 'GENESIS'
