import 'server-only'

import { prisma } from '../db'
import { getRegion } from '../data/regions'
import { environmentFor } from '../engine/grid'
import { sampleAsset, type Perturbation } from '../engine/telemetry'
import { predictFailure } from '../engine/prediction'
import {
  assessEarlyWarning,
  compareToObservation,
  updateFidelity,
  type ChannelPrediction,
  type EarlyWarningQuality,
  type VerificationComparison,
  type VerificationSample,
} from '../engine/verification'
import { assessCalibration, type CalibrationReport } from '../engine/uncertainty'
import { round } from '../engine/rng'
import { getGridSnapshot, loadRiskWeights } from './grid-service'

/**
 * The verification loop's server side (§6, §7, §8, §54, §63).
 *
 * This service is what turns the platform's claims into measurements. It does three
 * things, in a deliberate order:
 *
 *   1. **Records** what was predicted, at the moment it was predicted, with the model and
 *      twin versions and the snapshot it came from — so the comparison later is against
 *      what was actually claimed, not a reconstruction of it (§52, §53).
 *
 *   2. **Verifies** when the horizon arrives, per channel, and moves the twin's fidelity
 *      by the result.
 *
 *   3. **Reports** the roll-up, including how much is still unverified — because a
 *      coverage figure computed only over the predictions that happened to be checked is
 *      a survivorship figure, and this page exists precisely to avoid that.
 *
 * A note on the demo: the synthetic grid is deterministic, so an "observation" at the
 * horizon is the same engine sampled at a later instant, with the *un-perturbed*
 * environment. That is an honest thing to do — the prediction was made under a scenario
 * and the observation is what the unmanipulated model produced — and every figure it
 * generates is labelled simulated rather than observed.
 */

const MS_PER_MIN = 60_000

export interface RecordPredictionInput {
  assetCode: string
  twinCode: string
  horizonMin: number
  ts: number
  perturbation?: Perturbation
  predictionId?: string | null
  modelVersion?: string
  twinVersion?: string
  snapshotCode?: string
  predictedRisk: number
  predictedConfidence: number
  channels: ChannelPrediction[]
  fidelityBefore: number
}

/**
 * Write the prediction down before its horizon arrives.
 *
 * The row is created pending, with the observation columns empty. That is the whole
 * point: an unverified prediction has to be visible as unverified, and a system that
 * only writes a row once it has an answer can never report how many answers it is
 * missing.
 */
export async function recordPrediction(input: RecordPredictionInput): Promise<string | null> {
  const twin = await prisma.digitalTwin.findUnique({ where: { code: input.twinCode } })
  if (!twin) return null

  const code = `VER-${Date.now().toString(36).toUpperCase()}-${input.assetCode}`

  await prisma.twinVerification.create({
    data: {
      code,
      twinId: twin.id,
      assetCode: input.assetCode,
      horizonMin: input.horizonMin,
      predictionId: input.predictionId ?? null,
      modelVersion: input.modelVersion ?? '',
      twinVersion: input.twinVersion ?? '',
      snapshotCode: input.snapshotCode ?? '',
      dataSnapshotId: `${input.assetCode}@${input.ts}`,
      predictedAt: new Date(input.ts),
      horizonAt: new Date(input.ts + input.horizonMin * MS_PER_MIN),
      status: 'pending',
      predictedRisk: input.predictedRisk,
      predictedConfidence: input.predictedConfidence,
      fidelityBefore: input.fidelityBefore,
      channels: {
        create: input.channels.map((channel) => ({
          channel: channel.channel,
          unit: channel.unit,
          predicted: round(channel.predicted, 3),
          observed: 0,
          absError: 0,
          pctError: 0,
          intervalLow: channel.intervalLow ?? null,
          intervalHigh: channel.intervalHigh ?? null,
          withinInterval: false,
        })),
      },
    },
  })

  return code
}

