import 'server-only'

import { prisma } from '../db'
import { getRegion } from '../data/regions'
import { buildGridSnapshot } from '../engine/grid'
import { environmentFor } from '../engine/grid'
import { evaluateStrategy, type ActionResources } from '../engine/actions'
import { predictFailure } from '../engine/prediction'
import { findMinimalIntervention, type CounterfactualResult, type Lever } from '../engine/counterfactual'
import type { AppliedAction, Perturbation } from '../engine/telemetry'
import { round } from '../engine/rng'
import { getGridSnapshot, loadAssetProfiles, loadDnaPatterns, loadRiskWeights } from './grid-service'
import { analyseAsset, type AssetIntelligence } from './intelligence-service'

/**
 * The Counterfactual Explorer's server side (§9, §10).
 *
 * Three objectives an operator can actually state — get risk under a number, prevent the
 * breach outright, keep national stability above a floor — each expressed as a measure
 * function the search engine steers. The measures are deliberately the same computations
 * the rest of the console displays: the counterfactual answer is minimal *with respect
 * to the number on the screen*, not with respect to a private proxy for it.
 */

export type CounterfactualObjective = 'risk_below' | 'prevent_breach' | 'stability_above'

export interface CounterfactualInput {
  assetCode: string
  objective: CounterfactualObjective
  targetValue: number
  ts?: number
  perturbation?: Perturbation
}

export interface CounterfactualOutcome {
  assetCode: string
  objective: CounterfactualObjective
  targetValue: number
  result: CounterfactualResult
  riskBefore: number
  riskAfter: number
  horizonMin: number
  confidence: number
  summary: string
  summaryAr: string
  /** What was available to spend, so a dose can be read as a share of the headroom. */
  levers: Array<{ key: string; maxDose: number; unit: Lever['unit'] }>
}

/**
 * The levers, sized to what is physically available around the asset right now.
 *
 * `effortPerUnit` is what stops the search from reaching for demand response first
 * simply because it is effective: deferring a customer's process load costs something
 * that moving load between two feeders does not, and the ordering has to know that.
 */
/**
 * What the search is allowed to spend.
 *
 * Takes the three things it actually needs rather than a whole `AssetIntelligence`, so
 * the Risk Hunter can call it from a snapshot it already holds without paying for a
 * second full analysis. The alternative — a parallel lever list in the hunter — would
 * give the two surfaces different ideas of what a minimum intervention costs.
 */
export interface LeverContext {
  assetCode: string
  resources: ActionResources
  /** Load actually flowing through the asset, which bounds every lever. */
  loadMw: number
}

export function buildLevers(context: LeverContext | AssetIntelligence): Lever[] {
  const input: LeverContext =
    'focus' in context
      ? {
          assetCode: context.assetCode,
          resources: context.resources,
          loadMw: context.focus.sample.loadMw,
        }
      : context

  const code = input.assetCode
  const { resources } = input
  const levers: Lever[] = []

  // No lever may be larger than the load actually flowing through the asset. The region
  // can hold 654 MW of battery, but dispatching it "behind" a 63 MW transformer is not a
  // bigger intervention — it is a physically meaningless one, and an unbounded search
  // will happily walk there because the arithmetic keeps working after the physics stops.
  const removableMw = Math.max(0.5, Math.abs(input.loadMw))
  const cap = (available: number) => round(Math.min(available, removableMw), 1)

  if (resources.transferCapacityMw > 0.5) {
    levers.push({
      key: 'load_transfer',
      actionType: 'load_transfer',
      assetCode: code,
      unit: 'MW',
      minDose: 0.3,
      maxDose: cap(resources.transferCapacityMw),
      costPerUnit: 950,
      durationMin: 12,
      effortPerUnit: 1,
      label: (dose) => `Transfer ${dose} MW to adjacent feeders`,
      labelAr: (dose) => `تحويل ${dose} ميجاواط إلى المغذيات المجاورة`,
      toActions: (dose) => [{ type: 'load_transfer', assetCode: code, magnitudeMw: dose }],
    })
  }

  if (resources.batteryPowerMw > 0.5) {
    levers.push({
      key: 'battery_discharge',
      actionType: 'battery_discharge',
      assetCode: code,
      unit: 'MW',
      minDose: 0.3,
      maxDose: cap(resources.batteryPowerMw),
      costPerUnit: 540,
      durationMin: 45,
      effortPerUnit: 0.7,
      label: (dose) => `Dispatch ${dose} MW from battery storage`,
      labelAr: (dose) => `تشغيل ${dose} ميجاواط من تخزين البطاريات`,
      toActions: (dose) => [{ type: 'battery_discharge', assetCode: code, magnitudeMw: dose }],
    })
  }

  if (resources.flexibleLoadMw > 0.4) {
    levers.push({
      key: 'demand_response',
      actionType: 'demand_response',
      assetCode: code,
      unit: 'MW',
      minDose: 0.3,
      maxDose: cap(resources.flexibleLoadMw),
      costPerUnit: 1_400,
      durationMin: 60,
      effortPerUnit: 2.4,
      label: (dose) => `Defer ${dose} MW of contracted flexible demand`,
      labelAr: (dose) => `تأجيل ${dose} ميجاواط من الأحمال المرنة المتعاقد عليها`,
      toActions: (dose) => [{ type: 'demand_response', assetCode: code, magnitudeMw: dose }],
    })
  }

  if (resources.coolingHeadroom) {
    // A step lever: forced cooling is either engaged or it is not, so the search is
    // allowed only to decide whether to spend it, not how much of it to spend.
    levers.push({
      key: 'cooling_boost',
      actionType: 'cooling_boost',
      assetCode: code,
      unit: 'step',
      minDose: 1,
      maxDose: 1,
      costPerUnit: 800,
      durationMin: 5,
      effortPerUnit: 0.4,
      label: () => 'Force all cooling stages online',
      labelAr: () => 'تشغيل جميع مراحل التبريد',
      toActions: (dose) =>
        dose >= 0.5 ? [{ type: 'cooling_boost', assetCode: code, magnitudeMw: 0 }] : [],
    })
  }

  return levers
}

