/**
 * NABDH Orchestrator (§2).
 *
 * Eight agents produce eight partial views. Left side by side they are a dashboard;
 * what turns them into a decision is someone reconciling them — deciding how much weight
 * a strong DNA match deserves when the data behind it is thin, noticing that the load
 * agent's transfer capacity is what makes the prevention agent's strategy feasible at
 * all, and being explicit about how much time is left to act.
 *
 * That reconciliation is this module. It produces one assessment, one recommended
 * strategy, the alternatives it was chosen over, a confidence that cannot exceed its
 * weakest input, and a rationale naming the agents it rests on. It never executes
 * anything: the output is a proposal that a human approves or rejects (§13, §43).
 */

import { clamp, round } from './rng'
import { decisionConfidence, type DecisionConfidence } from './quality'
import type { AgentContext, AgentKey, AgentResult } from './agents'
import type { ScoredStrategy } from './optimize'

export type RiskBand = 'stable' | 'watch' | 'elevated' | 'high' | 'critical'

export interface OrchestrationFlowStep {
  agentKey: AgentKey | 'orchestrator'
  headline: string
  headlineAr: string
  status: AgentResult['status'] | 'decided'
  confidence: number
}

/**
 * The window in which acting still changes the outcome (§23).
 *
 * `latestActionAt` is not the predicted failure time. A strategy takes time to execute
 * and time to take effect, and both come off the front of the window — which is the
 * difference between knowing an event is coming and being able to do something about it.
 */
export interface PreventionWindow {
  etaMinutes: number | null
  executionMin: number
  /** Margin kept for approval and switching, in minutes. */
  bufferMin: number
  latestActionInMin: number | null
  safeWindowMin: number
  /** True when the window has already closed. */
  expired: boolean
}

/** Best / expected / worst framing so the estimate is never read as a countdown (§24). */
export interface UncertaintyRange {
  expectedMinutes: number | null
  bestCaseMinutes: number | null
  worstCaseMinutes: number | null
  expectedRisk: number
  bestCaseRisk: number
  worstCaseRisk: number
}

export interface Orchestration {
  ts: number
  assetCode: string
  regionCode: string
  /** Unified risk, 0–100, reconciled across the agent panel. */
  unifiedRisk: number
  band: RiskBand
  agents: AgentResult[]
  flow: OrchestrationFlowStep[]
  recommended: ScoredStrategy | null
  alternatives: ScoredStrategy[]
  confidence: DecisionConfidence
  preventionWindow: PreventionWindow
  uncertainty: UncertaintyRange
  rationale: string
  rationaleAr: string
  /** Agents whose findings materially moved the assessment. */
  contributingAgents: AgentKey[]
  dataWarning: string | null
  dataWarningAr: string | null
}

export function riskBand(risk: number): RiskBand {
  if (risk >= 78) return 'critical'
  if (risk >= 60) return 'high'
  if (risk >= 42) return 'elevated'
  if (risk >= 25) return 'watch'
  return 'stable'
}

/**
 * How much each agent's view weighs on the unified number.
 *
 * The prediction agents dominate because they are the ones making a forward claim; the
 * others qualify it. Prevention is deliberately absent: a strategy existing does not
 * make the situation less dangerous until somebody runs it.
 */
const RISK_WEIGHTS: Partial<Record<AgentKey, number>> = {
  failure_hunter: 0.4,
  simulation: 0.24,
  grid_watch: 0.16,
  asset_health: 0.1,
  load_guardian: 0.06,
  renewable_intelligence: 0.04,
}

/** Severity of an agent's worst finding, on the same 0–100 scale as risk. */
function agentSeverityScore(agent: AgentResult): number {
  const byLevel = { info: 8, watch: 34, warning: 62, critical: 88 }
  return agent.findings.reduce((max, finding) => Math.max(max, byLevel[finding.severity]), 0)
}

/** Minutes of buffer to keep in front of the latest safe action. */
const APPROVAL_BUFFER_MIN = 6

function buildPreventionWindow(
  etaMinutes: number | null,
  executionMin: number,
): PreventionWindow {
  if (etaMinutes === null) {
    return {
      etaMinutes: null,
      executionMin,
      bufferMin: APPROVAL_BUFFER_MIN,
      latestActionInMin: null,
      safeWindowMin: 0,
      expired: false,
    }
  }
  const latestActionInMin = etaMinutes - executionMin - APPROVAL_BUFFER_MIN
  return {
    etaMinutes,
    executionMin,
    bufferMin: APPROVAL_BUFFER_MIN,
    latestActionInMin: Math.round(latestActionInMin),
    safeWindowMin: Math.max(0, Math.round(latestActionInMin)),
    expired: latestActionInMin <= 0,
  }
}