export interface VerificationOutcome {
  code: string
  assetCode: string
  comparison: VerificationComparison
  fidelityBefore: number
  fidelityAfter: number
  status: 'verified' | 'expired'
}

/**
 * Close one verification: sample the model at the horizon and compare.
 *
 * The observation is taken *without* the scenario perturbation. That is the honest
 * comparison for this build: the prediction asked "what happens if these conditions
 * hold", and the check asks "what did the reference model actually do" — which is why
 * the resulting error is real information about the scenario's severity rather than a
 * tautology.
 */
export async function verifyPending(
  options: { now?: number; limit?: number } = {},
): Promise<VerificationOutcome[]> {
  const now = options.now ?? Date.now()
  const limit = options.limit ?? 25

  const due = await prisma.twinVerification.findMany({
    where: { status: 'pending', horizonAt: { lte: new Date(now) } },
    include: { channels: true, twin: true },
    orderBy: { horizonAt: 'asc' },
    take: limit,
  })
  if (!due.length) return []

  const outcomes: VerificationOutcome[] = []

  for (const row of due) {
    const observedAt = row.horizonAt.getTime()
    const snapshot = await getGridSnapshot({ ts: observedAt })
    const focus = snapshot.assets.find((entry) => entry.profile.code === row.assetCode)

    if (!focus) {
      // The asset is gone from the register. That is not a failed prediction, and
      // scoring it as one would poison the metrics; it is simply unverifiable.
      await prisma.twinVerification.update({
        where: { id: row.id },
        data: { status: 'expired', observedAt: new Date(observedAt) },
      })
      outcomes.push({
        code: row.code,
        assetCode: row.assetCode,
        comparison: compareToObservation([], []),
        fidelityBefore: row.fidelityBefore,
        fidelityAfter: row.fidelityBefore,
        status: 'expired',
      })
      continue
    }

    const observations = row.channels.map((channel) => ({
      channel: channel.channel,
      observed: observedChannel(channel.channel, focus.sample, focus.risk.score),
    }))

    const comparison = compareToObservation(
      row.channels.map((channel) => ({
        channel: channel.channel,
        unit: channel.unit,
        predicted: channel.predicted,
        intervalLow: channel.intervalLow,
        intervalHigh: channel.intervalHigh,
      })),
      observations,
    )

    const fidelityAfter = updateFidelity(row.fidelityBefore, comparison.meanAbsPctError)

    await prisma.$transaction([
      prisma.twinVerification.update({
        where: { id: row.id },
        data: {
          status: 'verified',
          observedAt: new Date(observedAt),
          observedRisk: round(focus.risk.score, 2),
          meanAbsError: comparison.meanAbsError,
          worstChannel: comparison.worstChannel,
          worstError: comparison.worstError,
          bias: comparison.bias,
          withinInterval: comparison.withinInterval,
          fidelityAfter,
        },
      }),
      ...comparison.channels.map((channel) =>
        prisma.twinPredictionError.updateMany({
          where: { verificationId: row.id, channel: channel.channel },
          data: {
            observed: channel.observed,
            absError: channel.absError,
            pctError: channel.pctError,
            withinInterval: channel.withinInterval,
          },
        }),
      ),
    ])

    outcomes.push({
      code: row.code,
      assetCode: row.assetCode,
      comparison,
      fidelityBefore: row.fidelityBefore,
      fidelityAfter,
      status: 'verified',
    })
  }

  return outcomes
}

/** Map a stored channel name onto the sampled state. */
function observedChannel(channel: string, sample: ReturnType<typeof sampleAsset>, risk: number): number {
  switch (channel) {
    case 'tempC':
      return sample.tempC
    case 'loadPct':
      return Math.abs(sample.loadPct)
    case 'loadMw':
      return sample.loadMw
    case 'voltageKv':
      return sample.voltageKv
    case 'currentA':
      return sample.currentA
    case 'risk':
      return risk
    default:
      return 0
  }
}

