/**
 * Red team / blue team resilience (§24, §25, §26).
 *
 * Not a security tool. The red team here is an operational stress generator — heat,
 * demand, lost generation, lost plant — and the blue team is a response search over the
 * same levers the self-healing lab already uses.
 *
 * The value is in running them as a pair. A stress test on its own tells you the grid
 * bends; a stress test with a response search tells you whether it can be *straightened*,
 * how fast, and at what cost. Those are different facts, and a resilience programme that
 * only measures the first one is measuring the easy half.
 *
 * One deliberate constraint: the blue team may only use resources the red team's own
 * scenario leaves available. A response that discharges a battery the scenario has just
 * taken out of service is not a response, and allowing it would make every stress test
 * look survivable.
 */

import { clamp, round } from './rng'
import type { AppliedAction, Perturbation } from './telemetry'

// ─────────────────────────────────────────────────────────────────────────────
// Red team
// ─────────────────────────────────────────────────────────────────────────────

export const STRESS_KINDS = [
  'heat',
  'demand',
  'renewable',
  'outage',
  'battery',
  'maintenance',
  'comms',
  'sensors',
  'combined',
] as const
export type StressKind = (typeof STRESS_KINDS)[number]

export interface StressDefinition {
  kind: StressKind
  title: string
  titleAr: string
  description: string
  descriptionAr: string
  /** Conditions applied to the twin. */
  perturbation: Perturbation
  /**
   * Degradation that is not a perturbation of physics but of *knowledge*: lost telemetry
   * does not change the grid, it changes what can be seen of it. Modelled as a hit to
   * data confidence rather than to load.
   */
  observabilityLossPct: number
  /** Resources the scenario removes, which the blue team may then not use. */
  removes: Array<'battery' | 'transfer' | 'flexible' | 'cooling'>
}

export function stressLibrary(): StressDefinition[] {
  return [
    {
      kind: 'heat',
      title: 'Extreme heat',
      titleAr: 'حرارة قصوى',
      description: 'Ambient 8 °C above forecast across the region for the afternoon peak.',
      descriptionAr: 'الحرارة المحيطة أعلى بـ٨° من التوقع خلال ذروة بعد الظهر.',
      perturbation: { tempDeltaC: 8 },
      observabilityLossPct: 0,
      removes: ['cooling'],
    },
    {
      kind: 'demand',
      title: 'Demand surge',
      titleAr: 'اندفاع الطلب',
      description: 'Demand 16 % above forecast, arriving over twenty minutes.',
      descriptionAr: 'الطلب أعلى بـ١٦٪ من التوقع، يصل خلال عشرين دقيقة.',
      perturbation: { loadDeltaPct: 16, rampMinutes: 20 },
      observabilityLossPct: 0,
      removes: [],
    },
    {
      kind: 'renewable',
      title: 'Renewable collapse',
      titleAr: 'انهيار المتجددة',
      description: 'Solar down 45 % and wind down 30 % as cloud and calm arrive together.',
      descriptionAr: 'انخفاض الطاقة الشمسية ٤٥٪ والرياح ٣٠٪ مع وصول الغيوم والهدوء معًا.',
      perturbation: { solarDeltaPct: -45, windDeltaPct: -30 },
      observabilityLossPct: 0,
      removes: [],
    },
    {
      kind: 'outage',
      title: 'Multiple asset loss',
      titleAr: 'فقد أصول متعددة',
      description: 'Two transformers out of service on the same bus.',
      descriptionAr: 'خروج محولين عن الخدمة على القضيب نفسه.',
      perturbation: { lineOutage: false },
      observabilityLossPct: 0,
      removes: ['transfer'],
    },
    {
      kind: 'battery',
      title: 'Storage unavailable',
      titleAr: 'تعذّر توفر التخزين',
      description: 'Regional storage offline for maintenance during the peak window.',
      descriptionAr: 'التخزين الإقليمي خارج الخدمة للصيانة خلال نافذة الذروة.',
      perturbation: { batteryOutage: true },
      observabilityLossPct: 0,
      removes: ['battery'],
    },
    {
      kind: 'maintenance',
      title: 'Maintenance outage during peak',
      titleAr: 'انقطاع صيانة خلال الذروة',
      description: 'A planned outage overruns into the evening peak.',
      descriptionAr: 'انقطاع مخطط يمتد إلى ذروة المساء.',
      perturbation: { loadDeltaPct: 6, lineOutage: true },
      observabilityLossPct: 0,
      removes: ['transfer'],
    },
    {
      kind: 'comms',
      title: 'Communication loss',
      titleAr: 'فقد الاتصالات',
      description: 'Telemetry from one substation stops arriving. The grid is unchanged; what can be seen of it is not.',
      descriptionAr: 'توقف وصول القياس من محطة واحدة. الشبكة لم تتغير، لكن ما يمكن رؤيته منها تغيّر.',
      perturbation: {},
      observabilityLossPct: 38,
      removes: [],
    },
    {
      kind: 'sensors',
      title: 'Sensor degradation',
      titleAr: 'تدهور الحساسات',
      description: 'A third of thermal sensors drift out of calibration.',
      descriptionAr: 'ثلث الحساسات الحرارية تخرج عن المعايرة.',
      perturbation: {},
      observabilityLossPct: 22,
      removes: [],
    },
    {
      kind: 'combined',
      title: 'Compound summer stress',
      titleAr: 'ضغط صيفي مركّب',
      description: 'Heat, demand and a solar shortfall arriving within the same hour.',
      descriptionAr: 'الحرارة والطلب ونقص الطاقة الشمسية في الساعة نفسها.',
      perturbation: { tempDeltaC: 6, loadDeltaPct: 12, solarDeltaPct: -20, rampMinutes: 25 },
      observabilityLossPct: 8,
      removes: ['cooling'],
    },
  ]
}

