import 'server-only'

import { prisma } from '../db'
import { getRegion } from '../data/regions'
import { environmentFor, resourcesFor } from '../engine/grid'
import { predictFailure } from '../engine/prediction'
import { simulateCascade } from '../engine/cascade'
import { buildCascadeTree, findCascadeBreakpoints, type CascadeBreakpoint } from '../engine/cascade-tree'
import { rankByCentrality } from '../engine/graph'
import { discoverScenarios, fingerprint, type DiscoveryObjective } from '../engine/discovery'
import { findMinimalIntervention, type LeverDose } from '../engine/counterfactual'
import { evaluateStrategy } from '../engine/actions'
import { assetEnvelope } from '../engine/physics'
import { assetAgeYears, maintenanceDebt } from '../engine/grid'
import { round } from '../engine/rng'
import type { Perturbation } from '../engine/telemetry'
import { getGridSnapshot, loadRiskWeights } from './grid-service'
import { cascadeStates, loadGridGraph } from './graph-service'
import { buildLevers } from './counterfactual-service'

/**
 * The Autonomous Risk Hunter.
 *
 * This service deliberately contains **no new search algorithm**. Everything it needs
 * already exists — the discovery engine perturbs conditions, the cascade engine walks
 * consequences, the break-point search finds where to cut, the counterfactual search finds
 * the smallest fix, the criticality ranking finds the weakest link. What was missing in
 * 4.0 was a single pass that runs them in order and hands back one answer to one question:
 * *what could go wrong next, and what would stop it?*
 *
 * Building a parallel engine here would have produced a second set of numbers that could
 * disagree with the Discovery Lab about the same grid. Orchestration means the Risk Hunter
 * and the Discovery Lab are provably looking at the same thing.
 *
 * Sandbox only. Every simulation is a pure evaluation of the model; the writes are to the
 * discovery, prevention and break-point tables and to nothing else.
 */

export const SEARCH_MODES = ['quick', 'balanced', 'deep', 'custom'] as const
export type SearchMode = (typeof SEARCH_MODES)[number]

/**
 * The budget each mode gets.
 *
 * A quick search has to return while somebody is still looking at the screen; a deep one
 * is allowed to take twenty seconds because it is doing twenty seconds of work. Both
 * report what they spent, so a shallow answer is never mistaken for an exhaustive one.
 */
export const MODE_BUDGET: Record<SearchMode, { simulations: number; ms: number; results: number }> = {
  quick: { simulations: 24, ms: 4_000, results: 3 },
  balanced: { simulations: 60, ms: 12_000, results: 5 },
  deep: { simulations: 160, ms: 30_000, results: 8 },
  custom: { simulations: 60, ms: 12_000, results: 5 },
}

export const HUNT_KINDS = [
  'hidden_risk',
  'weakest_link',
  'cascade_trigger',
  'breaking_point',
  'minimum_failure',
  'prevention',
] as const
export type HuntKind = (typeof HUNT_KINDS)[number]

/** Which discovery objective each hunt maps onto. */
const OBJECTIVE_FOR: Record<HuntKind, DiscoveryObjective> = {
  hidden_risk: 'hidden_risk',
  weakest_link: 'worst_combination',
  cascade_trigger: 'cascade_trigger',
  breaking_point: 'breaking_point',
  minimum_failure: 'minimum_failure',
  prevention: 'hidden_risk',
}

export interface HuntRequest {
  kind: HuntKind
  mode?: SearchMode
  regionCode?: string | null
  assetCode?: string | null
  ts?: number
  seed?: number
  maxSimulations?: number
  userId?: string | null
  persist?: boolean
}

export interface HuntFinding {
  code: string
  assetCode: string
  regionCode: string
  narrative: string
  narrativeAr: string
  riskBefore: number
  riskAfter: number
  probability: number
  confidence: number
  noveltyScore: number
  severity: number
  reproducibility: number
  cascadeProbability: number
  cascadeDepth: number
  assetsAffected: number
  conditions: Array<{
    leverKey: string
    label: string
    labelAr: string
    magnitude: number
    unit: string
    plausibility: number
  }>
  /** The path the failure would take, when it takes one. */
  failurePath: string[]
  /** The minimum intervention that prevents it, when one exists. */
  prevention: PreventionView | null
}