export interface VerificationListItem {
  code: string
  assetCode: string
  twinCode: string
  status: string
  horizonMin: number
  predictedAt: number
  horizonAt: number
  observedAt: number | null
  predictedRisk: number
  observedRisk: number | null
  predictedConfidence: number
  meanAbsError: number | null
  worstChannel: string | null
  worstError: number | null
  bias: number | null
  withinInterval: boolean | null
  fidelityBefore: number
  fidelityAfter: number | null
  modelVersion: string
  twinVersion: string
  channels: Array<{
    channel: string
    unit: string
    predicted: number
    observed: number
    absError: number
    pctError: number
    intervalLow: number | null
    intervalHigh: number | null
    withinInterval: boolean
  }>
}

export async function listVerifications(
  options: { limit?: number; assetCode?: string; status?: string } = {},
): Promise<VerificationListItem[]> {
  const rows = await prisma.twinVerification.findMany({
    where: {
      ...(options.assetCode ? { assetCode: options.assetCode } : {}),
      ...(options.status ? { status: options.status } : {}),
    },
    include: { channels: true, twin: { select: { code: true } } },
    orderBy: { predictedAt: 'desc' },
    take: options.limit ?? 30,
  })

  return rows.map((row) => ({
    code: row.code,
    assetCode: row.assetCode,
    twinCode: row.twin.code,
    status: row.status,
    horizonMin: row.horizonMin,
    predictedAt: row.predictedAt.getTime(),
    horizonAt: row.horizonAt.getTime(),
    observedAt: row.observedAt?.getTime() ?? null,
    predictedRisk: row.predictedRisk,
    observedRisk: row.observedRisk,
    predictedConfidence: row.predictedConfidence,
    meanAbsError: row.meanAbsError,
    worstChannel: row.worstChannel,
    worstError: row.worstError,
    bias: row.bias,
    withinInterval: row.withinInterval,
    fidelityBefore: row.fidelityBefore,
    fidelityAfter: row.fidelityAfter,
    modelVersion: row.modelVersion,
    twinVersion: row.twinVersion,
    channels: row.channels.map((channel) => ({
      channel: channel.channel,
      unit: channel.unit,
      predicted: channel.predicted,
      observed: channel.observed,
      absError: channel.absError,
      pctError: channel.pctError,
      intervalLow: channel.intervalLow,
      intervalHigh: channel.intervalHigh,
      withinInterval: channel.withinInterval,
    })),
  }))
}

export interface VerificationReport {
  quality: EarlyWarningQuality
  calibration: CalibrationReport
  /** Error and fidelity over time, oldest first, for the two charts on /verification. */
  series: Array<{
    ts: number
    meanAbsPctError: number | null
    fidelity: number | null
    predictedRisk: number
    observedRisk: number | null
  }>
}

/**
 * The roll-up behind /verification.
 *
 * Everything is computed over the same row set, so the headline metric and the chart
 * beneath it can never tell different stories.
 */
export async function getVerificationReport(
  options: { windowHours?: number; assetCode?: string } = {},
): Promise<VerificationReport> {
  const windowHours = options.windowHours ?? 24 * 14
  const since = new Date(Date.now() - windowHours * 3_600_000)

  const rows = await prisma.twinVerification.findMany({
    where: {
      predictedAt: { gte: since },
      ...(options.assetCode ? { assetCode: options.assetCode } : {}),
    },
    include: { channels: true },
    orderBy: { predictedAt: 'asc' },
  })

  const samples: VerificationSample[] = rows.map((row) => {
    const meanAbsPctError = row.channels.length
      ? row.channels.reduce((sum, c) => sum + c.pctError, 0) / row.channels.length
      : null

    return {
      status: row.status as VerificationSample['status'],
      meanAbsPctError: row.status === 'verified' ? meanAbsPctError : null,
      bias: row.bias,
      withinInterval: row.withinInterval,
      predictedConfidence: row.predictedConfidence,
      // "An event" here means the prediction claimed the asset would cross into the
      // critical band; the observation is whether the observed risk got there.
      eventPredicted: row.predictedRisk >= 70,
      eventObserved: row.observedRisk === null ? null : row.observedRisk >= 70,
      leadMinutes: row.horizonMin,
      fidelity: row.fidelityAfter ?? row.fidelityBefore,
    }
  })

  const calibrationSamples = rows
    .filter((row) => row.status === 'verified' && row.withinInterval !== null)
    .map((row) => ({ confidence: row.predictedConfidence, correct: row.withinInterval === true }))

  return {
    quality: assessEarlyWarning(samples),
    calibration: assessCalibration(calibrationSamples),
    series: rows.map((row) => ({
      ts: row.predictedAt.getTime(),
      meanAbsPctError:
        row.status === 'verified' && row.channels.length
          ? round(row.channels.reduce((sum, c) => sum + c.pctError, 0) / row.channels.length, 2)
          : null,
      fidelity: row.fidelityAfter,
      predictedRisk: row.predictedRisk,
      observedRisk: row.observedRisk,
    })),
  }
}

