/**
 * Prediction engine (§43) and the Energy Time Machine's forward model (§5).
 *
 * There is no magic constant for "43 minutes". The engine walks the digital twin forward
 * minute by minute under current conditions and reports the first moment a limit is
 * crossed. Change the weather, the loading or the actions applied and the number moves,
 * because it is a property of the model rather than a label on a slide.
 */

import type { EnvironmentSample } from './environment'
import {
  type AssetProfile,
  type Perturbation,
  type TelemetrySample,
  NO_PERTURBATION,
  loadGrowthRate,
  sampleAsset,
} from './telemetry'
import { assessRisk, type RiskAssessment, type RiskWeights } from './risk'
import { clamp, round } from './rng'
import type { EventType, Severity } from '../domain/enums'

const MS_PER_MIN = 60_000

/**
 * Limits that constitute a "failure event" for the forward walk.
 *
 * These are deliberately *physical*. The risk score is an early-warning indicator — it
 * tells an operator where to look and how urgently — but it is not itself a failure
 * criterion, and using it as one would make the predicted time-to-failure an artefact of
 * where the threshold happened to be set. What ends the forward walk is a real operating
 * limit being crossed.
 *
 * `overTempK` follows IEC 60076-7 loading guidance: the rated top-oil temperature is a
 * *normal cyclic* rating and may be exceeded for a limited period; the long-time
 * emergency limit sits roughly 20 K higher.
 *
 * `riskScore` remains only as a catch-all for pathological states that somehow avoid
 * every physical limit, and is set high enough never to bind in normal operation.
 */
export const FAILURE_LIMITS = {
  riskScore: 97,
  /** Degrees above the asset's rated temperature (long-time emergency limit). */
  overTempK: 20,
  /** Emergency loading limit, above the continuous rating. */
  loadPct: 108,
  voltageDeviationPct: 7,
}

export type EnvironmentProvider = (ts: number) => EnvironmentSample

export interface ForwardFrame {
  ts: number
  offsetMin: number
  sample: TelemetrySample
  risk: RiskAssessment
  env: EnvironmentSample
  state: 'normal' | 'watch' | 'warning' | 'critical' | 'emergency'
  breached: boolean
  breachReason: string | null
}

export interface ForwardWalkOptions {
  horizonMin?: number
  stepMin?: number
  perturbation?: Perturbation
  dnaSimilarity?: number
  neighbourStress?: number
  weights?: RiskWeights
}

function classify(risk: number): ForwardFrame['state'] {
  if (risk >= 90) return 'emergency'
  if (risk >= 75) return 'critical'
  if (risk >= 55) return 'warning'
  if (risk >= 35) return 'watch'
  return 'normal'
}

function breachOf(
  profile: AssetProfile,
  sample: TelemetrySample,
  risk: number,
): string | null {
  if (sample.tempC >= profile.ratedTempC + FAILURE_LIMITS.overTempK) return 'temperature'
  if (Math.abs(sample.loadPct) >= FAILURE_LIMITS.loadPct) return 'overload'
  if (Math.abs(sample.voltageDeviationPct) >= FAILURE_LIMITS.voltageDeviationPct) return 'voltage'
  if (risk >= FAILURE_LIMITS.riskScore) return 'risk'
  return null
}

/**
 * Walk the twin forward and return one frame per step. This single function backs the
 * Energy Time Machine, the ETA calculation and every scenario comparison chart.
 */
export function forwardWalk(
  profile: AssetProfile,
  ts: number,
  envAt: EnvironmentProvider,
  options: ForwardWalkOptions = {},
): ForwardFrame[] {
  const horizon = options.horizonMin ?? 180
  const step = options.stepMin ?? 5
  const perturbation = options.perturbation ?? NO_PERTURBATION
  const frames: ForwardFrame[] = []

  for (let offset = 0; offset <= horizon; offset += step) {
    const frameTs = ts + offset * MS_PER_MIN
    const env = envAt(frameTs)
    const sample = sampleAsset(profile, frameTs, env, perturbation)
    const risk = assessRisk({
      profile,
      sample,
      env,
      dnaSimilarity: options.dnaSimilarity ?? 0,
      neighbourStress: options.neighbourStress ?? 0,
      weights: options.weights,
    })
    const reason = breachOf(profile, sample, risk.score)
    frames.push({
      ts: frameTs,
      offsetMin: offset,
      sample,
      risk,
      env,
      state: classify(risk.score),
      breached: reason !== null,
      breachReason: reason,
    })
  }

  return frames
}

export interface PredictionResult {
  /** Peak risk reached anywhere in the horizon. */
  peakRisk: number
  /** Risk right now. */
  currentRisk: number
  probability: number
  confidence: number
  severity: Severity
  eventType: EventType
  /** Minutes until the first limit breach, or null when nothing breaches in horizon. */
  etaMinutes: number | null
  etaAt: number | null
  breachReason: string | null
  horizonMin: number
  frames: ForwardFrame[]
  riskNow: RiskAssessment
  trendPerHour: number
}

function sigmoid(x: number): number {
  return 1 / (1 + Math.exp(-x))
}