export interface PreventionView {
  achieved: boolean
  riskBefore: number
  riskAfter: number
  targetRisk: number
  totalMw: number
  totalCostSar: number
  evidenceKind: 'simulated'
  summary: string
  summaryAr: string
  doses: Array<{ key: string; label: string; labelAr: string; amount: number; unit: string; utilisation: number }>
}

export interface WeakestLinkView {
  assetCode: string
  assetName: string
  assetNameAr: string
  criticality: number
  currentPct: number
  safeCeilingPct: number
  /** Demand increase, in per cent, at which this asset crosses its ceiling. */
  breakingPointPct: number | null
  dependants: number
  cascadeExposure: number
  recoveryDifficulty: number
}

export interface HuntResult {
  runCode: string | null
  kind: HuntKind
  mode: SearchMode
  scopeKind: string
  scopeCode: string
  focusAsset: string
  findings: HuntFinding[]
  weakestLink: WeakestLinkView | null
  breakpoints: CascadeBreakpoint[]
  simulationsRun: number
  budgetSimulations: number
  budgetExhausted: boolean
  skippedImplausible: number
  durationMs: number
  summary: string
  summaryAr: string
  /** Never anything else in this build. Stated rather than implied (§TRUTH RULES). */
  evidenceKind: 'simulated'
}

// ─────────────────────────────────────────────────────────────────────────────
// The hunt
// ─────────────────────────────────────────────────────────────────────────────

