/**
 * Grid aggregation — turns per-asset telemetry into the national picture shown on the
 * Command Center, the map and the executive dashboard.
 *
 * Everything here is derived: national load is the sum of the assets that serve load,
 * frequency comes from the resulting supply/demand balance, and the stability index is
 * computed from the same eight inputs the UI displays next to it. No headline number on
 * screen is a constant.
 */

import { REGIONS, getRegion, type RegionSeed } from '../data/regions'
import { riskToState, type GridState } from '../domain/enums'
import {
  sampleEnvironment,
  type EnvironmentOverrides,
  type EnvironmentSample,
} from './environment'
import { dnaFromRow, extractDna, matchDna, type DnaVector } from './dna'
import { assessRisk, gridStabilityIndex, type RiskAssessment, type RiskWeights } from './risk'
import {
  type AssetProfile,
  type Perturbation,
  type TelemetrySample,
  NO_PERTURBATION,
  perturbationApplies,
  sampleAsset,
  systemFrequency,
} from './telemetry'
import { clamp, round } from './rng'

const MS_PER_MIN = 60_000
const MS_PER_YEAR = 365.25 * 24 * 3_600_000

/**
 * Asset types that consume from the grid rather than supply it.
 * Substations are deliberately excluded: they are containers for the transformers
 * inside them, so counting both would double-count the same megawatts.
 */
const LOAD_SERVING: ReadonlySet<string> = new Set([
  'transformer',
  'industrial_load',
  'ev_hub',
])

const RENEWABLE: ReadonlySet<string> = new Set(['solar_farm', 'wind_farm'])
const SUPPLY: ReadonlySet<string> = new Set([
  'power_plant',
  'solar_farm',
  'wind_farm',
  'battery_storage',
])

export interface DnaPatternRef {
  code: string
  name: string
  nameAr: string
  eventType: string
  severity: string
  occurredAt: number
  dna: DnaVector
}

export interface AssetSnapshot {
  profile: AssetProfile
  sample: TelemetrySample
  risk: RiskAssessment
  state: GridState
  dnaSimilarity: number
  dnaCode: string | null
  neighbourStress: number
  ageYears: number
  maintenanceDebt: number
}

export interface RegionSnapshot {
  code: string
  name: string
  nameAr: string
  lat: number
  lon: number
  loadMw: number
  capacityMw: number
  supplyMw: number
  renewableMw: number
  renewableSharePct: number
  reserveMarginPct: number
  averageRisk: number
  peakRisk: number
  state: GridState
  assetCount: number
  assetsAtRisk: number
  customersServed: number
  averageHealth: number
  env: EnvironmentSample
  renewableVariabilityPct: number
}

export interface GridSnapshot {
  ts: number
  totalLoadMw: number
  totalCapacityMw: number
  availableCapacityMw: number
  reserveMarginPct: number
  renewableMw: number
  renewableSharePct: number
  solarMw: number
  windMw: number
  batteryMw: number
  frequencyHz: number
  averageVoltageDeviationPct: number
  averageHealth: number
  stabilityIndex: number
  loadRatio: number
  state: GridState
  regions: RegionSnapshot[]
  assets: AssetSnapshot[]
  criticalAssets: AssetSnapshot[]
  renewableVariabilityPct: number
  weatherStress: number
  customersServed: number
}

export interface GridOptions {
  perturbation?: Perturbation
  weights?: RiskWeights
  patterns?: DnaPatternRef[]
  environmentOverrides?: EnvironmentOverrides
  /** Number of open incidents — an input to the stability index. */
  activeIncidents?: number
}

/** Environment provider bound to a region and the scenario in force. */
export function environmentFor(
  region: RegionSeed,
  perturbation: Perturbation,
  overrides: EnvironmentOverrides = {},
) {
  const scoped = !perturbation.regionCode || perturbation.regionCode === region.code
  return (ts: number): EnvironmentSample =>
    sampleEnvironment(region, ts, {
      ...overrides,
      tempDeltaC: (overrides.tempDeltaC ?? 0) + (scoped ? (perturbation.tempDeltaC ?? 0) : 0),
    })
}

export function assetAgeYears(profile: AssetProfile, ts: number): number {
  return round(Math.max(0, (ts - profile.installedAt) / MS_PER_YEAR), 2)
}

/** 0 = maintained on schedule, 1 = badly overdue (18 months or more). */
export function maintenanceDebt(profile: AssetProfile, ts: number): number {
  if (!profile.lastMaintenanceAt) return 1
  const days = (ts - profile.lastMaintenanceAt) / 86_400_000
  return round(clamp(days / 540, 0, 1), 4)
}

/**
 * Renewable variability for a region: how much renewable output can swing within an
 * hour either side of now, as a percentage of installed renewable capacity. This is what
 * makes a solar-heavy region genuinely harder to operate.
 */
