import 'server-only'

import { createHash } from 'node:crypto'

import { prisma } from '../db'
import { environmentFor } from '../engine/grid'
import { round } from '../engine/rng'
import {
  assessSensorHealth,
  buildProvenanceChain,
  injectSensorFault,
  SENSOR_FAULTS,
  SENSOR_SPEC,
  type ProvenanceNode,
  type Reading,
  type SensorFaultKind,
  type SensorHealth,
  type SensorKind,
} from '../engine/sensor'
import { NO_PERTURBATION, sampleAsset, type Perturbation } from '../engine/telemetry'
import { REGIONS } from '../data/regions'
import { loadAssetProfiles } from './grid-service'
import { getHubOverview } from './integration-service'

/**
 * Sensors as first-class objects (5.0 §4–§7).
 *
 * A sensor stops being a row behind a trust score and becomes something an operator can
 * select, inspect, trace and — in demonstration only — break on purpose to watch what the
 * platform does about it.
 *
 * Two guarantees this file holds:
 *
 * **Fault injection cannot touch live state.** It transforms a *stream of readings* on its
 * way to the health assessment. It writes nothing, it has no path to telemetry, an asset
 * or a twin, and the mode check that refuses it outside demonstration is server-side —
 * a client that stops sending the flag does not get to inject anything.
 *
 * **A sensor anomaly stays a sensor anomaly.** Everything the health engine returns is
 * classified `data_anomaly`, and nothing here promotes one to an electrical conclusion.
 */

const MIN = 60_000

/** The sensor kinds the seed uses, mapped onto the engine's catalogue. */
function toSensorKind(kind: string): SensorKind {
  switch (kind) {
    case 'voltage':
      return 'voltage'
    case 'current':
      return 'current'
    case 'temperature':
      return 'temperature'
    case 'frequency':
      return 'frequency'
    case 'power':
    case 'power_meter':
      return 'power_meter'
    case 'relay':
    case 'protection':
      return 'protection_relay'
    case 'weather':
    case 'ambient':
      return 'weather'
    case 'vibration':
      return 'vibration'
    case 'partial_discharge':
    case 'pd':
      return 'partial_discharge'
    default:
      return 'temperature'
  }
}

// ─────────────────────────────────────────────────────────────────────────────
// Reading the instrument
// ─────────────────────────────────────────────────────────────────────────────

export interface SensorView {
  code: string
  assetCode: string
  assetName: string
  assetNameAr: string
  kind: SensorKind
  rawKind: string
  unit: string
  minValue: number
  maxValue: number
  accuracy: number
  status: string
  firmware: string
  samplingIntervalMin: number
  installedAt: number
  lastCalibratedAt: number | null
  lastSeenAt: number | null
  /** Which connector this instrument's data arrives on, when one is mapped. */
  connectorCode: string | null
  connectorLabel: string | null
  protocol: string | null
  trustScore: number | null
  trustBand: string | null
}

export interface SensorDetail extends SensorView {
  /** The latest reading and how old it is. */
  latestValue: number | null
  latestTs: number | null
  dataAgeMin: number
  /** Where this reading came from. Never 'live' while the connector is a demo source. */
  origin: 'measured' | 'synthetic'
  quality: string
  health: SensorHealth
  /** The window the assessment ran over. */
  readings: Reading[]
  /** Present when a demonstration fault is in force on this instrument. */
  faultKind: SensorFaultKind | null
  faultLabel: string | null
}

/**
 * Faults in force, held in memory for the life of the process.
 *
 * Deliberately not persisted. A demonstration fault is a property of a demonstration, not
 * of the instrument, and writing it to the sensor row would make a temporary teaching
 * device look like a recorded condition — and would survive into a context where nobody
 * knew it had been injected.
 */
const activeFaults = new Map<string, SensorFaultKind>()

export function activeSensorFaults(): Array<{ sensorCode: string; faultKind: SensorFaultKind }> {
  return [...activeFaults.entries()].map(([sensorCode, faultKind]) => ({ sensorCode, faultKind }))
}

