/**
 * Grid resilience (§39–§42).
 *
 * Risk asks whether something is about to go wrong. Resilience asks a harder question:
 * if something *did* go wrong — anything, not the specific thing we happen to be
 * watching — how well would this grid take it? A network can sit at low risk and be
 * brittle, because everything is fine right up until the moment one unit trips and there
 * is nowhere for its load to go.
 *
 * The score is built from eight factors an operator can act on, each normalised against
 * what "good" means for that factor rather than against the fleet's own average — a
 * fleet-relative score would rate a uniformly fragile grid as resilient.
 */

import { clamp, round } from './rng'

export const RESILIENCE_FACTORS = [
  'redundancy',
  'availableCapacity',
  'assetHealth',
  'batteryReserve',
  'loadFlexibility',
  'renewableStability',
  'recoveryOptions',
  'cascadeContainment',
] as const
export type ResilienceFactor = (typeof RESILIENCE_FACTORS)[number]

export const RESILIENCE_WEIGHTS: Record<ResilienceFactor, number> = {
  redundancy: 0.2,
  availableCapacity: 0.17,
  assetHealth: 0.14,
  batteryReserve: 0.1,
  loadFlexibility: 0.1,
  renewableStability: 0.08,
  recoveryOptions: 0.1,
  cascadeContainment: 0.11,
}

export const RESILIENCE_UNITS: Record<ResilienceFactor, string> = {
  redundancy: '%',
  availableCapacity: '%',
  assetHealth: '%',
  batteryReserve: '%',
  loadFlexibility: '%',
  renewableStability: '%',
  recoveryOptions: '%',
  cascadeContainment: '%',
}

export interface ResilienceInput {
  /** Share of load-serving assets whose peers could carry them, 0–1. */
  redundantShare: number
  /** Reserve margin as a percentage of capacity. */
  reserveMarginPct: number
  /** Mean effective health across the fleet, 0–100. */
  meanHealth: number
  /** Battery power available as a share of current demand, 0–1. */
  batteryShare: number
  /** Deferrable demand as a share of current demand, 0–1. */
  flexibleShare: number
  /** Renewable variability, in per cent — higher is worse. */
  renewableVariabilityPct: number
  /** Mean number of distinct restoration paths per load-serving asset. */
  meanRecoveryPaths: number
  /** Share of assets whose failure stays contained, 0–1. */
  containedShare: number
}

export interface ResilienceComponent {
  key: ResilienceFactor
  /** The raw measurement, in the factor's own unit. */
  value: number
  /** Normalised so 100 is "as good as this factor gets", 0–100. */
  normalised: number
  weight: number
  unit: string
}

export type ResilienceBand = 'robust' | 'sound' | 'exposed' | 'brittle'

export interface ResilienceScore {
  score: number
  band: ResilienceBand
  components: ResilienceComponent[]
  /** The factor dragging the score down hardest, which is where to spend effort. */
  weakest: ResilienceComponent | null
  summary: string
  summaryAr: string
}

export function resilienceBand(score: number): ResilienceBand {
  if (score >= 80) return 'robust'
  if (score >= 62) return 'sound'
  if (score >= 42) return 'exposed'
  return 'brittle'
}

const FACTOR_COPY: Record<ResilienceFactor, { en: string; ar: string }> = {
  redundancy: { en: 'redundant cover', ar: 'التغطية الاحتياطية' },
  availableCapacity: { en: 'reserve margin', ar: 'هامش الاحتياطي' },
  assetHealth: { en: 'fleet condition', ar: 'حالة الأسطول' },
  batteryReserve: { en: 'storage reserve', ar: 'احتياطي التخزين' },
  loadFlexibility: { en: 'demand flexibility', ar: 'مرونة الطلب' },
  renewableStability: { en: 'renewable steadiness', ar: 'ثبات المتجددة' },
  recoveryOptions: { en: 'restoration paths', ar: 'مسارات الاستعادة' },
  cascadeContainment: { en: 'cascade containment', ar: 'احتواء الانتشار' },
}

/**
 * Normalisation targets: the value of each factor at which it stops contributing to
 * fragility. A 25 % reserve margin is comfortable; 4 restoration paths per asset is
 * generous; 30 % renewable variability is as volatile as this fleet gets.
 */
