import 'server-only'

import { round } from '../engine/rng'
import { scenarioFor } from '../engine/grid-scenarios'
import type { Perturbation } from '../engine/telemetry'
import {
  getLiveGrid,
  perturbationFor,
  type LiveGridAsset,
  type LiveGridKpis,
  type LiveGridNode,
  type LiveGridPayload,
  type StateKind,
} from './live-grid-service'

/**
 * Split future and three futures (5.0 §11–§12).
 *
 * The same grid, at the same instant, under two or three different decisions — solved by
 * the same engines and drawn in the same scene, so the only thing separating the panes is
 * the decision itself.
 *
 * Three commitments shape this file:
 *
 * **One instant, one topology.** Every future is solved from the same snapshot, against
 * the same twin, with the same stress applied. A comparison whose halves came from
 * different instants would be measuring the clock.
 *
 * **The differences are computed here, not in the browser.** A pane that let the client
 * subtract two payloads would be showing the subtraction, not the physics; and §34 keeps
 * computation server-side regardless.
 *
 * **A future that does not exist is reported absent, not invented.** When the optimiser
 * ranks only one strategy for an asset, the third pane says so. Padding the panel with a
 * fabricated alternative would be the one thing this surface cannot do and remain useful.
 *
 * Nothing here writes. Every future is labelled `simulated`, and the payload carries that
 * label to the surface rather than leaving it to be asserted.
 */

/** The three panes. `none` is the baseline every other future is differenced against. */
export type FutureKey = 'none' | 'recommended' | 'alternative'

/** The scene fields the compare panes render, narrowed from `LiveGridAsset`. */
export interface CompareAsset {
  code: string
  name: string
  nameAr: string
  assetTypeKey: string
  loadPct: number
  loadMw: number
  capacityMw: number
  activePowerMw: number
  reactivePowerMvar: number
  voltageKv: number
  voltageDeviationPct: number
  currentA: number
  ratedCurrentA: number
  powerFactor: number
  frequencyHz: number
  tempC: number
  ambientC: number
  thermalStress: number
  healthScore: number
  riskScore: number
  state: string
  isOutage: boolean
  socPct: number | null
  role: string
  customersServed: number
  rulDays: number
  dnaMatchPct: number
  cascadeDepth: number | null
  predictedRisk: number | null
  hasOpenWorkOrder: boolean
  dataQualityPct: number
  kind: string
}

export interface CompareEdge {
  from: string
  to: string
  kind: string
  sourceCode: string
  sinkCode: string
  direction: string
  activePowerMw: number
  currentA: number
  utilisationPct: number
  intensity: number
  state: string
}

export interface CompareStrategy {
  key: string
  label: string
  labelAr: string
  rank: number
  actions: Array<{ type: string; assetCode: string; magnitudeMw: number }>
  costSar: number
  executionMin: number
  confidence: number
}

export interface FutureScene {
  key: FutureKey
  /** False when the optimiser produced nothing for this pane. Never padded. */
  available: boolean
  /** Why a pane is unavailable, in the payload rather than inferred by the client. */
  unavailableReason: string | null
  /** Always `simulated` here — this surface never renders an observed outcome. */
  kind: StateKind
  strategy: CompareStrategy | null
  assets: CompareAsset[]
  edges: CompareEdge[]
  kpis: LiveGridKpis
  bottlenecks: string[]
  totalLossMw: number
}

/** One asset that the decision actually moved. */
export interface AssetDelta {
  code: string
  name: string
  nameAr: string
  loadPctBefore: number
  loadPctAfter: number
  tempCBefore: number
  tempCAfter: number
  riskBefore: number
  riskAfter: number
  stateBefore: string
  stateAfter: string
  /** Signed risk move. Negative is an improvement, and the sign is never dropped. */
  riskDelta: number
}

export interface FutureDifference {
  futureKey: FutureKey
  /** True when nothing measured moved — stated rather than shown as an empty table. */
  identical: boolean
  peakRiskDelta: number
  stabilityDelta: number
  totalLoadDelta: number
  assetsAtRiskDelta: number
  lossDelta: number
  changedCount: number
  /** Worst-first by absolute risk move, capped so a national compare stays readable. */
  assets: AssetDelta[]
}

export interface CompareFuturesResult {
  twinCode: string
  twinName: string
  twinNameAr: string
  assetCode: string | null
  scenario: string | null
  ts: number
  /** Shared by every pane: the same topology drawn three times, never re-laid out. */
  nodes: LiveGridNode[]
  radius: number
  kind: 'simulated'
  futures: FutureScene[]
  differences: FutureDifference[]
  note: string
  noteAr: string
}

/** Assets differing by less than this on every quantity are treated as unmoved. */
const MOVE_EPSILON = 0.05

/** Rows kept in one difference table. The full count is always reported alongside. */
const DELTA_CAP = 40

