/**
 * Optimisation engine (§11) and the Self-Healing Lab's candidate generator (§12).
 *
 * "Recommended" has to mean something. Here it means: several genuinely different
 * recovery strategies were constructed, each one was run through the digital twin, each
 * was scored against the same nine-term objective, and one of them won on the numbers.
 * Change the weights — a night shift that cannot call out crews, a peak hour where
 * customer impact dominates — and a different strategy wins, which is the point of
 * having an objective function rather than a preference list.
 *
 * Nothing is executed on a real grid from here (§43). The output is a ranking.
 */

import type { ActionType } from '../domain/enums'
import {
  evaluateStrategy,
  buildStrategy,
  type ActionContext,
  type EvaluatedAction,
  type StrategyEvaluation,
} from './actions'
import type { AppliedAction } from './telemetry'
import { clamp, round } from './rng'

export const OBJECTIVE_METRICS = [
  'riskReduction',
  'operationalCost',
  'customerImpact',
  'executionTime',
  'assetStress',
  'renewableCurtailment',
  'batteryUsage',
  'loadReduction',
  'maintenanceCost',
] as const
export type ObjectiveMetric = (typeof OBJECTIVE_METRICS)[number]

export type ObjectiveWeights = Record<ObjectiveMetric, number>

/**
 * Presets an operator can actually justify to a supervisor. `balanced` is the default;
 * the others exist because the right trade-off genuinely differs by shift and season.
 */
export const OBJECTIVE_PRESETS: Record<string, ObjectiveWeights> = {
  balanced: {
    riskReduction: 0.34,
    operationalCost: 0.12,
    customerImpact: 0.16,
    executionTime: 0.12,
    assetStress: 0.12,
    renewableCurtailment: 0.04,
    batteryUsage: 0.04,
    loadReduction: 0.03,
    maintenanceCost: 0.03,
  },
  risk_first: {
    riskReduction: 0.58,
    operationalCost: 0.05,
    customerImpact: 0.1,
    executionTime: 0.1,
    assetStress: 0.11,
    renewableCurtailment: 0.02,
    batteryUsage: 0.02,
    loadReduction: 0.01,
    maintenanceCost: 0.01,
  },
  cost_first: {
    riskReduction: 0.25,
    operationalCost: 0.34,
    customerImpact: 0.12,
    executionTime: 0.07,
    assetStress: 0.08,
    renewableCurtailment: 0.04,
    batteryUsage: 0.04,
    loadReduction: 0.03,
    maintenanceCost: 0.03,
  },
  speed_first: {
    riskReduction: 0.3,
    operationalCost: 0.07,
    customerImpact: 0.1,
    executionTime: 0.36,
    assetStress: 0.08,
    renewableCurtailment: 0.03,
    batteryUsage: 0.03,
    loadReduction: 0.02,
    maintenanceCost: 0.01,
  },
  customer_first: {
    riskReduction: 0.3,
    operationalCost: 0.08,
    customerImpact: 0.38,
    executionTime: 0.08,
    assetStress: 0.08,
    renewableCurtailment: 0.03,
    batteryUsage: 0.03,
    loadReduction: 0.01,
    maintenanceCost: 0.01,
  },
}

/** Whether a larger raw value is better for this term. */
const HIGHER_IS_BETTER: Record<ObjectiveMetric, boolean> = {
  riskReduction: true,
  operationalCost: false,
  customerImpact: false,
  executionTime: false,
  assetStress: false,
  renewableCurtailment: false,
  batteryUsage: false,
  loadReduction: false,
  maintenanceCost: false,
}

export const OBJECTIVE_UNITS: Record<ObjectiveMetric, string> = {
  riskReduction: 'pts',
  operationalCost: 'SAR',
  customerImpact: 'customers',
  executionTime: 'min',
  assetStress: '%',
  renewableCurtailment: 'MW',
  batteryUsage: 'MWh',
  loadReduction: 'MW',
  maintenanceCost: 'SAR',
}

export interface StrategyCandidate {
  key: string
  kind: 'minimal' | 'balanced' | 'defence' | 'maintenance' | 'counterfactual' | 'manual' | 'none'
  label: string
  labelAr: string
  actions: AppliedAction[]
  /** Chosen candidate actions, for cost and duration; may be empty for derived sets. */
  sourceActions: EvaluatedAction[]
}

export interface ScoredMetric {
  key: ObjectiveMetric
  value: number
  normalised: number
  weight: number
  unit: string
}

export interface ScoredStrategy extends StrategyCandidate {
  rank: number
  score: number
  evaluation: StrategyEvaluation
  metrics: ScoredMetric[]
  confidence: number
  customerImpact: number
  costSar: number
  executionMin: number
  isRecommended: boolean
}

const MS_PER_MIN = 60_000

/** Customers inconvenienced by a strategy: whoever sits behind the demand actually cut. */
function customersAffected(ctx: ActionContext, actions: AppliedAction[]): number {
  const curtailedMw = actions
    .filter((a) => a.type === 'demand_response')
    .reduce((sum, a) => sum + a.magnitudeMw, 0)
  if (curtailedMw <= 0) return 0
  const assetMw = Math.max(0.1, (ctx.profile.capacityMw * ctx.profile.baselineLoadPct) / 100)
  return Math.round(ctx.profile.customersServed * clamp(curtailedMw / assetMw, 0, 1))
}

