/**
 * Risk engine (§42).
 *
 * Risk is never random and never a lookup. It is composed from six measurable stress
 * factors, each in [0, 1], combined with configurable weights and then modulated by the
 * asset's vulnerability (condition and consequence).
 *
 *     risk = 100 · stress^γ · vulnerability
 *
 * The split matters for explainability: `stress` is what an operator can change in the
 * next hour (loading, temperature, configuration), while `vulnerability` is what they
 * cannot (asset condition, how many customers sit behind it). Preventive actions move
 * the first term, which is why a simulated action produces a large, honest drop.
 */

import { clamp, round } from './rng'
import type { AssetProfile, Perturbation, TelemetrySample } from './telemetry'
import { NO_PERTURBATION, sampleAsset } from './telemetry'
import type { EnvironmentSample } from './environment'

export const RISK_FACTOR_KEYS = [
  'anomaly',
  'dna',
  'load',
  'thermal',
  'weather',
] as const
export type RiskFactorKey = (typeof RISK_FACTOR_KEYS)[number]

export interface RiskWeights {
  anomaly: number
  dna: number
  load: number
  thermal: number
  weather: number
  /** Vulnerability shaping — how much condition and consequence amplify stress. */
  healthAmplifier: number
  networkAmplifier: number
  /** Convexity exponent applied to the stress index. */
  gamma: number
}

/**
 * Defaults. Every value is exposed through `app_settings` and editable in /settings,
 * satisfying the "weights must be configurable" requirement.
 */
export const DEFAULT_RISK_WEIGHTS: RiskWeights = {
  anomaly: 0.22,
  dna: 0.24,
  load: 0.26,
  thermal: 0.19,
  weather: 0.09,
  healthAmplifier: 0.35,
  networkAmplifier: 0.15,
  gamma: 1.15,
}

export const RISK_WEIGHT_SETTING_KEYS: Record<keyof RiskWeights, string> = {
  anomaly: 'risk.weight.anomaly',
  dna: 'risk.weight.dna',
  load: 'risk.weight.load',
  thermal: 'risk.weight.thermal',
  weather: 'risk.weight.weather',
  healthAmplifier: 'risk.amplifier.health',
  networkAmplifier: 'risk.amplifier.network',
  gamma: 'risk.gamma',
}

export interface RiskFactor {
  key: RiskFactorKey
  value: number
  weight: number
  /** Share of the final risk score attributable to this factor, in percentage points. */
  contribution: number
  rawValue: number
  unit: string
}

export interface RiskAssessment {
  score: number
  stressIndex: number
  vulnerability: number
  factors: RiskFactor[]
  /** Ordered highest-contribution first — drives the "WHY?" panel. */
  topFactors: RiskFactor[]
}

export interface RiskInputs {
  profile: AssetProfile
  sample: TelemetrySample
  env: EnvironmentSample
  /** Best Failure-DNA match, 0–100. */
  dnaSimilarity: number
  /** Stress of neighbouring assets, 0–1. */
  neighbourStress: number
  weights?: RiskWeights
}

// Thresholds where each stress factor starts to bite.
const LOAD_STRESS_FLOOR = 65
const LOAD_STRESS_SPAN = 30
const THERMAL_MARGIN_BELOW_RATING = 30
const THERMAL_STRESS_SPAN = 28
const WEATHER_STRESS_FLOOR = 36
const WEATHER_STRESS_SPAN = 12

/**
 * Anomaly score: how much worse the asset is behaving than the digital twin expects for
 * this asset, at this hour, under baseline conditions. Only *adverse* excursions count —
 * running cooler or lighter than expected is not an anomaly.
 */
export function anomalyScore(
  profile: AssetProfile,
  sample: TelemetrySample,
  env: EnvironmentSample,
): number {
  const expected = sampleAsset(profile, sample.ts, env, NO_PERTURBATION)
  const loadExcess = clamp((sample.loadPct - expected.loadPct) / 22, 0, 1)
  const tempExcess = clamp((sample.tempC - expected.tempC) / 18, 0, 1)
  const voltageExcess = clamp(
    (Math.abs(sample.voltageDeviationPct) - Math.abs(expected.voltageDeviationPct)) / 3.5,
    0,
    1,
  )
  const thdExcess = clamp((sample.thdPct - expected.thdPct) / 4, 0, 1)
  return round(
    clamp(0.4 * loadExcess + 0.32 * tempExcess + 0.16 * voltageExcess + 0.12 * thdExcess, 0, 1),
    4,
  )
}

export function loadStress(loadPct: number): number {
  return clamp((Math.abs(loadPct) - LOAD_STRESS_FLOOR) / LOAD_STRESS_SPAN, 0, 1)
}

export function thermalStress(tempC: number, ratedTempC: number): number {
  return clamp(
    (tempC - (ratedTempC - THERMAL_MARGIN_BELOW_RATING)) / THERMAL_STRESS_SPAN,
    0,
    1,
  )
}

export function weatherStress(env: EnvironmentSample): number {
  const heat = clamp((env.tempC - WEATHER_STRESS_FLOOR) / WEATHER_STRESS_SPAN, 0, 1)
  const dust = clamp(env.dustIndex, 0, 1) * 0.25
  return clamp(heat * 0.85 + dust, 0, 1)
}

