/**
 * Demo calibration and verification tool.
 *
 *   npx tsx scripts/calibrate.ts          verify the demo story against the seeded grid
 *   npx tsx scripts/calibrate.ts --search re-solve the perturbation parameters
 *
 * The demo narrative quotes concrete numbers (risk, ETA, loading, post-action risk).
 * Those numbers are outputs of the engine, not constants, so this script exists to prove
 * they still hold after any change to the model — and to re-solve the scenario
 * parameters if they no longer do.
 */

import { PrismaClient } from '@prisma/client'

import { REGIONS } from '../src/lib/data/regions'
import { toAssetProfile } from '../src/lib/data/asset-profile'
import { sampleEnvironment } from '../src/lib/engine/environment'
import { sampleAsset, type Perturbation } from '../src/lib/engine/telemetry'
import { assessRisk } from '../src/lib/engine/risk'
import { predictFailure } from '../src/lib/engine/prediction'
import { buildGridSnapshot, resourcesFor, type DnaPatternRef } from '../src/lib/engine/grid'
import { buildStrategy, evaluateCandidates, generateCandidates } from '../src/lib/engine/actions'
import { buildGridGraph, siblingsOf, type GraphAsset } from '../src/lib/engine/graph'
import { simulateCascade, type CascadeAssetState } from '../src/lib/engine/cascade'
import {
  DEMO_ASSET_CODE,
  DEMO_RAMP_LEAD_MIN,
  DEMO_REGION_CODE,
  DEMO_TEMP_DELTA_C,
  demoBaseTimestamp,
  demoPerturbation,
} from '../src/lib/demo/config'

const prisma = new PrismaClient()

/** What the demo narrative claims. Verification fails loudly if reality drifts away. */
const EXPECTED = {
  riskNow: { value: 78, tolerance: 6 },
  etaMinutes: { value: 44, tolerance: 12 },
  loadPct: { value: 90, tolerance: 6 },
  riskAfterStrategy: { value: 9, tolerance: 8 },
  dnaSimilarity: { value: 87, tolerance: 4 },
}

async function loadContext() {
  const assetRows = await prisma.asset.findMany({
    include: { assetType: { select: { key: true } }, region: { select: { code: true } } },
  })
  const profiles = assetRows.map(toAssetProfile)
  const demo = profiles.find((p) => p.code === DEMO_ASSET_CODE)
  if (!demo) throw new Error(`${DEMO_ASSET_CODE} not found — run \`npm run db:seed\` first`)

  const patternRows = await prisma.failurePattern.findMany({ include: { dna: true } })
  const patterns: DnaPatternRef[] = patternRows
    .filter((p) => p.dna)
    .map((p) => ({
      code: p.dna!.code,
      name: p.name,
      nameAr: p.nameAr,
      eventType: p.eventType,
      severity: p.severity,
      occurredAt: p.occurredAt.getTime(),
      dna: {
        voltageDeviation: p.dna!.voltageDeviation,
        frequencyDeviation: p.dna!.frequencyDeviation,
        loadPercentage: p.dna!.loadPercentage,
        temperature: p.dna!.temperature,
        loadGrowthRate: p.dna!.loadGrowthRate,
        renewableVariability: p.dna!.renewableVariability,
        assetAge: p.dna!.assetAge,
        maintenanceScore: p.dna!.maintenanceScore,
        neighboringFailures: p.dna!.neighboringFailures,
        weatherTemperature: p.dna!.weatherTemperature,
      },
    }))

  return { profiles, demo, patterns }
}

function evaluate(
  ctx: Awaited<ReturnType<typeof loadContext>>,
  ts: number,
  perturbation: Perturbation,
) {
  const region = REGIONS.find((r) => r.code === DEMO_REGION_CODE)!
  const envAt = (t: number) => sampleEnvironment(region, t, { tempDeltaC: perturbation.tempDeltaC })

  const snapshot = buildGridSnapshot(ctx.profiles, ts, {
    perturbation,
    patterns: ctx.patterns,
  })
  const target = snapshot.assets.find((a) => a.profile.code === DEMO_ASSET_CODE)!

  const prediction = predictFailure(target.profile, ts, envAt, {
    perturbation,
    dnaSimilarity: target.dnaSimilarity,
    neighbourStress: target.neighbourStress,
    horizonMin: 180,
    stepMin: 5,
  })

  const actionCtx = {
    profile: target.profile,
    ts,
    envAt,
    perturbation,
    dnaSimilarity: target.dnaSimilarity,
    neighbourStress: target.neighbourStress,
    resources: resourcesFor(target.profile, snapshot.assets),
    evaluateAtMin: prediction.etaMinutes ?? 45,
  }
  const candidates = evaluateCandidates(actionCtx, generateCandidates(actionCtx))
  const strategy = buildStrategy(actionCtx, candidates, 12)

  return { snapshot, target, prediction, candidates, strategy, envAt }
}