/**
 * Turn a point estimate into a range.
 *
 * The spread is driven by the prediction's own confidence: a trajectory the model is
 * sure about earns a narrow band, a jittery one earns a wide one. Presenting the point
 * estimate alone would imply a precision the forward walk does not have.
 */
function buildUncertainty(ctx: AgentContext): UncertaintyRange {
  const { prediction } = ctx
  const confidence = clamp(prediction.confidence, 0, 100) / 100
  // At full confidence the band is ±10 % of the estimate; at zero it is ±45 %.
  const spread = 0.45 - 0.35 * confidence

  const eta = prediction.etaMinutes
  const riskSpread = clamp(prediction.peakRisk * spread * 0.5, 2, 24)

  return {
    expectedMinutes: eta,
    bestCaseMinutes: eta === null ? null : Math.round(eta * (1 + spread)),
    worstCaseMinutes: eta === null ? null : Math.max(1, Math.round(eta * (1 - spread))),
    expectedRisk: round(prediction.peakRisk, 1),
    bestCaseRisk: round(clamp(prediction.peakRisk - riskSpread, 0, 100), 1),
    worstCaseRisk: round(clamp(prediction.peakRisk + riskSpread, 0, 100), 1),
  }
}

/**
 * Reconcile the panel into one assessment.
 *
 * The unified risk is a weighted blend of what each agent found, anchored to the
 * prediction's own peak risk so the headline number never drifts away from the
 * trajectory the forward walk actually computed.
 */
export function orchestrate(ctx: AgentContext, agents: AgentResult[]): Orchestration {
  const byKey = new Map(agents.map((agent) => [agent.key, agent]))

  let weighted = 0
  let weightUsed = 0
  for (const [key, weight] of Object.entries(RISK_WEIGHTS) as Array<[AgentKey, number]>) {
    const agent = byKey.get(key)
    if (!agent) continue
    weighted += agentSeverityScore(agent) * weight
    weightUsed += weight
  }
  const panelRisk = weightUsed > 0 ? weighted / weightUsed : 0

  // Anchored two-thirds on the computed trajectory, one-third on the panel: the agents
  // can raise or lower the headline, but they cannot invent one.
  const unifiedRisk = round(clamp(ctx.prediction.peakRisk * 0.66 + panelRisk * 0.34, 0, 100), 1)

  const recommended = ctx.optimisation.best && ctx.optimisation.best.actions.length > 0
    ? ctx.optimisation.best
    : null
  const alternatives = ctx.optimisation.strategies.filter(
    (strategy) => strategy.key !== recommended?.key,
  )

  const confidence = decisionConfidence({
    recommendation: recommended?.confidence ?? 60,
    data: ctx.dataQuality.score,
    prediction: ctx.prediction.confidence,
    simulation: byKey.get('simulation')?.confidence ?? 70,
  })

  const preventionWindow = buildPreventionWindow(
    ctx.prediction.etaMinutes,
    recommended?.executionMin ?? 0,
  )

  const contributingAgents = agents
    .filter((agent) => agent.status === 'flagged' || agent.status === 'alarming')
    .map((agent) => agent.key)

  const flow: OrchestrationFlowStep[] = agents.map((agent) => ({
    agentKey: agent.key,
    headline: agent.summary,
    headlineAr: agent.summaryAr,
    status: agent.status,
    confidence: agent.confidence,
  }))

  const rationale = buildRationale(ctx, byKey, unifiedRisk, recommended)
  flow.push({
    agentKey: 'orchestrator',
    headline: rationale.en,
    headlineAr: rationale.ar,
    status: 'decided',
    confidence: confidence.overall,
  })

  return {
    ts: ctx.ts,
    assetCode: ctx.focus.profile.code,
    regionCode: ctx.focus.profile.regionCode,
    unifiedRisk,
    band: riskBand(unifiedRisk),
    agents,
    flow,
    recommended,
    alternatives,
    confidence,
    preventionWindow,
    uncertainty: buildUncertainty(ctx),
    rationale: rationale.en,
    rationaleAr: rationale.ar,
    contributingAgents,
    dataWarning: ctx.dataQuality.warning,
    dataWarningAr: ctx.dataQuality.warningAr,
  }
}

/**
 * The decision rationale (§14).
 *
 * Names the evidence and the expected result, in one paragraph. It is assembled from the
 * agents' own findings rather than written freehand, so it cannot claim something no
 * agent reported.
 */
