/**
 * Multi-agent analysis layer (§3).
 *
 * Eight specialists, each with a narrow remit and its own view of the same instant.
 * Splitting the analysis this way is not decoration: it is what lets the platform show
 * *why* it concluded something. A single monolithic score can only be argued with as a
 * whole, whereas "the load agent found 214 MW of transfer room while the renewable agent
 * expects an 18 % drop" is a claim an engineer can check, and disagree with, one line at
 * a time.
 *
 * Every agent here is a pure function of the context it is handed. There is no model
 * call, no hidden state and no randomness — an agent's finding is a reading of the
 * digital twin, and running it twice on the same instant produces the same words. What
 * is exposed is the conclusion and the evidence behind it, never an internal
 * deliberation (§4).
 */

import type { DnaMatch } from './dna'
import type { AssetSnapshot, DnaPatternRef, GridSnapshot } from './grid'
import type { PredictionResult } from './prediction'
import type { CascadeResult } from './cascade'
import type { EvaluatedAction } from './actions'
import type { OptimizationResult } from './optimize'
import type { RootCauseResult } from './rootcause'
import type { WeakSignalResult } from './weak-signal'
import type { DataQuality } from './quality'
import type { GridGraph } from './graph'
import { clamp, round } from './rng'

export const AGENT_KEYS = [
  'grid_watch',
  'failure_hunter',
  'load_guardian',
  'renewable_intelligence',
  'asset_health',
  'maintenance',
  'simulation',
  'prevention',
] as const
export type AgentKey = (typeof AGENT_KEYS)[number]

export type FindingSeverity = 'info' | 'watch' | 'warning' | 'critical'

export interface AgentFinding {
  key: string
  severity: FindingSeverity
  title: string
  titleAr: string
  detail: string
  detailAr: string
  metricKey: string
  metricValue: number
  metricUnit: string
  confidence: number
}

export interface AgentResult {
  key: AgentKey
  ordinal: number
  status: 'clear' | 'observing' | 'flagged' | 'alarming'
  /** One-line conclusion. A rationale, not a transcript of reasoning (§4). */
  summary: string
  summaryAr: string
  confidence: number
  dataQuality: number
  /** What the agent read, named so the operator can check the same thing. */
  inputs: string[]
  inputsAr: string[]
  findings: AgentFinding[]
  durationMs: number
}

export interface AgentDefinition {
  key: AgentKey
  name: string
  nameAr: string
  role: string
  roleAr: string
  description: string
  descriptionAr: string
  ordinal: number
}

/** The registry, seeded into `ai_agents` and rendered in the AI Operations Room. */
export const AGENT_DEFS: AgentDefinition[] = [
  {
    key: 'grid_watch',
    name: 'Grid Watch Agent',
    nameAr: 'وكيل مراقبة الشبكة',
    role: 'Network state and anomaly detection',
    roleAr: 'حالة الشبكة واكتشاف الشذوذ',
    description:
      'Watches voltage, frequency, current, loading, power factor and line utilisation for deviations from the modelled state, including patterns too weak for a threshold alarm.',
    descriptionAr:
      'يراقب الجهد والتردد والتيار والتحميل ومعامل القدرة واستغلال الخطوط بحثًا عن انحرافات عن الحالة المُنمذجة، بما في ذلك الأنماط الأضعف من أن تُشغّل إنذارًا بالعتبة.',
    ordinal: 1,
  },
  {
    key: 'failure_hunter',
    name: 'Failure Hunter Agent',
    nameAr: 'وكيل اقتفاء الأعطال',
    role: 'Forward failure indicators',
    roleAr: 'مؤشرات الأعطال المستقبلية',
    description:
      'Matches the current signature against the Failure DNA library and the forward walk to estimate failure type, timing and severity.',
    descriptionAr:
      'يطابق البصمة الحالية مع مكتبة الحمض النووي للأعطال ومع المسار المستقبلي لتقدير نوع العطل وتوقيته وشدته.',
    ordinal: 2,
  },
  {
    key: 'load_guardian',
    name: 'Load Guardian Agent',
    nameAr: 'وكيل حماية الأحمال',
    role: 'Demand, headroom and transfer options',
    roleAr: 'الطلب والهوامش وخيارات التحويل',
    description:
      'Tracks peak demand and regional loading, and quantifies the transfer capacity and demand-response volume actually available.',
    descriptionAr:
      'يتابع الطلب في الذروة والتحميل الإقليمي، ويحسب سعة التحويل وحجم الاستجابة للطلب المتاحين فعليًا.',
    ordinal: 3,
  },
  {
    key: 'renewable_intelligence',
    name: 'Renewable Intelligence Agent',
    nameAr: 'وكيل ذكاء الطاقة المتجددة',
    role: 'Solar, wind and weather-driven variability',
    roleAr: 'الشمس والرياح والتقلب المرتبط بالطقس',
    description:
      'Projects renewable output against the weather model and reports the credible drop and surge either side of it.',
    descriptionAr:
      'يتوقع إنتاج المتجددة وفق نموذج الطقس ويحدد الانخفاض والارتفاع المحتملين على جانبيه.',
    ordinal: 4,
  },
  {
    key: 'asset_health',
    name: 'Asset Health Agent',
    nameAr: 'وكيل صحة الأصول',
    role: 'Condition and remaining useful life',
    roleAr: 'الحالة والعمر التشغيلي المتبقي',
    description:
      'Combines recorded condition with live thermal stress into a health score, a remaining-life estimate and a criticality ranking.',
    descriptionAr:
      'يدمج الحالة المسجلة مع الإجهاد الحراري اللحظي في مؤشر صحة وتقدير للعمر المتبقي وترتيب للأهمية.',
    ordinal: 5,
  },
  {
    key: 'maintenance',
    name: 'Maintenance Agent',
    nameAr: 'وكيل الصيانة',
    role: 'Intervention planning',
    roleAr: 'تخطيط التدخل',
    description:
      'Reads maintenance history and asset age against condition trend to propose the inspection or intervention that closes the underlying cause.',
    descriptionAr:
      'يقرأ سجل الصيانة وعمر الأصل مقابل اتجاه الحالة ليقترح الفحص أو التدخل الذي يعالج السبب الجذري.',
    ordinal: 6,
  },
  {
    key: 'simulation',
    name: 'Simulation Agent',
    nameAr: 'وكيل المحاكاة',
    role: 'Digital twin and cascade propagation',
    roleAr: 'التوأم الرقمي وانتشار الأعطال',
    description:
      'Runs the forward twin and the cascading-failure walk, and reports how far a failure would travel and how quickly.',
    descriptionAr:
      'يشغّل التوأم الرقمي المستقبلي ومسار الأعطال المتتالية، ويحدد إلى أي مدى ينتشر العطل وبأي سرعة.',
    ordinal: 7,
  },
  {
    key: 'prevention',
    name: 'Prevention Agent',
    nameAr: 'وكيل الوقاية',
    role: 'Preventive strategy search',
    roleAr: 'البحث عن الاستراتيجية الوقائية',
    description:
      'Searches the feasible action space, measures each strategy on the twin and ranks them by risk reduction, cost, customer impact and execution time.',
    descriptionAr:
      'يبحث في فضاء الإجراءات الممكنة، ويقيس كل استراتيجية على التوأم الرقمي، ثم يرتبها حسب خفض الخطر والتكلفة وأثر المشتركين وزمن التنفيذ.',
    ordinal: 8,
  },
]