function normalise(input: ResilienceInput): Record<ResilienceFactor, { value: number; normalised: number }> {
  return {
    redundancy: {
      value: round(input.redundantShare * 100, 1),
      normalised: round(clamp(input.redundantShare * 100, 0, 100), 1),
    },
    availableCapacity: {
      value: round(input.reserveMarginPct, 1),
      normalised: round(clamp((input.reserveMarginPct / 25) * 100, 0, 100), 1),
    },
    assetHealth: {
      value: round(input.meanHealth, 1),
      normalised: round(clamp(((input.meanHealth - 40) / 55) * 100, 0, 100), 1),
    },
    batteryReserve: {
      value: round(input.batteryShare * 100, 1),
      normalised: round(clamp((input.batteryShare / 0.12) * 100, 0, 100), 1),
    },
    loadFlexibility: {
      value: round(input.flexibleShare * 100, 1),
      normalised: round(clamp((input.flexibleShare / 0.15) * 100, 0, 100), 1),
    },
    renewableStability: {
      value: round(input.renewableVariabilityPct, 1),
      normalised: round(clamp(100 - (input.renewableVariabilityPct / 30) * 100, 0, 100), 1),
    },
    recoveryOptions: {
      value: round(input.meanRecoveryPaths, 2),
      normalised: round(clamp((input.meanRecoveryPaths / 4) * 100, 0, 100), 1),
    },
    cascadeContainment: {
      value: round(input.containedShare * 100, 1),
      normalised: round(clamp(input.containedShare * 100, 0, 100), 1),
    },
  }
}

export function assessResilience(input: ResilienceInput): ResilienceScore {
  const normalised = normalise(input)
  const components: ResilienceComponent[] = RESILIENCE_FACTORS.map((key) => ({
    key,
    value: normalised[key].value,
    normalised: normalised[key].normalised,
    weight: RESILIENCE_WEIGHTS[key],
    unit: RESILIENCE_UNITS[key],
  }))

  const score = round(
    clamp(
      components.reduce((sum, component) => sum + component.normalised * component.weight, 0),
      0,
      100,
    ),
    1,
  )
  const band = resilienceBand(score)

  // The weakest link is the one costing the most *weighted* points, not the one with the
  // lowest raw reading — a poor score on a factor that barely matters is not the problem.
  const weakest =
    [...components].sort(
      (a, b) => (100 - a.normalised) * a.weight - (100 - b.normalised) * b.weight,
    )[components.length - 1] ?? null

  const summary = weakest
    ? `Resilience scores ${score} of 100 (${band}); the largest single weakness is ${FACTOR_COPY[weakest.key].en} at ${weakest.normalised} %.`
    : `Resilience scores ${score} of 100 (${band}).`
  const summaryAr = weakest
    ? `مؤشر الصمود ${score} من ١٠٠، وأكبر نقطة ضعف منفردة هي ${FACTOR_COPY[weakest.key].ar} عند ${weakest.normalised}٪.`
    : `مؤشر الصمود ${score} من ١٠٠.`

  return { score, band, components, weakest, summary, summaryAr }
}

// ─────────────────────────────────────────────────────────────────────────────
// Safe operating envelope (§41)
// ─────────────────────────────────────────────────────────────────────────────

export interface EnvelopeInput {
  currentLoadPct: number
  /** Loading at which the first asset exceeded a limit in the stress sweep, or null. */
  breakingPointDeltaPct: number | null
  ambientC: number
  reserveMarginPct: number
}

export interface SafeEnvelope {
  currentLoadPct: number
  /** The highest loading the sweep found no breach at, with margin held back. */
  maxRecommendedPct: number
  /** The same figure after the day's heat is taken off it. */
  heatAdjustedMaxPct: number
  reserveMarginPct: number
  /** How much of the safe band is still unused, in percentage points. */
  headroomPct: number
  isInsideEnvelope: boolean
}

/**
 * Turn a stress sweep into an operating band.
 *
 * The heat adjustment is the part that matters operationally: a transformer's rating is
 * quoted at a reference ambient, and every degree above it eats headroom the nameplate
 * does not know about. Above 40 °C roughly half a point of loading per degree is a
 * defensible planning rule and errs towards caution.
 */