export async function runHunt(request: HuntRequest): Promise<HuntResult | null> {
  const startedAt = Date.now()
  const ts = request.ts ?? Date.now()
  const mode: SearchMode = request.mode ?? 'balanced'
  const budget = MODE_BUDGET[mode]
  const maxSimulations = Math.min(
    request.maxSimulations ?? budget.simulations,
    MODE_BUDGET.deep.simulations,
  )

  const [snapshot, graph, weights, known] = await Promise.all([
    getGridSnapshot({ ts }),
    loadGridGraph(),
    loadRiskWeights(),
    knownFingerprints(),
  ])

  const inScope = snapshot.assets.filter((entry) => {
    if (request.assetCode) return entry.profile.code === request.assetCode
    if (request.regionCode) return entry.profile.regionCode === request.regionCode
    return true
  })
  const focus = [...inScope].sort((a, b) => b.risk.score - a.risk.score)[0]
  if (!focus) return null

  const region = getRegion(focus.profile.regionCode)
  const scopeRegion = request.regionCode ?? region.code
  const baseStates = cascadeStates(snapshot.assets)

  // ── Scenario generation and simulation ────────────────────────────────────
  const evaluate = (perturbation: Perturbation) => {
    if (Date.now() - startedAt > budget.ms) return null

    const scoped: Perturbation = { ...perturbation, regionCode: scopeRegion }
    const envAt = environmentFor(region, scoped)

    const prediction = predictFailure(focus.profile, ts, envAt, {
      perturbation: scoped,
      dnaSimilarity: focus.dnaSimilarity,
      neighbourStress: focus.neighbourStress,
      weights,
      horizonMin: 120,
      stepMin: 15,
    })

    if (prediction.peakRisk < 70) {
      return {
        riskBefore: round(focus.risk.score, 1),
        riskAfter: round(prediction.peakRisk, 1),
        cascadeProbability: 0,
        cascadeDepth: 0,
        assetsAffected: 1,
        originAsset: focus.profile.code,
        regionCode: scopeRegion,
      }
    }

    const cascade = simulateCascade(graph, baseStates, focus.profile.code, { horizonMin: 60 })
    return {
      riskBefore: round(focus.risk.score, 1),
      riskAfter: round(prediction.peakRisk, 1),
      cascadeProbability: cascade.steps.length > 1 ? cascade.steps[1].probability : 0,
      cascadeDepth: cascade.maxDepth,
      assetsAffected: cascade.affectedAssets,
      originAsset: focus.profile.code,
      regionCode: scopeRegion,
    }
  }

  const search = discoverScenarios(evaluate, {
    objective: OBJECTIVE_FOR[request.kind],
    seed: request.seed ?? 20260829,
    maxSimulations,
    maxResults: budget.results,
    known,
  })

  // ── Failure path and break points, from the cascade the findings implied ──
  const cascade = simulateCascade(graph, baseStates, focus.profile.code, { horizonMin: 60 })
  const tree = buildCascadeTree(cascade)
  const breakpoints = findCascadeBreakpoints(tree)
  const failurePath = cascade.steps.map((stepEntry) => stepEntry.assetCode)

  // ── The weakest link, from criticality rather than a second search ────────
  const weakestLink =
    request.kind === 'weakest_link' || request.kind === 'breaking_point'
      ? weakestLinkFor(snapshot, graph, ts, scopeRegion)
      : null

  // ── Prevention for the strongest finding ─────────────────────────────────
  const strongest = search.discoveries[0]
  let prevention: PreventionView | null = null
  if (strongest && (request.kind === 'prevention' || request.kind === 'hidden_risk')) {
    prevention = await preventionFor({
      assetCode: focus.profile.code,
      ts,
      perturbation: { ...strongest.perturbation, regionCode: scopeRegion },
      snapshot,
      weights,
    })
  }

  const durationMs = Date.now() - startedAt
  const scopeKind = request.assetCode ? 'asset' : request.regionCode ? 'region' : 'national'

  const findings: HuntFinding[] = search.discoveries.map((discovery, index) => ({
    code: '',
    assetCode: discovery.evaluation.originAsset,
    regionCode: discovery.evaluation.regionCode,
    narrative: discovery.narrative,
    narrativeAr: discovery.narrativeAr,
    riskBefore: discovery.evaluation.riskBefore,
    riskAfter: discovery.evaluation.riskAfter,
    probability: discovery.probability,
    confidence: discovery.confidence,
    noveltyScore: discovery.noveltyScore,
    severity: discovery.severity,
    reproducibility: discovery.reproducibility,
    cascadeProbability: discovery.evaluation.cascadeProbability,
    cascadeDepth: discovery.evaluation.cascadeDepth,
    assetsAffected: discovery.evaluation.assetsAffected,
    conditions: discovery.conditions,
    failurePath: index === 0 ? failurePath : [],
    prevention: index === 0 ? prevention : null,
  }))

  const result: HuntResult = {
    runCode: null,
    kind: request.kind,
    mode,
    scopeKind,
    scopeCode: request.assetCode ?? request.regionCode ?? '',
    focusAsset: focus.profile.code,
    findings,
    weakestLink,
    breakpoints,
    simulationsRun: search.simulationsRun,
    budgetSimulations: maxSimulations,
    budgetExhausted: search.budgetExhausted,
    skippedImplausible: search.skippedImplausible,
    durationMs,
    summary: search.summary,
    summaryAr: search.summaryAr,
    evidenceKind: 'simulated',
  }

  if (request.persist === false) return result
  return persistHunt(result, search, prevention, request.userId ?? null, request.seed ?? 20260829)
}

// ─────────────────────────────────────────────────────────────────────────────
// Weakest link
// ─────────────────────────────────────────────────────────────────────────────

/**
 * The asset the grid can least afford to lose.
 *
 * Ranked by centrality — how much of the network sits behind it — rather than by risk,
 * because those are different questions. The riskiest asset is the one most likely to
 * fail; the weakest link is the one whose failure hurts most, and a resilience programme
 * needs the second.
 */
