import 'server-only'

import { prisma } from '../db'
import {
  summariseCrossSensor,
  validateAgainstPeers,
  type CrossSensorResult,
  type CrossSensorSummary,
} from '../engine/cross-sensor'
import { sensorTrust, trustBand, type SensorTrust } from '../engine/quality'
import { round } from '../engine/rng'
import { getGridSnapshot } from './grid-service'

/**
 * The Data Health Command Center (§78, §79, §80).
 *
 * Two questions, answered separately because they have different remedies:
 *
 *   - **Is the instrumentation working?** Coverage, freshness, staleness, trust. Fixed by
 *     maintenance.
 *   - **Is it telling the truth?** Cross-sensor corroboration. Fixed by calibration — or,
 *     when the sensors agree and the model does not, by looking at the model.
 *
 * The second is the one that only appeared in 4.0, and it is the more valuable of the
 * two: a sensor that reports cleanly and is wrong is invisible to every check that looks
 * only at its own record.
 */

const STALE_AFTER_MS = 15 * 60_000
const WINDOW_HOURS = 24

export interface SensorHealthRow {
  code: string
  assetCode: string
  assetName: string
  regionCode: string
  kind: string
  unit: string
  status: string
  trust: number
  band: string
  missingRate: number
  driftPct: number
  outlierRate: number
  lastSeenMinAgo: number | null
  lastCalibratedDaysAgo: number | null
  reason: string | null
  reasonAr: string | null
  /** Populated once cross-validation has run for this channel. */
  verdict: string | null
  verdictReason: string | null
  verdictReasonAr: string | null
  adjustedTrust: number | null
}

export interface DataHealthOverview {
  ts: number
  sensorsTotal: number
  sensorsOnline: number
  sensorsOffline: number
  sensorsStale: number
  sensorsLowTrust: number
  missingReadings: number
  outliers: number
  meanLatencyMin: number
  coveragePct: number
  meanTrust: number
  overdueCalibration: number
  crossValidation: CrossSensorSummary
  rows: SensorHealthRow[]
}

/**
 * The fleet-wide picture.
 *
 * Bounded by `limit` on the row list but not on the counts: the headline figures are over
 * the whole fleet, and a page that silently counted only the worst forty sensors would be
 * reporting a different grid from the one it names.
 */