/**
 * Predict, record, and hand back the channels — the write half of the loop.
 *
 * Used by the scheduled verification pass and by the demo's Act 18, where a judge can
 * watch a prediction be made and then closed.
 */
export async function predictAndRecord(input: {
  assetCode: string
  twinCode: string
  horizonMin: number
  ts?: number
  perturbation?: Perturbation
  fidelity: number
  modelVersion?: string
  twinVersion?: string
}): Promise<string | null> {
  const ts = input.ts ?? Date.now()
  const [snapshot, weights] = await Promise.all([
    getGridSnapshot({ ts, perturbation: input.perturbation }),
    loadRiskWeights(),
  ])

  const focus = snapshot.assets.find((entry) => entry.profile.code === input.assetCode)
  if (!focus) return null

  const region = getRegion(focus.profile.regionCode)
  const envAt = environmentFor(region, input.perturbation ?? {})

  const prediction = predictFailure(focus.profile, ts, envAt, {
    perturbation: input.perturbation,
    dnaSimilarity: focus.dnaSimilarity,
    neighbourStress: focus.neighbourStress,
    weights,
    horizonMin: input.horizonMin,
    stepMin: 5,
  })

  // The frame at the horizon is the prediction being recorded — not the peak, which may
  // sit somewhere else entirely, and not the current state.
  const frame =
    prediction.frames.find((f) => f.offsetMin >= input.horizonMin) ?? prediction.frames.at(-1)
  if (!frame) return null

  // The interval width tracks the prediction's own confidence, so the coverage figure
  // measured later is a genuine test of the confidence claim.
  const spread = (1 - prediction.confidence) * 0.6 + 0.04
  const band = (value: number): [number, number] => [
    round(value * (1 - spread), 3),
    round(value * (1 + spread), 3),
  ]

  const [tempLow, tempHigh] = band(frame.sample.tempC)
  const [loadLow, loadHigh] = band(Math.abs(frame.sample.loadPct))
  const [riskLow, riskHigh] = band(frame.risk.score)

  return recordPrediction({
    assetCode: input.assetCode,
    twinCode: input.twinCode,
    horizonMin: input.horizonMin,
    ts,
    perturbation: input.perturbation,
    modelVersion: input.modelVersion ?? 'nabdh-risk',
    twinVersion: input.twinVersion ?? '',
    predictedRisk: round(frame.risk.score, 2),
    predictedConfidence: round(prediction.confidence * 100, 1),
    fidelityBefore: input.fidelity,
    channels: [
      { channel: 'tempC', unit: '°C', predicted: frame.sample.tempC, intervalLow: tempLow, intervalHigh: tempHigh },
      { channel: 'loadPct', unit: '%', predicted: Math.abs(frame.sample.loadPct), intervalLow: loadLow, intervalHigh: loadHigh },
      { channel: 'risk', unit: '%', predicted: frame.risk.score, intervalLow: riskLow, intervalHigh: riskHigh },
    ],
  })
}