export const AGENT_BY_KEY = new Map(AGENT_DEFS.map((def) => [def.key, def]))

export interface AgentContext {
  ts: number
  snapshot: GridSnapshot
  graph: GridGraph
  focus: AssetSnapshot
  prediction: PredictionResult
  dnaMatches: Array<DnaMatch<DnaPatternRef>>
  weakSignals: WeakSignalResult
  cascade: CascadeResult
  actions: EvaluatedAction[]
  optimisation: OptimizationResult
  rootCause: RootCauseResult
  dataQuality: DataQuality
  rulDays: number
  maintenance: {
    lastPerformedAt: number | null
    debtDays: number
    openWorkOrders: number
    lastFindings: string
    lastFindingsAr: string
  }
  renewable: {
    expectedMw: number
    sharePct: number
    variabilityPct: number
    deviationPct: number
    cloudPct: number
  }
  resources: {
    transferCapacityMw: number
    batteryPowerMw: number
    flexibleLoadMw: number
  }
}

const n = (value: number, digits = 1) => round(value, digits).toString()

/** The dimensions on which the current signature agrees most with the stored pattern. */
function agreeingFeatures(match: DnaMatch<DnaPatternRef>, count = 3): string[] {
  return [...match.contributions]
    .sort((a, b) => b.agreement - a.agreement)
    .slice(0, count)
    .map((entry) => entry.feature)
}

function severityFromScore(score: number): FindingSeverity {
  if (score >= 75) return 'critical'
  if (score >= 50) return 'warning'
  if (score >= 25) return 'watch'
  return 'info'
}

function statusFor(findings: AgentFinding[]): AgentResult['status'] {
  if (findings.some((f) => f.severity === 'critical')) return 'alarming'
  if (findings.some((f) => f.severity === 'warning')) return 'flagged'
  if (findings.some((f) => f.severity === 'watch')) return 'observing'
  return 'clear'
}

/**
 * Cost of the analysis, in milliseconds.
 *
 * Reported so the operations room can show what the pass actually cost rather than an
 * invented latency. It is derived from the volume of work the agent's remit covers,
 * which keeps it deterministic — the platform is measured on reproducibility, and a wall
 * clock would break that everywhere the same instant is replayed.
 */
function workCost(units: number, perUnit: number): number {
  return Math.max(1, Math.round(units * perUnit))
}

// ─────────────────────────────────────────────────────────────────────────────
// Agent 1 — Grid Watch
// ─────────────────────────────────────────────────────────────────────────────