export async function getDataHealth(
  options: { now?: number; limit?: number } = {},
): Promise<DataHealthOverview> {
  const now = options.now ?? Date.now()
  const limit = options.limit ?? 40
  const since = new Date(now - WINDOW_HOURS * 3_600_000)

  const sensors = await prisma.sensor.findMany({
    select: {
      id: true,
      code: true,
      kind: true,
      unit: true,
      accuracy: true,
      status: true,
      lastSeenAt: true,
      installedAt: true,
      lastCalibratedAt: true,
      samplingIntervalMin: true,
      asset: {
        select: { code: true, name: true, region: { select: { code: true } } },
      },
    },
  })

  const grouped = await prisma.sensorReading.groupBy({
    by: ['sensorId'],
    where: { ts: { gte: since } },
    _count: { _all: true },
  })
  const countBySensor = new Map(grouped.map((row) => [row.sensorId, row._count._all]))

  const expectedFor = (intervalMin: number) =>
    Math.max(1, Math.round((WINDOW_HOURS * 60) / Math.max(1, intervalMin)))

  const trusts: Array<{ sensor: (typeof sensors)[number]; trust: SensorTrust }> = sensors.map(
    (sensor) => ({
      sensor,
      trust: sensorTrust(
        {
          code: sensor.code,
          kind: sensor.kind,
          accuracy: sensor.accuracy,
          status: sensor.status,
          lastSeenAt: sensor.lastSeenAt?.getTime() ?? null,
          readingCount: countBySensor.get(sensor.id) ?? 0,
          expectedCount: expectedFor(sensor.samplingIntervalMin),
          samplingIntervalMin: sensor.samplingIntervalMin,
          outlierCount: 0,
          installedAt: sensor.installedAt.getTime(),
          lastCalibratedAt: sensor.lastCalibratedAt?.getTime() ?? null,
        },
        now,
      ),
    }),
  )

  const online = trusts.filter(
    ({ sensor }) => sensor.lastSeenAt && now - sensor.lastSeenAt.getTime() <= STALE_AFTER_MS,
  ).length
  const offline = trusts.filter(({ sensor }) => sensor.status === 'faulty').length
  const stale = trusts.length - online
  const lowTrust = trusts.filter(({ trust }) => trust.band === 'suspect' || trust.band === 'untrusted')
    .length

  const missingReadings = trusts.reduce(
    ({ sum }, { sensor }) => ({
      sum:
        sum +
        Math.max(0, expectedFor(sensor.samplingIntervalMin) - (countBySensor.get(sensor.id) ?? 0)),
    }),
    { sum: 0 },
  ).sum

  const latencies = trusts
    .map(({ sensor }) => (sensor.lastSeenAt ? (now - sensor.lastSeenAt.getTime()) / 60_000 : null))
    .filter((value): value is number => value !== null)

  const overdue = trusts.filter(({ sensor }) => {
    const at = sensor.lastCalibratedAt?.getTime() ?? sensor.installedAt.getTime()
    return now - at > 2 * 365.25 * 24 * 3_600_000
  }).length

  const crossValidation = await crossValidate({ now, limit: 60 })
  const verdictByCode = new Map(crossValidation.results.map((result) => [result.code, result]))

  const rows: SensorHealthRow[] = trusts
    .map(({ sensor, trust }) => {
      const verdict = verdictByCode.get(sensor.code)
      const calibratedAt = sensor.lastCalibratedAt?.getTime() ?? null
      return {
        code: sensor.code,
        assetCode: sensor.asset.code,
        assetName: sensor.asset.name,
        regionCode: sensor.asset.region.code,
        kind: sensor.kind,
        unit: sensor.unit,
        status: sensor.status,
        trust: trust.score,
        band: trust.band,
        missingRate: round(trust.missingRate * 100, 1),
        driftPct: trust.driftPct,
        outlierRate: round(trust.outlierRate * 100, 1),
        lastSeenMinAgo: sensor.lastSeenAt
          ? round((now - sensor.lastSeenAt.getTime()) / 60_000, 1)
          : null,
        lastCalibratedDaysAgo: calibratedAt
          ? Math.round((now - calibratedAt) / (24 * 3_600_000))
          : null,
        reason: trust.reason,
        reasonAr: trust.reasonAr,
        verdict: verdict?.verdict ?? null,
        verdictReason: verdict?.reason ?? null,
        verdictReasonAr: verdict?.reasonAr ?? null,
        adjustedTrust: verdict?.adjustedTrust ?? null,
      }
    })
    // Least trustworthy first: this page exists to find problems, not to list inventory.
    .sort((a, b) => a.trust - b.trust)
    .slice(0, limit)

  return {
    ts: now,
    sensorsTotal: trusts.length,
    sensorsOnline: online,
    sensorsOffline: offline,
    sensorsStale: stale,
    sensorsLowTrust: lowTrust,
    missingReadings,
    outliers: 0,
    meanLatencyMin: latencies.length
      ? round(latencies.reduce((a, b) => a + b, 0) / latencies.length, 1)
      : 0,
    coveragePct: trusts.length ? round((online / trusts.length) * 100, 1) : 0,
    meanTrust: trusts.length
      ? round(trusts.reduce((sum, { trust }) => sum + trust.score, 0) / trusts.length, 1)
      : 0,
    overdueCalibration: overdue,
    crossValidation: crossValidation.summary,
    rows,
  }
}

export interface CrossValidationOutcome {
  results: CrossSensorResult[]
  summary: CrossSensorSummary
}

/**
 * Corroborate readings against their peers, their own history, and the twin (§80).
 *
 * "Peers" are sensors of the same kind on assets of the same type in the same region:
 * physically comparable plant under the same weather. Comparing a Riyadh transformer's
 * top-oil temperature against a Tabuk wind farm's would produce disagreement that means
 * nothing.
 */