export function renewableVariability(
  profiles: AssetProfile[],
  ts: number,
  envAt: (ts: number) => EnvironmentSample,
  perturbation: Perturbation,
): number {
  const renewables = profiles.filter((p) => RENEWABLE.has(p.assetTypeKey))
  if (renewables.length === 0) return 0
  const installed = renewables.reduce((sum, p) => sum + p.capacityMw, 0)
  if (installed <= 0) return 0

  const outputAt = (t: number) =>
    renewables.reduce((sum, p) => sum + sampleAsset(p, t, envAt(t), perturbation).loadMw, 0)

  const now = outputAt(ts)
  const past = outputAt(ts - 30 * MS_PER_MIN)
  const future = outputAt(ts + 30 * MS_PER_MIN)
  const swing = Math.max(Math.abs(now - past), Math.abs(future - now))
  return round(clamp((swing / installed) * 100, 0, 100), 2)
}

/**
 * Reduce one asset's live state to its Failure-DNA signature.
 *
 * Exported because the seed script derives the historical DNA library from exactly this
 * function. If the two ever computed the signature differently, the seeded similarity
 * scores would not reproduce at runtime — which is precisely the bug this indirection
 * exists to prevent.
 */
export function extractAssetDna(args: {
  profile: AssetProfile
  ts: number
  sample: TelemetrySample
  env: EnvironmentSample
  envAt: (ts: number) => EnvironmentSample
  perturbation: Perturbation
  neighbourStress: number
  renewableVariabilityPct: number
}): DnaVector {
  const { profile, ts, sample, env, envAt, perturbation } = args
  const previousTs = ts - 60 * MS_PER_MIN
  const previous = sampleAsset(profile, previousTs, envAt(previousTs), perturbation)
  return extractDna({
    voltageDeviationPct: sample.voltageDeviationPct,
    frequencyHz: 60,
    loadPct: sample.loadPct,
    tempC: sample.tempC,
    loadGrowthPpPerHour: sample.loadPct - previous.loadPct,
    renewableVariabilityPct: args.renewableVariabilityPct,
    assetAgeYears: assetAgeYears(profile, ts),
    maintenanceDebt: maintenanceDebt(profile, ts),
    neighbourStress: args.neighbourStress,
    ambientTempC: env.tempC,
  })
}

/**
 * Build the complete grid snapshot for an instant.
 *
 * Deliberately a single pass over the assets followed by two derived passes (neighbour
 * stress, then risk), because risk depends on how an asset's neighbours are doing.
 */
