import 'server-only'

import { prisma } from '../db'
import { detectDrift, summariseDrift, type DriftSignal, type DriftVerdict } from '../engine/drift'
import { round } from '../engine/rng'

/**
 * Model governance (§81, §82, §83).
 *
 * The point of this page is not to show that the models are good. It is to show that
 * somebody would notice if they stopped being good — which is a different and much more
 * checkable claim.
 *
 * Drift is measured by comparing two windows of the verification record: a reference
 * window the model's stated performance was established over, and a recent one. Both
 * windows and their sample counts are reported, because a drift verdict computed over
 * eleven predictions is not a verdict.
 *
 * Nothing here retrains, promotes or retires a model. Detection raises a review; the
 * lifecycle transitions are separate, audited, human acts.
 */

const HOUR_MS = 3_600_000

export interface ModelSummary {
  id: string
  name: string
  version: string
  status: string
  accuracy: number
  trainedAt: number
  datasetVersion: string
  notes: string
  driftOpen: number
}

export async function listModels(): Promise<ModelSummary[]> {
  const [models, driftCounts] = await Promise.all([
    prisma.modelVersion.findMany({ orderBy: [{ status: 'asc' }, { trainedAt: 'desc' }] }),
    prisma.modelDriftEvent.groupBy({
      by: ['modelKey'],
      where: { status: 'open' },
      _count: { _all: true },
    }),
  ])

  const openByKey = new Map(driftCounts.map((row) => [row.modelKey, row._count._all]))

  return models.map((model) => ({
    id: model.id,
    name: model.name,
    version: model.version,
    status: model.status,
    accuracy: model.accuracy,
    trainedAt: model.trainedAt.getTime(),
    datasetVersion: model.datasetVersion,
    notes: model.notes,
    driftOpen: openByKey.get(model.name) ?? 0,
  }))
}

export interface DriftReport {
  verdict: DriftVerdict
  baselineWindow: { fromTs: number; toTs: number; sampleCount: number }
  recentWindow: { fromTs: number; toTs: number; sampleCount: number }
  /** True when neither window holds enough verified predictions to compare. */
  underpowered: boolean
}

/**
 * Compare a reference window against a recent one.
 *
 * The default split is "the last 48 hours against the fortnight before it", which is the
 * shortest pair of windows the demo's verification volume can populate while still being
 * a genuine comparison rather than a comparison of one prediction with another.
 */
export async function assessModelDrift(
  options: { modelKey?: string; recentHours?: number; baselineHours?: number; now?: number } = {},
): Promise<DriftReport> {
  const now = options.now ?? Date.now()
  const recentHours = options.recentHours ?? 48
  const baselineHours = options.baselineHours ?? 24 * 14

  const recentFrom = now - recentHours * HOUR_MS
  const baselineFrom = recentFrom - baselineHours * HOUR_MS

  const rows = await prisma.twinVerification.findMany({
    where: {
      status: 'verified',
      predictedAt: { gte: new Date(baselineFrom) },
      ...(options.modelKey ? { modelVersion: { contains: options.modelKey } } : {}),
    },
    include: { channels: true },
    orderBy: { predictedAt: 'asc' },
  })

  const recent = rows.filter((row) => row.predictedAt.getTime() >= recentFrom)
  const baseline = rows.filter((row) => row.predictedAt.getTime() < recentFrom)

  const summarise = (set: typeof rows) => {
    const mean = (values: number[]) =>
      values.length ? values.reduce((a, b) => a + b, 0) / values.length : 0

    const channelOf = (name: string) =>
      set.flatMap((row) => row.channels.filter((c) => c.channel === name))

    return {
      meanAbsPctError: round(
        mean(set.flatMap((row) => row.channels.map((c) => c.pctError))),
        3,
      ),
      meanPredictedRisk: round(mean(set.map((row) => row.predictedRisk)), 3),
      meanInputLoadPct: round(mean(channelOf('loadPct').map((c) => c.observed)), 3),
      meanInputTempC: round(mean(channelOf('tempC').map((c) => c.observed)), 3),
      twinFidelity: round(mean(set.map((row) => row.fidelityAfter ?? row.fidelityBefore)), 3),
      sampleCount: set.length,
    }
  }

  const signals = detectDrift({ baseline: summarise(baseline), observed: summarise(recent) })

  return {
    verdict: summariseDrift(signals),
    baselineWindow: { fromTs: baselineFrom, toTs: recentFrom, sampleCount: baseline.length },
    recentWindow: { fromTs: recentFrom, toTs: now, sampleCount: recent.length },
    underpowered: baseline.length === 0 || recent.length === 0,
  }
}

