import 'server-only'

import { prisma } from '../db'
import { buildGridSnapshot, type GridSnapshot } from '../engine/grid'
import { siblingsOf, type GridGraph } from '../engine/graph'
import { simulateCascade } from '../engine/cascade'
import { round, clamp } from '../engine/rng'
import type { Perturbation } from '../engine/telemetry'
import {
  assessResilience,
  findAdversarialConditions,
  safeOperatingEnvelope,
  type AdversarialLever,
  type AdversarialResult,
  type ResilienceInput,
  type ResilienceScore,
  type SafeEnvelope,
} from '../engine/resilience'
import { getGridSnapshot, loadAssetProfiles, loadDnaPatterns, loadRiskWeights } from './grid-service'
import { cascadeStates, loadGridGraph } from './graph-service'
import { runStressTest } from './stress-service'

/**
 * Resilience (§39–§42) and the adversarial twin (§38).
 *
 * The resilience score is deliberately expensive to compute honestly: two of its eight
 * factors — cascade containment and restoration paths — require walking the graph for
 * every load-serving asset. Approximating them would produce a number that moves for the
 * wrong reasons, and a resilience score nobody can trace is worse than none.
 */

const LOAD_SERVING = new Set(['transformer', 'industrial_load', 'ev_hub'])

/** Derive the eight resilience inputs from one snapshot of the fleet. */
function measure(snapshot: GridSnapshot, graph: GridGraph): Parameters<typeof assessResilience>[0] {
  const states = cascadeStates(snapshot.assets)
  const serving = snapshot.assets.filter((asset) => LOAD_SERVING.has(asset.profile.assetTypeKey))

  let redundant = 0
  let contained = 0
  let recoveryPaths = 0

  for (const asset of serving) {
    const peers = siblingsOf(graph, asset.profile.code).filter((peer) => {
      const state = states.get(peer.code)
      return state && !state.isOutage && state.loadPct < 100
    })
    const spare = peers.reduce((sum, peer) => {
      const state = states.get(peer.code)
      if (!state) return sum
      return sum + Math.max(0, peer.capacityMw - state.loadMw)
    }, 0)
    const ownLoad = Math.max(0.1, states.get(asset.profile.code)?.loadMw ?? 0)

    if (spare >= ownLoad) redundant += 1
    recoveryPaths += peers.length

    const cascade = simulateCascade(graph, states, asset.profile.code, { horizonMin: 60 })
    if (cascade.steps.filter((step) => step.outcome === 'failed' && step.depth > 0).length === 0) {
      contained += 1
    }
  }

  const count = Math.max(1, serving.length)
  const demandMw = Math.max(1, snapshot.totalLoadMw)
  const batteryMw = snapshot.assets
    .filter((asset) => asset.profile.assetTypeKey === 'battery_storage')
    .reduce((sum, asset) => sum + asset.profile.capacityMw * 0.6, 0)
  const flexibleMw = snapshot.assets
    .filter((asset) => asset.profile.isFlexibleLoad)
    .reduce((sum, asset) => sum + Math.max(0, asset.sample.loadMw), 0)

  return {
    redundantShare: redundant / count,
    reserveMarginPct: snapshot.reserveMarginPct,
    meanHealth:
      snapshot.assets.reduce((sum, asset) => sum + asset.sample.effectiveHealth, 0) /
      Math.max(1, snapshot.assets.length),
    batteryShare: batteryMw / demandMw,
    flexibleShare: flexibleMw / demandMw,
    renewableVariabilityPct: snapshot.renewableVariabilityPct,
    meanRecoveryPaths: recoveryPaths / count,
    containedShare: contained / count,
  }
}

export interface ResilienceView extends ResilienceScore {
  ts: number
  scope: string
  envelope: SafeEnvelope
  source: 'live' | 'simulated'
  /**
   * The measured factors the score was built from. Exposed so a caller can re-score the
   * same grid with one factor moved — which is what the blue team does when it asks
   * "how much of that would this response give back" — without re-deriving the other
   * seven and quietly getting a different baseline.
   */
  input: ResilienceInput
}