function toCompareAsset(asset: LiveGridAsset): CompareAsset {
  return {
    code: asset.code,
    name: asset.name,
    nameAr: asset.nameAr,
    assetTypeKey: asset.assetTypeKey,
    loadPct: asset.loadPct,
    loadMw: asset.loadMw,
    capacityMw: asset.capacityMw,
    activePowerMw: asset.activePowerMw,
    reactivePowerMvar: asset.reactivePowerMvar,
    voltageKv: asset.voltageKv,
    voltageDeviationPct: asset.voltageDeviationPct,
    currentA: asset.currentA,
    ratedCurrentA: asset.ratedCurrentA,
    powerFactor: asset.powerFactor,
    frequencyHz: asset.frequencyHz,
    tempC: asset.tempC,
    ambientC: asset.ambientC,
    thermalStress: asset.thermalStress,
    healthScore: asset.healthScore,
    riskScore: asset.riskScore,
    state: asset.state,
    isOutage: asset.isOutage,
    socPct: asset.socPct,
    role: asset.role,
    customersServed: asset.customersServed,
    rulDays: asset.rulDays,
    dnaMatchPct: asset.dnaMatchPct,
    cascadeDepth: asset.cascadeDepth,
    predictedRisk: asset.predictedRisk,
    hasOpenWorkOrder: asset.hasOpenWorkOrder,
    dataQualityPct: asset.dataQualityPct,
    kind: asset.kind,
  }
}

function toScene(
  key: FutureKey,
  payload: LiveGridPayload,
  strategy: CompareStrategy | null,
): FutureScene {
  return {
    key,
    available: true,
    unavailableReason: null,
    kind: payload.kind,
    strategy,
    assets: payload.assets.map(toCompareAsset),
    edges: payload.flow.edges.map((edge) => ({
      from: edge.from,
      to: edge.to,
      kind: edge.kind,
      sourceCode: edge.sourceCode,
      sinkCode: edge.sinkCode,
      direction: edge.direction,
      activePowerMw: edge.activePowerMw,
      currentA: edge.currentA,
      utilisationPct: edge.utilisationPct,
      intensity: edge.intensity,
      state: edge.state,
    })),
    kpis: payload.kpis,
    bottlenecks: payload.flow.bottlenecks,
    totalLossMw: payload.flow.totalLossMw,
  }
}

/**
 * What one decision changed, relative to doing nothing.
 *
 * Every quantity is differenced against the same baseline pane rather than against the
 * previous one, so a third future is a comparison with inaction, not with the second
 * future — two different questions that would otherwise share a column heading.
 */
function difference(baseline: FutureScene, future: FutureScene): FutureDifference {
  const before = new Map(baseline.assets.map((asset) => [asset.code, asset]))
  const moved: AssetDelta[] = []

  for (const after of future.assets) {
    const start = before.get(after.code)
    if (!start) continue
    const stateChanged = start.state !== after.state
    const numericMove =
      Math.abs(after.loadPct - start.loadPct) >= MOVE_EPSILON ||
      Math.abs(after.tempC - start.tempC) >= MOVE_EPSILON ||
      Math.abs(after.riskScore - start.riskScore) >= MOVE_EPSILON
    if (!stateChanged && !numericMove) continue
    moved.push({
      code: after.code,
      name: after.name,
      nameAr: after.nameAr,
      loadPctBefore: start.loadPct,
      loadPctAfter: after.loadPct,
      tempCBefore: start.tempC,
      tempCAfter: after.tempC,
      riskBefore: start.riskScore,
      riskAfter: after.riskScore,
      stateBefore: start.state,
      stateAfter: after.state,
      riskDelta: round(after.riskScore - start.riskScore, 2),
    })
  }

  moved.sort((a, b) => Math.abs(b.riskDelta) - Math.abs(a.riskDelta))

  return {
    futureKey: future.key,
    identical: moved.length === 0,
    peakRiskDelta: round(future.kpis.peakRisk - baseline.kpis.peakRisk, 2),
    stabilityDelta: round(future.kpis.stabilityIndex - baseline.kpis.stabilityIndex, 2),
    totalLoadDelta: round(future.kpis.totalLoadMw - baseline.kpis.totalLoadMw, 2),
    assetsAtRiskDelta: future.kpis.assetsAtRisk - baseline.kpis.assetsAtRisk,
    lossDelta: round(future.totalLossMw - baseline.totalLossMw, 3),
    changedCount: moved.length,
    assets: moved.slice(0, DELTA_CAP),
  }
}

function unavailable(key: FutureKey, reason: string, baseline: FutureScene): FutureScene {
  return {
    key,
    available: false,
    unavailableReason: reason,
    kind: baseline.kind,
    strategy: null,
    assets: [],
    edges: [],
    kpis: baseline.kpis,
    bottlenecks: [],
    totalLossMw: 0,
  }
}