export function stressByKind(kind: StressKind): StressDefinition {
  return stressLibrary().find((s) => s.kind === kind) ?? stressLibrary()[stressLibrary().length - 1]
}

// ─────────────────────────────────────────────────────────────────────────────
// Blue team
// ─────────────────────────────────────────────────────────────────────────────

export interface BlueTeamResources {
  transferCapacityMw: number
  batteryPowerMw: number
  flexibleLoadMw: number
  coolingHeadroom: boolean
}

export interface BlueTeamCandidate {
  key: string
  label: string
  labelAr: string
  actions: AppliedAction[]
  /** How long the response takes to have its full effect, in minutes. */
  durationMin: number
  /** Relative cost of executing it, 0–100. */
  costIndex: number
}

/**
 * Execution characteristics per response type.
 *
 * These mirror the action library the self-healing lab uses, so "storage discharge takes
 * 45 minutes" is the same claim on both pages. A response with no duration would rank
 * first on speed for free, which is how a comparison stops meaning anything.
 */
const RESPONSE_PROFILE: Record<string, { durationMin: number; costPerMw: number; baseCost: number }> = {
  transfer: { durationMin: 12, costPerMw: 950, baseCost: 0 },
  storage: { durationMin: 45, costPerMw: 540, baseCost: 0 },
  flexible: { durationMin: 60, costPerMw: 1_400, baseCost: 0 },
  cooling: { durationMin: 5, costPerMw: 0, baseCost: 800 },
  combined: { durationMin: 45, costPerMw: 900, baseCost: 800 },
}

function profileFor(key: string, mw: number): { durationMin: number; costIndex: number } {
  const profile = RESPONSE_PROFILE[key] ?? RESPONSE_PROFILE.transfer
  const sar = profile.baseCost + mw * profile.costPerMw
  return { durationMin: profile.durationMin, costIndex: round(clamp(sar / 400, 0, 100), 1) }
}

/**
 * Strip the resources the stress scenario has taken away.
 *
 * Returned as a new object; the caller's resource record is what the rest of the
 * platform reads and must not be mutated by a what-if.
 */