/**
 * Build the measure the search steers, per objective.
 *
 * Every measure is synchronous: the search calls it dozens of times, and a version that
 * went back to the database on each call would turn a sub-second search into a timeout.
 * The snapshot inputs are therefore read once, up front, and the pure engine is used for
 * the rest.
 */
async function buildEvaluator(intel: AssetIntelligence, input: CounterfactualInput) {
  const horizonMin = intel.prediction.horizonMin
  const region = getRegion(intel.focus.profile.regionCode)

  if (input.objective === 'risk_below') {
    return {
      measure: (actions: AppliedAction[]) => evaluateStrategy(intel.actionContext, actions).riskAfter,
      satisfied: (metric: number) => metric <= input.targetValue,
      higherIsBetter: false,
    }
  }

  if (input.objective === 'prevent_breach') {
    const weights = await loadRiskWeights()
    return {
      // Minutes to the first breach, with "no breach in the horizon" represented as one
      // minute past it so the search has a gradient to climb rather than a cliff.
      measure: (actions: AppliedAction[]) => {
        const perturbation: Perturbation = {
          ...intel.perturbation,
          actions: [...(intel.perturbation.actions ?? []), ...actions],
        }
        const prediction = predictFailure(intel.focus.profile, intel.ts, environmentFor(region, perturbation), {
          perturbation,
          dnaSimilarity: intel.focus.dnaSimilarity,
          neighbourStress: intel.focus.neighbourStress,
          weights,
          horizonMin,
          stepMin: 5,
        })
        return prediction.etaMinutes ?? horizonMin + 1
      },
      satisfied: (metric: number) => metric > horizonMin,
      higherIsBetter: true,
    }
  }

  const [profiles, patterns, weights] = await Promise.all([
    loadAssetProfiles(),
    loadDnaPatterns(),
    loadRiskWeights(),
  ])
  return {
    measure: (actions: AppliedAction[]) => {
      const perturbation: Perturbation = {
        ...intel.perturbation,
        actions: [...(intel.perturbation.actions ?? []), ...actions],
      }
      return buildGridSnapshot(profiles, intel.ts, { perturbation, patterns, weights }).stabilityIndex
    },
    satisfied: (metric: number) => metric >= input.targetValue,
    higherIsBetter: true,
  }
}

const OBJECTIVE_COPY: Record<CounterfactualObjective, { en: string; ar: string }> = {
  risk_below: { en: 'risk below', ar: 'خفض الخطر دون' },
  prevent_breach: { en: 'prevent the predicted breach', ar: 'منع التجاوز المتوقع' },
  stability_above: { en: 'stability above', ar: 'إبقاء الاستقرار فوق' },
}

