import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'
import { HydratedDocument, Types } from 'mongoose'

export type AuditEventDocument = HydratedDocument<AuditEvent>

export type AuditActorType = 'user' | 'system'
export type AuditOutcome = 'success' | 'failure'
export type AuditResourceType =
  | 'tenant'
  | 'partner'
  | 'user'
  | 'flag'
  | 'subscription'
  | 'domain'
  | 'system'
export type AuditSource =
  | 'platform-api'
  | 'operator-ui'
  | 'portal'
  | 'authentik'
  | 'ocis'
  | 'stalwart'

// Append-only audit trail. Records are written by AuditService.record (called
// from every mutation in the API layer). The schema has no UPDATE/DELETE
// surface — older entries can only be removed by Mongo TTL / archive jobs,
// not application code. See docs/FEATURE-FLAGS.md ... no wait, see the audit
// plan when it lands. The hash fields are nullable placeholders for the
// hash-chain we add in a later phase.
@Schema({ collection: 'audit_events', timestamps: { createdAt: 'recordedAt', updatedAt: false } })
export class AuditEvent {
  // Wall-clock time of the event itself. Distinct from recordedAt (timestamps)
  // because a future ingestion adapter (Authentik / OCIS / Stalwart) may
  // backfill events whose real time is older than when we recorded them.
  @Prop({ required: true, default: () => new Date(), index: true })
  at!: Date

  @Prop({ enum: ['user', 'system'], default: 'user', index: true })
  actorType!: AuditActorType

  @Prop({ type: Types.ObjectId, ref: 'User', index: true })
  actorId?: Types.ObjectId

  @Prop()
  actorEmail?: string

  // Originating IP — forwarded via X-Forwarded-For from the Nuxt proxies.
  // May be undefined for system-triggered events (seed, scheduled jobs).
  @Prop()
  actorIp?: string

  // dotted.snake_case verb. Examples:
  //   tenant.created, tenant.suspended, tenant.resumed
  //   partner.created, partner.terminated
  //   flag.created, flag.state_changed, flag.killed
  //   user.deactivated
  // Future ingestion adapters add: authentik.login, ocis.share_created, ...
  @Prop({ required: true, index: true })
  action!: string

  @Prop({ enum: ['success', 'failure'], default: 'success', index: true })
  outcome!: AuditOutcome

  @Prop({ enum: ['tenant', 'partner', 'user', 'flag', 'subscription', 'domain', 'system'] })
  resourceType?: AuditResourceType

  // Free-form (slug, ObjectId-as-string, external id, etc.) since some
  // resources don't have an ObjectId we can ref directly.
  @Prop()
  resourceId?: string

  // Human-friendly label shown in the operator UI (saves a join).
  @Prop()
  resourceName?: string

  // Bubbled-up tenant/partner refs so per-tenant/per-partner timelines
  // are an index scan, not a join.
  @Prop({ index: true })
  tenantSlug?: string

  @Prop({ index: true })
  partnerSlug?: string

  @Prop({
    enum: ['platform-api', 'operator-ui', 'portal', 'authentik', 'ocis', 'stalwart'],
    default: 'platform-api',
    index: true,
  })
  source!: AuditSource

  // Free-form additional context. Keep small — no large blobs. Common keys:
  //   before, after: the diff for an update
  //   reason: operator-supplied note (impersonation reason, kill-switch reason)
  //   error: failure message when outcome === 'failure'
  @Prop({ type: Object })
  metadata?: Record<string, unknown>

  // External event id (Authentik `pk`, OCIS event id, Stalwart event id…).
  // Combined with `source` it's the dedup key for ingest workers — re-pulling
  // the same Authentik event window is harmless. The compound unique index on
  // (source, externalId) below — with a partialFilterExpression — gives both
  // the dedup constraint and the lookup index in one shot.
  @Prop()
  externalId?: string

  // ── Tamper-evidence (Phase 3) ──────────────────────────────────────────
  // Monotonic sequence number from the audit_counters atomic counter. Defines
  // the canonical order of the hash chain — events are insertion-ordered by
  // seq, NOT by `at` (which may be backfilled by ingest workers).
  @Prop({ index: true })
  seq?: number

  // True for events written by Phase 3+ AuditService. Pre-Phase-3 entries
  // exist in the collection without seq/prevHash/hash — the verifier walks
  // only the chained set.
  @Prop({ default: false, index: true })
  chained!: boolean

  // sha256 of the previous event's hash, in seq order. The first chained
  // event uses the literal "GENESIS".
  @Prop()
  prevHash?: string

  // sha256 of canonical(this event without `hash`). Recomputed by the verify
  // endpoint to detect modification.
  @Prop()
  hash?: string
}

export const AuditEventSchema = SchemaFactory.createForClass(AuditEvent)
// Compound indexes for the common query shapes — see audit.service.ts.
AuditEventSchema.index({ at: -1 })
AuditEventSchema.index({ tenantSlug: 1, at: -1 })
AuditEventSchema.index({ actorId: 1, at: -1 })
AuditEventSchema.index({ action: 1, at: -1 })
// Dedup guard for ingest workers — same (source, externalId) only stored once.
// Use a partial filter rather than `sparse` because the latter only skips
// missing fields; Mongo will happily index `null` values from internally-
// recorded events and break the uniqueness invariant. Partial filter includes
// only documents that actually carry a string externalId.
AuditEventSchema.index(
  { source: 1, externalId: 1 },
  { unique: true, partialFilterExpression: { externalId: { $type: 'string' } } },
)

// Hash-chain sequence — exactly one event per seq. Partial filter so
// pre-Phase-3 entries (no seq) don't conflict at the null spot.
AuditEventSchema.index(
  { seq: 1 },
  { unique: true, partialFilterExpression: { seq: { $type: 'number' } } },
)