export function buildGridSnapshot(
  profiles: AssetProfile[],
  ts: number,
  options: GridOptions = {},
): GridSnapshot {
  const perturbation = options.perturbation ?? NO_PERTURBATION
  const patterns = options.patterns ?? []

  const envByRegion = new Map<string, EnvironmentSample>()
  const envProviderByRegion = new Map<string, (t: number) => EnvironmentSample>()
  for (const region of REGIONS) {
    const provider = environmentFor(region, perturbation, options.environmentOverrides)
    envProviderByRegion.set(region.code, provider)
    envByRegion.set(region.code, provider(ts))
  }

  // Pass 1 — raw telemetry for every asset.
  const samples = profiles.map((profile) => {
    const env = envByRegion.get(profile.regionCode)!
    return { profile, env, sample: sampleAsset(profile, ts, env, perturbation) }
  })

  // Pass 2 — neighbour stress, measured within each region.
  const stressByRegion = new Map<string, number[]>()
  for (const { profile, sample } of samples) {
    const stress = clamp((Math.abs(sample.loadPct) - 70) / 35, 0, 1)
    const list = stressByRegion.get(profile.regionCode) ?? []
    list.push(stress)
    stressByRegion.set(profile.regionCode, list)
  }
  const meanStressByRegion = new Map<string, number>()
  for (const [code, list] of stressByRegion) {
    meanStressByRegion.set(code, list.reduce((a, b) => a + b, 0) / Math.max(list.length, 1))
  }

  // Renewable variability is a per-region property and a Failure-DNA input, so it has to
  // be known before the risk pass rather than derived from it afterwards.
  const variabilityByRegion = new Map<string, number>()
  for (const region of REGIONS) {
    variabilityByRegion.set(
      region.code,
      renewableVariability(
        profiles.filter((p) => p.regionCode === region.code),
        ts,
        envProviderByRegion.get(region.code)!,
        perturbation,
      ),
    )
  }

  // Pass 3 — Failure DNA match and risk for every asset.
  const assets: AssetSnapshot[] = samples.map(({ profile, env, sample }) => {
    const ageYears = assetAgeYears(profile, ts)
    const debt = maintenanceDebt(profile, ts)
    const neighbourStress = meanStressByRegion.get(profile.regionCode) ?? 0
    const envProvider = envProviderByRegion.get(profile.regionCode)!

    let dnaSimilarity = 0
    let dnaCode: string | null = null
    if (patterns.length > 0) {
      const current = extractAssetDna({
        profile,
        ts,
        sample,
        env,
        envAt: envProvider,
        perturbation,
        neighbourStress,
        renewableVariabilityPct: variabilityByRegion.get(profile.regionCode) ?? 0,
      })
      const [top] = matchDna(current, patterns, 1)
      if (top) {
        dnaSimilarity = top.similarity
        dnaCode = top.pattern.code
      }
    }

    const risk = assessRisk({
      profile,
      sample,
      env,
      dnaSimilarity,
      neighbourStress,
      weights: options.weights,
    })

    return {
      profile,
      sample,
      risk,
      state: riskToState(risk.score),
      dnaSimilarity,
      dnaCode,
      neighbourStress: round(neighbourStress, 4),
      ageYears,
      maintenanceDebt: debt,
    }
  })

  // ── National aggregates ────────────────────────────────────────────────────
  const loadServing = assets.filter((a) => LOAD_SERVING.has(a.profile.assetTypeKey))
  const totalLoadMw = loadServing.reduce((sum, a) => sum + Math.max(0, a.sample.loadMw), 0)

  const supplyAssets = assets.filter((a) => SUPPLY.has(a.profile.assetTypeKey))
  const totalCapacityMw = supplyAssets.reduce((sum, a) => sum + a.profile.capacityMw, 0)
  const solarMw = assets
    .filter((a) => a.profile.assetTypeKey === 'solar_farm')
    .reduce((sum, a) => sum + Math.max(0, a.sample.loadMw), 0)
  const windMw = assets
    .filter((a) => a.profile.assetTypeKey === 'wind_farm')
    .reduce((sum, a) => sum + Math.max(0, a.sample.loadMw), 0)
  const batteryMw = assets
    .filter((a) => a.profile.assetTypeKey === 'battery_storage')
    .reduce((sum, a) => sum + a.sample.loadMw, 0)

  const renewableMw = solarMw + windMw
  const availableCapacityMw = Math.max(0, totalCapacityMw - totalLoadMw)
  const loadRatio = totalCapacityMw > 0 ? totalLoadMw / totalCapacityMw : 0
  const frequencyHz = systemFrequency(totalLoadMw, totalCapacityMw, ts)

  const activeAssets = assets.filter((a) => !a.sample.isOutage)
  const averageVoltageDeviationPct = activeAssets.length
    ? activeAssets.reduce((sum, a) => sum + Math.abs(a.sample.voltageDeviationPct), 0) /
      activeAssets.length
    : 0
  const averageHealth = activeAssets.length
    ? activeAssets.reduce((sum, a) => sum + a.sample.effectiveHealth, 0) / activeAssets.length
    : 100

  // ── Regional aggregates ────────────────────────────────────────────────────
  const regions: RegionSnapshot[] = REGIONS.map((region) => {
    const regionAssets = assets.filter((a) => a.profile.regionCode === region.code)
    const regionLoad = regionAssets
      .filter((a) => LOAD_SERVING.has(a.profile.assetTypeKey))
      .reduce((sum, a) => sum + Math.max(0, a.sample.loadMw), 0)
    const regionSupply = regionAssets
      .filter((a) => SUPPLY.has(a.profile.assetTypeKey))
      .reduce((sum, a) => sum + Math.max(0, a.sample.loadMw), 0)
    const regionCapacity = regionAssets
      .filter((a) => SUPPLY.has(a.profile.assetTypeKey))
      .reduce((sum, a) => sum + a.profile.capacityMw, 0)
    const regionRenewable = regionAssets
      .filter((a) => RENEWABLE.has(a.profile.assetTypeKey))
      .reduce((sum, a) => sum + Math.max(0, a.sample.loadMw), 0)

    const risks = regionAssets.map((a) => a.risk.score)
    const averageRisk = risks.length ? risks.reduce((a, b) => a + b, 0) / risks.length : 0
    const peakRisk = risks.length ? Math.max(...risks) : 0
    const health = regionAssets.filter((a) => !a.sample.isOutage)

    return {
      code: region.code,
      name: region.name,
      nameAr: region.nameAr,
      lat: region.lat,
      lon: region.lon,
      loadMw: round(regionLoad, 1),
      capacityMw: round(regionCapacity, 1),
      supplyMw: round(regionSupply, 1),
      renewableMw: round(regionRenewable, 1),
      renewableSharePct: round(regionLoad > 0 ? (regionRenewable / regionLoad) * 100 : 0, 1),
      reserveMarginPct: round(
        regionCapacity > 0 ? ((regionCapacity - regionLoad) / regionCapacity) * 100 : 0,
        1,
      ),
      averageRisk: round(averageRisk, 1),
      peakRisk: round(peakRisk, 1),
      state: riskToState(peakRisk),
      assetCount: regionAssets.length,
      assetsAtRisk: regionAssets.filter((a) => a.risk.score >= 55).length,
      customersServed: regionAssets.reduce((sum, a) => sum + a.profile.customersServed, 0),
      averageHealth: round(
        health.length
          ? health.reduce((sum, a) => sum + a.sample.effectiveHealth, 0) / health.length
          : 100,
        1,
      ),
      env: envByRegion.get(region.code)!,
      renewableVariabilityPct: variabilityByRegion.get(region.code) ?? 0,
    }
  })

  const renewableVariabilityPct = round(
    regions.reduce((sum, r) => sum + r.renewableVariabilityPct * r.renewableMw, 0) /
      Math.max(renewableMw, 1),
    2,
  )

  const weatherStressValue = round(
    regions.reduce(
      (sum, r) => sum + clamp((r.env.tempC - 36) / 12, 0, 1) * r.loadMw,
      0,
    ) / Math.max(totalLoadMw, 1),
    4,
  )

  const reserveMarginPct = totalCapacityMw > 0 ? ((totalCapacityMw - totalLoadMw) / totalCapacityMw) * 100 : 0

  const stabilityIndex = gridStabilityIndex({
    voltageDeviationPct: averageVoltageDeviationPct,
    frequencyHz,
    loadRatio,
    reserveMarginPct,
    renewableVariabilityPct,
    averageHealth,
    activeIncidents: options.activeIncidents ?? 0,
    weatherStress: weatherStressValue,
  })

  const criticalAssets = [...assets]
    .sort((a, b) => b.risk.score - a.risk.score)
    .filter((a) => a.risk.score >= 45)
    .slice(0, 12)

  return {
    ts,
    totalLoadMw: round(totalLoadMw, 1),
    totalCapacityMw: round(totalCapacityMw, 1),
    availableCapacityMw: round(availableCapacityMw, 1),
    reserveMarginPct: round(reserveMarginPct, 2),
    renewableMw: round(renewableMw, 1),
    renewableSharePct: round(totalLoadMw > 0 ? (renewableMw / totalLoadMw) * 100 : 0, 1),
    solarMw: round(solarMw, 1),
    windMw: round(windMw, 1),
    batteryMw: round(batteryMw, 1),
    frequencyHz,
    averageVoltageDeviationPct: round(averageVoltageDeviationPct, 3),
    averageHealth: round(averageHealth, 1),
    stabilityIndex,
    loadRatio: round(loadRatio, 4),
    state: riskToState(Math.max(...assets.map((a) => a.risk.score), 0)),
    regions,
    assets,
    criticalAssets,
    renewableVariabilityPct,
    weatherStress: weatherStressValue,
    customersServed: assets.reduce((sum, a) => sum + a.profile.customersServed, 0),
  }
}

