/**
 * Load and renewable forecasting (§17, §18).
 *
 * The forecast is the same digital twin evaluated ahead of now, so the forecast line and
 * the realised line are produced by one model and meet exactly at t=0. Uncertainty bands
 * widen with horizon in the way a real forecast's do — as the square root of lead time —
 * and renewable bands widen further with the weather's own variability.
 */

import { REGIONS, getRegion, type RegionSeed } from '../data/regions'
import { sampleEnvironment, type EnvironmentSample } from './environment'
import {
  type AssetProfile,
  type Perturbation,
  NO_PERTURBATION,
  sampleAsset,
} from './telemetry'
import { clamp, round } from './rng'

const MS_PER_MIN = 60_000

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

export interface ForecastPoint {
  ts: number
  offsetMin: number
  expectedMw: number
  lowerMw: number
  upperMw: number
  confidence: number
}

export interface LoadForecast {
  points: ForecastPoint[]
  peak: ForecastPoint
  capacityMw: number
  minReserveMarginPct: number
  horizonMin: number
}

export interface RenewableForecast extends LoadForecast {
  kind: 'solar' | 'wind'
  variabilityMw: number
}

/**
 * Forecast uncertainty as a fraction of the expected value.
 *
 * Grows with the square root of lead time (a standard property of diffusion-like error
 * growth), starting from a small nowcast error floor.
 */
export function uncertaintyFraction(offsetMin: number, base = 0.012, growth = 0.02): number {
  return base + growth * Math.sqrt(offsetMin / 60)
}

export function confidenceFor(offsetMin: number, base = 0.97, decay = 0.055): number {
  return round(clamp(base - decay * Math.sqrt(offsetMin / 60), 0.45, 0.99), 4)
}

/** Total load-serving demand across a set of assets at an instant. */
export function aggregateLoadMw(
  profiles: AssetProfile[],
  ts: number,
  envFor: (regionCode: string, ts: number) => EnvironmentSample,
  perturbation: Perturbation = NO_PERTURBATION,
): number {
  let total = 0
  for (const profile of profiles) {
    if (!LOAD_SERVING.has(profile.assetTypeKey)) continue
    total += Math.max(0, sampleAsset(profile, ts, envFor(profile.regionCode, ts), perturbation).loadMw)
  }
  return total
}

/** Total supply-side output at an instant, optionally filtered to one asset type. */
export function aggregateSupplyMw(
  profiles: AssetProfile[],
  ts: number,
  envFor: (regionCode: string, ts: number) => EnvironmentSample,
  perturbation: Perturbation = NO_PERTURBATION,
  typeKey?: string,
): number {
  let total = 0
  for (const profile of profiles) {
    if (typeKey ? profile.assetTypeKey !== typeKey : !SUPPLY.has(profile.assetTypeKey)) continue
    total += Math.max(0, sampleAsset(profile, ts, envFor(profile.regionCode, ts), perturbation).loadMw)
  }
  return total
}

function makeEnvProvider(
  perturbation: Perturbation,
): (regionCode: string, ts: number) => EnvironmentSample {
  const cache = new Map<string, EnvironmentSample>()
  return (regionCode, ts) => {
    const key = `${regionCode}:${ts}`
    const cached = cache.get(key)
    if (cached) return cached
    const region = getRegion(regionCode)
    const scoped = !perturbation.regionCode || perturbation.regionCode === regionCode
    const env = sampleEnvironment(region, ts, {
      tempDeltaC: scoped ? (perturbation.tempDeltaC ?? 0) : 0,
    })
    cache.set(key, env)
    return env
  }
}

export interface ForecastOptions {
  horizonMin?: number
  stepMin?: number
  perturbation?: Perturbation
  /** Restrict to one region; omit for the national forecast. */
  regionCode?: string | null
}

export function forecastLoad(
  profiles: AssetProfile[],
  ts: number,
  options: ForecastOptions = {},
): LoadForecast {
  const horizonMin = options.horizonMin ?? 1440
  const stepMin = options.stepMin ?? 60
  const perturbation = options.perturbation ?? NO_PERTURBATION
  const envFor = makeEnvProvider(perturbation)

  const scope = options.regionCode
    ? profiles.filter((p) => p.regionCode === options.regionCode)
    : profiles

  const capacityMw = round(
    scope.filter((p) => SUPPLY.has(p.assetTypeKey)).reduce((sum, p) => sum + p.capacityMw, 0),
    1,
  )

  const points: ForecastPoint[] = []
  for (let offset = 0; offset <= horizonMin; offset += stepMin) {
    const pointTs = ts + offset * MS_PER_MIN
    const expectedMw = aggregateLoadMw(scope, pointTs, envFor, perturbation)
    const band = expectedMw * uncertaintyFraction(offset)
    points.push({
      ts: pointTs,
      offsetMin: offset,
      expectedMw: round(expectedMw, 1),
      lowerMw: round(Math.max(0, expectedMw - band), 1),
      upperMw: round(expectedMw + band, 1),
      confidence: confidenceFor(offset),
    })
  }

  const peak = points.reduce((max, p) => (p.expectedMw > max.expectedMw ? p : max), points[0])
  const minReserveMarginPct = round(
    points.reduce(
      (min, p) =>
        Math.min(min, capacityMw > 0 ? ((capacityMw - p.upperMw) / capacityMw) * 100 : 0),
      Infinity,
    ),
    2,
  )

  return { points, peak, capacityMw, minReserveMarginPct, horizonMin }
}