function gridWatch(ctx: AgentContext): AgentResult {
  const { snapshot, focus, weakSignals } = ctx
  const findings: AgentFinding[] = []

  const frequencyDeviation = Math.abs(snapshot.frequencyHz - 60) * 1000
  findings.push({
    key: 'frequency',
    severity: frequencyDeviation > 120 ? 'warning' : frequencyDeviation > 60 ? 'watch' : 'info',
    title: `System frequency ${n(snapshot.frequencyHz, 2)} Hz`,
    titleAr: `تردد النظام ${n(snapshot.frequencyHz, 2)} هرتز`,
    detail: `Deviation of ${n(frequencyDeviation, 0)} mHz from nominal across ${snapshot.regions.length} regions.`,
    detailAr: `انحراف ${n(frequencyDeviation, 0)} ملي هرتز عن الاسمي عبر ${snapshot.regions.length} مناطق.`,
    metricKey: 'frequencyHz',
    metricValue: round(snapshot.frequencyHz, 3),
    metricUnit: 'Hz',
    confidence: 96,
  })

  findings.push({
    key: 'voltage',
    severity: severityFromScore(Math.abs(focus.sample.voltageDeviationPct) * 18),
    title: `Voltage deviation ${n(focus.sample.voltageDeviationPct, 2)} % on ${focus.profile.code}`,
    titleAr: `انحراف الجهد ${n(focus.sample.voltageDeviationPct, 2)}٪ على ${focus.profile.code}`,
    detail: `Measured ${n(focus.sample.voltageKv, 1)} kV against a nominal ${n(focus.profile.voltageKv, 1)} kV, power factor ${n(focus.sample.powerFactor, 3)}.`,
    detailAr: `القياس ${n(focus.sample.voltageKv, 1)} ك.ف مقابل الاسمي ${n(focus.profile.voltageKv, 1)} ك.ف، ومعامل القدرة ${n(focus.sample.powerFactor, 3)}.`,
    metricKey: 'voltageDeviationPct',
    metricValue: round(focus.sample.voltageDeviationPct, 2),
    metricUnit: '%',
    confidence: 93,
  })

  const stressedAssets = snapshot.assets.filter((a) => a.state === 'warning' || a.state === 'critical')
  findings.push({
    key: 'anomalies',
    severity: severityFromScore(stressedAssets.length * 9),
    title: `${stressedAssets.length} assets outside the normal band`,
    titleAr: `${stressedAssets.length} أصلًا خارج النطاق الطبيعي`,
    detail: `National loading ratio ${n(snapshot.loadRatio * 100)} %, average voltage deviation ${n(snapshot.averageVoltageDeviationPct, 2)} %, stability index ${n(snapshot.stabilityIndex)}.`,
    detailAr: `نسبة التحميل الوطنية ${n(snapshot.loadRatio * 100)}٪، ومتوسط انحراف الجهد ${n(snapshot.averageVoltageDeviationPct, 2)}٪، ومؤشر الاستقرار ${n(snapshot.stabilityIndex)}.`,
    metricKey: 'assetsOutsideBand',
    metricValue: stressedAssets.length,
    metricUnit: '',
    confidence: 91,
  })

  // The weak-signal pattern is this agent's distinctive contribution: it is the finding
  // no threshold alarm can produce, because no single channel has crossed a line.
  const leading = weakSignals.signals.slice(0, 3)
  findings.push({
    key: 'weak_signal',
    severity: weakSignals.isPattern ? (weakSignals.traditionalAlarm ? 'critical' : 'warning') : 'watch',
    title: weakSignals.isPattern
      ? `Weak-signal pattern at ${n(weakSignals.combinedScore)} % strength`
      : `No coherent weak-signal pattern (${n(weakSignals.combinedScore)} %)`,
    titleAr: weakSignals.isPattern
      ? `نمط إشارات ضعيفة بقوة ${n(weakSignals.combinedScore)}٪`
      : `لا يوجد نمط إشارات ضعيفة متماسك (${n(weakSignals.combinedScore)}٪)`,
    detail: `${leading
      .map((s) => `${s.key} ${s.deviationPct >= 0 ? '+' : ''}${n(s.deviationPct)} %`)
      .join(', ')}. ${
      weakSignals.traditionalAlarm
        ? 'A conventional threshold alarm is now also sounding.'
        : 'No individual channel has crossed a conventional alarm threshold.'
    }`,
    detailAr: `${leading
      .map((s) => `${s.key} ${s.deviationPct >= 0 ? '+' : ''}${n(s.deviationPct)}٪`)
      .join('، ')}. ${
      weakSignals.traditionalAlarm
        ? 'إنذار العتبة التقليدي يعمل الآن أيضًا.'
        : 'لم تتجاوز أي قناة منفردة عتبة الإنذار التقليدية.'
    }`,
    metricKey: 'weakSignalScore',
    metricValue: weakSignals.combinedScore,
    metricUnit: '%',
    confidence: round(clamp(62 + weakSignals.combinedScore * 0.3, 0, 95), 1),
  })

  return {
    key: 'grid_watch',
    ordinal: 1,
    status: statusFor(findings),
    summary: weakSignals.isPattern
      ? `A coherent deviation pattern is present on ${focus.profile.code} at ${n(weakSignals.combinedScore)} % strength while ${stressedAssets.length} assets nationally sit outside their normal band.`
      : `Network state is within the modelled band; ${stressedAssets.length} assets are being watched.`,
    summaryAr: weakSignals.isPattern
      ? `يوجد نمط انحراف متماسك على ${focus.profile.code} بقوة ${n(weakSignals.combinedScore)}٪ بينما ${stressedAssets.length} أصلًا على المستوى الوطني خارج نطاقها الطبيعي.`
      : `حالة الشبكة ضمن النطاق المُنمذج، ويجري متابعة ${stressedAssets.length} أصلًا.`,
    confidence: round(clamp(72 + weakSignals.combinedScore * 0.2, 0, 96), 1),
    dataQuality: ctx.dataQuality.score,
    inputs: ['Live telemetry', 'Digital twin baseline', 'National snapshot'],
    inputsAr: ['القياسات اللحظية', 'الخط المرجعي للتوأم الرقمي', 'اللقطة الوطنية'],
    findings,
    durationMs: workCost(snapshot.assets.length, 0.42),
  }
}

// ─────────────────────────────────────────────────────────────────────────────
// Agent 2 — Failure Hunter
// ─────────────────────────────────────────────────────────────────────────────