async function connectorFor(): Promise<{ code: string; label: string; protocol: string; isLive: boolean } | null> {
  // One hub read, shared across a listing. The seed has no per-sensor connector mapping,
  // so the instrument is attributed to the SCADA source that would carry it — and the
  // label carries the demo prefix, so nothing here can read as a live feed.
  const hub = await getHubOverview()
  const scada = hub.connectors.find((entry) => entry.kind === 'scada' && entry.status === 'connected')
  if (!scada) return null
  return { code: scada.code, label: scada.label, protocol: scada.protocol, isLive: scada.mode === 'live' }
}

function toView(
  row: {
    code: string
    kind: string
    unit: string
    minValue: number
    maxValue: number
    accuracy: number
    status: string
    firmware: string
    samplingIntervalMin: number
    installedAt: Date
    lastCalibratedAt: Date | null
    lastSeenAt: Date | null
    asset: { code: string; name: string; nameAr: string }
    trust: { score: number; band: string } | null
  },
  connector: { code: string; label: string; protocol: string } | null,
): SensorView {
  const kind = toSensorKind(row.kind)
  return {
    code: row.code,
    assetCode: row.asset.code,
    assetName: row.asset.name,
    assetNameAr: row.asset.nameAr,
    kind,
    rawKind: row.kind,
    unit: row.unit || SENSOR_SPEC[kind].unit,
    minValue: row.minValue,
    maxValue: row.maxValue,
    accuracy: row.accuracy,
    status: row.status,
    firmware: row.firmware,
    samplingIntervalMin: row.samplingIntervalMin,
    installedAt: row.installedAt.getTime(),
    lastCalibratedAt: row.lastCalibratedAt?.getTime() ?? null,
    lastSeenAt: row.lastSeenAt?.getTime() ?? null,
    connectorCode: connector?.code ?? null,
    connectorLabel: connector?.label ?? null,
    protocol: connector?.protocol ?? null,
    trustScore: row.trust?.score ?? null,
    trustBand: row.trust?.band ?? null,
  }
}

const SELECT = {
  code: true,
  kind: true,
  unit: true,
  minValue: true,
  maxValue: true,
  accuracy: true,
  status: true,
  firmware: true,
  samplingIntervalMin: true,
  installedAt: true,
  lastCalibratedAt: true,
  lastSeenAt: true,
  asset: { select: { code: true, name: true, nameAr: true } },
  trust: { select: { score: true, band: true } },
} as const

/** Every instrument on an asset, for the scene's sensor layer. */
export async function listSensorsForAssets(assetCodes: string[]): Promise<SensorView[]> {
  if (assetCodes.length === 0) return []
  const [rows, connector] = await Promise.all([
    prisma.sensor.findMany({
      where: { asset: { code: { in: assetCodes } } },
      orderBy: [{ assetId: 'asc' }, { kind: 'asc' }],
      select: SELECT,
    }),
    connectorFor(),
  ])
  return rows.map((row) => toView(row, connector))
}

/**
 * Build the reading window for an instrument.
 *
 * Sampled from the same telemetry engine the rest of the platform runs on, so the sensor's
 * value and the asset's value are the same number rather than two independent fictions.
 * Where stored readings exist they are used instead — a recorded measurement beats a
 * regenerated one.
 */
