import 'server-only'

import { prisma } from '../db'
import { REGIONS } from '../data/regions'
import { environmentFor, resourcesFor } from '../engine/grid'
import { predictFailure } from '../engine/prediction'
import {
  buildStrategy,
  compareStrategies,
  evaluateCandidates,
  evaluateStrategy,
  generateCandidates,
  type ActionContext,
  type EvaluatedAction,
} from '../engine/actions'
import type { AppliedAction, Perturbation } from '../engine/telemetry'
import { round } from '../engine/rng'
import { getGridSnapshot, loadRiskWeights } from './grid-service'
import { VALUE_OF_LOST_LOAD_SAR_PER_MWH } from './prevention-service'
import { ApiError, ERROR_CODES } from '../api/errors'

/**
 * Simulation service (§6, §7, §24).
 *
 * Two kinds of question, one engine:
 *   • "what if X happened?"  — apply a perturbation nationally or regionally and diff the
 *     resulting grid against the baseline.
 *   • "what if we did Y?"    — apply preventive actions to one asset and diff its
 *     projected state at the moment the failure was predicted for.
 *
 * Both persist their inputs and results, so a simulation shown to a decision-maker can
 * be reproduced and audited later rather than being a transient screen.
 */

export interface ScenarioParams {
  loadDeltaPct?: number
  solarDeltaPct?: number
  windDeltaPct?: number
  tempDeltaC?: number
  assetOutage?: boolean
  lineOutage?: boolean
  batteryOutage?: boolean
  evSurgeMw?: number
  industrialMw?: number
  regionCode?: string | null
  assetCode?: string | null
  durationMin?: number
}

export function toPerturbation(params: ScenarioParams): Perturbation {
  return {
    loadDeltaPct: params.loadDeltaPct || 0,
    solarDeltaPct: params.solarDeltaPct || 0,
    windDeltaPct: params.windDeltaPct || 0,
    tempDeltaC: params.tempDeltaC || 0,
    lineOutage: params.lineOutage || false,
    batteryOutage: params.batteryOutage || false,
    evSurgeMw: params.evSurgeMw || 0,
    industrialMw: params.industrialMw || 0,
    regionCode: params.regionCode ?? null,
    outageAssetCodes: params.assetOutage && params.assetCode ? [params.assetCode] : undefined,
  }
}

/** A scenario that changes nothing produces a meaningless comparison — reject it early. */
export function isMeaningfulScenario(params: ScenarioParams): boolean {
  return Boolean(
    params.loadDeltaPct ||
      params.solarDeltaPct ||
      params.windDeltaPct ||
      params.tempDeltaC ||
      params.assetOutage ||
      params.lineOutage ||
      params.batteryOutage ||
      params.evSurgeMw ||
      params.industrialMw,
  )
}

export interface ScenarioOutcome {
  code: string
  simulationId: string
  riskBefore: number
  riskAfter: number
  stabilityBefore: number
  stabilityAfter: number
  voltageBefore: number
  voltageAfter: number
  frequencyBefore: number
  frequencyAfter: number
  loadBeforeMw: number
  loadAfterMw: number
  assetStressBefore: number
  assetStressAfter: number
  failureProbBefore: number
  failureProbAfter: number
  regionsAffected: number
  customersImpacted: number
  economicImpactSar: number
  recoveryTimeMin: number
  energyNotServedMwh: number
  verdict: string
  verdictAr: string
  impacts: Array<{
    scope: 'region' | 'asset'
    code: string
    label: string
    labelAr: string
    metricKey: string
    baselineValue: number
    scenarioValue: number
    unit: string
    severity: string
  }>
}

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

/** Average duration of recorded incidents, used to scale the recovery-time estimate. */
async function averageIncidentDurationMin(): Promise<number> {
  const result = await prisma.incident.aggregate({
    _avg: { durationMin: true },
    where: { durationMin: { gt: 0 } },
  })
  return result._avg.durationMin ?? 120
}