async function verify() {
  const ctx = await loadContext()
  const ts = demoBaseTimestamp()
  const result = evaluate(ctx, ts, demoPerturbation())
  const { snapshot, target, prediction, candidates, strategy } = result
  const env = sampleEnvironment(REGIONS.find((r) => r.code === DEMO_REGION_CODE)!, ts, {
    tempDeltaC: DEMO_TEMP_DELTA_C,
  })

  console.log(`\nNABDH demo verification — ${DEMO_ASSET_CODE} at ${new Date(ts).toISOString()}\n`)
  console.log(`  ambient            ${env.tempC.toFixed(1)} °C`)
  console.log(`  loading            ${target.sample.loadPct.toFixed(1)} %`)
  console.log(`  top-oil            ${target.sample.tempC.toFixed(1)} °C (rated ${target.profile.ratedTempC} °C)`)
  console.log(`  voltage            ${target.sample.voltageKv.toFixed(2)} kV`)
  console.log(`  DNA match          ${target.dnaCode} at ${target.dnaSimilarity.toFixed(1)} %`)
  console.log(`  risk now           ${target.risk.score.toFixed(1)} %`)
  console.log(`  peak risk          ${prediction.peakRisk.toFixed(1)} %`)
  console.log(`  probability        ${(prediction.probability * 100).toFixed(0)} %`)
  console.log(`  confidence         ${(prediction.confidence * 100).toFixed(0)} %`)
  console.log(`  ETA                ${prediction.etaMinutes ?? '—'} min (${prediction.breachReason ?? 'no breach'})`)
  console.log('\n  candidate actions')
  for (const c of candidates) {
    console.log(
      `    ${c.key.padEnd(22)} ${String(c.magnitudeMw).padStart(5)} MW   ${c.riskBefore.toFixed(1)} → ${c.riskAfter.toFixed(1)}  (−${c.riskReduction.toFixed(1)})`,
    )
  }
  console.log(
    `\n  strategy           ${strategy.chosen.map((c) => c.key).join(' + ') || 'none'}\n` +
      `  risk at horizon    ${strategy.evaluation.riskBefore.toFixed(1)} → ${strategy.evaluation.riskAfter.toFixed(1)}\n` +
      `  loading            ${strategy.evaluation.loadBeforePct.toFixed(1)} % → ${strategy.evaluation.loadAfterPct.toFixed(1)} %\n` +
      `  top-oil            ${strategy.evaluation.tempBeforeC.toFixed(1)} °C → ${strategy.evaluation.tempAfterC.toFixed(1)} °C`,
  )

  // ── Cascade ─────────────────────────────────────────────────────────────
  // The demo asserts two things about propagation, and both have to stay true:
  // that a T-108 failure is absorbed by its own bus, and that the engine is capable of
  // producing a chain at all. A containment result that came from a broken walk would
  // otherwise look exactly like a containment result that came from real headroom.
  const graphRows = await prisma.asset.findMany({
    include: {
      assetType: { select: { key: true } },
      region: { select: { code: true } },
      site: { select: { code: true } },
    },
  })
  const graph = buildGridGraph(
    graphRows.map(
      (row): GraphAsset => ({
        code: row.code,
        name: row.name,
        nameAr: row.nameAr,
        assetTypeKey: row.assetType.key,
        regionCode: row.region.code,
        siteCode: row.site.code,
        lat: row.lat,
        lon: row.lon,
        capacityMw: row.capacityMw,
        voltageKv: row.voltageKv,
        customersServed: row.customersServed,
      }),
    ),
  )
  const cascadeStates = new Map<string, CascadeAssetState>(
    snapshot.assets.map((asset) => [
      asset.profile.code,
      {
        code: asset.profile.code,
        loadMw: Math.max(0, asset.sample.loadMw),
        capacityMw: asset.profile.capacityMw,
        loadPct: asset.sample.loadPct,
        isOutage: asset.sample.isOutage,
      },
    ]),
  )

  const demoCascade = simulateCascade(graph, cascadeStates, DEMO_ASSET_CODE, { horizonMin: 60 })
  const busHeadroomMw = siblingsOf(graph, DEMO_ASSET_CODE).reduce((sum, peer) => {
    const state = cascadeStates.get(peer.code)
    if (!state || state.isOutage || state.loadPct >= 100) return sum
    return sum + Math.max(0, peer.capacityMw - state.loadMw)
  }, 0)
  const propagating = snapshot.assets.filter(
    (asset) => simulateCascade(graph, cascadeStates, asset.profile.code, { horizonMin: 60 }).steps.length > 1,
  ).length

  console.log(
    `\n  cascade from ${DEMO_ASSET_CODE}\n` +
      `    steps              ${demoCascade.steps.length} (${demoCascade.affectedAssets} asset(s) touched)\n` +
      `    bus headroom       ${busHeadroomMw.toFixed(1)} MW on ${siblingsOf(graph, DEMO_ASSET_CODE).length} sibling unit(s)\n` +
      `    fleet propagating  ${propagating} of ${snapshot.assets.length} assets`,
  )

  const checks: Array<[string, number, { value: number; tolerance: number }]> = [
    ['risk now', target.risk.score, EXPECTED.riskNow],
    ['ETA minutes', prediction.etaMinutes ?? -1, EXPECTED.etaMinutes],
    ['loading', target.sample.loadPct, EXPECTED.loadPct],
    ['risk after strategy', strategy.evaluation.riskAfter, EXPECTED.riskAfterStrategy],
    ['DNA similarity', target.dnaSimilarity, EXPECTED.dnaSimilarity],
    // Containment is the claim, and the headroom is the reason for it.
    ['cascade steps', demoCascade.steps.length, { value: 1, tolerance: 0 }],
    ['bus headroom MW', busHeadroomMw, { value: 298, tolerance: 60 }],
    // …and the engine must still be able to find chains elsewhere on the same fleet.
    ['assets that propagate', propagating, { value: 200, tolerance: 60 }],
  ]

  console.log('\n  narrative checks')
  let failed = 0
  for (const [label, actual, expected] of checks) {
    const ok = Math.abs(actual - expected.value) <= expected.tolerance
    if (!ok) failed += 1
    console.log(
      `    ${ok ? 'PASS' : 'FAIL'}  ${label.padEnd(22)} ${actual.toFixed(1)} (expected ${expected.value} ± ${expected.tolerance})`,
    )
  }
  console.log('')
  if (failed > 0) {
    console.error(`  ${failed} check(s) failed — re-run with --search to re-solve the scenario.\n`)
    process.exitCode = 1
  }
}

