/**
 * T1 · P5-local types + Zod schemas (frozen after T1, changed only via T1).
 *
 * Re-exports the frozen relay-contracts §4 shapes for P5 consumers (import, NEVER redefine)
 * and declares the P5-local records that §4 explicitly delegates to "P5-owned"
 * (webauthn_credentials / oidc_identities / audit / revocation policy / SPIFFE-ID).
 *
 * SECURITY (INV3): `accountId` originates ONLY inside `AuthenticatedPrincipal`, derived from a
 * verified credential/cert — never from a client field. `CapabilityToken.sub` carries THAT SAME
 * `accountId` (CAP_TOKEN_SUB_IS_ACCOUNT_ID). No schema here accepts a client-supplied
 * `accountId`/`tenant_id` as authoritative.
 */
import { z } from 'zod'

// ── Re-export frozen §4 types (import, never redefine) ────────────────────────────────────────
export type { CapabilityToken, CapabilityRight } from 'relay-contracts' // §4.3
export type { HostRecord, HostStatus, PlanTier, SessionRecord } from 'relay-contracts' // §4.2
// Revocation kill-signal + bus are FROZEN in relay-contracts §4.2 (FIX 4) — re-export, never redeclare:
export type { RevocationScope, KillSignal, RevocationBus } from 'relay-contracts' // §4.2, FIX 4
export { RELAY_REVOCATIONS_CHANNEL, REVOCATION_PUSH_BUDGET_MS } from 'relay-contracts' // 'relay:revocations' · 2000ms

// ── Auth method / principal ───────────────────────────────────────────────────────────────────
export type PrincipalKind = 'human' | 'agent' | 'share-grant'
export type AuthMethod = 'passkey' | 'totp' | 'oidc' | 'mtls' | 'stepup'

/**
 * The ONLY source of `accountId` in an authz decision (INV3). All fields are derived from
 * authenticated material; `accountId` is NEVER read from client input.
 */
export interface AuthenticatedPrincipal {
  readonly kind: PrincipalKind
  readonly accountId: string
  readonly principalId: string // device/credential id (WebAuthn credId, SPIFFE-ID, or grant jti)
  readonly amr: readonly AuthMethod[] // methods satisfied this session (for step-up, T8)
  readonly authAt: number // epoch seconds of last successful auth (login freshness)
  readonly stepUpAt: number | null // epoch seconds of last step-up (T8 freshness); null = never
  readonly stepUpMethod?: AuthMethod | null // method used for the LAST step-up (F1 method-binding); absent/null ⇒ none
}

/**
 * FROZEN P5 CONVENTION (resolves open-Q #3): §4.3 `CapabilityToken.sub` carries
 * `principal.accountId`. The single reader is T3's `accountIdFromToken`. This chooses the VALUE
 * P5 writes into an existing frozen field — it does NOT redefine §4.3.
 */
export const CAP_TOKEN_SUB_IS_ACCOUNT_ID = true as const

export interface StepUpPolicy {
  readonly required: boolean
  readonly maxAgeSeconds: number
  readonly requiredMethod: AuthMethod
}

// ── P5-owned auth records (§4.2 delegated to P5) ────────────────────────────────────────────────
export interface WebAuthnCredential {
  readonly credentialId: string
  readonly accountId: string
  readonly publicKey: Uint8Array
  readonly signCount: number
  readonly transports: readonly string[]
  readonly createdAt: string
}
export interface OidcIdentity {
  readonly issuer: string
  readonly subject: string
  readonly accountId: string
}
export interface TotpSecretRecord {
  readonly accountId: string
  readonly encSecret: Uint8Array // encrypted at rest (INV5); never SMS
  readonly confirmedAt: string | null
}

export type AuditAction =
  | 'attach'
  | 'reattach'
  | 'manage'
  | 'kill'
  | 'enroll'
  | 'revoke'
  | 'login'
  | 'stepup'
  | 'token-issue'
  | 'cross-tenant-attempt'
export type AuditOutcome = 'allow' | 'deny'

/** INV10 — metadata only, ZERO payload. No field may carry keystrokes/output (redact.ts + Zod guard). */
export interface AuditEvent {
  readonly ts: string
  readonly action: AuditAction
  readonly principalId: string
  readonly accountId: string
  readonly hostId: string | null
  readonly sessionId: string | null
  readonly jti: string | null
  readonly outcome: AuditOutcome
  readonly reason: string
  readonly remoteAddrHash: string
}

export type SpiffeId = string // 'spiffe://relay.<domain>/account/<accountId>/(host|device)/<id>'

export interface RateLimitPolicy {
  readonly connectPerMin: number
  readonly enrollPerHour: number
  readonly maxConcurrentSessions: number
  readonly maxPairedHosts: number
  readonly preAuthPerMinPerIp: number // pre-principal throttle keyed on remoteAddrHash (T11)
  readonly totpMaxFailsPerWindow: number
  readonly totpLockoutWindowSec: number
}