function buildRationale(
  ctx: AgentContext,
  byKey: Map<AgentKey, AgentResult>,
  unifiedRisk: number,
  recommended: ScoredStrategy | null,
): { en: string; ar: string } {
  const code = ctx.focus.profile.code
  const cause = ctx.rootCause.primary
  const eta = ctx.prediction.etaMinutes
  const cascadeSteps = ctx.cascade.steps.filter((s) => s.outcome === 'failed' && s.depth > 0).length
  const hunter = byKey.get('failure_hunter')

  const evidenceEn: string[] = []
  const evidenceAr: string[] = []

  if (cause) {
    evidenceEn.push(`${cause.label.toLowerCase()} (${cause.evidence.toLowerCase()})`)
    evidenceAr.push(`${cause.labelAr} (${cause.evidenceAr})`)
  }
  if (ctx.dnaMatches[0]) {
    evidenceEn.push(`a ${round(ctx.dnaMatches[0].similarity, 1)} % match to pattern ${ctx.dnaMatches[0].pattern.code}`)
    evidenceAr.push(`تطابق ${round(ctx.dnaMatches[0].similarity, 1)}٪ مع النمط ${ctx.dnaMatches[0].pattern.code}`)
  }
  if (cascadeSteps > 0) {
    evidenceEn.push(`a cascade reaching ${cascadeSteps} further assets`)
    evidenceAr.push(`سلسلة تصل إلى ${cascadeSteps} أصول إضافية`)
  }
  if (ctx.resources.transferCapacityMw > 0) {
    evidenceEn.push(`${round(ctx.resources.transferCapacityMw, 1)} MW of transfer room on the bus`)
    evidenceAr.push(`${round(ctx.resources.transferCapacityMw, 1)} ميجاواط من مساحة التحويل على القضيب`)
  }

  const opening =
    eta === null
      ? `Unified risk on ${code} is ${unifiedRisk} % with no limit breach inside the ${ctx.prediction.horizonMin}-minute horizon.`
      : `Unified risk on ${code} is ${unifiedRisk} %, with a ${ctx.prediction.breachReason ?? 'limit'} breach estimated in ${eta} minutes.`
  const openingAr =
    eta === null
      ? `الخطر الموحّد على ${code} هو ${unifiedRisk}٪ دون تجاوز أي حد داخل أفق ${ctx.prediction.horizonMin} دقيقة.`
      : `الخطر الموحّد على ${code} هو ${unifiedRisk}٪، مع تقدير تجاوز ${ctx.prediction.breachReason ?? 'الحد'} خلال ${eta} دقيقة.`

  const basis =
    evidenceEn.length > 0
      ? ` The assessment rests on ${evidenceEn.join(', ')}.`
      : ' No agent reported evidence above its reporting threshold.'
  const basisAr =
    evidenceAr.length > 0
      ? ` يستند التقييم إلى ${evidenceAr.join('، ')}.`
      : ' لم يُبلّغ أي وكيل عن أدلة تتجاوز عتبة الإبلاغ.'

  const decision = recommended
    ? ` ${recommended.label} is recommended: it takes risk to ${round(recommended.evaluation.riskAfter, 1)} % for ${recommended.costSar.toLocaleString('en-US')} SAR and ${recommended.executionMin} minutes, ahead of ${ctx.optimisation.strategies.length - 1} alternatives on the same objective.`
    : ' No intervention scores better than leaving the asset under observation.'
  const decisionAr = recommended
    ? ` يُوصى بـ${recommended.labelAr}: تخفض الخطر إلى ${round(recommended.evaluation.riskAfter, 1)}٪ بتكلفة ${recommended.costSar.toLocaleString('en-US')} ريال وخلال ${recommended.executionMin} دقيقة، متقدمة على ${ctx.optimisation.strategies.length - 1} بدائل على الهدف نفسه.`
    : ' لا يوجد تدخل يتفوق على إبقاء الأصل تحت المراقبة.'

  const caveat = hunter && hunter.confidence < 60 ? ' Prediction confidence is low; treat the timing as indicative.' : ''
  const caveatAr = hunter && hunter.confidence < 60 ? ' ثقة التنبؤ منخفضة؛ تعامل مع التوقيت كمؤشر.' : ''

  return {
    en: `${opening}${basis}${decision}${caveat}`,
    ar: `${openingAr}${basisAr}${decisionAr}${caveatAr}`,
  }
}

/** A single future the platform can show side by side with the others (§8). */
export interface FutureBranch {
  key: 'no_action' | 'operator_action' | 'nabdh_strategy'
  label: string
  labelAr: string
  risk: number
  assetsAffected: number
  customersAffected: number
  potentialDowntimeMin: number
  loadPct: number
  tempC: number
  breaches: boolean
  actionCount: number
}