/** Resources available to relieve a given asset, read off the regional fleet. */
export function resourcesFor(
  profile: AssetProfile,
  siblings: AssetSnapshot[],
): {
  transferCapacityMw: number
  batteryPowerMw: number
  flexibleLoadMw: number
  coolingHeadroom: boolean
  reconfigurable: boolean
} {
  const region = getRegion(profile.regionCode)

  const peers = siblings.filter(
    (s) =>
      s.profile.code !== profile.code &&
      s.profile.regionCode === profile.regionCode &&
      s.profile.assetTypeKey === profile.assetTypeKey &&
      !s.sample.isOutage,
  )
  const transferCapacityMw = round(
    peers.reduce(
      (sum, s) => sum + Math.max(0, (0.82 - Math.abs(s.sample.loadPct) / 100) * s.profile.capacityMw),
      0,
    ),
    1,
  )

  const batteryPowerMw = round(
    siblings
      .filter(
        (s) =>
          s.profile.regionCode === profile.regionCode &&
          s.profile.assetTypeKey === 'battery_storage' &&
          !s.sample.isOutage,
      )
      .reduce((sum, s) => sum + s.profile.capacityMw * 0.6, 0),
    1,
  )

  const assetLoadMw = Math.max(
    0,
    siblings.find((s) => s.profile.code === profile.code)?.sample.loadMw ?? 0,
  )

  return {
    transferCapacityMw,
    batteryPowerMw,
    flexibleLoadMw: round(assetLoadMw * region.flexibleShare, 1),
    coolingHeadroom: profile.coolingType !== 'NONE',
    reconfigurable: profile.assetTypeKey === 'transformer' || profile.assetTypeKey === 'substation',
  }
}

export { LOAD_SERVING, RENEWABLE, SUPPLY, dnaFromRow, perturbationApplies }
