import 'server-only'

import { prisma } from '../db'
import {
  assessDataQuality,
  sensorTrust,
  type DataQuality,
  type SensorObservation,
  type SensorTrust,
} from '../engine/quality'

/**
 * Data confidence, computed from the instrumentation record (§25, §26).
 *
 * The counts come from `sensor_readings`, which is the same table the charts read, so
 * the confidence figure and the history it qualifies can never disagree about what was
 * recorded. Outliers are counted here rather than stored: what counts as implausible is
 * a property of the channel, and hard-coding it into the row would freeze it.
 */

/** Physically plausible bounds per channel, outside which a reading is not believable. */
const PLAUSIBLE_RANGE: Record<string, { min: number; max: number }> = {
  voltage: { min: 0, max: 800 },
  current: { min: 0, max: 6_000 },
  frequency: { min: 55, max: 65 },
  temperature: { min: -10, max: 160 },
  load: { min: -130, max: 160 },
  power_factor: { min: 0.5, max: 1.05 },
  thd: { min: 0, max: 25 },
}

/** Readings are recorded at 15-minute intervals by the seed and the live sampler. */

export interface AssetDataQuality extends DataQuality {
  assetCode: string
  sensors: SensorTrust[]
}

/**
 * Assess one asset's instrumentation over the trailing window.
 *
 * A single grouped query per asset keeps this cheap enough to run on every prediction:
 * the alternative — reading every row to count them — is the kind of thing that works
 * in a demo and falls over on a real fleet.
 */
export async function getAssetDataQuality(
  assetCode: string,
  options: { windowHours?: number; now?: number } = {},
): Promise<AssetDataQuality> {
  const windowHours = options.windowHours ?? 24
  const now = options.now ?? Date.now()
  const since = new Date(now - windowHours * 3_600_000)

  const asset = await prisma.asset.findUnique({
    where: { code: assetCode },
    select: {
      id: true,
      sensors: {
        select: {
          id: true,
          code: true,
          kind: true,
          accuracy: true,
          status: true,
          lastSeenAt: true,
          installedAt: true,
          lastCalibratedAt: true,
          samplingIntervalMin: true,
        },
        orderBy: { kind: 'asc' },
      },
    },
  })

  if (!asset) {
    return {
      assetCode,
      sensors: [],
      ...assessDataQuality([], now, { expectedSensors: 0, coverageHours: 0 }),
    }
  }

  const readings = await prisma.sensorReading.findMany({
    where: { assetId: asset.id, ts: { gte: since } },
    select: { sensorId: true, kind: true, value: true, ts: true },
  })

  const counts = new Map<string, { total: number; outliers: number; earliest: number }>()
  for (const reading of readings) {
    const entry = counts.get(reading.sensorId) ?? { total: 0, outliers: 0, earliest: reading.ts.getTime() }
    entry.total += 1
    entry.earliest = Math.min(entry.earliest, reading.ts.getTime())
    const range = PLAUSIBLE_RANGE[reading.kind]
    if (range && (reading.value < range.min || reading.value > range.max)) entry.outliers += 1
    counts.set(reading.sensorId, entry)
  }

  // Per instrument rather than fleet-wide: a unit polled hourly is not missing three
  // quarters of its readings, it is being read hourly.
  const expectedFor = (intervalMin: number) =>
    Math.max(1, Math.round((windowHours * 60) / Math.max(1, intervalMin)))
  const observations: SensorObservation[] = asset.sensors.map((sensor) => {
    const entry = counts.get(sensor.id)
    return {
      code: sensor.code,
      kind: sensor.kind,
      accuracy: sensor.accuracy,
      status: sensor.status,
      lastSeenAt: sensor.lastSeenAt?.getTime() ?? null,
      readingCount: entry?.total ?? 0,
      expectedCount: expectedFor(sensor.samplingIntervalMin),
      samplingIntervalMin: sensor.samplingIntervalMin,
      outlierCount: entry?.outliers ?? 0,
      installedAt: sensor.installedAt.getTime(),
      lastCalibratedAt: sensor.lastCalibratedAt?.getTime() ?? null,
    }
  })

  const earliest = readings.length > 0 ? Math.min(...readings.map((r) => r.ts.getTime())) : now
  const coverageHours = readings.length > 0 ? (now - earliest) / 3_600_000 : 0

  const quality = assessDataQuality(observations, now, {
    expectedSensors: asset.sensors.length,
    coverageHours,
  })

  return {
    assetCode,
    sensors: observations.map((observation) => sensorTrust(observation, now)),
    ...quality,
  }
}

/**
 * Persist trust and quality scores so the Model Center and the audit trail can show what
 * the confidence was at the time, not only what it is now.
 */
export async function persistDataQuality(assetCode: string, quality: AssetDataQuality): Promise<void> {
  await prisma.dataQualityScore.upsert({
    where: { scopeKind_scopeCode: { scopeKind: 'asset', scopeCode: assetCode } },
    create: {
      scopeKind: 'asset',
      scopeCode: assetCode,
      score: quality.score,
      sensorsExpected: quality.sensorsExpected,
      sensorsReporting: quality.sensorsReporting,
      staleSensors: quality.staleSensors,
      outlierCount: quality.outlierCount,
      coverageHours: quality.coverageHours,
      band: quality.band,
    },
    update: {
      score: quality.score,
      sensorsExpected: quality.sensorsExpected,
      sensorsReporting: quality.sensorsReporting,
      staleSensors: quality.staleSensors,
      outlierCount: quality.outlierCount,
      coverageHours: quality.coverageHours,
      band: quality.band,
      computedAt: new Date(),
    },
  })

  const sensors = await prisma.sensor.findMany({
    where: { asset: { code: assetCode } },
    select: { id: true, code: true },
  })
  const idByCode = new Map(sensors.map((sensor) => [sensor.code, sensor.id]))

  for (const trust of quality.sensors) {
    const sensorId = idByCode.get(trust.code)
    if (!sensorId) continue
    await prisma.sensorTrustScore.upsert({
      where: { sensorId },
      create: {
        sensorId,
        score: trust.score,
        missingRate: trust.missingRate,
        noiseLevel: trust.noiseLevel,
        driftPct: trust.driftPct,
        outlierRate: trust.outlierRate,
        band: trust.band,
      },
      update: {
        score: trust.score,
        missingRate: trust.missingRate,
        noiseLevel: trust.noiseLevel,
        driftPct: trust.driftPct,
        outlierRate: trust.outlierRate,
        band: trust.band,
        computedAt: new Date(),
      },
    })
  }
}