export async function runScenarioSimulation(input: {
  params: ScenarioParams
  name: string
  nameAr: string
  scenarioId?: string | null
  userId?: string | null
  ts?: number
}): Promise<ScenarioOutcome> {
  if (!isMeaningfulScenario(input.params)) {
    throw new ApiError(ERROR_CODES.invalidScenario)
  }

  const started = Date.now()
  const ts = input.ts ?? Date.now()
  const perturbation = toPerturbation(input.params)
  const durationMin = input.params.durationMin ?? 120

  const [baseline, scenario, avgIncidentMin] = await Promise.all([
    getGridSnapshot({ ts }),
    getGridSnapshot({ ts, perturbation }),
    averageIncidentDurationMin(),
  ])

  const baselineByCode = new Map(baseline.assets.map((entry) => [entry.profile.code, entry]))

  // Unserved energy: demand the scenario can no longer deliver, integrated over the
  // scenario's duration. An asset taken out of service loses all of its load; an
  // overloaded one is counted for the part above its emergency rating.
  let unservedMw = 0
  let customersImpacted = 0
  for (const entry of scenario.assets) {
    if (!LOAD_SERVING.has(entry.profile.assetTypeKey)) continue
    const before = baselineByCode.get(entry.profile.code)
    if (!before) continue

    if (entry.sample.isOutage && !before.sample.isOutage) {
      unservedMw += Math.max(0, before.sample.loadMw)
      customersImpacted += entry.profile.customersServed
    } else if (entry.sample.loadPct > 108) {
      const excess = ((entry.sample.loadPct - 108) / 100) * entry.profile.capacityMw
      unservedMw += Math.max(0, excess)
      customersImpacted += Math.round(
        entry.profile.customersServed * Math.min(1, (entry.sample.loadPct - 108) / 40),
      )
    }
  }

  const energyNotServedMwh = round((unservedMw * durationMin) / 60, 2)
  const economicImpactSar = round(energyNotServedMwh * VALUE_OF_LOST_LOAD_SAR_PER_MWH, 0)

  const peakRiskBefore = Math.max(...baseline.assets.map((entry) => entry.risk.score), 0)
  const peakRiskAfter = Math.max(...scenario.assets.map((entry) => entry.risk.score), 0)

  const meanLoad = (snapshotAssets: typeof baseline.assets) =>
    snapshotAssets.length
      ? snapshotAssets.reduce((sum, entry) => sum + Math.abs(entry.sample.loadPct), 0) /
        snapshotAssets.length
      : 0

  const regionsAffected = scenario.regions.filter((region) => {
    const before = baseline.regions.find((entry) => entry.code === region.code)
    return region.peakRisk >= 55 && (before ? region.peakRisk - before.peakRisk > 2 : true)
  }).length

  // Recovery scales with how much load was actually lost, anchored on the average
  // duration of the incidents already in the record.
  const severityFactor = Math.min(2.5, 0.35 + (customersImpacted > 0 ? 1 : 0.2) + peakRiskAfter / 120)
  const recoveryTimeMin = Math.round(avgIncidentMin * severityFactor)

  const worsened = peakRiskAfter - peakRiskBefore
  const verdict =
    worsened > 15
      ? 'The scenario pushes the grid materially past its present risk level and requires a mitigation plan.'
      : worsened > 4
        ? 'The scenario is absorbable but erodes margin; monitor the affected region closely.'
        : 'The grid absorbs this scenario within its existing margins.'
  const verdictAr =
    worsened > 15
      ? 'يدفع السيناريو الشبكة إلى ما يتجاوز مستوى خطرها الحالي بوضوح ويستدعي خطة تخفيف.'
      : worsened > 4
        ? 'يمكن استيعاب السيناريو لكنه يقلّص الهامش؛ يلزم متابعة المنطقة المتأثرة عن قرب.'
        : 'تستوعب الشبكة هذا السيناريو ضمن هوامشها الحالية.'

  const code = `SIM-${Date.now().toString(36).toUpperCase()}`

  const simulation = await prisma.simulation.create({
    data: {
      code,
      scenarioId: input.scenarioId ?? null,
      name: input.name,
      nameAr: input.nameAr,
      kind: 'what_if',
      status: 'completed',
      createdById: input.userId ?? null,
      startedAt: new Date(started),
      completedAt: new Date(),
      durationMs: Date.now() - started,
      horizonMin: durationMin,
      appliedActions: null,
    },
  })

  const result = {
    riskBefore: round(peakRiskBefore, 1),
    riskAfter: round(peakRiskAfter, 1),
    stabilityBefore: baseline.stabilityIndex,
    stabilityAfter: scenario.stabilityIndex,
    voltageBefore: baseline.averageVoltageDeviationPct,
    voltageAfter: scenario.averageVoltageDeviationPct,
    frequencyBefore: baseline.frequencyHz,
    frequencyAfter: scenario.frequencyHz,
    loadBeforeMw: baseline.totalLoadMw,
    loadAfterMw: scenario.totalLoadMw,
    assetStressBefore: round(meanLoad(baseline.assets), 2),
    assetStressAfter: round(meanLoad(scenario.assets), 2),
    failureProbBefore: round(Math.min(0.99, peakRiskBefore / 100), 4),
    failureProbAfter: round(Math.min(0.99, peakRiskAfter / 100), 4),
    regionsAffected,
    customersImpacted,
    economicImpactSar,
    recoveryTimeMin,
    energyNotServedMwh,
    co2AvoidedTons: 0,
    verdict,
    verdictAr,
  }

  await prisma.simulationResult.create({
    data: { simulationId: simulation.id, ...result },
  })

  // Per-region impact rows, so the result can be read as a map rather than a single number.
  const regionRows = await prisma.region.findMany({ select: { id: true, code: true, name: true, nameAr: true } })
  const regionIdByCode = new Map(regionRows.map((row) => [row.code, row.id]))

  const impacts = scenario.regions
    .map((region) => {
      const before = baseline.regions.find((entry) => entry.code === region.code)
      return {
        scope: 'region' as const,
        code: region.code,
        label: region.name,
        labelAr: region.nameAr,
        metricKey: 'peak_risk',
        baselineValue: before?.peakRisk ?? 0,
        scenarioValue: region.peakRisk,
        unit: '%',
        severity:
          region.peakRisk - (before?.peakRisk ?? 0) > 15
            ? 'critical'
            : region.peakRisk - (before?.peakRisk ?? 0) > 5
              ? 'warning'
              : 'info',
      }
    })
    .filter((impact) => Math.abs(impact.scenarioValue - impact.baselineValue) > 0.5)
    .sort((a, b) => b.scenarioValue - b.baselineValue - (a.scenarioValue - a.baselineValue))

  if (impacts.length > 0) {
    await prisma.simulationImpact.createMany({
      data: impacts.map((impact) => ({
        simulationId: simulation.id,
        regionId: regionIdByCode.get(impact.code) ?? null,
        assetId: null,
        metricKey: impact.metricKey,
        label: impact.label,
        labelAr: impact.labelAr,
        baselineValue: impact.baselineValue,
        scenarioValue: impact.scenarioValue,
        unit: impact.unit,
        severity: impact.severity,
      })),
    })
  }

  return { code, simulationId: simulation.id, ...result, impacts }
}