/**
 * Record the drift signals that crossed their thresholds.
 *
 * Only escalating signals are written: a table containing every measurement of every
 * metric that did *not* move is a table nobody opens, and the calm case is already
 * visible in the report.
 */
export async function recordDrift(
  modelKey: string,
  signals: DriftSignal[],
): Promise<number> {
  const notable = signals.filter((signal) => signal.severity !== 'info')
  if (!notable.length) return 0

  await prisma.modelDriftEvent.createMany({
    data: notable.map((signal) => ({
      modelKey,
      driftKind: signal.kind,
      metricKey: signal.metricKey,
      baseline: signal.baseline,
      observed: signal.observed,
      deltaPct: signal.deltaPct,
      threshold: signal.threshold,
      severity: signal.severity,
      sampleCount: signal.sampleCount,
      detail: signal.detail,
      detailAr: signal.detailAr,
      status: 'open',
    })),
  })

  return notable.length
}

export async function listDriftEvents(options: { limit?: number; status?: string } = {}) {
  const rows = await prisma.modelDriftEvent.findMany({
    where: options.status ? { status: options.status } : undefined,
    orderBy: [{ severity: 'desc' }, { detectedAt: 'desc' }],
    take: options.limit ?? 20,
  })

  return rows.map((row) => ({
    id: row.id,
    modelKey: row.modelKey,
    driftKind: row.driftKind,
    metricKey: row.metricKey,
    baseline: row.baseline,
    observed: row.observed,
    deltaPct: row.deltaPct,
    threshold: row.threshold,
    severity: row.severity,
    sampleCount: row.sampleCount,
    detail: row.detail,
    detailAr: row.detailAr,
    status: row.status,
    detectedAt: row.detectedAt.getTime(),
  }))
}

/**
 * The model lifecycle (§83).
 *
 * Deliberately narrow: the only transitions this build allows are the ones a person can
 * make deliberately from the governance page, each of which is audited by the route
 * wrapper. There is no code path from a drift signal to a deployment.
 */
export const MODEL_TRANSITIONS: Record<string, string[]> = {
  draft: ['validation'],
  validation: ['approved', 'draft'],
  approved: ['active', 'retired'],
  active: ['retired'],
  retired: [],
}

export async function transitionModel(input: {
  modelId: string
  next: string
  userId: string
  note?: string
}): Promise<{ ok: boolean; reason?: string }> {
  const model = await prisma.modelVersion.findUnique({
    where: { id: input.modelId },
    select: { id: true, status: true, name: true, version: true },
  })
  if (!model) return { ok: false, reason: 'not_found' }

  const allowed = MODEL_TRANSITIONS[model.status] ?? []
  if (!allowed.includes(input.next)) return { ok: false, reason: 'invalid_transition' }

  await prisma.$transaction(async (tx) => {
    // Only one active version per model name: promoting one retires the incumbent, which
    // is the behaviour an operator expects and the alternative to two models silently
    // claiming to be live.
    if (input.next === 'active') {
      await tx.modelVersion.updateMany({
        where: { name: model.name, status: 'active', NOT: { id: model.id } },
        data: { status: 'retired' },
      })
    }

    await tx.modelVersion.update({ where: { id: model.id }, data: { status: input.next } })

    await tx.modelAudit.create({
      data: {
        modelVersionId: model.id,
        action: `status.${input.next}`,
        actor: input.userId,
        detail: input.note ?? `${model.status} → ${input.next}`,
        detailAr: input.note ?? `${model.status} ← ${input.next}`,
      },
    })
  })

  return { ok: true }
}

export async function listModelAudit(limit = 25) {
  return prisma.modelAudit.findMany({
    orderBy: { ts: 'desc' },
    take: limit,
    include: { modelVersion: { select: { name: true, version: true } } },
  })
}
