/**
 * Criticality, weakest link and the national risk radar (§30, §31, §32).
 *
 * Risk answers "how likely is this to go wrong". Criticality answers a different and
 * equally operational question: "how much would it matter". A rural transformer at 88 %
 * risk and a bulk supply point at 40 % are not the same problem, and a maintenance queue
 * ordered by risk alone will service the wrong one first.
 *
 * The scores here are structural — customers behind the node, share of the network that
 * depends on it, whether anything else can carry its load — combined with the live state
 * so the ranking moves with the grid rather than being a static asset attribute.
 */

import type { AssetSnapshot } from './grid'
import type { GridGraph } from './graph'
import { siblingsOf } from './graph'
import { clamp, round } from './rng'

export interface AssetCriticality {
  code: string
  name: string
  nameAr: string
  regionCode: string
  assetTypeKey: string
  /** Live condition, 0–100. */
  healthScore: number
  riskScore: number
  /** How much the network depends on this asset, 0–100. */
  criticalityScore: number
  /** Consequence of losing it, 0–100. */
  failureImpactScore: number
  /** How hard it would be to restore, 0–100. */
  recoveryDifficulty: number
  /** Risk × consequence — the ranking a maintenance queue should actually use. */
  exposureScore: number
  customersServed: number
  downstreamCustomers: number
  /** Spare MW on peers that could carry this asset's load, 0 when nothing can. */
  backupCapacityMw: number
  hasRedundancy: boolean
  reason: string
  reasonAr: string
}

/** Restoration difficulty by asset class, before redundancy is considered. */
const RESTORATION_BASE: Record<string, number> = {
  power_plant: 78,
  substation: 72,
  transformer: 64,
  transmission_line: 58,
  battery_storage: 34,
  solar_farm: 30,
  wind_farm: 36,
  industrial_load: 22,
  ev_hub: 18,
}

/**
 * Score one asset.
 *
 * `backupCapacityMw` is measured against the peers that are actually on the same bus and
 * still in service, which is what decides whether losing this unit is a switching
 * operation or an interruption.
 */
export function scoreCriticality(
  asset: AssetSnapshot,
  graph: GridGraph,
  fleet: AssetSnapshot[],
): AssetCriticality {
  const node = graph.byCode.get(asset.profile.code)
  const byCode = new Map(fleet.map((entry) => [entry.profile.code, entry]))

  const peers = siblingsOf(graph, asset.profile.code)
  const backupCapacityMw = round(
    peers.reduce((sum, peer) => {
      const state = byCode.get(peer.code)
      if (!state || state.sample.isOutage) return sum
      // Headroom up to 100 % of nameplate: an emergency transfer does not get to plan
      // on running the neighbours past their own rating indefinitely.
      return sum + Math.max(0, peer.capacityMw - Math.max(0, state.sample.loadMw))
    }, 0),
    1,
  )

  const ownLoadMw = Math.max(0, asset.sample.loadMw)
  const coverage = ownLoadMw > 0 ? clamp(backupCapacityMw / ownLoadMw, 0, 1) : 1
  const hasRedundancy = coverage >= 0.9

  const centralityScore = clamp((node?.centrality ?? 0) * 100 * 22, 0, 100)
  const customerScore = clamp(((node?.downstreamCustomers ?? asset.profile.customersServed) / 400_000) * 100, 0, 100)
  const capacityScore = clamp((asset.profile.capacityMw / 1_200) * 100, 0, 100)

  const criticalityScore = round(
    clamp(centralityScore * 0.34 + customerScore * 0.4 + capacityScore * 0.26, 0, 100),
    1,
  )

  // Consequence: what is behind it, minus whatever the neighbours could pick up.
  const failureImpactScore = round(clamp(criticalityScore * (1 - coverage * 0.62), 0, 100), 1)

  const base = RESTORATION_BASE[asset.profile.assetTypeKey] ?? 50
  const recoveryDifficulty = round(
    clamp(base * (1 - coverage * 0.45) + (100 - asset.sample.effectiveHealth) * 0.22, 0, 100),
    1,
  )

  const exposureScore = round(clamp((asset.risk.score / 100) * failureImpactScore, 0, 100), 1)

  const reasonParts: string[] = []
  const reasonPartsAr: string[] = []
  if (!hasRedundancy) {
    reasonParts.push(`peers can absorb only ${Math.round(coverage * 100)} % of its load`)
    reasonPartsAr.push(`الوحدات المجاورة تستوعب ${Math.round(coverage * 100)}٪ فقط من حمله`)
  }
  if (customerScore > 45) {
    reasonParts.push(`${(node?.downstreamCustomers ?? 0).toLocaleString('en-US')} customers downstream`)
    reasonPartsAr.push(`${(node?.downstreamCustomers ?? 0).toLocaleString('en-US')} مشترك في المسار التابع`)
  }
  if (asset.risk.score > 55) {
    reasonParts.push(`risk at ${round(asset.risk.score, 1)} %`)
    reasonPartsAr.push(`الخطر عند ${round(asset.risk.score, 1)}٪`)
  }
  if (reasonParts.length === 0) {
    reasonParts.push('within normal limits with redundant cover available')
    reasonPartsAr.push('ضمن الحدود الطبيعية مع توفر تغطية احتياطية')
  }

  return {
    code: asset.profile.code,
    name: node?.name ?? asset.profile.code,
    nameAr: node?.nameAr ?? asset.profile.code,
    regionCode: asset.profile.regionCode,
    assetTypeKey: asset.profile.assetTypeKey,
    healthScore: round(asset.sample.effectiveHealth, 1),
    riskScore: round(asset.risk.score, 1),
    criticalityScore,
    failureImpactScore,
    recoveryDifficulty,
    exposureScore,
    customersServed: asset.profile.customersServed,
    downstreamCustomers: node?.downstreamCustomers ?? asset.profile.customersServed,
    backupCapacityMw,
    hasRedundancy,
    reason: reasonParts.join(', '),
    reasonAr: reasonPartsAr.join('، '),
  }
}