function weakestLinkFor(
  snapshot: Awaited<ReturnType<typeof getGridSnapshot>>,
  graph: Awaited<ReturnType<typeof loadGridGraph>>,
  ts: number,
  regionCode: string | null,
): WeakestLinkView | null {
  const ranked = rankByCentrality(graph).filter((node) =>
    regionCode ? node.regionCode === regionCode : true,
  )
  if (!ranked.length) return null

  // Weight centrality by how close the asset already is to its ceiling: a critical asset
  // with plenty of headroom is not the weakest link.
  const scored = ranked
    .map((node) => {
      const entry = snapshot.assets.find((asset) => asset.profile.code === node.code)
      if (!entry) return null
      const envelope = assetEnvelope({
        profile: entry.profile,
        sample: entry.sample,
        ageYears: assetAgeYears(entry.profile, ts),
        maintenanceDebt: Math.min(1, maintenanceDebt(entry.profile, ts) / 365),
      })
      const pressure = Math.max(0, envelope.currentPct - envelope.adjustedMaxPct + 20) / 20
      return { node, entry, envelope, score: node.centrality * (0.5 + pressure) }
    })
    .filter((value): value is NonNullable<typeof value> => value !== null)
    .sort((a, b) => b.score - a.score)

  const top = scored[0]
  if (!top) return null

  const dependants = (graph.out.get(top.node.code) ?? []).length
  const headroom = top.envelope.adjustedMaxPct - top.envelope.currentPct

  return {
    assetCode: top.node.code,
    assetName: top.node.name,
    assetNameAr: top.node.nameAr,
    criticality: round(top.node.centrality * 100, 1),
    currentPct: top.envelope.currentPct,
    safeCeilingPct: top.envelope.adjustedMaxPct,
    // Expressed as the demand rise this asset can still absorb. Negative headroom means
    // it is already past its ceiling, which is reported as null rather than a negative
    // "breaking point" that reads like a forecast.
    breakingPointPct:
      headroom > 0 && top.envelope.currentPct > 0
        ? round((headroom / top.envelope.currentPct) * 100, 1)
        : null,
    dependants,
    cascadeExposure: round(top.node.downstreamCustomers / 1_000, 1),
    recoveryDifficulty: round(Math.min(100, dependants * 12 + (headroom < 0 ? 40 : 0)), 1),
  }
}

// ─────────────────────────────────────────────────────────────────────────────
// Prevention
// ─────────────────────────────────────────────────────────────────────────────

/**
 * The smallest intervention that takes a discovered risk back below the target.
 *
 * Runs the same counterfactual search the Self-Healing Lab uses, against the conditions
 * the hunt just found. Reusing it is what makes "the minimum intervention" one number
 * across the product rather than two.
 */
async function preventionFor(input: {
  assetCode: string
  ts: number
  perturbation: Perturbation
  snapshot: Awaited<ReturnType<typeof getGridSnapshot>>
  weights: Awaited<ReturnType<typeof loadRiskWeights>>
}): Promise<PreventionView | null> {
  const focus = input.snapshot.assets.find((entry) => entry.profile.code === input.assetCode)
  if (!focus) return null

  const region = getRegion(focus.profile.regionCode)
  const envAt = environmentFor(region, input.perturbation)
  const resources = resourcesFor(focus.profile, input.snapshot.assets)

  const context = {
    profile: focus.profile,
    ts: input.ts,
    envAt,
    perturbation: input.perturbation,
    dnaSimilarity: focus.dnaSimilarity,
    neighbourStress: focus.neighbourStress,
    weights: input.weights,
    resources,
  }

  const targetRisk = 12
  const levers = buildLevers({
    assetCode: input.assetCode,
    resources,
    loadMw: focus.sample.loadMw,
  })
  const maxDoseByKey = new Map(levers.map((lever) => [lever.key, lever.maxDose]))

  const result = findMinimalIntervention(levers, {
    measure: (applied) => evaluateStrategy(context, applied).riskAfter,
    satisfied: (metric) => metric <= targetRisk,
    higherIsBetter: false,
  })

  const doses = result.doses.map((entry: LeverDose) => {
    const maxDose = maxDoseByKey.get(entry.key) ?? 0
    return {
      key: entry.key,
      label: entry.label,
      labelAr: entry.labelAr,
      amount: round(entry.dose, 2),
      unit: entry.unit,
      // Reported against the lever's own ceiling, so "3.8 MW" reads as a share of what
      // was available rather than as a bare figure.
      utilisation: round(maxDose > 0 ? (entry.dose / maxDose) * 100 : 0, 1),
    }
  })

  const doseText = doses.map((dose) => `${dose.label} ${dose.amount} ${dose.unit}`).join(', ')
  const doseTextAr = doses.map((dose) => `${dose.labelAr} ${dose.amount} ${dose.unit}`).join('، ')

  return {
    achieved: result.achieved,
    riskBefore: round(result.metricBefore, 1),
    riskAfter: round(result.metricAfter, 1),
    targetRisk,
    totalMw: round(doses.reduce((sum, dose) => sum + (dose.unit === 'MW' ? dose.amount : 0), 0), 2),
    totalCostSar: round(result.totalCostSar, 0),
    evidenceKind: 'simulated',
    summary: result.achieved
      ? `${doseText} takes risk from ${round(result.metricBefore, 0)} % to ${round(result.metricAfter, 0)} %.`
      : `No combination of the available levers reaches ${targetRisk} %; the closest reached ${round(result.metricAfter, 0)} %.`,
    summaryAr: result.achieved
      ? `${doseTextAr} تخفض الخطر من ${round(result.metricBefore, 0)}٪ إلى ${round(result.metricAfter, 0)}٪.`
      : `لا تركيبة من الإجراءات المتاحة تبلغ ${targetRisk}٪؛ وأقرب ما بلغته ${round(result.metricAfter, 0)}٪.`,
    doses,
  }
}