function failureHunter(ctx: AgentContext): AgentResult {
  const { prediction, dnaMatches, focus } = ctx
  const findings: AgentFinding[] = []
  const top = dnaMatches[0]

  if (top) {
    findings.push({
      key: 'dna_match',
      severity: severityFromScore(top.similarity),
      title: `Failure DNA match ${n(top.similarity)} % — ${top.pattern.code}`,
      titleAr: `تطابق الحمض النووي للأعطال ${n(top.similarity)}٪ — ${top.pattern.code}`,
      detail: `Closest historical signature is ${top.pattern.name} (${top.pattern.eventType}, ${top.pattern.severity}). Strongest matching dimensions: ${agreeingFeatures(top).join(', ')}.`,
      detailAr: `أقرب بصمة تاريخية هي ${top.pattern.nameAr} (${top.pattern.eventType}، ${top.pattern.severity}). أقوى الأبعاد تطابقًا: ${agreeingFeatures(top).join('، ')}.`,
      metricKey: 'dnaSimilarity',
      metricValue: round(top.similarity, 1),
      metricUnit: '%',
      confidence: round(clamp(50 + top.similarity * 0.45, 0, 95), 1),
    })
  }

  findings.push({
    key: 'failure_probability',
    severity: severityFromScore(prediction.probability * 100),
    title: `Failure probability ${n(prediction.probability * 100)} % within ${prediction.horizonMin} minutes`,
    titleAr: `احتمال العطل ${n(prediction.probability * 100)}٪ خلال ${prediction.horizonMin} دقيقة`,
    detail: `Predicted event type ${prediction.eventType}, severity ${prediction.severity}, peak risk ${n(prediction.peakRisk)} % rising at ${n(prediction.trendPerHour)} points per hour.`,
    detailAr: `نوع الحدث المتوقع ${prediction.eventType}، والشدة ${prediction.severity}، وذروة الخطر ${n(prediction.peakRisk)}٪ بارتفاع ${n(prediction.trendPerHour)} نقطة في الساعة.`,
    metricKey: 'failureProbability',
    metricValue: round(prediction.probability * 100, 1),
    metricUnit: '%',
    confidence: round(prediction.confidence, 1),
  })

  if (prediction.etaMinutes !== null) {
    findings.push({
      key: 'eta',
      severity: prediction.etaMinutes <= 30 ? 'critical' : prediction.etaMinutes <= 90 ? 'warning' : 'watch',
      title: `Estimated time to limit breach ${prediction.etaMinutes} minutes`,
      titleAr: `الزمن التقديري لتجاوز الحد ${prediction.etaMinutes} دقيقة`,
      detail: `First breach is a ${prediction.breachReason ?? 'limit'} criterion on ${focus.profile.code}. Estimate, not a countdown to a certain event.`,
      detailAr: `أول تجاوز هو معيار ${prediction.breachReason ?? 'الحد'} على ${focus.profile.code}. تقدير وليس عدًا تنازليًا لحدث مؤكد.`,
      metricKey: 'etaMinutes',
      metricValue: prediction.etaMinutes,
      metricUnit: 'min',
      confidence: round(prediction.confidence * 0.94, 1),
    })
  }

  return {
    key: 'failure_hunter',
    ordinal: 2,
    status: statusFor(findings),
    summary:
      prediction.etaMinutes !== null
        ? `${focus.profile.code} matches ${top ? `${n(top.similarity)} % of pattern ${top.pattern.code}` : 'no stored pattern'} and reaches a ${prediction.breachReason ?? 'limit'} breach in about ${prediction.etaMinutes} minutes.`
        : `${focus.profile.code} shows no limit breach inside the ${prediction.horizonMin}-minute horizon; peak risk ${n(prediction.peakRisk)} %.`,
    summaryAr:
      prediction.etaMinutes !== null
        ? `${focus.profile.code} يطابق ${top ? `${n(top.similarity)}٪ من النمط ${top.pattern.code}` : 'لا نمط مخزن'} ويصل إلى تجاوز ${prediction.breachReason ?? 'الحد'} خلال نحو ${prediction.etaMinutes} دقيقة.`
        : `${focus.profile.code} لا يُظهر تجاوزًا لأي حد داخل أفق ${prediction.horizonMin} دقيقة، وذروة الخطر ${n(prediction.peakRisk)}٪.`,
    confidence: round(prediction.confidence, 1),
    dataQuality: ctx.dataQuality.score,
    inputs: ['Failure DNA library', 'Forward twin walk', 'Asset condition record'],
    inputsAr: ['مكتبة الحمض النووي للأعطال', 'المسار المستقبلي للتوأم الرقمي', 'سجل حالة الأصل'],
    findings,
    durationMs: workCost(prediction.frames.length + dnaMatches.length * 4, 1.6),
  }
}

// ─────────────────────────────────────────────────────────────────────────────
// Agent 3 — Load Guardian
// ─────────────────────────────────────────────────────────────────────────────

function loadGuardian(ctx: AgentContext): AgentResult {
  const { snapshot, focus, resources } = ctx
  const region = snapshot.regions.find((r) => r.code === focus.profile.regionCode)
  const findings: AgentFinding[] = []

  const peakRiskScore = clamp((snapshot.loadRatio - 0.72) * 320, 0, 100)
  findings.push({
    key: 'peak_risk',
    severity: severityFromScore(peakRiskScore),
    title: `National load ${n(snapshot.totalLoadMw, 0)} MW against ${n(snapshot.totalCapacityMw, 0)} MW of capacity`,
    titleAr: `الحمل الوطني ${n(snapshot.totalLoadMw, 0)} ميجاواط مقابل سعة ${n(snapshot.totalCapacityMw, 0)} ميجاواط`,
    detail: `Reserve margin ${n(snapshot.reserveMarginPct)} %, available capacity ${n(snapshot.availableCapacityMw, 0)} MW.`,
    detailAr: `هامش الاحتياطي ${n(snapshot.reserveMarginPct)}٪، والسعة المتاحة ${n(snapshot.availableCapacityMw, 0)} ميجاواط.`,
    metricKey: 'reserveMarginPct',
    metricValue: round(snapshot.reserveMarginPct, 1),
    metricUnit: '%',
    confidence: 94,
  })

  if (region) {
    findings.push({
      key: 'regional_demand',
      severity: severityFromScore(region.peakRisk),
      title: `${region.name} at ${n(region.loadMw, 0)} MW, peak asset risk ${n(region.peakRisk)} %`,
      titleAr: `${region.nameAr} عند ${n(region.loadMw, 0)} ميجاواط، وذروة خطر الأصول ${n(region.peakRisk)}٪`,
      detail: `${region.assetsAtRisk} of ${region.assetCount} assets in the region are at risk; reserve margin ${n(region.reserveMarginPct)} %.`,
      detailAr: `${region.assetsAtRisk} من ${region.assetCount} أصلًا في المنطقة معرّضة للخطر، وهامش الاحتياطي ${n(region.reserveMarginPct)}٪.`,
      metricKey: 'regionPeakRisk',
      metricValue: round(region.peakRisk, 1),
      metricUnit: '%',
      confidence: 92,
    })
  }

  const transferScore = resources.transferCapacityMw > 0 ? 100 - clamp(resources.transferCapacityMw, 0, 100) : 100
  findings.push({
    key: 'transfer_room',
    severity: resources.transferCapacityMw < 5 ? 'warning' : 'info',
    title: `${n(resources.transferCapacityMw)} MW of transfer capacity on adjacent units`,
    titleAr: `${n(resources.transferCapacityMw)} ميجاواط من سعة التحويل على الوحدات المجاورة`,
    detail: `Battery dispatch available ${n(resources.batteryPowerMw)} MW; contracted flexible demand behind this asset ${n(resources.flexibleLoadMw)} MW.`,
    detailAr: `تفريغ البطاريات المتاح ${n(resources.batteryPowerMw)} ميجاواط، والأحمال المرنة المتعاقد عليها خلف هذا الأصل ${n(resources.flexibleLoadMw)} ميجاواط.`,
    metricKey: 'transferCapacityMw',
    metricValue: round(resources.transferCapacityMw, 1),
    metricUnit: 'MW',
    confidence: round(clamp(88 - transferScore * 0.1, 0, 95), 1),
  })

  return {
    key: 'load_guardian',
    ordinal: 3,
    status: statusFor(findings),
    summary: `${n(resources.transferCapacityMw)} MW of transfer room and ${n(resources.flexibleLoadMw)} MW of deferrable demand are available around ${focus.profile.code}${
      region ? ` while ${region.name} runs at ${n(region.reserveMarginPct)} % reserve` : ''
    }.`,
    summaryAr: `تتوفر ${n(resources.transferCapacityMw)} ميجاواط من مساحة التحويل و${n(resources.flexibleLoadMw)} ميجاواط من الأحمال القابلة للتأجيل حول ${focus.profile.code}${
      region ? ` بينما تعمل ${region.nameAr} بهامش احتياطي ${n(region.reserveMarginPct)}٪` : ''
    }.`,
    confidence: 91,
    dataQuality: ctx.dataQuality.score,
    inputs: ['Regional demand', 'Fleet headroom', 'Demand-response register'],
    inputsAr: ['الطلب الإقليمي', 'هوامش الأسطول', 'سجل الاستجابة للطلب'],
    findings,
    durationMs: workCost(snapshot.regions.length * 6, 2.1),
  }
}