/** Consequence weighting: how much the grid cares if this particular asset fails. */
export function networkImpact(profile: AssetProfile): number {
  const customers = clamp(profile.customersServed / 220_000, 0, 1)
  const size = clamp(profile.capacityMw / 900, 0, 1)
  return clamp(0.65 * customers + 0.35 * size, 0, 1)
}

export function assessRisk(inputs: RiskInputs): RiskAssessment {
  const weights = inputs.weights ?? DEFAULT_RISK_WEIGHTS
  const { profile, sample, env } = inputs

  const raw: Record<RiskFactorKey, { value: number; rawValue: number; unit: string }> = {
    anomaly: {
      value: anomalyScore(profile, sample, env),
      rawValue: anomalyScore(profile, sample, env),
      unit: 'idx',
    },
    dna: {
      value: clamp(inputs.dnaSimilarity / 100, 0, 1),
      rawValue: inputs.dnaSimilarity,
      unit: '%',
    },
    load: { value: loadStress(sample.loadPct), rawValue: sample.loadPct, unit: '%' },
    thermal: {
      value: thermalStress(sample.tempC, profile.ratedTempC),
      rawValue: sample.tempC,
      unit: '°C',
    },
    weather: { value: weatherStress(env), rawValue: env.tempC, unit: '°C' },
  }

  const weightSum =
    weights.anomaly + weights.dna + weights.load + weights.thermal + weights.weather
  const stressIndex = clamp(
    RISK_FACTOR_KEYS.reduce((sum, key) => sum + weights[key] * raw[key].value, 0) /
      (weightSum || 1),
    0,
    1,
  )

  const healthDeficit = 1 - clamp(sample.effectiveHealth, 0, 100) / 100
  const impact = networkImpact(profile)
  const neighbour = clamp(inputs.neighbourStress, 0, 1)
  const vulnerability =
    0.75 +
    weights.healthAmplifier * healthDeficit +
    weights.networkAmplifier * impact +
    0.08 * neighbour

  const score = clamp(100 * Math.pow(stressIndex, weights.gamma) * vulnerability, 0, 100)

  // Attribute the final score back to each factor in proportion to its weighted share,
  // so the numbers in the "WHY?" panel add up to the score the operator was shown.
  const factors: RiskFactor[] = RISK_FACTOR_KEYS.map((key) => {
    const weighted = weights[key] * raw[key].value
    const share = stressIndex > 0 ? weighted / (stressIndex * (weightSum || 1)) : 0
    return {
      key,
      value: round(raw[key].value, 4),
      weight: weights[key],
      contribution: round(share * score, 2),
      rawValue: round(raw[key].rawValue, 2),
      unit: raw[key].unit,
    }
  })

  const topFactors = [...factors].sort((a, b) => b.contribution - a.contribution)

  return {
    score: round(score, 1),
    stressIndex: round(stressIndex, 4),
    vulnerability: round(vulnerability, 4),
    factors,
    topFactors,
  }
}

/**
 * NABDH Grid Stability Index, 0–100 (§19). A weighted blend of the eight published
 * inputs, so the headline number is reproducible from the values shown beside it.
 */
export interface StabilityInputs {
  voltageDeviationPct: number
  frequencyHz: number
  loadRatio: number
  reserveMarginPct: number
  renewableVariabilityPct: number
  averageHealth: number
  activeIncidents: number
  weatherStress: number
}

export function gridStabilityIndex(input: StabilityInputs): number {
  const voltage = 1 - clamp(Math.abs(input.voltageDeviationPct) / 6, 0, 1)
  const frequency = 1 - clamp(Math.abs(input.frequencyHz - 60) / 0.4, 0, 1)
  const load = 1 - clamp((input.loadRatio - 0.7) / 0.32, 0, 1)
  const reserve = clamp(input.reserveMarginPct / 22, 0, 1)
  const renewable = 1 - clamp(input.renewableVariabilityPct / 35, 0, 1)
  const health = clamp(input.averageHealth / 100, 0, 1)
  const incidents = 1 - clamp(input.activeIncidents / 8, 0, 1)
  const weather = 1 - clamp(input.weatherStress, 0, 1)

  const score =
    100 *
    (0.17 * voltage +
      0.17 * frequency +
      0.16 * load +
      0.14 * reserve +
      0.1 * renewable +
      0.13 * health +
      0.08 * incidents +
      0.05 * weather)

  return round(clamp(score, 0, 100), 1)
}

export function stabilityBand(index: number): 'excellent' | 'good' | 'adequate' | 'strained' | 'critical' {
  if (index >= 90) return 'excellent'
  if (index >= 78) return 'good'
  if (index >= 65) return 'adequate'
  if (index >= 50) return 'strained'
  return 'critical'
}

/** Convenience wrapper used where the caller has a perturbation but no sample yet. */
export function assessAssetRisk(
  profile: AssetProfile,
  ts: number,
  env: EnvironmentSample,
  options: {
    perturbation?: Perturbation
    dnaSimilarity?: number
    neighbourStress?: number
    weights?: RiskWeights
  } = {},
): { sample: TelemetrySample; risk: RiskAssessment } {
  const sample = sampleAsset(profile, ts, env, options.perturbation ?? NO_PERTURBATION)
  const risk = assessRisk({
    profile,
    sample,
    env,
    dnaSimilarity: options.dnaSimilarity ?? 0,
    neighbourStress: options.neighbourStress ?? 0,
    weights: options.weights,
  })
  return { sample, risk }
}