// ─────────────────────────────────────────────────────────────────────────────
// Preventive action simulation
// ─────────────────────────────────────────────────────────────────────────────

export interface ActionOutcome {
  code: string
  simulationId: string
  assetCode: string
  evaluatedAtMin: number
  etaMinutes: number | null
  riskNow: number
  riskBefore: number
  riskAfter: number
  loadBeforePct: number
  loadAfterPct: number
  tempBeforeC: number
  tempAfterC: number
  totalMw: number
  totalCostSar: number
  actions: EvaluatedAction[]
  chosen: string[]
  comparison: ReturnType<typeof compareStrategies>
  customersProtected: number
  downtimeAvoidedMin: number
  energyProtectedMwh: number
}

/**
 * Simulate preventive actions on one asset.
 *
 * `actionKeys` selects which candidates to apply; omitting it runs the strategy the
 * engine would recommend. Either way the benefit is measured by re-running the twin —
 * the SIMULATE button and the recommendation list are the same computation.
 */
export async function runActionSimulation(input: {
  assetCode: string
  actionKeys?: string[]
  userId?: string | null
  ts?: number
  perturbation?: Perturbation
  kind?: string
}): Promise<ActionOutcome> {
  const started = Date.now()
  const ts = input.ts ?? Date.now()
  const perturbation = input.perturbation

  const [snapshot, weights, avgIncidentMin] = await Promise.all([
    getGridSnapshot({ ts, perturbation }),
    loadRiskWeights(),
    averageIncidentDurationMin(),
  ])

  const target = snapshot.assets.find((entry) => entry.profile.code === input.assetCode)
  if (!target) throw new ApiError(ERROR_CODES.notFound)

  const region = REGIONS.find((entry) => entry.code === target.profile.regionCode)!
  const envAt = environmentFor(region, perturbation ?? {})

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

  const context: ActionContext = {
    profile: target.profile,
    ts,
    envAt,
    perturbation,
    dnaSimilarity: target.dnaSimilarity,
    neighbourStress: target.neighbourStress,
    weights,
    resources: resourcesFor(target.profile, snapshot.assets),
    evaluateAtMin: prediction.etaMinutes ?? 45,
  }

  const candidates = evaluateCandidates(context, generateCandidates(context))

  let chosen: EvaluatedAction[]
  let applied: AppliedAction[]

  if (input.actionKeys && input.actionKeys.length > 0) {
    chosen = candidates.filter((candidate) => input.actionKeys!.includes(candidate.key))
    if (chosen.length === 0) throw new ApiError(ERROR_CODES.invalidScenario)
    applied = chosen.flatMap((candidate) => candidate.applied)
  } else {
    const strategy = buildStrategy(context, candidates, 30)
    chosen = strategy.chosen
    applied = strategy.evaluation.actions
  }

  const evaluation = evaluateStrategy(context, applied, {
    costSar: chosen.reduce((sum, candidate) => sum + candidate.costEstimateSar, 0),
    durationMin: chosen.reduce((max, candidate) => Math.max(max, candidate.durationMin), 0),
  })

  const bestSingle = candidates.find((candidate) => candidate.applied.length > 0) ?? null
  const comparison = compareStrategies(context, bestSingle, applied)

  // Avoided-impact estimate, stated as such in the UI: if the predicted failure had
  // occurred, it would have looked like the average recorded incident.
  const prevented = evaluation.riskAfter < 55 && evaluation.riskBefore >= 75
  const customersProtected = prevented ? target.profile.customersServed : 0
  const downtimeAvoidedMin = prevented ? Math.round(avgIncidentMin) : 0
  const energyProtectedMwh = prevented
    ? round((Math.max(0, target.sample.loadMw) * downtimeAvoidedMin) / 60, 1)
    : 0

  const code = `SIM-${Date.now().toString(36).toUpperCase()}`
  const assetRecord = await prisma.asset.findUnique({
    where: { code: input.assetCode },
    select: { id: true, name: true, nameAr: true, regionId: true },
  })

  const simulation = await prisma.simulation.create({
    data: {
      code,
      name: `Preventive action — ${input.assetCode}`,
      nameAr: `إجراء وقائي — ${input.assetCode}`,
      kind: input.kind ?? 'preventive_action',
      status: 'completed',
      createdById: input.userId ?? null,
      startedAt: new Date(started),
      completedAt: new Date(),
      durationMs: Date.now() - started,
      horizonMin: context.evaluateAtMin ?? 45,
      appliedActions: chosen.map((candidate) => candidate.key).join(','),
    },
  })

  await prisma.simulationResult.create({
    data: {
      simulationId: simulation.id,
      riskBefore: evaluation.riskBefore,
      riskAfter: evaluation.riskAfter,
      stabilityBefore: snapshot.stabilityIndex,
      stabilityAfter: snapshot.stabilityIndex,
      voltageBefore: target.sample.voltageKv,
      voltageAfter: target.sample.voltageKv,
      frequencyBefore: snapshot.frequencyHz,
      frequencyAfter: snapshot.frequencyHz,
      loadBeforeMw: round((evaluation.loadBeforePct / 100) * target.profile.capacityMw, 2),
      loadAfterMw: round((evaluation.loadAfterPct / 100) * target.profile.capacityMw, 2),
      assetStressBefore: evaluation.loadBeforePct,
      assetStressAfter: evaluation.loadAfterPct,
      failureProbBefore: round(Math.min(0.99, evaluation.riskBefore / 100), 4),
      failureProbAfter: round(Math.min(0.99, evaluation.riskAfter / 100), 4),
      regionsAffected: prevented ? 1 : 0,
      customersImpacted: customersProtected,
      economicImpactSar: round(energyProtectedMwh * VALUE_OF_LOST_LOAD_SAR_PER_MWH, 0),
      recoveryTimeMin: downtimeAvoidedMin,
      energyNotServedMwh: energyProtectedMwh,
      co2AvoidedTons: round(energyProtectedMwh * 0.55, 2),
      verdict: prevented
        ? 'The combined strategy holds the asset inside its operating envelope through the predicted event.'
        : 'The selected actions reduce risk but do not fully clear the predicted event.',
      verdictAr: prevented
        ? 'تُبقي الاستراتيجية المدمجة الأصل داخل نطاقه التشغيلي طوال الحدث المتوقع.'
        : 'تخفّض الإجراءات المختارة الخطر لكنها لا تزيل الحدث المتوقع بالكامل.',
    },
  })

  if (assetRecord) {
    await prisma.simulationImpact.create({
      data: {
        simulationId: simulation.id,
        assetId: assetRecord.id,
        regionId: assetRecord.regionId,
        metricKey: 'risk',
        label: assetRecord.name,
        labelAr: assetRecord.nameAr,
        baselineValue: evaluation.riskBefore,
        scenarioValue: evaluation.riskAfter,
        unit: '%',
        severity: prevented ? 'info' : 'warning',
      },
    })
  }

  return {
    code,
    simulationId: simulation.id,
    assetCode: input.assetCode,
    evaluatedAtMin: context.evaluateAtMin ?? 45,
    etaMinutes: prediction.etaMinutes,
    riskNow: target.risk.score,
    riskBefore: evaluation.riskBefore,
    riskAfter: evaluation.riskAfter,
    loadBeforePct: evaluation.loadBeforePct,
    loadAfterPct: evaluation.loadAfterPct,
    tempBeforeC: evaluation.tempBeforeC,
    tempAfterC: evaluation.tempAfterC,
    totalMw: evaluation.totalMw,
    totalCostSar: evaluation.totalCostSar,
    actions: candidates,
    chosen: chosen.map((candidate) => candidate.key),
    comparison,
    customersProtected,
    downtimeAvoidedMin,
    energyProtectedMwh,
  }
}
