import 'server-only'

import { prisma } from '../db'
import { validatePhysics } from '../engine/physics'
import {
  METRIC_LABELS,
  METRIC_UNITS,
  TRADEOFF_METRICS,
  exploreParetoFrontier,
  type ParetoResult,
  type TradeoffCandidate,
  type TradeoffMetric,
} from '../engine/pareto'
import { sampleAsset } from '../engine/telemetry'
import { environmentFor } from '../engine/grid'
import { getRegion } from '../data/regions'
import { round } from '../engine/rng'
import { analyseAsset, type AssetIntelligence } from './intelligence-service'
import { loadConstraints } from './physics-service'
import { getGridSnapshot } from './grid-service'
import { resourcesFor } from '../engine/grid'
import type { Perturbation } from '../engine/telemetry'

/**
 * The Pareto explorer's server side (§34–§39).
 *
 * The important part is not the frontier maths — that lives in the engine — but the order
 * of operations here: **every candidate is physics-validated before the frontier is
 * computed**. A strategy that breaches a hard limit is not a cheap option or a fast
 * option, it is not an option, and letting it into the dominance calculation would
 * change which of the *real* strategies looks best.
 *
 * That is the difference between an optimiser that respects engineering and one that
 * merely mentions it.
 */

export interface ParetoRequest {
  assetCode: string
  ts?: number
  perturbation?: Perturbation
  persist?: boolean
}

export interface ParetoView {
  assetCode: string
  runCode: string | null
  result: ParetoResult
  /** Column metadata for the trade-off matrix (§36). */
  metrics: Array<{
    key: TradeoffMetric
    label: string
    labelAr: string
    unit: string
    direction: 'lower_is_better' | 'higher_is_better'
  }>
  /** Raw values per candidate, so the matrix can show real figures beside the bars. */
  raw: Array<{ id: string; label: string; labelAr: string; values: Record<TradeoffMetric, number> }>
  rejectedByPhysics: number
}

/**
 * Build the frontier for one asset's strategy set.
 *
 * Reuses the strategies the optimiser already generated rather than producing a second
 * set: the Pareto view is a different *reading* of the same options, and generating them
 * twice would let the two pages recommend different things.
 */
export async function exploreStrategies(request: ParetoRequest): Promise<ParetoView | null> {
  const ts = request.ts ?? Date.now()
  const intel = await analyseAsset(request.assetCode, { ts, perturbation: request.perturbation })
  if (!intel) return null

  const [snapshot, constraints] = await Promise.all([
    getGridSnapshot({ ts, perturbation: request.perturbation }),
    loadConstraints(),
  ])

  const focus = snapshot.assets.find((entry) => entry.profile.code === request.assetCode)
  if (!focus) return null

  const region = getRegion(focus.profile.regionCode)
  const env = environmentFor(region, request.perturbation ?? {})(ts)
  const resources = resourcesFor(focus.profile, snapshot.assets)
  const regionSnapshot = snapshot.regions.find((entry) => entry.code === region.code)

  const candidates: TradeoffCandidate[] = intel.optimisation.strategies.map((strategy) => {
    const after = sampleAsset(focus.profile, ts, env, {
      ...(request.perturbation ?? {}),
      actions: strategy.actions,
    })

    const validation = validatePhysics({
      profile: focus.profile,
      after,
      actions: strategy.actions,
      resources: {
        transferCapacityMw: resources.transferCapacityMw,
        batteryPowerMw: resources.batteryPowerMw,
        flexibleLoadMw: resources.flexibleLoadMw,
      },
      reserveMarginPct: regionSnapshot?.reserveMarginPct ?? snapshot.reserveMarginPct,
      frequencyDeviationHz: Math.abs(snapshot.frequencyHz - 60),
      unavailableTargets: strategy.actions
        .map((action) => action.assetCode)
        .filter((code) => {
          const target = snapshot.assets.find((entry) => entry.profile.code === code)
          return Boolean(target && (target.sample.isOutage || target.profile.status !== 'online'))
        }),
      constraints,
    })

    return {
      id: strategy.key,
      key: strategy.key,
      label: strategy.label,
      labelAr: strategy.labelAr,
      metrics: metricsFor(strategy, intel),
      physicsRejection:
        validation.verdict === 'rejected'
          ? {
              reason: validation.failed[0]?.reason ?? validation.summary,
              reasonAr: validation.failed[0]?.reasonAr ?? validation.summaryAr,
            }
          : null,
    }
  })

  const result = exploreParetoFrontier(candidates)

  let runCode: string | null = null
  if (request.persist) {
    runCode = `PAR-${Date.now().toString(36).toUpperCase()}`
    await persist(runCode, result)
  }

  return {
    assetCode: request.assetCode,
    runCode,
    result,
    metrics: TRADEOFF_METRICS.map((key) => ({
      key,
      label: METRIC_LABELS[key][0],
      labelAr: METRIC_LABELS[key][1],
      unit: METRIC_UNITS[key],
      direction:
        key === 'risk_reduction' || key === 'confidence' ? 'higher_is_better' : 'lower_is_better',
    })),
    raw: candidates.map((candidate) => ({
      id: candidate.id,
      label: candidate.label,
      labelAr: candidate.labelAr,
      values: candidate.metrics,
    })),
    rejectedByPhysics: candidates.filter((c) => c.physicsRejection).length,
  }
}