export function forecastRenewable(
  profiles: AssetProfile[],
  ts: number,
  kind: 'solar' | 'wind',
  options: ForecastOptions = {},
): RenewableForecast {
  const horizonMin = options.horizonMin ?? 1440
  const stepMin = options.stepMin ?? 60
  const perturbation = options.perturbation ?? NO_PERTURBATION
  const envFor = makeEnvProvider(perturbation)
  const typeKey = kind === 'solar' ? 'solar_farm' : 'wind_farm'

  const scope = (
    options.regionCode ? profiles.filter((p) => p.regionCode === options.regionCode) : profiles
  ).filter((p) => p.assetTypeKey === typeKey)

  const capacityMw = round(scope.reduce((sum, p) => sum + p.capacityMw, 0), 1)

  // Wind is intrinsically harder to forecast than clear-sky solar.
  const base = kind === 'solar' ? 0.03 : 0.07
  const growth = kind === 'solar' ? 0.035 : 0.075

  const points: ForecastPoint[] = []
  for (let offset = 0; offset <= horizonMin; offset += stepMin) {
    const pointTs = ts + offset * MS_PER_MIN
    const expectedMw = aggregateSupplyMw(scope, pointTs, envFor, perturbation, typeKey)
    // Band is proportional to installed capacity, not to output, so a night-time zero
    // still carries the honest "could be anything by sunrise" width at long horizons.
    const band = capacityMw * uncertaintyFraction(offset, base, growth) * 0.6 + expectedMw * 0.04
    points.push({
      ts: pointTs,
      offsetMin: offset,
      expectedMw: round(expectedMw, 1),
      lowerMw: round(Math.max(0, expectedMw - band), 1),
      upperMw: round(Math.min(capacityMw, expectedMw + band), 1),
      confidence: confidenceFor(offset, kind === 'solar' ? 0.96 : 0.9, kind === 'solar' ? 0.05 : 0.09),
    })
  }

  const peak = points.reduce((max, p) => (p.expectedMw > max.expectedMw ? p : max), points[0])

  // Variability: the largest hour-to-hour swing anywhere in the horizon.
  let variabilityMw = 0
  for (let i = 1; i < points.length; i += 1) {
    variabilityMw = Math.max(variabilityMw, Math.abs(points[i].expectedMw - points[i - 1].expectedMw))
  }

  return {
    kind,
    points,
    peak,
    capacityMw,
    minReserveMarginPct: 0,
    horizonMin,
    variabilityMw: round(variabilityMw, 1),
  }
}

/**
 * Renewable Confidence Score (§18) — a single 0–100 figure blending forecast confidence
 * at the operational horizon with how much of the fleet's output is currently swinging.
 */
export function renewableConfidenceScore(
  solar: RenewableForecast,
  wind: RenewableForecast,
): number {
  const horizonIndex = Math.min(6, solar.points.length - 1)
  const solarConfidence = solar.points[horizonIndex]?.confidence ?? 0.9
  const windConfidence = wind.points[Math.min(6, wind.points.length - 1)]?.confidence ?? 0.8

  const solarCapacity = Math.max(solar.capacityMw, 1)
  const windCapacity = Math.max(wind.capacityMw, 1)
  const totalCapacity = solarCapacity + windCapacity

  const weighted =
    (solarConfidence * solarCapacity + windConfidence * windCapacity) / totalCapacity

  const swingPenalty = clamp(
    (solar.variabilityMw + wind.variabilityMw) / totalCapacity,
    0,
    0.5,
  )

  return round(clamp((weighted - swingPenalty * 0.35) * 100, 0, 100), 1)
}

/** Per-region peak summary used by the forecast page's regional table. */
export function regionalPeaks(
  profiles: AssetProfile[],
  ts: number,
  options: ForecastOptions = {},
): Array<{ region: RegionSeed; peakMw: number; peakAt: number; capacityMw: number; reserveMarginPct: number }> {
  return REGIONS.map((region) => {
    const forecast = forecastLoad(profiles, ts, { ...options, regionCode: region.code })
    return {
      region,
      peakMw: forecast.peak.expectedMw,
      peakAt: forecast.peak.ts,
      capacityMw: forecast.capacityMw,
      reserveMarginPct: forecast.minReserveMarginPct,
    }
  })
}