/**
 * Solve two or three futures of the same grid.
 *
 * The baseline is the stressed grid with nothing done to it; the second applies the
 * optimiser's recommended strategy; the third, when the optimiser ranked one, applies the
 * next-best alternative. All three come from `getLiveGrid` at the same timestamp with the
 * same perturbation, so a pane differs from its neighbour by exactly the actions applied.
 */
export async function compareFutures(input: {
  twinCode?: string
  scenario?: string | null
  overlay?: Perturbation
  assetCode?: string | null
  offsetMin?: number
  ts?: number
  /** Solve the third pane too. Off by default: it costs a full extra scene solve. */
  includeAlternative?: boolean
}): Promise<CompareFuturesResult | null> {
  const { analyseAsset } = await import('./intelligence-service')
  const scenario = scenarioFor(input.scenario ?? null)
  const offsetMin = input.offsetMin ?? 0
  const ts = input.ts ?? Date.now()

  const topology = await getLiveGrid({ twinCode: input.twinCode, ts, offsetMin })
  if (!topology) return null

  const stressed: Perturbation = {
    ...(scenario.key === 'normal' ? {} : perturbationFor(scenario, topology.assets)),
    ...(input.overlay ?? {}),
  }
  const hasStress = Object.keys(stressed).length > 0

  const basePayload = await getLiveGrid({
    twinCode: input.twinCode,
    ts,
    offsetMin,
    mode: 'simulation',
    scenario: scenario.key,
    perturbation: hasStress ? stressed : undefined,
  })
  if (!basePayload) return null

  const baseline = toScene('none', basePayload, null)

  const target =
    input.assetCode ??
    [...basePayload.assets].sort((a, b) => b.riskScore - a.riskScore)[0]?.code ??
    null

  const intel = target
    ? await analyseAsset(target, {
        ts: ts + offsetMin * 60_000,
        perturbation: hasStress ? stressed : undefined,
      })
    : null

  // Strategies that actually do something. A ranked "do nothing" candidate is the baseline
  // pane by another name, and showing it twice would read as two futures that agree.
  const ranked = (intel?.optimisation.strategies ?? []).filter(
    (strategy) => strategy.kind !== 'none' && strategy.actions.length > 0,
  )
  const recommended = ranked.find((strategy) => strategy.isRecommended) ?? ranked[0] ?? null
  const alternative = ranked.find((strategy) => strategy.key !== recommended?.key) ?? null

  const solveWith = async (
    key: FutureKey,
    strategy: NonNullable<typeof recommended>,
  ): Promise<FutureScene | null> => {
    const payload = await getLiveGrid({
      twinCode: input.twinCode,
      ts,
      offsetMin,
      mode: 'simulation',
      scenario: scenario.key,
      perturbation: {
        ...stressed,
        actions: [...(stressed.actions ?? []), ...strategy.actions],
      },
    })
    if (!payload) return null
    return toScene(key, payload, {
      key: strategy.key,
      label: strategy.label,
      labelAr: strategy.labelAr,
      rank: strategy.rank,
      actions: strategy.actions.map((action) => ({
        type: action.type,
        assetCode: action.assetCode,
        magnitudeMw: round(action.magnitudeMw, 2),
      })),
      costSar: round(strategy.costSar, 0),
      executionMin: strategy.executionMin,
      confidence: round(strategy.confidence, 1),
    })
  }

  const recommendedScene = recommended
    ? ((await solveWith('recommended', recommended)) ??
      unavailable('recommended', 'The scene did not resolve under the recommended strategy.', baseline))
    : unavailable(
        'recommended',
        target
          ? 'The optimiser ranked no strategy with actions for this asset under these conditions.'
          : 'No asset in this twin to plan against.',
        baseline,
      )

  const futures: FutureScene[] = [baseline, recommendedScene]

  if (input.includeAlternative) {
    futures.push(
      alternative
        ? ((await solveWith('alternative', alternative)) ??
          unavailable('alternative', 'The scene did not resolve under the alternative strategy.', baseline))
        : unavailable(
            'alternative',
            'The optimiser ranked only one strategy worth applying here. There is no second future to draw.',
            baseline,
          ),
    )
  }

  const differences = futures
    .filter((future) => future.key !== 'none' && future.available)
    .map((future) => difference(baseline, future))

  return {
    twinCode: basePayload.twin.code,
    twinName: basePayload.twin.name,
    twinNameAr: basePayload.twin.nameAr,
    assetCode: target,
    scenario: scenario.key === 'normal' ? null : scenario.key,
    ts: basePayload.ts,
    nodes: basePayload.nodes,
    radius: basePayload.radius,
    kind: 'simulated',
    futures,
    differences,
    note: 'Every pane is a simulation of the same instant. Nothing has been applied to a grid.',
    noteAr: 'كل مشهد محاكاة للحظة نفسها. لم يُنفَّذ أي إجراء على الشبكة.',
  }
}