// ─────────────────────────────────────────────────────────────────────────────
// Persistence
// ─────────────────────────────────────────────────────────────────────────────

async function knownFingerprints(): Promise<string[]> {
  const rows = await prisma.scenarioDiscovery.findMany({
    include: { conditions: true },
    take: 200,
    orderBy: { createdAt: 'desc' },
  })
  return rows.map((row) =>
    fingerprint(
      row.conditions.map((condition) => ({
        leverKey: condition.leverKey,
        label: condition.label,
        labelAr: condition.labelAr,
        magnitude: condition.magnitude,
        unit: condition.unit,
        plausibility: condition.plausibility,
      })),
    ),
  )
}

async function persistHunt(
  result: HuntResult,
  search: ReturnType<typeof discoverScenarios>,
  prevention: PreventionView | null,
  userId: string | null,
  seed: number,
): Promise<HuntResult> {
  const runCode = `HUNT-${Date.now().toString(36).toUpperCase()}`
  const codes: string[] = []

  const run = await prisma.discoveryRun.create({
    data: {
      code: runCode,
      objective: OBJECTIVE_FOR[result.kind],
      // The two columns 4.1 added to the 4.0 run: how hard it searched, and who asked.
      mode: result.mode,
      origin: 'hunt',
      scopeKind: result.scopeKind,
      scopeCode: result.scopeCode,
      seed,
      maxSimulations: result.budgetSimulations,
      simulationsRun: result.simulationsRun,
      maxDurationMs: MODE_BUDGET[result.mode].ms,
      durationMs: result.durationMs,
      status: 'completed',
      discovered: search.discoveries.length,
      summary: result.summary,
      summaryAr: result.summaryAr,
      startedById: userId,
      completedAt: new Date(),
      discoveries: {
        create: search.discoveries.map((discovery, index) => {
          const code = `RH-${runCode.slice(5)}-${index + 1}`
          codes.push(code)
          return {
            code,
            title: discovery.narrative.slice(0, 160),
            titleAr: discovery.narrativeAr.slice(0, 160),
            originAsset: discovery.evaluation.originAsset,
            regionCode: discovery.evaluation.regionCode,
            riskBefore: discovery.evaluation.riskBefore,
            riskAfter: discovery.evaluation.riskAfter,
            cascadeProbability: discovery.evaluation.cascadeProbability,
            cascadeDepth: discovery.evaluation.cascadeDepth,
            assetsAffected: discovery.evaluation.assetsAffected,
            noveltyScore: discovery.noveltyScore,
            riskScore: discovery.riskScore,
            probability: discovery.probability,
            severity: discovery.severity,
            reproducibility: discovery.reproducibility,
            confidence: discovery.confidence,
            status: 'new',
            narrative: discovery.narrative,
            narrativeAr: discovery.narrativeAr,
            conditions: {
              create: discovery.conditions.map((condition) => ({
                leverKey: condition.leverKey,
                label: condition.label,
                labelAr: condition.labelAr,
                magnitude: condition.magnitude,
                unit: condition.unit,
                plausibility: condition.plausibility,
              })),
            },
          }
        }),
      },
    },
    include: { discoveries: { orderBy: { code: 'asc' } } },
  })

  // The prevention belongs to the finding it prevents, not to the run.
  if (prevention && run.discoveries.length) {
    const first = run.discoveries.find((discovery) => discovery.code === codes[0]) ?? run.discoveries[0]
    await prisma.riskPrevention.create({
      data: {
        code: `PREV-${runCode.slice(5)}`,
        discoveryId: first.id,
        assetCode: result.focusAsset,
        achieved: prevention.achieved,
        riskBefore: prevention.riskBefore,
        riskAfter: prevention.riskAfter,
        targetRisk: prevention.targetRisk,
        totalMw: prevention.totalMw,
        totalCostSar: prevention.totalCostSar,
        evidenceKind: 'simulated',
        summary: prevention.summary,
        summaryAr: prevention.summaryAr,
        doses: {
          create: prevention.doses.map((dose) => ({
            leverKey: dose.key,
            label: dose.label,
            labelAr: dose.labelAr,
            amount: dose.amount,
            unit: dose.unit,
            utilisation: dose.utilisation,
          })),
        },
      },
    })
  }

  // Break points carry the hunt code so they can be read back without a second table.
  if (result.breakpoints.length) {
    await prisma.cascadeBreakpoint.createMany({
      data: result.breakpoints.map((point) => ({
        simulationId: runCode,
        huntCode: runCode,
        assetCode: point.assetCode,
        depth: point.depth,
        riskReduction: point.riskReduction,
        branchesStopped: point.branchesStopped,
        customersProtected: point.customersProtected,
        interventionMw: point.interventionMw,
        costIndex: point.costIndex,
        feasible: point.feasible,
        isBest: point.isBest,
        rationale: point.rationale,
        rationaleAr: point.rationaleAr,
      })),
    })
  }

  return {
    ...result,
    runCode,
    findings: result.findings.map((finding, index) => ({ ...finding, code: codes[index] ?? '' })),
  }
}