/**
 * The eight trade-off axes for one strategy.
 *
 * Every figure comes from the optimiser's own evaluation of that strategy, so the matrix
 * and the strategy table can never disagree about what a strategy costs.
 */
function metricsFor(
  strategy: AssetIntelligence['optimisation']['strategies'][number],
  intel: AssetIntelligence,
): Record<TradeoffMetric, number> {
  const battery = strategy.actions
    .filter((action) => action.type === 'battery_discharge')
    .reduce((sum, action) => sum + Math.abs(action.magnitudeMw), 0)

  const curtailment = strategy.actions
    .filter((action) => action.type === 'renewable_curtail')
    .reduce((sum, action) => sum + Math.abs(action.magnitudeMw), 0)

  const batteryAvailable = Math.max(intel.resources.batteryPowerMw, 0.1)

  return {
    risk_reduction: round(strategy.evaluation.riskReduction, 2),
    // Cost as an index rather than currency: the money figure carries an assumed tariff
    // and belongs in the impact panel where its assumptions are printed alongside it.
    cost: round(Math.min(100, strategy.costSar / 1_000), 2),
    customer_impact: strategy.customerImpact,
    execution_time: strategy.executionMin,
    // Asset stress after the strategy, as distance into the warning band.
    asset_stress: round(Math.max(0, strategy.evaluation.loadAfterPct - 80), 2),
    battery_consumption: round((battery / batteryAvailable) * 100, 2),
    renewable_curtailment: round(curtailment, 2),
    confidence: round(strategy.confidence, 2),
  }
}

async function persist(runCode: string, result: ParetoResult): Promise<void> {
  await prisma.$transaction([
    prisma.paretoStrategy.createMany({
      data: result.entries.map((entry) => ({
        runId: runCode,
        strategyId: entry.candidate.id,
        strategyLabel: entry.candidate.label,
        strategyLabelAr: entry.candidate.labelAr,
        isOnFrontier: entry.isOnFrontier,
        corner: entry.corner,
        dominatedBy: entry.dominatedBy,
        crowdingDistance: entry.crowdingDistance,
        rank: entry.rank,
        rejectionReason: entry.rejectionReason,
        rejectionReasonAr: entry.rejectionReasonAr,
      })),
    }),
    prisma.strategyTradeoff.createMany({
      data: result.entries.flatMap((entry) =>
        TRADEOFF_METRICS.map((metric) => ({
          runId: runCode,
          strategyId: entry.candidate.id,
          metricKey: metric,
          value: entry.candidate.metrics[metric],
          normalised:
            result.matrix.find((row) => row.id === entry.candidate.id)?.values[metric] ?? 0,
          unit: METRIC_UNITS[metric],
          direction:
            metric === 'risk_reduction' || metric === 'confidence'
              ? 'higher_is_better'
              : 'lower_is_better',
        })),
      ),
    }),
  ])
}

/** Read a stored frontier back for a decision that has already been made. */
export async function loadParetoRun(runCode: string) {
  const [strategies, tradeoffs] = await Promise.all([
    prisma.paretoStrategy.findMany({ where: { runId: runCode }, orderBy: { rank: 'asc' } }),
    prisma.strategyTradeoff.findMany({ where: { runId: runCode } }),
  ])
  if (!strategies.length) return null
  return { runCode, strategies, tradeoffs }
}