async function windowFor(
  sensor: SensorView,
  options: { ts: number; windowMin: number; perturbation?: Perturbation },
): Promise<{ readings: Reading[]; origin: 'measured' | 'synthetic' }> {
  const stored = await prisma.sensorReading.findMany({
    where: {
      sensor: { code: sensor.code },
      ts: { gte: new Date(options.ts - options.windowMin * MIN), lte: new Date(options.ts) },
    },
    orderBy: { ts: 'asc' },
    select: { ts: true, value: true },
    take: 400,
  })

  if (stored.length >= 5) {
    return {
      readings: stored.map((row) => ({ ts: row.ts.getTime(), value: row.value })),
      origin: 'measured',
    }
  }

  // No stored history: regenerate from the engine at this instrument's own cadence.
  const profiles = await loadAssetProfiles()
  const profile = profiles.find((entry) => entry.code === sensor.assetCode)
  if (!profile) return { readings: [], origin: 'synthetic' }

  const region = REGIONS.find((entry) => entry.code === profile.regionCode) ?? REGIONS[0]
  const envAt = environmentFor(region, options.perturbation ?? NO_PERTURBATION)

  const step = Math.max(0.25, sensor.samplingIntervalMin) * MIN
  const count = Math.min(240, Math.max(6, Math.floor((options.windowMin * MIN) / step)))

  const readings: Reading[] = []
  for (let index = count - 1; index >= 0; index -= 1) {
    const ts = options.ts - index * step
    const sample = sampleAsset(profile, ts, envAt(ts), options.perturbation)
    readings.push({ ts, value: pickQuantity(sensor.kind, sample) })
  }

  return { readings, origin: 'synthetic' }
}

function pickQuantity(kind: SensorKind, sample: ReturnType<typeof sampleAsset>): number {
  switch (kind) {
    case 'voltage':
      return sample.voltageKv
    case 'current':
      return sample.currentA
    case 'temperature':
      return sample.tempC
    case 'power_meter':
      return sample.activePowerMw
    case 'weather':
      return sample.ambientC
    case 'vibration':
      // Not separately modelled; derived from thermal and loading stress so it moves with
      // the asset rather than independently of it.
      return round(0.4 + Math.abs(sample.loadPct) / 60, 3)
    case 'partial_discharge':
      return round(Math.max(0, (sample.tempC - 60) * 12), 2)
    case 'protection_relay':
      return sample.isOutage ? 1 : 0
    case 'frequency':
      // Frequency is a property of the synchronous area; a per-asset instrument reads the
      // area's value, which the caller stamps.
      return 60
    default:
      return sample.tempC
  }
}

export async function getSensorDetail(
  code: string,
  options: { ts?: number; windowMin?: number; perturbation?: Perturbation } = {},
): Promise<SensorDetail | null> {
  const ts = options.ts ?? Date.now()
  const windowMin = options.windowMin ?? 30

  const [row, connector] = await Promise.all([
    prisma.sensor.findUnique({ where: { code }, select: SELECT }),
    connectorFor(),
  ])
  if (!row) return null

  const view = toView(row, connector)
  const { readings, origin } = await windowFor(view, { ts, windowMin, perturbation: options.perturbation })

  // A demonstration fault, if one is in force. Applied to the stream on its way to the
  // assessment and nowhere else.
  const faultKind = activeFaults.get(code) ?? null
  const streamed = faultKind ? injectSensorFault(readings, faultKind, view.kind) : readings

  // Peers: other instruments measuring the same quantity on the same asset, for the
  // disagreement check.
  const peerRows = await prisma.sensor.findMany({
    where: { asset: { code: view.assetCode }, kind: row.kind, NOT: { code } },
    select: { code: true },
    take: 4,
  })

  const peers: Array<{ sensorCode: string; value: number }> = []
  for (const peer of peerRows) {
    const peerView = { ...view, code: peer.code }
    const peerWindow = await windowFor(peerView, { ts, windowMin: 5, perturbation: options.perturbation })
    const last = peerWindow.readings[peerWindow.readings.length - 1]
    if (last) peers.push({ sensorCode: peer.code, value: last.value })
  }

  const health = assessSensorHealth({
    sensorCode: code,
    kind: view.kind,
    unit: view.unit,
    readings: streamed,
    samplingIntervalMin: view.samplingIntervalMin,
    now: ts,
    accuracy: view.accuracy,
    lastCalibratedAt: view.lastCalibratedAt,
    peers,
  })

  const latest = streamed[streamed.length - 1] ?? null

  return {
    ...view,
    latestValue: latest ? round(latest.value, 4) : null,
    latestTs: latest?.ts ?? null,
    dataAgeMin: health.dataAgeMin,
    // Never 'live': a regenerated stream is synthetic, and a stored reading in this build
    // was itself seeded. `measured` here means "read from a stored record", not "read from
    // a live instrument", and the connector label carries the demo prefix regardless.
    origin,
    quality: health.status,
    health,
    readings: streamed,
    faultKind,
    faultLabel: faultKind ? SENSOR_FAULTS[faultKind].label : null,
  }
}