export function resourcesUnderStress(
  resources: BlueTeamResources,
  stress: StressDefinition,
): BlueTeamResources {
  const out = { ...resources }
  if (stress.removes.includes('battery')) out.batteryPowerMw = 0
  if (stress.removes.includes('cooling')) out.coolingHeadroom = false
  // A scenario that takes plant out of service takes its share of the neighbouring spare
  // capacity with it — that is the whole point of an N-2 test.
  if (stress.removes.includes('transfer')) out.transferCapacityMw = round(out.transferCapacityMw * 0.45, 2)
  if (stress.removes.includes('flexible')) out.flexibleLoadMw = 0
  return out
}

/**
 * The response set the blue team searches over.
 *
 * Sized from what is actually available rather than from a fixed table, so a scenario
 * that removed the battery simply produces no battery strategies instead of producing
 * one that cannot be executed.
 */
export function blueTeamCandidates(
  assetCode: string,
  resources: BlueTeamResources,
  deficitMw: number,
): BlueTeamCandidate[] {
  const need = Math.max(0.5, deficitMw)
  const out: BlueTeamCandidate[] = []

  const transfer = Math.min(resources.transferCapacityMw, need)
  const battery = Math.min(resources.batteryPowerMw, need)
  const flexible = Math.min(resources.flexibleLoadMw, need)

  if (transfer > 0.4) {
    out.push({
      key: 'transfer',
      label: 'Load transfer to adjacent feeders',
      labelAr: 'نقل الحمل إلى المغذيات المجاورة',
      actions: [{ type: 'load_transfer', assetCode, magnitudeMw: round(transfer, 2) }],
      ...profileFor('transfer', transfer),
    })
  }
  if (battery > 0.4) {
    out.push({
      key: 'storage',
      label: 'Storage discharge',
      labelAr: 'تفريغ التخزين',
      actions: [{ type: 'battery_discharge', assetCode, magnitudeMw: round(battery, 2) }],
      ...profileFor('storage', battery),
    })
  }
  if (flexible > 0.4) {
    out.push({
      key: 'flexible',
      label: 'Flexible demand deferral',
      labelAr: 'تأجيل الطلب المرن',
      actions: [{ type: 'demand_response', assetCode, magnitudeMw: round(flexible, 2) }],
      ...profileFor('flexible', flexible),
    })
  }
  if (resources.coolingHeadroom) {
    out.push({
      key: 'cooling',
      label: 'Forced cooling to maximum',
      labelAr: 'رفع التبريد القسري إلى أقصاه',
      actions: [{ type: 'cooling_boost', assetCode, magnitudeMw: 0 }],
      ...profileFor('cooling', 0),
    })
  }

  // The combined response: split the deficit across whatever is left, which is what an
  // operator actually does under compound stress rather than leaning on one lever.
  if (out.length >= 2) {
    const share = need / Math.min(3, out.length)
    const combined: AppliedAction[] = []
    if (transfer > 0.4) combined.push({ type: 'load_transfer', assetCode, magnitudeMw: round(Math.min(transfer, share), 2) })
    if (battery > 0.4) combined.push({ type: 'battery_discharge', assetCode, magnitudeMw: round(Math.min(battery, share), 2) })
    if (flexible > 0.4) combined.push({ type: 'demand_response', assetCode, magnitudeMw: round(Math.min(flexible, share), 2) })
    if (resources.coolingHeadroom) combined.push({ type: 'cooling_boost', assetCode, magnitudeMw: 0 })
    out.push({
      key: 'combined',
      label: 'Combined response',
      labelAr: 'استجابة مركّبة',
      actions: combined,
      // A combined response is bounded by its slowest component, not by their sum: the
      // levers are pulled together, not in sequence.
      ...profileFor('combined', combined.reduce((sum, a) => sum + Math.abs(a.magnitudeMw), 0)),
    })
  }

  return out
}

// ─────────────────────────────────────────────────────────────────────────────
// The comparison (§26)
// ─────────────────────────────────────────────────────────────────────────────