function sumMagnitude(actions: AppliedAction[], type: ActionType | string): number {
  return actions.filter((a) => a.type === type).reduce((sum, a) => sum + a.magnitudeMw, 0)
}

/**
 * Raw objective values for one candidate. Every term is read off the simulated outcome
 * or off the actions themselves — none of them is a stored constant.
 */
function rawMetrics(
  ctx: ActionContext,
  candidate: StrategyCandidate,
  evaluation: StrategyEvaluation,
): Record<ObjectiveMetric, number> {
  const costSar = candidate.sourceActions.reduce((sum, a) => sum + a.costEstimateSar, 0)
  const executionMin = candidate.sourceActions.reduce((max, a) => Math.max(max, a.durationMin), 0)
  const maintenanceCost = candidate.sourceActions
    .filter((a) => a.actionType === 'maintenance_dispatch')
    .reduce((sum, a) => sum + a.costEstimateSar, 0)

  const batteryMw = sumMagnitude(candidate.actions, 'battery_discharge')
  const batteryHours =
    candidate.sourceActions.find((a) => a.actionType === 'battery_discharge')?.durationMin ?? 0

  return {
    riskReduction: round(evaluation.riskReduction, 2),
    operationalCost: round(costSar, 0),
    customerImpact: customersAffected(ctx, candidate.actions),
    executionTime: executionMin,
    // Stress left on the asset after the strategy: how hard it is still working.
    assetStress: round(Math.max(0, evaluation.loadAfterPct), 2),
    renewableCurtailment: round(sumMagnitude(candidate.actions, 'generation_redispatch'), 2),
    batteryUsage: round((batteryMw * batteryHours) / 60, 2),
    loadReduction: round(
      sumMagnitude(candidate.actions, 'demand_response') + sumMagnitude(candidate.actions, 'load_transfer'),
      2,
    ),
    maintenanceCost: round(maintenanceCost, 0),
  }
}

/**
 * Min–max normalisation across the candidate set, oriented so 1 is always "better".
 *
 * Normalising within the set rather than against absolute scales is deliberate: the
 * question is which of *these* strategies to pick, and a SAR figure only means something
 * next to the alternatives on the table tonight.
 */
function normalise(values: number[], higherIsBetter: boolean): number[] {
  const min = Math.min(...values)
  const max = Math.max(...values)
  if (max - min < 1e-9) return values.map(() => 1)
  return values.map((value) => {
    const scaled = (value - min) / (max - min)
    return round(higherIsBetter ? scaled : 1 - scaled, 4)
  })
}

/**
 * Confidence in a strategy: how much of its benefit survives being measured at a
 * different moment. A strategy whose advantage evaporates fifteen minutes either side of
 * the evaluation point is a coincidence of timing, not a plan.
 */
function strategyConfidence(ctx: ActionContext, candidate: StrategyCandidate, baseline: number): number {
  if (candidate.actions.length === 0) return 60
  const horizon = ctx.evaluateAtMin ?? 45
  const probes = [horizon - 15, horizon, horizon + 15].filter((min) => min > 0)
  const reductions = probes.map((min) => {
    const probeCtx: ActionContext = { ...ctx, evaluateAtMin: min }
    return evaluateStrategy(probeCtx, candidate.actions).riskReduction
  })
  const mean = reductions.reduce((sum, r) => sum + r, 0) / reductions.length
  if (mean <= 0) return 35
  const spread = Math.max(...reductions) - Math.min(...reductions)
  const stability = clamp(1 - spread / Math.max(mean, 1), 0, 1)
  const strength = clamp(mean / Math.max(baseline * 0.6, 1), 0, 1)
  return round(clamp(46 + stability * 34 + strength * 20, 0, 97), 1)
}

/**
 * Build genuinely different recovery strategies from the same candidate pool (§12).
 *
 * They differ in posture, not in wording: the smallest single move; the greedy strategy
 * that stops once risk is back in the watch band; a deeper one that keeps going to a
 * comfortable margin; and a condition-led one that pairs a light operational move with
 * an inspection so the underlying cause is actually closed out.
 */