export async function runCounterfactual(
  input: CounterfactualInput,
): Promise<CounterfactualOutcome | null> {
  const intel = await analyseAsset(input.assetCode, {
    ts: input.ts,
    perturbation: input.perturbation,
  })
  if (!intel) return null

  const levers = buildLevers(intel)
  const evaluator = await buildEvaluator(intel, input)
  const result = findMinimalIntervention(levers, evaluator)

  const riskBefore = intel.actionContext ? evaluateStrategy(intel.actionContext, []).riskAfter : 0
  const riskAfter = evaluateStrategy(intel.actionContext, result.actions).riskAfter

  // Confidence in a counterfactual is confidence in the twin that measured it, discounted
  // when the answer sits close to the ceiling of what was available: a solution that
  // spends 96 % of the headroom is one bad assumption away from not existing.
  const utilisation =
    levers.length > 0
      ? result.doses.reduce((max, dose) => {
          const lever = levers.find((entry) => entry.key === dose.key)
          return Math.max(max, lever && lever.maxDose > 0 ? dose.dose / lever.maxDose : 0)
        }, 0)
      : 0
  const confidence = round(
    Math.max(
      25,
      Math.min(intel.prediction.confidence, intel.dataQuality.score) * (result.achieved ? 1 : 0.7) -
        utilisation * 14,
    ),
    1,
  )

  const objectiveCopy = OBJECTIVE_COPY[input.objective]
  const summary = result.achieved
    ? `${result.doses.length} lever(s) reach ${objectiveCopy.en} ${input.objective === 'prevent_breach' ? '' : input.targetValue}: ${result.doses.map((d) => d.label).join(' + ')}. Risk ${round(riskBefore, 1)} % → ${round(riskAfter, 1)} %.`
    : `No combination of the ${levers.length} available levers reaches the target; the closest achievable outcome moves the measure from ${result.metricBefore} to ${result.metricAfter}.`
  const summaryAr = result.achieved
    ? `${result.doses.length} إجراء يحقق ${objectiveCopy.ar} ${input.objective === 'prevent_breach' ? '' : input.targetValue}: ${result.doses.map((d) => d.labelAr).join(' + ')}. الخطر ${round(riskBefore, 1)}٪ ← ${round(riskAfter, 1)}٪.`
    : `لا يوجد مزيج من الإجراءات المتاحة (${levers.length}) يحقق الهدف؛ وأقرب نتيجة ممكنة تنقل المؤشر من ${result.metricBefore} إلى ${result.metricAfter}.`

  return {
    assetCode: input.assetCode,
    objective: input.objective,
    targetValue: input.targetValue,
    result,
    riskBefore: round(riskBefore, 1),
    riskAfter: round(riskAfter, 1),
    horizonMin: intel.prediction.horizonMin,
    confidence,
    summary,
    summaryAr,
    levers: levers.map((lever) => ({ key: lever.key, maxDose: lever.maxDose, unit: lever.unit })),
  }
}

export async function persistCounterfactual(
  outcome: CounterfactualOutcome,
  options: { userId?: string | null; ts?: number } = {},
): Promise<string> {
  const asset = await prisma.asset.findUnique({
    where: { code: outcome.assetCode },
    select: { id: true },
  })
  const ts = options.ts ?? Date.now()
  const code = `CF-${ts.toString(36).toUpperCase()}-${outcome.assetCode}`

  await prisma.counterfactualRun.create({
    data: {
      code,
      assetId: asset?.id ?? null,
      assetCode: outcome.assetCode,
      objective: outcome.objective,
      targetValue: outcome.targetValue,
      riskBefore: outcome.riskBefore,
      riskAfter: outcome.riskAfter,
      achieved: outcome.result.achieved,
      iterations: outcome.result.iterations,
      totalMw: outcome.result.totalMw,
      totalCostSar: outcome.result.totalCostSar,
      confidence: outcome.confidence,
      horizonMin: outcome.horizonMin,
      summary: outcome.summary,
      summaryAr: outcome.summaryAr,
      createdById: options.userId ?? null,
      actions: {
        create: outcome.result.doses.map((dose, index) => ({
          ordinal: index,
          actionType: dose.actionType,
          actionKey: dose.key,
          magnitudeMw: dose.unit === 'MW' ? dose.dose : 0,
          durationMin: dose.durationMin,
          unit: dose.unit,
          label: dose.label,
          labelAr: dose.labelAr,
          contribution: dose.contribution,
        })),
      },
    },
  })

  return code
}

export async function listCounterfactualRuns(limit = 20) {
  return prisma.counterfactualRun.findMany({
    orderBy: { createdAt: 'desc' },
    take: limit,
    include: { actions: { orderBy: { ordinal: 'asc' } } },
  })
}

export { getGridSnapshot }