// ─────────────────────────────────────────────────────────────────────────────
// Fault injection (§7)
// ─────────────────────────────────────────────────────────────────────────────

export type InjectionMode = 'live' | 'demo' | 'simulation' | 'replay'

export interface InjectionResult {
  ok: boolean
  reason?: 'not_found' | 'live_refused'
  sensorCode: string
  faultKind: SensorFaultKind | null
  message: string
  messageAr: string
}

/**
 * Inject or clear a demonstration fault.
 *
 * **Refused outside demonstration and simulation, server-side.** §7 requires the live mode
 * to reject this and the check lives here rather than in a component, because a check a
 * client performs is a check an attacker skips.
 */
export async function setSensorFault(input: {
  sensorCode: string
  faultKind: SensorFaultKind | null
  mode: InjectionMode
}): Promise<InjectionResult> {
  if (input.mode === 'live' || input.mode === 'replay') {
    return {
      ok: false,
      reason: 'live_refused',
      sensorCode: input.sensorCode,
      faultKind: null,
      message:
        'Faults can only be injected in demonstration or simulation. Degrading an instrument that is feeding live decisions is not a demonstration.',
      messageAr:
        'لا يمكن حقن الأعطال إلا في وضع العرض أو المحاكاة. تدهور جهاز يغذي قرارات حية ليس عرضًا توضيحيًا.',
    }
  }

  const sensor = await prisma.sensor.findUnique({ where: { code: input.sensorCode }, select: { code: true } })
  if (!sensor) {
    return {
      ok: false,
      reason: 'not_found',
      sensorCode: input.sensorCode,
      faultKind: null,
      message: 'No such instrument.',
      messageAr: 'لا يوجد جهاز بهذا الرمز.',
    }
  }

  if (input.faultKind) activeFaults.set(input.sensorCode, input.faultKind)
  else activeFaults.delete(input.sensorCode)

  return {
    ok: true,
    sensorCode: input.sensorCode,
    faultKind: input.faultKind,
    message: input.faultKind
      ? `${SENSOR_FAULTS[input.faultKind].label} injected. The reading stream is affected; nothing about the asset has changed.`
      : 'Fault cleared.',
    messageAr: input.faultKind
      ? `${SENSOR_FAULTS[input.faultKind].labelAr} مُطبق. تيار القراءات متأثر، ولم يتغير شيء في الأصل نفسه.`
      : 'أُزيل العطل.',
  }
}

/** Clear every injected fault. Used when a demonstration is reset. */
export function clearSensorFaults(): number {
  const count = activeFaults.size
  activeFaults.clear()
  return count
}

// ─────────────────────────────────────────────────────────────────────────────
// Provenance (§5)
// ─────────────────────────────────────────────────────────────────────────────

export interface AssetProvenance {
  assetCode: string
  subject: string
  chain: ReturnType<typeof buildProvenanceChain>
  /** Health of each contributing instrument, so a weak link can be opened. */
  sensorHealth: Array<{ sensorCode: string; kind: string; healthScore: number; status: string }>
}

/**
 * The chain from instruments to a recommendation about one asset.
 *
 * Every stage carries its own confidence and the chain is capped by the weakest, so a
 * recommendation resting on a drifting instrument cannot present itself as more certain
 * than that instrument. Answering "which sensors are behind this decision" is the whole
 * point: the sensor codes are on the chain, not summarised away.
 */