// ─────────────────────────────────────────────────────────────────────────────
// Agent 4 — Renewable Intelligence
// ─────────────────────────────────────────────────────────────────────────────

function renewableIntelligence(ctx: AgentContext): AgentResult {
  const { snapshot, renewable } = ctx
  const findings: AgentFinding[] = []

  findings.push({
    key: 'expected_output',
    severity: 'info',
    title: `Renewable output ${n(snapshot.renewableMw, 0)} MW — ${n(snapshot.renewableSharePct)} % of supply`,
    titleAr: `إنتاج المتجددة ${n(snapshot.renewableMw, 0)} ميجاواط — ${n(snapshot.renewableSharePct)}٪ من التغذية`,
    detail: `Solar ${n(snapshot.solarMw, 0)} MW, wind ${n(snapshot.windMw, 0)} MW, storage ${n(snapshot.batteryMw, 0)} MW.`,
    detailAr: `الشمسية ${n(snapshot.solarMw, 0)} ميجاواط، والرياح ${n(snapshot.windMw, 0)} ميجاواط، والتخزين ${n(snapshot.batteryMw, 0)} ميجاواط.`,
    metricKey: 'renewableMw',
    metricValue: round(snapshot.renewableMw, 1),
    metricUnit: 'MW',
    confidence: 90,
  })

  // A credible drop is the variability band applied to what is currently being produced;
  // it is the number that decides whether the network can lean on renewables right now.
  const possibleDropMw = round((snapshot.renewableMw * renewable.variabilityPct) / 100, 1)
  findings.push({
    key: 'variability',
    severity: severityFromScore(renewable.variabilityPct * 3.4),
    title: `Variability ${n(renewable.variabilityPct)} % — a credible swing of ±${n(possibleDropMw, 0)} MW`,
    titleAr: `التقلب ${n(renewable.variabilityPct)}٪ — تأرجح محتمل بمقدار ±${n(possibleDropMw, 0)} ميجاواط`,
    detail: `Cloud cover ${n(renewable.cloudPct, 0)} %. A drop of this size transfers directly onto the network within minutes.`,
    detailAr: `الغطاء السحابي ${n(renewable.cloudPct, 0)}٪. انخفاض بهذا الحجم ينتقل مباشرة إلى الشبكة خلال دقائق.`,
    metricKey: 'renewableVariabilityPct',
    metricValue: round(renewable.variabilityPct, 1),
    metricUnit: '%',
    confidence: 86,
  })

  if (Math.abs(renewable.deviationPct) > 1) {
    findings.push({
      key: 'deviation',
      severity: renewable.deviationPct < -8 ? 'warning' : 'watch',
      title: `Output ${renewable.deviationPct < 0 ? 'below' : 'above'} the baseline model by ${n(Math.abs(renewable.deviationPct))} %`,
      titleAr: `الإنتاج ${renewable.deviationPct < 0 ? 'أقل من' : 'أعلى من'} النموذج المرجعي بنسبة ${n(Math.abs(renewable.deviationPct))}٪`,
      detail: `Equivalent to ${n((Math.abs(renewable.deviationPct) / 100) * snapshot.renewableMw, 0)} MW that the conventional fleet is ${renewable.deviationPct < 0 ? 'covering' : 'released from'}.`,
      detailAr: `يعادل ${n((Math.abs(renewable.deviationPct) / 100) * snapshot.renewableMw, 0)} ميجاواط ${renewable.deviationPct < 0 ? 'يغطيها' : 'يُعفى منها'} الأسطول التقليدي.`,
      metricKey: 'renewableDeviationPct',
      metricValue: round(renewable.deviationPct, 1),
      metricUnit: '%',
      confidence: 84,
    })
  }

  return {
    key: 'renewable_intelligence',
    ordinal: 4,
    status: statusFor(findings),
    summary: `Renewables are carrying ${n(snapshot.renewableSharePct)} % of supply with ${n(renewable.variabilityPct)} % variability — a credible ${n(possibleDropMw, 0)} MW swing the network would have to absorb.`,
    summaryAr: `تحمل المتجددة ${n(snapshot.renewableSharePct)}٪ من التغذية بتقلب ${n(renewable.variabilityPct)}٪ — أي تأرجح محتمل بمقدار ${n(possibleDropMw, 0)} ميجاواط يتعين على الشبكة استيعابه.`,
    confidence: 86,
    dataQuality: ctx.dataQuality.score,
    inputs: ['Solar geometry model', 'Weather sample', 'Plant capacity register'],
    inputsAr: ['نموذج الهندسة الشمسية', 'عينة الطقس', 'سجل سعات المحطات'],
    findings,
    durationMs: workCost(snapshot.regions.length * 4, 1.8),
  }
}

// ─────────────────────────────────────────────────────────────────────────────
// Agent 5 — Asset Health
// ─────────────────────────────────────────────────────────────────────────────