export async function getResilience(
  options: { ts?: number; perturbation?: Perturbation; includeEnvelope?: boolean } = {},
): Promise<ResilienceView> {
  const ts = options.ts ?? Date.now()
  const [snapshot, graph] = await Promise.all([
    getGridSnapshot({ ts, perturbation: options.perturbation }),
    loadGridGraph(),
  ])

  const measured = measure(snapshot, graph)
  const score = assessResilience(measured)

  // The envelope needs a stress sweep, which is the most expensive read in the platform;
  // callers that only want the score do not pay for it.
  let envelope: SafeEnvelope
  if (options.includeEnvelope === false) {
    envelope = safeOperatingEnvelope({
      currentLoadPct: (snapshot.loadRatio ?? 0) * 100,
      breakingPointDeltaPct: null,
      ambientC: snapshot.regions[0]?.env.tempC ?? 40,
      reserveMarginPct: snapshot.reserveMarginPct,
    })
  } else {
    const stress = await runStressTest({ ts, maxDeltaPct: 40, stepPct: 5 })
    envelope = safeOperatingEnvelope({
      currentLoadPct: (snapshot.loadRatio ?? 0) * 100,
      breakingPointDeltaPct: stress.breakingPointPct,
      ambientC: snapshot.regions[0]?.env.tempC ?? 40,
      reserveMarginPct: snapshot.reserveMarginPct,
    })
  }

  return {
    ...score,
    ts,
    scope: 'national',
    envelope,
    source: options.perturbation ? 'simulated' : 'live',
    input: measured,
  }
}

// ─────────────────────────────────────────────────────────────────────────────
// Resilience Lab (§40)
// ─────────────────────────────────────────────────────────────────────────────

export const DISTURBANCES = [
  'heat_wave',
  'multi_failure',
  'demand_surge',
  'renewable_drop',
  'maintenance_outage',
] as const
export type Disturbance = (typeof DISTURBANCES)[number]

/** What each disturbance does to the twin, in the engine's own terms. */
function perturbationFor(
  disturbance: Disturbance,
  magnitude: number,
  outageCodes: string[],
): Perturbation {
  switch (disturbance) {
    case 'heat_wave':
      return { tempDeltaC: magnitude }
    case 'demand_surge':
      return { loadDeltaPct: magnitude }
    case 'renewable_drop':
      return { solarDeltaPct: -magnitude, windDeltaPct: -magnitude * 0.6 }
    case 'multi_failure':
    case 'maintenance_outage':
      return { outageAssetCodes: outageCodes }
  }
}

export interface ResilienceTestResult {
  code: string
  disturbance: Disturbance
  magnitude: number
  before: ResilienceScore
  during: ResilienceScore
  after: ResilienceScore
  recoveryMin: number
  verdict: string
  verdictAr: string
  outageCodes: string[]
}

/**
 * Apply a disturbance and measure the grid's answer to it.
 *
 * Three readings: before, at the height of the disturbance, and after it has passed. The
 * third is the one that matters — a grid that dips and returns is resilient; one that
 * dips and stays down has lost something it does not get back on its own.
 */