export interface BlueTeamOutcome {
  candidate: BlueTeamCandidate
  resilienceAfter: number
  riskAfter: number
  recoveryMin: number
  costIndex: number
  customersImpacted: number
}

export interface RedBlueComparison {
  stress: StressDefinition
  resilienceBefore: number
  resilienceDuring: number
  riskBefore: number
  riskDuring: number
  /** Best response, or null when nothing available improved the outcome. */
  best: BlueTeamOutcome | null
  ranked: Array<BlueTeamOutcome & { rank: number; isRecommended: boolean }>
  /** Resilience recovered, in points, by the recommended response. */
  recovered: number
  narrative: string
  narrativeAr: string
}

/**
 * Rank the responses and produce the headline.
 *
 * The ranking is not "highest resilience wins". A response that recovers two more points
 * of resilience while taking three times as long and impacting four times the customers
 * is not the better answer, and §25 asks for the *best* response rather than the
 * strongest one.
 */
export function compareRedBlue(input: {
  stress: StressDefinition
  resilienceBefore: number
  resilienceDuring: number
  riskBefore: number
  riskDuring: number
  outcomes: BlueTeamOutcome[]
}): RedBlueComparison {
  const { stress, resilienceBefore, resilienceDuring, riskBefore, riskDuring, outcomes } = input

  const improving = outcomes.filter((o) => o.resilienceAfter > resilienceDuring + 0.5)

  const score = (o: BlueTeamOutcome) => {
    const gain = clamp((o.resilienceAfter - resilienceDuring) / 40, 0, 1)
    const riskDrop = clamp((riskDuring - o.riskAfter) / 60, 0, 1)
    const speed = 1 - clamp(o.recoveryMin / 90, 0, 1)
    const cheap = 1 - clamp(o.costIndex / 100, 0, 1)
    const gentle = 1 - clamp(o.customersImpacted / 50_000, 0, 1)
    return gain * 0.34 + riskDrop * 0.28 + speed * 0.16 + cheap * 0.12 + gentle * 0.1
  }

  const ranked = [...improving]
    .sort((a, b) => score(b) - score(a))
    .map((o, index) => ({ ...o, rank: index + 1, isRecommended: index === 0 }))

  const best = ranked[0] ?? null
  const recovered = best ? round(best.resilienceAfter - resilienceDuring, 1) : 0

  const narrative = best
    ? `${stress.title} takes resilience from ${round(resilienceBefore, 0)} to ${round(resilienceDuring, 0)}. ${best.candidate.label} recovers it to ${round(best.resilienceAfter, 0)} in ${best.recoveryMin} minutes, with risk falling from ${round(riskDuring, 0)} % to ${round(best.riskAfter, 0)} %.`
    : `${stress.title} takes resilience from ${round(resilienceBefore, 0)} to ${round(resilienceDuring, 0)}. No response available under this scenario recovered it — the resources it needs are the ones the scenario removed.`

  const narrativeAr = best
    ? `${stress.titleAr} تخفض المرونة من ${round(resilienceBefore, 0)} إلى ${round(resilienceDuring, 0)}. ${best.candidate.labelAr} تعيدها إلى ${round(best.resilienceAfter, 0)} خلال ${best.recoveryMin} دقيقة، مع انخفاض الخطر من ${round(riskDuring, 0)}٪ إلى ${round(best.riskAfter, 0)}٪.`
    : `${stress.titleAr} تخفض المرونة من ${round(resilienceBefore, 0)} إلى ${round(resilienceDuring, 0)}. لا استجابة متاحة ضمن هذا السيناريو استعادتها — الموارد المطلوبة هي نفسها التي أزالها السيناريو.`

  return {
    stress,
    resilienceBefore: round(resilienceBefore, 1),
    resilienceDuring: round(resilienceDuring, 1),
    riskBefore: round(riskBefore, 1),
    riskDuring: round(riskDuring, 1),
    best,
    ranked,
    recovered,
    narrative,
    narrativeAr,
  }
}