function assetHealth(ctx: AgentContext): AgentResult {
  const { focus, rulDays, graph } = ctx
  const node = graph.byCode.get(focus.profile.code)
  const findings: AgentFinding[] = []

  findings.push({
    key: 'health',
    severity: severityFromScore(100 - focus.sample.effectiveHealth),
    title: `Effective health ${n(focus.sample.effectiveHealth)} % (recorded ${n(focus.profile.healthScore)} %)`,
    titleAr: `الصحة الفعلية ${n(focus.sample.effectiveHealth)}٪ (المسجلة ${n(focus.profile.healthScore)}٪)`,
    detail: `Live thermal and loading stress account for the ${n(focus.profile.healthScore - focus.sample.effectiveHealth)} point difference.`,
    detailAr: `الإجهاد الحراري والتحميل اللحظي يفسران فارق ${n(focus.profile.healthScore - focus.sample.effectiveHealth)} نقطة.`,
    metricKey: 'effectiveHealth',
    metricValue: round(focus.sample.effectiveHealth, 1),
    metricUnit: '%',
    confidence: 89,
  })

  findings.push({
    key: 'rul',
    severity: rulDays < 180 ? 'warning' : rulDays < 540 ? 'watch' : 'info',
    title: `Remaining useful life about ${Math.round(rulDays)} days at the current duty`,
    titleAr: `العمر التشغيلي المتبقي نحو ${Math.round(rulDays)} يومًا عند نمط التشغيل الحالي`,
    detail: `Estimated from thermal ageing at ${n(focus.sample.tempC)} °C against a ${n(focus.profile.ratedTempC)} °C rating. Projection, not a warranty statement.`,
    detailAr: `مُقدَّر من التقادم الحراري عند ${n(focus.sample.tempC)} مئوية مقابل تصنيف ${n(focus.profile.ratedTempC)} مئوية. إسقاط وليس ضمانًا.`,
    metricKey: 'rulDays',
    metricValue: Math.round(rulDays),
    metricUnit: 'd',
    confidence: 74,
  })

  if (node) {
    findings.push({
      key: 'criticality',
      severity: severityFromScore(node.centrality * 100 * 3),
      title: `${node.downstreamCustomers.toLocaleString('en-US')} customers depend on this node`,
      titleAr: `${node.downstreamCustomers.toLocaleString('en-US')} مشترك يعتمدون على هذه العقدة`,
      detail: `Network centrality ${n(node.centrality * 100, 2)} % of national demand, ${n(node.downstreamMw, 0)} MW of downstream capacity.`,
      detailAr: `مركزية الشبكة ${n(node.centrality * 100, 2)}٪ من الطلب الوطني، و${n(node.downstreamMw, 0)} ميجاواط من السعة التابعة.`,
      metricKey: 'centrality',
      metricValue: round(node.centrality * 100, 3),
      metricUnit: '%',
      confidence: 95,
    })
  }

  return {
    key: 'asset_health',
    ordinal: 5,
    status: statusFor(findings),
    summary: `${focus.profile.code} is operating at ${n(focus.sample.effectiveHealth)} % effective health with roughly ${Math.round(rulDays)} days of useful life at the current duty cycle.`,
    summaryAr: `يعمل ${focus.profile.code} بصحة فعلية ${n(focus.sample.effectiveHealth)}٪ مع نحو ${Math.round(rulDays)} يومًا من العمر المفيد عند دورة التشغيل الحالية.`,
    confidence: 84,
    dataQuality: ctx.dataQuality.score,
    inputs: ['Condition record', 'Thermal ageing model', 'Network topology'],
    inputsAr: ['سجل الحالة', 'نموذج التقادم الحراري', 'طوبولوجيا الشبكة'],
    findings,
    durationMs: workCost(24, 2.4),
  }
}

// ─────────────────────────────────────────────────────────────────────────────
// Agent 6 — Maintenance
// ─────────────────────────────────────────────────────────────────────────────

function maintenanceAgent(ctx: AgentContext): AgentResult {
  const { focus, maintenance, rulDays, prediction } = ctx
  const findings: AgentFinding[] = []

  findings.push({
    key: 'interval',
    severity: maintenance.debtDays > 120 ? 'warning' : maintenance.debtDays > 0 ? 'watch' : 'info',
    title:
      maintenance.debtDays > 0
        ? `Maintenance overdue by ${Math.round(maintenance.debtDays)} days`
        : 'Maintenance within interval',
    titleAr:
      maintenance.debtDays > 0
        ? `الصيانة متأخرة بمقدار ${Math.round(maintenance.debtDays)} يومًا`
        : 'الصيانة ضمن الدورة المحددة',
    detail: maintenance.lastFindings || 'No findings recorded on the most recent visit.',
    detailAr: maintenance.lastFindingsAr || 'لا توجد ملاحظات مسجلة في آخر زيارة.',
    metricKey: 'maintenanceDebtDays',
    metricValue: Math.round(maintenance.debtDays),
    metricUnit: 'd',
    confidence: 93,
  })

  // Which intervention is right depends on how fast the condition is moving, not on a
  // fixed calendar: a unit heading for a breach in the hour needs eyes on it today.
  const kind =
    prediction.etaMinutes !== null && prediction.etaMinutes < 120
      ? 'inspection'
      : maintenance.debtDays > 120 || rulDays < 240
        ? 'preventive'
        : rulDays < 540
          ? 'predictive'
          : 'routine'
  const kindCopy: Record<string, { en: string; ar: string }> = {
    inspection: { en: 'Immediate condition inspection', ar: 'فحص حالة فوري' },
    preventive: { en: 'Preventive maintenance window', ar: 'نافذة صيانة وقائية' },
    predictive: { en: 'Predictive monitoring package', ar: 'حزمة مراقبة تنبؤية' },
    routine: { en: 'Routine cycle, no change', ar: 'دورة روتينية دون تغيير' },
  }
  const priority = clamp(
    (maintenance.debtDays / 3) + (100 - focus.sample.effectiveHealth) * 1.4 + (rulDays < 365 ? 22 : 0),
    0,
    100,
  )

  findings.push({
    key: 'recommendation',
    severity: severityFromScore(priority),
    title: kindCopy[kind].en,
    titleAr: kindCopy[kind].ar,
    detail: `Priority score ${n(priority)} of 100, from maintenance debt, effective health and remaining life. ${maintenance.openWorkOrders} open work order(s) on this asset.`,
    detailAr: `درجة الأولوية ${n(priority)} من ١٠٠، محسوبة من تأخر الصيانة والصحة الفعلية والعمر المتبقي. ${maintenance.openWorkOrders} أمر عمل مفتوح على هذا الأصل.`,
    metricKey: 'maintenancePriority',
    metricValue: round(priority, 1),
    metricUnit: '',
    confidence: 82,
  })

  return {
    key: 'maintenance',
    ordinal: 6,
    status: statusFor(findings),
    summary: `${kindCopy[kind].en} is indicated for ${focus.profile.code} at priority ${n(priority)}${
      maintenance.debtDays > 0 ? `, with the interval already ${Math.round(maintenance.debtDays)} days overdue` : ''
    }.`,
    summaryAr: `${kindCopy[kind].ar} هو الإجراء المشار إليه للأصل ${focus.profile.code} بأولوية ${n(priority)}${
      maintenance.debtDays > 0 ? `، مع تأخر الدورة ${Math.round(maintenance.debtDays)} يومًا` : ''
    }.`,
    confidence: 82,
    dataQuality: ctx.dataQuality.score,
    inputs: ['Maintenance history', 'Condition trend', 'Work order register'],
    inputsAr: ['سجل الصيانة', 'اتجاه الحالة', 'سجل أوامر العمل'],
    findings,
    durationMs: workCost(18, 2.2),
  }
}