export async function getAssetProvenance(
  assetCode: string,
  options: { subject?: string; ts?: number; perturbation?: Perturbation } = {},
): Promise<AssetProvenance | null> {
  const ts = options.ts ?? Date.now()

  const asset = await prisma.asset.findUnique({
    where: { code: assetCode },
    select: { code: true, name: true, nameAr: true },
  })
  if (!asset) return null

  const sensors = await listSensorsForAssets([assetCode])
  const connector = await connectorFor()

  const healths: AssetProvenance['sensorHealth'] = []
  const sensorNodes: ProvenanceNode[] = []

  for (const sensor of sensors.slice(0, 8)) {
    const detail = await getSensorDetail(sensor.code, { ts, windowMin: 30, perturbation: options.perturbation })
    if (!detail) continue
    healths.push({
      sensorCode: sensor.code,
      kind: sensor.kind,
      healthScore: detail.health.healthScore,
      status: detail.health.status,
    })
    sensorNodes.push({
      stage: 'sensor',
      id: sensor.code,
      label: `${SENSOR_SPEC[sensor.kind].label} — ${sensor.code}`,
      labelAr: `${SENSOR_SPEC[sensor.kind].labelAr} — ${sensor.code}`,
      confidence: detail.health.healthScore,
      kind: detail.origin === 'measured' ? 'measured' : 'simulated',
      detail: `${detail.latestValue ?? '—'} ${sensor.unit}, ${round(detail.dataAgeMin, 1)} min old`,
      detailAr: `${detail.latestValue ?? '—'} ${sensor.unit}، عمرها ${round(detail.dataAgeMin, 1)} دقيقة`,
    })
  }

  // The stages above the instruments. Each confidence is derived from something real:
  // the connector's own quality, the twin's fidelity, the share of the state that is
  // measured rather than estimated.
  const hub = await getHubOverview()
  const { getTwinState, listTwins } = await import('./twin-service')
  const twins = await listTwins()
  const twin = twins.find((entry) => entry.assetCode === assetCode) ?? twins.find((entry) => entry.level === 'national')
  const twinState = twin ? await getTwinState(twin.code, { ts, perturbation: options.perturbation }) : null

  const measuredShare = sensors.length > 0 ? Math.min(100, sensors.length * 25) : 0

  const nodes: ProvenanceNode[] = [
    ...sensorNodes,
    {
      stage: 'gateway',
      id: connector?.protocol ?? 'none',
      label: `Gateway — ${connector?.protocol ?? 'none'}`,
      labelAr: `البوابة — ${connector?.protocol ?? 'لا يوجد'}`,
      confidence: connector ? 92 : 40,
      kind: 'derived',
      detail: connector ? 'Protocol gateway carrying this asset’s telemetry.' : 'No gateway is mapped.',
      detailAr: connector ? 'بوابة البروتوكول التي تحمل قياسات هذا الأصل.' : 'لا توجد بوابة مرتبطة.',
    },
    {
      stage: 'connector',
      id: connector?.code ?? 'none',
      label: connector ? `${connector.code} (${connector.label})` : 'No connector',
      labelAr: connector ? `${connector.code} (${connector.label})` : 'لا يوجد موصّل',
      confidence: hub.meanQuality,
      kind: 'derived',
      detail: `Hub data quality ${round(hub.meanQuality, 1)} %.`,
      detailAr: `جودة بيانات المركز ${round(hub.meanQuality, 1)}٪.`,
    },
    {
      stage: 'mapping',
      id: 'field-mapping',
      label: 'Field mapping and unit conversion',
      labelAr: 'ربط الحقول وتحويل الوحدات',
      confidence: 96,
      kind: 'derived',
      detail: 'Source fields mapped to platform fields with units converted.',
      detailAr: 'حقول المصدر مربوطة بحقول المنصة مع تحويل الوحدات.',
    },
    {
      stage: 'validation',
      id: 'range-and-duplicate',
      label: 'Range, duplicate and freshness validation',
      labelAr: 'التحقق من المدى والتكرار والحداثة',
      confidence: hub.meanQuality,
      kind: 'derived',
      detail: 'Records outside range or arriving twice are rejected before the twin sees them.',
      detailAr: 'تُرفض السجلات خارج المدى أو المكررة قبل أن يراها التوأم.',
    },
    {
      stage: 'twin_asset',
      id: assetCode,
      label: `${asset.name}`,
      labelAr: `${asset.nameAr}`,
      confidence: twinState?.fidelity.score ?? 60,
      kind: 'derived',
      detail: `Twin fidelity ${round(twinState?.fidelity.score ?? 0, 1)} %.`,
      detailAr: `دقة التوأم ${round(twinState?.fidelity.score ?? 0, 1)}٪.`,
    },
    {
      stage: 'physics_engine',
      id: 'newton_raphson',
      label: 'AC load flow',
      labelAr: 'سريان الحمل المتردد',
      // Capped by how much of the state is measured rather than inferred: a solver reading
      // an estimated state produces an estimated answer.
      confidence: Math.min(94, 55 + measuredShare * 0.4),
      kind: 'derived',
      detail: 'Impedance is typical for the voltage class, not measured on these assets.',
      detailAr: 'الممانعة نمطية لفئة الجهد، وليست مقيسة على هذه الأصول.',
    },
    {
      stage: 'risk_engine',
      id: 'risk',
      label: 'Risk assessment',
      labelAr: 'تقييم الخطر',
      confidence: 88,
      kind: 'derived',
      detail: 'Weighted risk over thermal, loading, condition and pattern factors.',
      detailAr: 'خطر مرجح على عوامل الحرارة والتحميل والحالة والنمط.',
    },
    {
      stage: 'recommendation',
      id: options.subject ?? assetCode,
      label: 'Recommendation',
      labelAr: 'التوصية',
      confidence: 86,
      kind: 'simulated',
      detail: 'The minimum intervention, evaluated in the sandbox.',
      detailAr: 'أقل تدخل، مُقيّم داخل البيئة المعزولة.',
    },
  ]

  return {
    assetCode,
    subject: options.subject ?? `Recommendation for ${assetCode}`,
    chain: buildProvenanceChain(options.subject ?? assetCode, nodes),
    sensorHealth: healths,
  }
}