export function generateStrategies(
  ctx: ActionContext,
  candidates: EvaluatedAction[],
): StrategyCandidate[] {
  const operational = candidates.filter((c) => c.applied.length > 0)
  const strategies: StrategyCandidate[] = [
    {
      key: 'none',
      kind: 'none',
      label: 'No action',
      labelAr: 'بدون إجراء',
      actions: [],
      sourceActions: [],
    },
  ]

  const best = operational[0]
  if (best) {
    strategies.push({
      key: 'minimal',
      kind: 'minimal',
      label: `Single move — ${best.title}`,
      labelAr: `تحرك منفرد — ${best.titleAr}`,
      actions: best.applied,
      sourceActions: [best],
    })
  }

  const balanced = buildStrategy(ctx, candidates, 30)
  if (balanced.chosen.length > 0) {
    strategies.push({
      key: 'balanced',
      kind: 'balanced',
      label: `Balanced strategy — ${balanced.chosen.length} coordinated actions`,
      labelAr: `استراتيجية متوازنة — ${balanced.chosen.length} إجراءات منسقة`,
      actions: balanced.evaluation.actions,
      sourceActions: balanced.chosen,
    })
  }

  const defence = buildStrategy(ctx, candidates, 12)
  if (defence.chosen.length > balanced.chosen.length) {
    strategies.push({
      key: 'defence',
      kind: 'defence',
      label: `Defence in depth — ${defence.chosen.length} actions to a wide margin`,
      labelAr: `دفاع متعمق — ${defence.chosen.length} إجراءات بهامش واسع`,
      actions: defence.evaluation.actions,
      sourceActions: defence.chosen,
    })
  }

  const maintenance = candidates.find((c) => c.actionType === 'maintenance_dispatch')
  const gentle = operational.find((c) => c.actionType === 'cooling_boost') ?? operational.at(-1)
  if (maintenance && gentle) {
    strategies.push({
      key: 'maintenance_led',
      kind: 'maintenance',
      label: `Condition-led — ${gentle.title} plus inspection`,
      labelAr: `معالجة الحالة — ${gentle.titleAr} مع فحص ميداني`,
      actions: gentle.applied,
      sourceActions: [gentle, maintenance],
    })
  }

  // Deduplicate by the action set: two postures that resolve to the same switching
  // orders are the same strategy, and presenting them twice would inflate the choice.
  const seen = new Set<string>()
  return strategies.filter((strategy) => {
    const signature = strategy.actions
      .map((a) => `${a.type}:${a.assetCode}:${round(a.magnitudeMw, 2)}`)
      .sort()
      .join('|')
    if (seen.has(signature)) return false
    seen.add(signature)
    return true
  })
}

export interface OptimizationResult {
  objectiveKey: string
  weights: ObjectiveWeights
  strategies: ScoredStrategy[]
  best: ScoredStrategy | null
}

/**
 * Score and rank the candidates. The winner is whichever maximises the weighted sum;
 * "no action" competes on the same terms, and wins when nothing is actually wrong.
 */
export function optimiseStrategies(
  ctx: ActionContext,
  candidates: StrategyCandidate[],
  options: { objectiveKey?: string; weights?: Partial<ObjectiveWeights> } = {},
): OptimizationResult {
  const objectiveKey = options.objectiveKey ?? 'balanced'
  const preset = OBJECTIVE_PRESETS[objectiveKey] ?? OBJECTIVE_PRESETS.balanced
  const weights: ObjectiveWeights = { ...preset, ...options.weights }

  const evaluated = candidates.map((candidate) => {
    const evaluation = evaluateStrategy(ctx, candidate.actions, {
      costSar: candidate.sourceActions.reduce((sum, a) => sum + a.costEstimateSar, 0),
      durationMin: candidate.sourceActions.reduce((max, a) => Math.max(max, a.durationMin), 0),
    })
    return { candidate, evaluation, raw: rawMetrics(ctx, candidate, evaluation) }
  })

  const normalisedByMetric = new Map<ObjectiveMetric, number[]>()
  for (const metric of OBJECTIVE_METRICS) {
    normalisedByMetric.set(
      metric,
      normalise(
        evaluated.map((entry) => entry.raw[metric]),
        HIGHER_IS_BETTER[metric],
      ),
    )
  }

  const baselineRisk = evaluated.find((e) => e.candidate.kind === 'none')?.evaluation.riskBefore ?? 0

  const scored: ScoredStrategy[] = evaluated.map((entry, index) => {
    const metrics: ScoredMetric[] = OBJECTIVE_METRICS.map((metric) => ({
      key: metric,
      value: entry.raw[metric],
      normalised: normalisedByMetric.get(metric)![index],
      weight: weights[metric],
      unit: OBJECTIVE_UNITS[metric],
    }))
    const score = round(
      metrics.reduce((sum, metric) => sum + metric.normalised * metric.weight, 0) * 100,
      2,
    )
    return {
      ...entry.candidate,
      rank: 0,
      score,
      evaluation: entry.evaluation,
      metrics,
      confidence: strategyConfidence(ctx, entry.candidate, baselineRisk),
      customerImpact: entry.raw.customerImpact,
      costSar: entry.raw.operationalCost,
      executionMin: entry.raw.executionTime,
      isRecommended: false,
    }
  })

  scored.sort((a, b) => b.score - a.score)
  scored.forEach((strategy, index) => {
    strategy.rank = index + 1
  })
  const best = scored[0] ?? null
  if (best) best.isRecommended = true

  return { objectiveKey, weights, strategies: scored, best }
}

/** Evaluation horizon in milliseconds, shared by callers that need the same instant. */
export function horizonMs(ctx: ActionContext): number {
  return ctx.ts + (ctx.evaluateAtMin ?? 45) * MS_PER_MIN
}