export function scoreFleet(fleet: AssetSnapshot[], graph: GridGraph): AssetCriticality[] {
  return fleet.map((asset) => scoreCriticality(asset, graph, fleet))
}

/**
 * The weakest link (§30).
 *
 * Not the highest risk and not the most critical, but the asset where those two meet:
 * the one whose failure is both plausible and consequential, and which the network is
 * least able to work around.
 */
export function weakestLink(scores: AssetCriticality[]): AssetCriticality | null {
  return (
    [...scores].sort(
      (a, b) =>
        b.exposureScore + b.recoveryDifficulty * 0.25 - (a.exposureScore + a.recoveryDifficulty * 0.25),
    )[0] ?? null
  )
}

export interface RiskRadar {
  criticalAssets: AssetCriticality[]
  regions: Array<{
    code: string
    peakRisk: number
    averageRisk: number
    assetsAtRisk: number
    exposure: number
  }>
  emergingRisks: AssetCriticality[]
  cascadeRisks: AssetCriticality[]
  maintenancePriorities: AssetCriticality[]
}

/**
 * The national risk radar (§32).
 *
 * Four different orderings of the same fleet, because "what should I look at" has four
 * different answers depending on whether you are running the shift, planning the week,
 * worrying about the region or booking the crews.
 */
export function buildRiskRadar(
  scores: AssetCriticality[],
  fleet: AssetSnapshot[],
  options: { limit?: number } = {},
): RiskRadar {
  const limit = options.limit ?? 10
  const byCode = new Map(fleet.map((entry) => [entry.profile.code, entry]))

  const regionTotals = new Map<
    string,
    { peakRisk: number; sum: number; count: number; atRisk: number; exposure: number }
  >()
  for (const score of scores) {
    const entry = regionTotals.get(score.regionCode) ?? {
      peakRisk: 0,
      sum: 0,
      count: 0,
      atRisk: 0,
      exposure: 0,
    }
    entry.peakRisk = Math.max(entry.peakRisk, score.riskScore)
    entry.sum += score.riskScore
    entry.count += 1
    if (score.riskScore >= 55) entry.atRisk += 1
    entry.exposure = Math.max(entry.exposure, score.exposureScore)
    regionTotals.set(score.regionCode, entry)
  }

  return {
    criticalAssets: [...scores].sort((a, b) => b.exposureScore - a.exposureScore).slice(0, limit),
    regions: [...regionTotals.entries()]
      .map(([code, entry]) => ({
        code,
        peakRisk: round(entry.peakRisk, 1),
        averageRisk: round(entry.sum / Math.max(1, entry.count), 1),
        assetsAtRisk: entry.atRisk,
        exposure: round(entry.exposure, 1),
      }))
      .sort((a, b) => b.exposure - a.exposure)
      .slice(0, 5),
    // Emerging: risk is climbing but has not arrived yet — the ones worth watching now
    // precisely because nothing has fired on them.
    emergingRisks: [...scores]
      .filter((score) => score.riskScore >= 30 && score.riskScore < 62)
      .sort((a, b) => b.criticalityScore - a.criticalityScore)
      .slice(0, limit / 2),
    cascadeRisks: [...scores]
      .filter((score) => !score.hasRedundancy)
      .sort((a, b) => b.failureImpactScore - a.failureImpactScore)
      .slice(0, limit / 2),
    maintenancePriorities: [...scores]
      .map((score) => ({
        score,
        priority:
          (100 - score.healthScore) * 1.2 +
          score.criticalityScore * 0.5 +
          (byCode.get(score.code)?.maintenanceDebt ?? 0) * 0.4,
      }))
      .sort((a, b) => b.priority - a.priority)
      .slice(0, limit / 2)
      .map((entry) => entry.score),
  }
}