// ─────────────────────────────────────────────────────────────────────────────
// Reading
// ─────────────────────────────────────────────────────────────────────────────

export async function listHunts(limit = 10) {
  const rows = await prisma.discoveryRun.findMany({
    where: { origin: 'hunt' },
    orderBy: { createdAt: 'desc' },
    take: limit,
    include: {
      discoveries: {
        orderBy: { riskScore: 'desc' },
        include: { conditions: true, prevention: { include: { doses: true } } },
      },
    },
  })

  return rows.map((row) => ({
    code: row.code,
    mode: row.mode,
    objective: row.objective,
    scopeKind: row.scopeKind,
    scopeCode: row.scopeCode,
    simulationsRun: row.simulationsRun,
    maxSimulations: row.maxSimulations,
    durationMs: row.durationMs,
    discovered: row.discovered,
    summary: row.summary,
    summaryAr: row.summaryAr,
    createdAt: row.createdAt.getTime(),
    findings: row.discoveries.map((discovery) => ({
      code: discovery.code,
      assetCode: discovery.originAsset,
      regionCode: discovery.regionCode,
      narrative: discovery.narrative,
      narrativeAr: discovery.narrativeAr,
      riskBefore: discovery.riskBefore,
      riskAfter: discovery.riskAfter,
      probability: discovery.probability,
      confidence: discovery.confidence,
      noveltyScore: discovery.noveltyScore,
      severity: discovery.severity,
      reproducibility: discovery.reproducibility,
      cascadeProbability: discovery.cascadeProbability,
      cascadeDepth: discovery.cascadeDepth,
      assetsAffected: discovery.assetsAffected,
      status: discovery.status,
      conditions: discovery.conditions.map((condition) => ({
        leverKey: condition.leverKey,
        label: condition.label,
        labelAr: condition.labelAr,
        magnitude: condition.magnitude,
        unit: condition.unit,
        plausibility: condition.plausibility,
      })),
      prevention: discovery.prevention
        ? {
            achieved: discovery.prevention.achieved,
            riskBefore: discovery.prevention.riskBefore,
            riskAfter: discovery.prevention.riskAfter,
            targetRisk: discovery.prevention.targetRisk,
            totalMw: discovery.prevention.totalMw,
            totalCostSar: discovery.prevention.totalCostSar,
            evidenceKind: discovery.prevention.evidenceKind,
            summary: discovery.prevention.summary,
            summaryAr: discovery.prevention.summaryAr,
            doses: discovery.prevention.doses.map((dose) => ({
              key: dose.leverKey,
              label: dose.label,
              labelAr: dose.labelAr,
              amount: dose.amount,
              unit: dose.unit,
              utilisation: dose.utilisation,
            })),
          }
        : null,
    })),
  }))
}