async function search() {
  const ctx = await loadContext()
  const region = REGIONS.find((r) => r.code === DEMO_REGION_CODE)!
  const ts = demoBaseTimestamp()

  // Solve for the ramp shape that puts risk at the narrative value now and a genuine
  // thermal limit crossing at the narrative ETA.
  let best:
    | { rampMinutes: number; loadDelta: number; risk: number; eta: number; error: number }
    | null = null

  for (let rampMinutes = 60; rampMinutes <= 150; rampMinutes += 2) {
    for (let loadDelta = 25; loadDelta <= 90; loadDelta += 1) {
      const perturbation: Perturbation = {
        tempDeltaC: DEMO_TEMP_DELTA_C,
        loadDeltaPct: loadDelta,
        rampMinutes,
        rampStartMs: ts - DEMO_RAMP_LEAD_MIN * 60_000,
        regionCode: DEMO_REGION_CODE,
      }
      const envAt = (t: number) => sampleEnvironment(region, t, { tempDeltaC: DEMO_TEMP_DELTA_C })
      const env = envAt(ts)
      const sample = sampleAsset(ctx.demo, ts, env, perturbation)
      const risk = assessRisk({
        profile: ctx.demo,
        sample,
        env,
        dnaSimilarity: 87,
        neighbourStress: 0.55,
      })
      const prediction = predictFailure(ctx.demo, ts, envAt, {
        perturbation,
        dnaSimilarity: 87,
        neighbourStress: 0.55,
        horizonMin: 180,
        stepMin: 5,
      })
      if (prediction.etaMinutes === null || prediction.etaMinutes < 10) continue

      const error =
        Math.abs(risk.score - EXPECTED.riskNow.value) * 1.6 +
        Math.abs(prediction.etaMinutes - EXPECTED.etaMinutes.value) * 0.8 +
        Math.abs(sample.loadPct - EXPECTED.loadPct.value) * 0.5
      if (!best || error < best.error) {
        best = {
          rampMinutes,
          loadDelta,
          risk: risk.score,
          eta: prediction.etaMinutes,
          error,
        }
      }
    }
  }

  console.log('\n  best-fit ramp parameters\n')
  console.log(`    DEMO_RAMP_MINUTES    ${best?.rampMinutes}`)
  console.log(`    DEMO_LOAD_DELTA_PCT  ${best?.loadDelta}`)
  console.log(`    -> risk now          ${best?.risk.toFixed(1)} %`)
  console.log(`    -> ETA               ${best?.eta} min`)
  console.log(`    residual error       ${best?.error.toFixed(2)}`)
  console.log('\n  Copy these into src/lib/demo/config.ts, then re-run without --search.\n')
}

const run = process.argv.includes('--search') ? search : verify

run()
  .catch((error) => {
    console.error(error)
    process.exit(1)
  })
  .finally(() => prisma.$disconnect())