export async function runResilienceTest(
  disturbance: Disturbance,
  options: { magnitude?: number; ts?: number; durationMin?: number } = {},
): Promise<ResilienceTestResult> {
  const ts = options.ts ?? Date.now()
  const durationMin = options.durationMin ?? 120
  const magnitude =
    options.magnitude ??
    ({ heat_wave: 8, demand_surge: 22, renewable_drop: 45, multi_failure: 2, maintenance_outage: 1 }[
      disturbance
    ] ?? 10)

  const [profiles, patterns, weights, graph] = await Promise.all([
    loadAssetProfiles(),
    loadDnaPatterns(),
    loadRiskWeights(),
    loadGridGraph(),
  ])

  // Outage disturbances take out the most consequential units rather than random ones:
  // a resilience test against the assets nobody depends on measures nothing.
  const baseSnapshot = buildGridSnapshot(profiles, ts, { patterns, weights })
  const outageCodes =
    disturbance === 'multi_failure' || disturbance === 'maintenance_outage'
      ? [...baseSnapshot.assets]
          .filter((asset) => LOAD_SERVING.has(asset.profile.assetTypeKey))
          .sort((a, b) => b.profile.customersServed - a.profile.customersServed)
          .slice(0, Math.max(1, Math.round(magnitude)))
          .map((asset) => asset.profile.code)
      : []

  const perturbation = perturbationFor(disturbance, magnitude, outageCodes)

  const at = (offsetMin: number, applied: Perturbation | undefined) =>
    assessResilience(
      measure(
        buildGridSnapshot(profiles, ts + offsetMin * 60_000, {
          perturbation: applied,
          patterns,
          weights,
        }),
        graph,
      ),
    )

  const before = at(0, undefined)
  const during = at(Math.round(durationMin / 2), perturbation)
  const after = at(durationMin + 60, undefined)

  // Recovery is measured, not assumed: step forward until the score is back within two
  // points of where it started, or report that it has not recovered inside the window.
  let recoveryMin = 0
  for (let offset = durationMin; offset <= durationMin + 240; offset += 30) {
    const score = at(offset, undefined)
    if (score.score >= before.score - 2) {
      recoveryMin = offset - durationMin
      break
    }
    recoveryMin = offset - durationMin
  }

  const drop = round(before.score - during.score, 1)
  const verdict =
    drop < 5
      ? `The grid absorbed this disturbance: resilience moved ${drop} points and returned within ${recoveryMin} minutes.`
      : drop < 18
        ? `Resilience fell ${drop} points during the disturbance and recovered in about ${recoveryMin} minutes.`
        : `This disturbance cost ${drop} resilience points — the grid is materially exposed to it.`
  const verdictAr =
    drop < 5
      ? `استوعبت الشبكة هذا الاضطراب: تحرك مؤشر الصمود ${drop} نقطة وعاد خلال ${recoveryMin} دقيقة.`
      : drop < 18
        ? `انخفض مؤشر الصمود ${drop} نقطة أثناء الاضطراب وتعافى خلال نحو ${recoveryMin} دقيقة.`
        : `كلّف هذا الاضطراب ${drop} نقطة من مؤشر الصمود — الشبكة معرّضة له بدرجة ملموسة.`

  return {
    code: `RES-${ts.toString(36).toUpperCase()}-${disturbance}`,
    disturbance,
    magnitude,
    before,
    during,
    after,
    recoveryMin,
    verdict,
    verdictAr,
    outageCodes,
  }
}

export async function persistResilienceTest(
  result: ResilienceTestResult,
  options: { twinCode?: string; userId?: string | null } = {},
): Promise<string | null> {
  const twin = await prisma.digitalTwin.findFirst({
    where: options.twinCode ? { code: options.twinCode } : { level: 'national' },
    select: { id: true },
  })
  if (!twin) return null

  const test = await prisma.resilienceTest.create({
    data: {
      code: result.code,
      twinId: twin.id,
      name: `${result.disturbance} resilience test`,
      nameAr: `اختبار صمود — ${result.disturbance}`,
      disturbance: result.disturbance,
      magnitude: result.magnitude,
      scoreBefore: result.before.score,
      scoreDuring: result.during.score,
      scoreAfter: result.after.score,
      recoveryMin: result.recoveryMin,
      verdict: result.verdict,
      verdictAr: result.verdictAr,
      createdById: options.userId ?? null,
    },
  })

  await prisma.resilienceResult.createMany({
    data: (
      [
        ['before', result.before],
        ['during', result.during],
        ['after', result.after],
      ] as const
    ).flatMap(([phase, score]) =>
      score.components.map((component) => ({
        testId: test.id,
        phase,
        factorKey: component.key,
        value: component.value,
        normalised: component.normalised,
        weight: component.weight,
        unit: component.unit,
      })),
    ),
  })

  return result.code
}

export async function listResilienceTests(limit = 12) {
  return prisma.resilienceTest.findMany({
    orderBy: { createdAt: 'desc' },
    take: limit,
    include: { results: true },
  })
}