export function safeOperatingEnvelope(input: EnvelopeInput): SafeEnvelope {
  // Hold 6 points back from wherever the sweep found the first breach: an envelope that
  // ends exactly at the breaking point is not an envelope, it is the cliff edge.
  const ceiling =
    input.breakingPointDeltaPct === null
      ? clamp(input.currentLoadPct + 18, 0, 100)
      : clamp(input.currentLoadPct * (1 + input.breakingPointDeltaPct / 100) - 6, 0, 100)

  const heatPenalty = clamp((input.ambientC - 40) * 0.5, 0, 12)
  const heatAdjusted = clamp(ceiling - heatPenalty, 0, 100)

  return {
    currentLoadPct: round(input.currentLoadPct, 1),
    maxRecommendedPct: round(ceiling, 1),
    heatAdjustedMaxPct: round(heatAdjusted, 1),
    reserveMarginPct: round(input.reserveMarginPct, 1),
    headroomPct: round(heatAdjusted - input.currentLoadPct, 1),
    isInsideEnvelope: input.currentLoadPct <= heatAdjusted,
  }
}

// ─────────────────────────────────────────────────────────────────────────────
// Adversarial search (§38)
// ─────────────────────────────────────────────────────────────────────────────

/** One dimension the adversary is allowed to push. */
export interface AdversarialLever {
  key: string
  label: string
  labelAr: string
  unit: string
  /** Smallest step worth applying. */
  step: number
  max: number
  /** Relative plausibility cost of one unit; the search prefers cheap, ordinary events. */
  costPerUnit: number
}

export interface AdversarialResult {
  /** True when a combination reached the target state. */
  found: boolean
  /** The doses applied, cheapest plausible combination first. */
  doses: Array<{ key: string; label: string; labelAr: string; amount: number; unit: string }>
  /** Metric reached, in the same units the evaluator returns. */
  metric: number
  target: number
  /** Total plausibility cost — a lower number is a more ordinary set of conditions. */
  plausibilityCost: number
  evaluations: number
}

/**
 * Find the *least unusual* set of conditions that pushes the grid past a threshold.
 *
 * This is the inverse of the counterfactual search and answers the question a planner
 * actually loses sleep over: not "what happens if this specific bad thing occurs" but
 * "what is the most ordinary combination of things that would break us". A coordinated
 * one-in-fifty-year event is not interesting. A hot afternoon plus a routine demand rise
 * plus a cloud bank is.
 *
 * Coordinate ascent, cheapest-first: raise the lever with the best metric movement per
 * unit of plausibility cost, one step at a time, until the target is met or the budget
 * runs out.
 */
export function findAdversarialConditions(
  levers: AdversarialLever[],
  evaluate: (doses: Record<string, number>) => number,
  target: number,
  options: { maxSteps?: number; maxCost?: number } = {},
): AdversarialResult {
  const maxSteps = options.maxSteps ?? 40
  const maxCost = options.maxCost ?? Number.POSITIVE_INFINITY

  const doses: Record<string, number> = Object.fromEntries(levers.map((lever) => [lever.key, 0]))
  let evaluations = 0
  let metric = evaluate(doses)
  evaluations += 1
  let cost = 0

  for (let step = 0; step < maxSteps && metric < target && cost < maxCost; step += 1) {
    let best: { lever: AdversarialLever; metric: number; gainPerCost: number } | null = null

    for (const lever of levers) {
      if (doses[lever.key] + lever.step > lever.max) continue
      const trial = { ...doses, [lever.key]: doses[lever.key] + lever.step }
      evaluations += 1
      const trialMetric = evaluate(trial)
      const gain = trialMetric - metric
      if (gain <= 0.001) continue
      const gainPerCost = gain / Math.max(lever.step * lever.costPerUnit, 0.001)
      if (!best || gainPerCost > best.gainPerCost) {
        best = { lever, metric: trialMetric, gainPerCost }
      }
    }

    if (!best) break
    doses[best.lever.key] += best.lever.step
    cost += best.lever.step * best.lever.costPerUnit
    metric = best.metric
  }

  return {
    found: metric >= target,
    doses: levers
      .filter((lever) => doses[lever.key] > 0)
      .map((lever) => ({
        key: lever.key,
        label: lever.label,
        labelAr: lever.labelAr,
        amount: round(doses[lever.key], 2),
        unit: lever.unit,
      })),
    metric: round(metric, 2),
    target,
    plausibilityCost: round(cost, 2),
    evaluations,
  }
}