// ─────────────────────────────────────────────────────────────────────────────
// Evidence integrity (§26)
// ─────────────────────────────────────────────────────────────────────────────

/**
 * A stable SHA-256 over the parts of an evidence bundle that must not change.
 *
 * Keys are sorted before hashing so a re-serialisation in a different order does not
 * produce a different digest. Floating-point values are rounded to six places for the same
 * reason: a value that round-trips through JSON must hash the same on the way back.
 *
 * This is an integrity check, not a signature. It proves the record has not been altered
 * since it was written; it does not prove who wrote it. Saying otherwise would be claiming
 * a guarantee the platform does not offer.
 */
export function evidenceHash(payload: unknown): string {
  return createHash('sha256').update(canonicalise(payload)).digest('hex')
}

function canonicalise(value: unknown): string {
  if (value === null || value === undefined) return 'null'
  if (typeof value === 'number') {
    return Number.isFinite(value) ? String(round(value, 6)) : 'null'
  }
  if (typeof value === 'string' || typeof value === 'boolean') return JSON.stringify(value)
  if (Array.isArray(value)) return `[${value.map(canonicalise).join(',')}]`
  if (value instanceof Date) return JSON.stringify(value.toISOString())
  if (typeof value === 'object') {
    const entries = Object.entries(value as Record<string, unknown>)
      .filter(([, item]) => item !== undefined)
      .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
    return `{${entries.map(([key, item]) => `${JSON.stringify(key)}:${canonicalise(item)}`).join(',')}}`
  }
  return 'null'
}

export function verifyEvidenceHash(payload: unknown, expected: string): {
  valid: boolean
  computed: string
  expected: string
} {
  const computed = evidenceHash(payload)
  return { valid: computed === expected, computed, expected }
}