// ─────────────────────────────────────────────────────────────────────────────
// Agent 7 — Simulation
// ─────────────────────────────────────────────────────────────────────────────

function simulationAgent(ctx: AgentContext): AgentResult {
  const { cascade, prediction, focus } = ctx
  const findings: AgentFinding[] = []

  const propagating = cascade.steps.filter((s) => s.outcome === 'failed' && s.depth > 0)
  findings.push({
    key: 'cascade',
    severity: severityFromScore(cascade.regionalRisk),
    title:
      propagating.length > 0
        ? `Failure propagates to ${propagating.length} further assets within ${cascade.horizonMin} minutes`
        : 'Failure stays contained at the origin',
    titleAr:
      propagating.length > 0
        ? `العطل ينتشر إلى ${propagating.length} أصول إضافية خلال ${cascade.horizonMin} دقيقة`
        : 'العطل يبقى محصورًا عند المصدر',
    detail:
      propagating.length > 0
        ? `Chain: ${cascade.steps
            .slice(0, 5)
            .map((s) => `${s.assetCode} @ +${s.offsetMin} min`)
            .join(' → ')}. ${cascade.affectedCustomers.toLocaleString('en-US')} customers exposed.`
        : `Adjacent units absorb the released ${n(cascade.steps[0]?.transferredMw ?? 0)} MW without exceeding their own limits.`,
    detailAr:
      propagating.length > 0
        ? `السلسلة: ${cascade.steps
            .slice(0, 5)
            .map((s) => `${s.assetCode} عند +${s.offsetMin} دقيقة`)
            .join(' ← ')}. ${cascade.affectedCustomers.toLocaleString('en-US')} مشترك معرّض.`
        : `الوحدات المجاورة تستوعب ${n(cascade.steps[0]?.transferredMw ?? 0)} ميجاواط المحررة دون تجاوز حدودها.`,
    metricKey: 'regionalRisk',
    metricValue: cascade.regionalRisk,
    metricUnit: '%',
    confidence: round(clamp(58 + (cascade.steps[1]?.probability ?? 0) * 34, 0, 92), 1),
  })

  findings.push({
    key: 'forward_walk',
    severity: severityFromScore(prediction.peakRisk),
    title: `Forward twin walked ${prediction.frames.length} steps to +${prediction.horizonMin} minutes`,
    titleAr: `مشى التوأم الرقمي ${prediction.frames.length} خطوة حتى +${prediction.horizonMin} دقيقة`,
    detail: `Peak risk ${n(prediction.peakRisk)} % at the horizon, from ${n(prediction.currentRisk)} % now. Loading reaches ${n(Math.max(...prediction.frames.map((f) => f.sample.loadPct)))} %.`,
    detailAr: `ذروة الخطر ${n(prediction.peakRisk)}٪ عند الأفق مقارنة بـ${n(prediction.currentRisk)}٪ الآن. ويبلغ التحميل ${n(Math.max(...prediction.frames.map((f) => f.sample.loadPct)))}٪.`,
    metricKey: 'peakRisk',
    metricValue: round(prediction.peakRisk, 1),
    metricUnit: '%',
    confidence: round(prediction.confidence, 1),
  })

  if (cascade.unservedMw > 0) {
    findings.push({
      key: 'unserved',
      severity: 'critical',
      title: `${n(cascade.unservedMw)} MW would have nowhere to go`,
      titleAr: `${n(cascade.unservedMw)} ميجاواط لن تجد مسارًا بديلًا`,
      detail: 'The surrounding network cannot absorb the full transfer, so part of the demand would be interrupted.',
      detailAr: 'لا تستطيع الشبكة المحيطة استيعاب كامل التحويل، ما يعني انقطاع جزء من الطلب.',
      metricKey: 'unservedMw',
      metricValue: cascade.unservedMw,
      metricUnit: 'MW',
      confidence: 80,
    })
  }

  return {
    key: 'simulation',
    ordinal: 7,
    status: statusFor(findings),
    summary:
      propagating.length > 0
        ? `A ${focus.profile.code} failure reaches ${propagating.length} further assets and ${cascade.affectedCustomers.toLocaleString('en-US')} customers, with the first knock-on at +${propagating[0]?.offsetMin ?? 0} minutes.`
        : `A ${focus.profile.code} failure stays contained: the surrounding network absorbs the transfer.`,
    summaryAr:
      propagating.length > 0
        ? `عطل ${focus.profile.code} يصل إلى ${propagating.length} أصول إضافية و${cascade.affectedCustomers.toLocaleString('en-US')} مشترك، وأول أثر تبعي عند +${propagating[0]?.offsetMin ?? 0} دقيقة.`
        : `عطل ${focus.profile.code} يبقى محصورًا: الشبكة المحيطة تستوعب التحويل.`,
    confidence: round(clamp(60 + (1 - cascade.maxDepth * 0.08) * 28, 0, 90), 1),
    inputs: ['Grid graph', 'Live loading', 'Forward twin'],
    inputsAr: ['الرسم البياني للشبكة', 'التحميل اللحظي', 'التوأم الرقمي المستقبلي'],
    dataQuality: ctx.dataQuality.score,
    findings,
    durationMs: workCost(cascade.steps.length * 9 + prediction.frames.length, 1.9),
  }
}