// ── Read-only ports P3/P1 implement (P5 stays storage-free & pure) ──────────────────────────────
import type { HostRecord as _HostRecord } from 'relay-contracts'
export interface HostRegistryPort {
  getById(hostId: string): Promise<_HostRecord | null>
}
export interface SessionRegistryPort {
  getById(sessionId: string): Promise<{ hostId: string; accountId: string } | null>
}
export interface RevocationStore {
  isRevoked(jti: string): Promise<boolean>
  revokeJti(jti: string, exp: number): Promise<void>
  /** single-use consumption: true = first use (proceed); false = replay (deny). */
  consumeOnce(jti: string, exp: number): Promise<boolean>
}
export interface AuditSink {
  append(e: AuditEvent): Promise<void>
}
export interface TokenBucketStore {
  /** false = throttled. Keys are OPAQUE to P5. */
  take(key: string, refillPerSec: number, burst: number, now: number): Promise<boolean>
}

// ── Zod schemas (boundary validation, coding-style "Input Validation") ──────────────────────────
export const PrincipalKindSchema = z.enum(['human', 'agent', 'share-grant'])
export const AuthMethodSchema = z.enum(['passkey', 'totp', 'oidc', 'mtls', 'stepup'])

export const AuthenticatedPrincipalSchema = z
  .object({
    kind: PrincipalKindSchema,
    accountId: z.string().min(1),
    principalId: z.string().min(1),
    amr: z.array(AuthMethodSchema).readonly(),
    authAt: z.number().int().nonnegative(),
    stepUpAt: z.number().int().nonnegative().nullable(),
    stepUpMethod: AuthMethodSchema.nullable().optional(),
  })
  .strict()

export const StepUpPolicySchema = z
  .object({
    required: z.boolean(),
    maxAgeSeconds: z.number().int().nonnegative(),
    requiredMethod: AuthMethodSchema,
  })
  .strict()

export const WebAuthnCredentialSchema = z
  .object({
    credentialId: z.string().min(1),
    accountId: z.string().min(1),
    publicKey: z.instanceof(Uint8Array),
    signCount: z.number().int().nonnegative(),
    transports: z.array(z.string()).readonly(),
    createdAt: z.string().min(1),
  })
  .strict()

export const OidcIdentitySchema = z
  .object({
    issuer: z.string().min(1),
    subject: z.string().min(1),
    accountId: z.string().min(1),
  })
  .strict()

export const TotpSecretRecordSchema = z
  .object({
    accountId: z.string().min(1),
    encSecret: z.instanceof(Uint8Array),
    confirmedAt: z.string().nullable(),
  })
  .strict()

export const AuditActionSchema = z.enum([
  'attach',
  'reattach',
  'manage',
  'kill',
  'enroll',
  'revoke',
  'login',
  'stepup',
  'token-issue',
  'cross-tenant-attempt',
])

/**
 * INV10 zero-payload guard at the type layer: `.strict()` REJECTS any unknown key (e.g. a
 * `data`/`bytes` field carrying terminal output). Length/control-char checks are in redact.ts (T4).
 */
export const AuditEventSchema = z
  .object({
    ts: z.string().min(1),
    action: AuditActionSchema,
    principalId: z.string(),
    accountId: z.string(),
    hostId: z.string().nullable(),
    sessionId: z.string().nullable(),
    jti: z.string().nullable(),
    outcome: z.enum(['allow', 'deny']),
    reason: z.string(),
    remoteAddrHash: z.string(),
  })
  .strict()

export const RateLimitPolicySchema = z
  .object({
    connectPerMin: z.number().int().positive(),
    enrollPerHour: z.number().int().positive(),
    maxConcurrentSessions: z.number().int().positive(),
    maxPairedHosts: z.number().int().positive(),
    preAuthPerMinPerIp: z.number().int().positive(),
    totpMaxFailsPerWindow: z.number().int().positive(),
    totpLockoutWindowSec: z.number().int().positive(),
  })
  .strict()

/**
 * SPIFFE-ID validator: accepts BOTH the host arm `spiffe://relay.<domain>/account/<a>/host/<h>`
 * and the device arm `.../account/<a>/device/<d>` (FIX H-cp-2 / H-native-6, added in lockstep
 * with the control-plane device issuer). REJECTS path-traversal (`..`) or wildcard (`*`); the
 * kind segment is exactly `host` or `device`. Segment chars are restricted (no `/`).
 */
const SPIFFE_ID_RE =
  /^spiffe:\/\/relay\.[a-z0-9.-]+\/account\/[A-Za-z0-9_-]+\/(?:host|device)\/[A-Za-z0-9_-]+$/
export const SpiffeIdSchema = z
  .string()
  .regex(SPIFFE_ID_RE, 'invalid SPIFFE-ID form')
  .refine((s) => !s.includes('..') && !s.includes('*'), 'path-traversal/wildcard rejected')