export async function getHunt(code: string) {
  const [runs, breakpoints] = await Promise.all([
    listHunts(50),
    prisma.cascadeBreakpoint.findMany({ where: { huntCode: code }, orderBy: { riskReduction: 'desc' } }),
  ])
  const run = runs.find((entry) => entry.code === code)
  if (!run) return null
  return { ...run, breakpoints }
}

/**
 * Run the prevention search for a finding that did not get one at hunt time.
 *
 * Separate from the hunt so a deep search does not spend its budget solving every finding
 * it turns up — an operator picks the one worth preventing and asks for that.
 */
export async function preventFinding(input: {
  discoveryCode: string
  ts?: number
  persist?: boolean
}): Promise<PreventionView | null> {
  const discovery = await prisma.scenarioDiscovery.findUnique({
    where: { code: input.discoveryCode },
    include: { conditions: true, prevention: true },
  })
  if (!discovery) return null

  const ts = input.ts ?? Date.now()
  const [snapshot, weights] = await Promise.all([getGridSnapshot({ ts }), loadRiskWeights()])

  // Rebuild the exact conditions the finding was made under, so the prevention answers
  // the question the finding asked.
  const perturbation: Perturbation = { regionCode: discovery.regionCode }
  for (const condition of discovery.conditions) {
    switch (condition.leverKey) {
      case 'temperature':
        perturbation.tempDeltaC = (perturbation.tempDeltaC ?? 0) + condition.magnitude
        break
      case 'demand':
        perturbation.loadDeltaPct = (perturbation.loadDeltaPct ?? 0) + condition.magnitude
        break
      case 'solar':
        perturbation.solarDeltaPct = (perturbation.solarDeltaPct ?? 0) + condition.magnitude
        break
      case 'wind':
        perturbation.windDeltaPct = (perturbation.windDeltaPct ?? 0) + condition.magnitude
        break
      case 'industrial':
        perturbation.industrialMw = (perturbation.industrialMw ?? 0) + condition.magnitude
        break
      case 'ev':
        perturbation.evSurgeMw = (perturbation.evSurgeMw ?? 0) + condition.magnitude
        break
      case 'battery':
        perturbation.batteryOutage = true
        break
      default:
        break
    }
  }

  const prevention = await preventionFor({
    assetCode: discovery.originAsset,
    ts,
    perturbation,
    snapshot,
    weights,
  })
  if (!prevention) return null

  if (input.persist !== false && !discovery.prevention) {
    await prisma.riskPrevention.create({
      data: {
        code: `PREV-${Date.now().toString(36).toUpperCase()}`,
        discoveryId: discovery.id,
        assetCode: discovery.originAsset,
        achieved: prevention.achieved,
        riskBefore: prevention.riskBefore,
        riskAfter: prevention.riskAfter,
        targetRisk: prevention.targetRisk,
        totalMw: prevention.totalMw,
        totalCostSar: prevention.totalCostSar,
        evidenceKind: 'simulated',
        summary: prevention.summary,
        summaryAr: prevention.summaryAr,
        doses: {
          create: prevention.doses.map((dose) => ({
            leverKey: dose.key,
            label: dose.label,
            labelAr: dose.labelAr,
            amount: dose.amount,
            unit: dose.unit,
            utilisation: dose.utilisation,
          })),
        },
      },
    })
  }

  return prevention
}