// ─────────────────────────────────────────────────────────────────────────────
// Adversarial twin (§38)
// ─────────────────────────────────────────────────────────────────────────────

export interface AdversarialView extends AdversarialResult {
  assetCode: string
  riskBefore: number
  summary: string
  summaryAr: string
}

/**
 * Ask the twin to break itself.
 *
 * The levers are ordinary weather and demand movements with plausibility costs attached,
 * so the search returns the most *unremarkable* combination that pushes an asset past the
 * target — which is the combination worth planning against. A scenario that needs a
 * once-in-a-century coincidence tells a planner nothing.
 */
export async function runAdversarial(
  assetCode: string,
  options: { ts?: number; targetRisk?: number } = {},
): Promise<AdversarialView | null> {
  const ts = options.ts ?? Date.now()
  const targetRisk = options.targetRisk ?? 90

  const [profiles, patterns, weights] = await Promise.all([
    loadAssetProfiles(),
    loadDnaPatterns(),
    loadRiskWeights(),
  ])
  if (!profiles.some((profile) => profile.code === assetCode)) return null

  const levers: AdversarialLever[] = [
    {
      key: 'tempDeltaC',
      label: 'Ambient temperature',
      labelAr: 'الحرارة المحيطة',
      unit: '°C',
      step: 1,
      max: 12,
      costPerUnit: 1.4,
    },
    {
      key: 'loadDeltaPct',
      label: 'Regional demand',
      labelAr: 'الطلب الإقليمي',
      unit: '%',
      step: 2,
      max: 40,
      costPerUnit: 1,
    },
    {
      key: 'solarDeltaPct',
      label: 'Solar output shortfall',
      labelAr: 'نقص الإنتاج الشمسي',
      unit: '%',
      step: 5,
      max: 60,
      costPerUnit: 0.6,
    },
    {
      key: 'windDeltaPct',
      label: 'Wind output shortfall',
      labelAr: 'نقص إنتاج الرياح',
      unit: '%',
      step: 5,
      max: 60,
      costPerUnit: 0.5,
    },
  ]

  const evaluate = (doses: Record<string, number>) => {
    const perturbation: Perturbation = {
      tempDeltaC: doses.tempDeltaC || undefined,
      loadDeltaPct: doses.loadDeltaPct || undefined,
      solarDeltaPct: doses.solarDeltaPct ? -doses.solarDeltaPct : undefined,
      windDeltaPct: doses.windDeltaPct ? -doses.windDeltaPct : undefined,
    }
    const snapshot = buildGridSnapshot(profiles, ts, { perturbation, patterns, weights })
    return snapshot.assets.find((entry) => entry.profile.code === assetCode)?.risk.score ?? 0
  }

  const riskBefore = evaluate({})
  const result = findAdversarialConditions(levers, evaluate, targetRisk, { maxSteps: 36 })

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

  const summary = result.found
    ? `${assetCode} reaches ${round(result.metric, 0)} % risk under an ordinary combination: ${doseText}.`
    : `No combination of ordinary conditions inside the search bounds pushes ${assetCode} to ${targetRisk} %; the closest reached ${round(result.metric, 0)} %.`
  const summaryAr = result.found
    ? `يبلغ ${assetCode} خطرًا قدره ${round(result.metric, 0)}٪ تحت مزيج اعتيادي: ${doseTextAr}.`
    : `لا يوجد مزيج من الظروف الاعتيادية ضمن حدود البحث يرفع ${assetCode} إلى ${targetRisk}٪؛ وأقرب ما بلغه ${round(result.metric, 0)}٪.`

  return {
    ...result,
    assetCode,
    riskBefore: round(riskBefore, 1),
    summary,
    summaryAr,
  }
}

/** Loading as a share of the safe band, for the envelope gauge. */
export function envelopeUtilisation(envelope: SafeEnvelope): number {
  return round(clamp((envelope.currentLoadPct / Math.max(envelope.heatAdjustedMaxPct, 1)) * 100, 0, 130), 1)
}
