// Periodic signed anchors over the audit hash chain. The chain itself
// (event[N].hash references event[N-1].hash) protects integrity between
// events. Checkpoints layer signed attestations on top: "the head at time T
// was seq=S, hash=H, signed by us". An attacker who modifies a historical
// event must also forge every checkpoint signature past that event — which
// requires AUDIT_SIGNING_KEY, kept separately from Mongo write access.
//
// Trigger model: every N events (THRESHOLD_COUNT) or T seconds
// (INTERVAL_MS), whichever first, plus one on startup so a long quiet
// system still has a recent checkpoint to verify against.

import {
  Injectable,
  Logger,
  type OnApplicationBootstrap,
  type OnModuleDestroy,
} from '@nestjs/common'
import { ConfigService } from '@nestjs/config'
import { InjectModel } from '@nestjs/mongoose'
import type { Model } from 'mongoose'
import {
  AuditCheckpoint,
  type AuditCheckpointDocument,
} from '../schemas/audit-checkpoint.schema.js'
import { AuditService } from './audit.service.js'
import { checkpointSignature } from './canonical.js'

const DEFAULT_INTERVAL_MS = 5 * 60 * 1_000  // 5 minutes
const DEFAULT_THRESHOLD = 100

@Injectable()
export class CheckpointService implements OnApplicationBootstrap, OnModuleDestroy {
  private readonly logger = new Logger(CheckpointService.name)
  private readonly key: string
  private readonly intervalMs: number
  private readonly threshold: number
  private timer: NodeJS.Timeout | null = null
  private writing = false

  constructor(
    @InjectModel(AuditCheckpoint.name) private readonly model: Model<AuditCheckpointDocument>,
    private readonly audit: AuditService,
    config: ConfigService,
  ) {
    this.key = config.getOrThrow<string>('AUDIT_SIGNING_KEY')
    this.intervalMs = Number(config.get('AUDIT_CHECKPOINT_INTERVAL_MS') ?? DEFAULT_INTERVAL_MS)
    this.threshold = Number(config.get('AUDIT_CHECKPOINT_THRESHOLD') ?? DEFAULT_THRESHOLD)
  }

  async onApplicationBootstrap(): Promise<void> {
    this.logger.log(
      `Audit checkpoint scheduler starting · interval=${this.intervalMs / 1000}s threshold=${this.threshold} events`,
    )
    // Fire one on startup so a fresh deployment / restart immediately has a
    // verifiable checkpoint covering the head.
    await this.tryWrite('startup')
    this.timer = setInterval(() => void this.tryWrite('interval'), this.intervalMs)
  }

  onModuleDestroy(): void {
    if (this.timer) clearInterval(this.timer)
  }

  // Called by AuditService writes (via the threshold path) and by the
  // interval ticker. Skips when no chained events exist yet OR when the
  // current head matches the previous checkpoint (nothing new to anchor).
  async tryWrite(reason: AuditCheckpointDocument['reason']): Promise<AuditCheckpointDocument | null> {
    if (this.writing) return null
    this.writing = true
    try {
      const head = await this.audit.headChainState()
      if (!head) return null  // no chained events yet

      const last = await this.model.findOne().sort({ headSeq: -1 }).exec()
      if (last && last.headSeq === head.headSeq) return null  // nothing new

      // For threshold reason, only fire if N events have accumulated since the
      // last checkpoint. Other reasons (startup, interval, manual) always
      // proceed past the headSeq guard above.
      if (reason === 'threshold' && last && head.headSeq - last.headSeq < this.threshold) {
        return null
      }

      const at = new Date()
      const signature = checkpointSignature(this.key, head.headSeq, head.headHash, at)
      const doc = await this.model.create({
        at,
        headSeq: head.headSeq,
        headHash: head.headHash,
        signature,
        sigAlg: 'HMAC-SHA-256',
        reason,
      })
      this.logger.log(`Audit checkpoint written · seq=${head.headSeq} reason=${reason}`)
      return doc
    } catch (err) {
      this.logger.error(
        `checkpoint write failed: ${err instanceof Error ? err.message : String(err)}`,
      )
      return null
    } finally {
      this.writing = false
    }
  }

  // Used by the verify endpoint to walk anchors in order.
  list(): Promise<AuditCheckpointDocument[]> {
    return this.model.find().sort({ headSeq: 1 }).exec()
  }

  latest(): Promise<AuditCheckpointDocument | null> {
    return this.model.findOne().sort({ headSeq: -1 }).exec()
  }
}