// ─────────────────────────────────────────────────────────────────────────────
// Agent 8 — Prevention
// ─────────────────────────────────────────────────────────────────────────────

function preventionAgent(ctx: AgentContext): AgentResult {
  const { optimisation, actions, focus } = ctx
  const findings: AgentFinding[] = []
  const best = optimisation.best

  if (best && best.actions.length > 0) {
    findings.push({
      key: 'best_strategy',
      severity: 'info',
      title: `${best.label} — risk ${n(best.evaluation.riskBefore)} % → ${n(best.evaluation.riskAfter)} %`,
      titleAr: `${best.labelAr} — الخطر ${n(best.evaluation.riskBefore)}٪ ← ${n(best.evaluation.riskAfter)}٪`,
      detail: `Objective score ${n(best.score)} of 100 across ${best.metrics.length} weighted terms. ${n(best.evaluation.totalMw)} MW moved, ${best.costSar.toLocaleString('en-US')} SAR, ${best.executionMin} minutes to execute, ${best.customerImpact.toLocaleString('en-US')} customers affected.`,
      detailAr: `درجة الهدف ${n(best.score)} من ١٠٠ عبر ${best.metrics.length} بندًا موزونًا. ${n(best.evaluation.totalMw)} ميجاواط محوَّلة، و${best.costSar.toLocaleString('en-US')} ريال، و${best.executionMin} دقيقة للتنفيذ، و${best.customerImpact.toLocaleString('en-US')} مشترك متأثر.`,
      metricKey: 'riskAfter',
      metricValue: round(best.evaluation.riskAfter, 1),
      metricUnit: '%',
      confidence: best.confidence,
    })
  }

  const alternatives = optimisation.strategies.filter((s) => s.kind !== 'none' && !s.isRecommended)
  if (alternatives.length > 0) {
    findings.push({
      key: 'alternatives',
      severity: 'info',
      title: `${alternatives.length} alternative strategies evaluated`,
      titleAr: `تم تقييم ${alternatives.length} استراتيجيات بديلة`,
      detail: alternatives
        .slice(0, 3)
        .map((s) => `${s.label}: risk ${n(s.evaluation.riskAfter)} %, score ${n(s.score)}`)
        .join(' · '),
      detailAr: alternatives
        .slice(0, 3)
        .map((s) => `${s.labelAr}: الخطر ${n(s.evaluation.riskAfter)}٪، الدرجة ${n(s.score)}`)
        .join(' · '),
      metricKey: 'alternativeCount',
      metricValue: alternatives.length,
      metricUnit: '',
      confidence: 88,
    })
  }

  const strongest = actions[0]
  if (strongest) {
    findings.push({
      key: 'single_lever',
      severity: 'info',
      title: `Strongest single lever: ${strongest.title}`,
      titleAr: `أقوى إجراء منفرد: ${strongest.titleAr}`,
      detail: `Removes ${n(strongest.riskReduction)} risk points on its own, bringing loading to ${n(strongest.loadAfterPct)} % and temperature to ${n(strongest.tempAfterC)} °C.`,
      detailAr: `يزيل ${n(strongest.riskReduction)} نقطة خطر بمفرده، فيصبح التحميل ${n(strongest.loadAfterPct)}٪ والحرارة ${n(strongest.tempAfterC)} مئوية.`,
      metricKey: 'riskReduction',
      metricValue: round(strongest.riskReduction, 1),
      metricUnit: 'pts',
      confidence: 87,
    })
  }

  if (!best || best.actions.length === 0) {
    findings.push({
      key: 'no_action_needed',
      severity: 'info',
      title: 'No intervention scores better than leaving the asset alone',
      titleAr: 'لا يوجد تدخل يتفوق على ترك الأصل دون إجراء',
      detail: 'Every candidate strategy costs more than the risk it removes at the current state.',
      detailAr: 'كل استراتيجية مرشحة تكلف أكثر من الخطر الذي تزيله في الحالة الراهنة.',
      metricKey: 'candidateCount',
      metricValue: optimisation.strategies.length,
      metricUnit: '',
      confidence: 85,
    })
  }

  return {
    key: 'prevention',
    ordinal: 8,
    status: 'clear',
    summary:
      best && best.actions.length > 0
        ? `${best.label} is the best available trade-off for ${focus.profile.code}, taking risk from ${n(best.evaluation.riskBefore)} % to ${n(best.evaluation.riskAfter)} % at ${best.costSar.toLocaleString('en-US')} SAR.`
        : `No preventive action is justified for ${focus.profile.code} at the current state.`,
    summaryAr:
      best && best.actions.length > 0
        ? `${best.labelAr} هي أفضل مقايضة متاحة للأصل ${focus.profile.code}، وتخفض الخطر من ${n(best.evaluation.riskBefore)}٪ إلى ${n(best.evaluation.riskAfter)}٪ بتكلفة ${best.costSar.toLocaleString('en-US')} ريال.`
        : `لا يوجد إجراء وقائي مبرر للأصل ${focus.profile.code} في الحالة الراهنة.`,
    confidence: best?.confidence ?? 80,
    dataQuality: ctx.dataQuality.score,
    inputs: ['Available resources', 'Digital twin re-runs', 'Objective weights'],
    inputsAr: ['الموارد المتاحة', 'إعادة تشغيل التوأم الرقمي', 'أوزان دالة الهدف'],
    findings,
    durationMs: workCost(optimisation.strategies.length * 12, 2.6),
  }
}

const RUNNERS: Record<AgentKey, (ctx: AgentContext) => AgentResult> = {
  grid_watch: gridWatch,
  failure_hunter: failureHunter,
  load_guardian: loadGuardian,
  renewable_intelligence: renewableIntelligence,
  asset_health: assetHealth,
  maintenance: maintenanceAgent,
  simulation: simulationAgent,
  prevention: preventionAgent,
}

/** Run the full panel, in the order the operations room displays them. */
export function runAgents(ctx: AgentContext, keys: AgentKey[] = [...AGENT_KEYS]): AgentResult[] {
  return keys
    .map((key) => RUNNERS[key](ctx))
    .sort((a, b) => a.ordinal - b.ordinal)
}

/** Run one agent by key, for the drill-down panels. */
export function runAgent(key: AgentKey, ctx: AgentContext): AgentResult {
  return RUNNERS[key](ctx)
}