export async function crossValidate(
  options: { now?: number; limit?: number; assetCode?: string } = {},
): Promise<CrossValidationOutcome> {
  const now = options.now ?? Date.now()
  const limit = options.limit ?? 60

  const snapshot = await getGridSnapshot({ ts: now })
  const byCode = new Map(snapshot.assets.map((entry) => [entry.profile.code, entry]))

  const sensors = await prisma.sensor.findMany({
    where: options.assetCode ? { asset: { code: options.assetCode } } : undefined,
    select: {
      id: true,
      code: true,
      kind: true,
      unit: true,
      accuracy: true,
      status: true,
      lastSeenAt: true,
      installedAt: true,
      lastCalibratedAt: true,
      samplingIntervalMin: true,
      asset: {
        select: { code: true, assetType: { select: { key: true } }, region: { select: { code: true } } },
      },
    },
    take: limit,
    orderBy: { code: 'asc' },
  })
  if (!sensors.length) return { results: [], summary: summariseCrossSensor([]) }

  const recent = await prisma.sensorReading.findMany({
    where: { sensorId: { in: sensors.map((s) => s.id) }, ts: { gte: new Date(now - 6 * 3_600_000) } },
    select: { sensorId: true, value: true, ts: true },
    orderBy: { ts: 'asc' },
  })
  const historyBySensor = new Map<string, number[]>()
  const latestBySensor = new Map<string, number>()
  for (const reading of recent) {
    const list = historyBySensor.get(reading.sensorId) ?? []
    list.push(reading.value)
    historyBySensor.set(reading.sensorId, list)
    // Ordered ascending, so the last write wins and this ends up as the most recent.
    latestBySensor.set(reading.sensorId, reading.value)
  }

  const expectedFor = (intervalMin: number) =>
    Math.max(1, Math.round((WINDOW_HOURS * 60) / Math.max(1, intervalMin)))

  // What the twin *expects* this channel to read, from the continuous model.
  const expectedOf = (kind: string, assetCode: string): number | null => {
    const entry = byCode.get(assetCode)
    if (!entry) return null
    switch (kind) {
      case 'temperature':
        return entry.sample.tempC
      case 'load':
        return Math.abs(entry.sample.loadPct)
      case 'voltage':
        return entry.sample.voltageKv
      case 'current':
        return entry.sample.currentA
      case 'power':
        return entry.sample.activePowerMw
      case 'thd':
        return entry.sample.thdPct
      default:
        return null
    }
  }

  const results: CrossSensorResult[] = []

  for (const sensor of sensors) {
    const expected = expectedOf(sensor.kind, sensor.asset.code)
    if (expected === null) continue

    // The reading is what the instrument last actually reported; the expectation is what
    // the model says it should be reporting now. Comparing the model against itself —
    // which is what passing the modelled value as both would do — makes the model-suspect
    // verdict unreachable by construction and turns the whole check into decoration.
    // The two legitimately diverge here because readings arrive on a cadence while the
    // model is continuous, which is exactly the staleness this is meant to surface.
    const value = latestBySensor.get(sensor.id) ?? expected

    const peers = sensors
      .filter(
        (other) =>
          other.id !== sensor.id &&
          other.kind === sensor.kind &&
          other.asset.region.code === sensor.asset.region.code &&
          other.asset.assetType.key === sensor.asset.assetType.key,
      )
      .map((other) => {
        const peerExpected = expectedOf(other.kind, other.asset.code)
        if (peerExpected === null) return null
        const peerValue = latestBySensor.get(other.id) ?? peerExpected
        const trust = sensorTrust(
          {
            code: other.code,
            kind: other.kind,
            accuracy: other.accuracy,
            status: other.status,
            lastSeenAt: other.lastSeenAt?.getTime() ?? null,
            readingCount: expectedFor(other.samplingIntervalMin),
            expectedCount: expectedFor(other.samplingIntervalMin),
            outlierCount: 0,
            installedAt: other.installedAt.getTime(),
            lastCalibratedAt: other.lastCalibratedAt?.getTime() ?? null,
          },
          now,
        )
        return { code: other.code, value: peerValue, trust: trust.score }
      })
      .filter((peer): peer is { code: string; value: number; trust: number } => peer !== null)

    const base = sensorTrust(
      {
        code: sensor.code,
        kind: sensor.kind,
        accuracy: sensor.accuracy,
        status: sensor.status,
        lastSeenAt: sensor.lastSeenAt?.getTime() ?? null,
        readingCount: expectedFor(sensor.samplingIntervalMin),
        expectedCount: expectedFor(sensor.samplingIntervalMin),
        outlierCount: 0,
        installedAt: sensor.installedAt.getTime(),
        lastCalibratedAt: sensor.lastCalibratedAt?.getTime() ?? null,
      },
      now,
    )

    results.push(
      validateAgainstPeers({
        code: sensor.code,
        kind: sensor.kind,
        assetCode: sensor.asset.code,
        value,
        unit: sensor.unit,
        peers,
        // Excludes the latest point, which is the value being judged: a reading cannot be
        // evidence for itself.
        history: (historyBySensor.get(sensor.id) ?? []).slice(0, -1),
        twinExpected: expected,
        twinFidelity: 85,
        baseTrust: base.score,
      }),
    )
  }

  return { results, summary: summariseCrossSensor(results) }
}

/** Trust bands across the fleet, for the distribution chart. */
export async function trustDistribution(now = Date.now()) {
  const health = await getDataHealth({ now, limit: 10_000 })
  const bands = { trusted: 0, usable: 0, suspect: 0, untrusted: 0 }
  for (const row of health.rows) bands[trustBand(row.trust)] += 1
  return bands
}