function pickEventType(
  profile: AssetProfile,
  reason: string | null,
  frame: ForwardFrame | undefined,
): EventType {
  const isLine = profile.assetTypeKey === 'transmission_line'
  switch (reason) {
    case 'temperature':
      return frame && frame.sample.tempC > profile.ratedTempC + 22
        ? 'thermal_runaway'
        : isLine
          ? 'line_overload'
          : 'transformer_overload'
    case 'overload':
      return isLine ? 'line_overload' : 'transformer_overload'
    case 'voltage':
      return 'voltage_collapse'
    default:
      break
  }
  if (profile.assetTypeKey === 'battery_storage') return 'battery_derate'
  if (profile.assetTypeKey === 'solar_farm' || profile.assetTypeKey === 'wind_farm')
    return 'renewable_ramp_shortfall'
  return isLine ? 'line_overload' : 'transformer_overload'
}

function severityFrom(probability: number, peakRisk: number): Severity {
  const combined = probability * 100 * 0.5 + peakRisk * 0.5
  if (combined >= 82) return 'critical'
  if (combined >= 66) return 'high'
  if (combined >= 42) return 'medium'
  return 'low'
}

/**
 * Full prediction for one asset.
 *
 * `probability` is a calibrated function of how far peak risk exceeds the alerting
 * threshold, blended with the Failure-DNA prior. `confidence` reflects how *consistent*
 * the forward trajectory is: a smoothly rising trend is trustworthy, a jittery one is
 * not, and a strong DNA match raises it further.
 */
export function predictFailure(
  profile: AssetProfile,
  ts: number,
  envAt: EnvironmentProvider,
  options: ForwardWalkOptions & { dataQuality?: number } = {},
): PredictionResult {
  const horizonMin = options.horizonMin ?? 180
  const stepMin = options.stepMin ?? 5
  const frames = forwardWalk(profile, ts, envAt, { ...options, horizonMin, stepMin })

  const riskNow = frames[0].risk
  const peakRisk = frames.reduce((max, f) => Math.max(max, f.risk.score), 0)

  const firstBreach = frames.find((f) => f.breached) ?? null
  // Refine the ETA to the minute by re-walking the step that contains the crossing.
  let etaMinutes: number | null = null
  let breachReason: string | null = null
  if (firstBreach) {
    breachReason = firstBreach.breachReason
    const from = Math.max(0, firstBreach.offsetMin - stepMin)
    const refined = forwardWalk(profile, ts + from * MS_PER_MIN, envAt, {
      ...options,
      horizonMin: stepMin,
      stepMin: 1,
    })
    const hit = refined.find((f) => f.breached)
    etaMinutes = from + (hit ? hit.offsetMin : stepMin)
    if (hit?.breachReason) breachReason = hit.breachReason
  }

  // Trend: how fast risk is climbing over the first hour.
  const oneHourFrame =
    frames.find((f) => f.offsetMin >= 60) ?? frames[frames.length - 1]
  const trendPerHour = round(
    ((oneHourFrame.risk.score - riskNow.score) * 60) / Math.max(oneHourFrame.offsetMin, 1),
    2,
  )

  // Monotonicity of the risk trajectory — the core of the confidence estimate.
  let rising = 0
  for (let i = 1; i < frames.length; i += 1) {
    if (frames[i].risk.score >= frames[i - 1].risk.score - 0.4) rising += 1
  }
  const monotonicity = frames.length > 1 ? rising / (frames.length - 1) : 0

  const dnaPrior = clamp((options.dnaSimilarity ?? 0) / 100, 0, 1)
  const dataQuality = clamp(options.dataQuality ?? 0.95, 0, 1)

  const probability = clamp(
    0.75 * sigmoid((peakRisk - 70) / 9) + 0.25 * dnaPrior,
    0.01,
    0.985,
  )
  const confidence = clamp(
    0.45 + 0.25 * monotonicity + 0.2 * dnaPrior + 0.1 * dataQuality,
    0.3,
    0.98,
  )

  return {
    peakRisk: round(peakRisk, 1),
    currentRisk: riskNow.score,
    probability: round(probability, 4),
    confidence: round(confidence, 4),
    severity: severityFrom(probability, peakRisk),
    eventType: pickEventType(profile, breachReason, firstBreach ?? undefined),
    etaMinutes,
    etaAt: etaMinutes === null ? null : ts + etaMinutes * MS_PER_MIN,
    breachReason,
    horizonMin,
    frames,
    riskNow,
    trendPerHour,
  }
}

/** Growth in loading over the past hour — a Failure-DNA input and a "WHY?" factor. */
export function hourlyLoadGrowth(
  profile: AssetProfile,
  ts: number,
  envAt: EnvironmentProvider,
  perturbation: Perturbation = NO_PERTURBATION,
): number {
  return loadGrowthRate(profile, ts, envAt(ts), envAt(ts - 60 * MS_PER_MIN), perturbation)
}

/** The fixed offsets offered by the Energy Time Machine control (§5). */
export const TIME_MACHINE_OFFSETS_MIN = [0, 5, 15, 30, 60, 180, 360, 720, 1440] as const
