/**
 * Counterfactual engine (§9, §10) — "what is the smallest change that prevents this?"
 *
 * The rest of the platform answers "what will happen". This answers the inverse, which
 * is the question an operator with a shift to run actually has: not *whether* 8 MW of
 * transfer would fix it — of course it would — but whether 3.8 MW is enough, because
 * every megawatt moved is a customer inconvenienced and a switching operation performed.
 *
 * The search is a greedy pass followed by a pruning pass. Greedy alone overshoots: it
 * adds the most efficient lever at full dose, reaches the target, and stops, leaving a
 * dose larger than necessary. The pruning pass then bisects every chosen lever back down
 * to the smallest dose that still holds the target, which is what makes the answer
 * *minimal* rather than merely sufficient.
 *
 * The engine never evaluates risk itself. It is handed a measure function so the same
 * search works for "risk below 10", "no limit breach in the horizon" and "national
 * stability above 90" without knowing anything about any of them.
 */

import type { AppliedAction } from './telemetry'
import { clamp, round } from './rng'

/** One continuously adjustable intervention. */
export interface Lever {
  key: string
  actionType: string
  assetCode: string
  unit: 'MW' | 'min' | 'step'
  /** Smallest dose worth performing at all — below this the switching is not worth it. */
  minDose: number
  maxDose: number
  costPerUnit: number
  durationMin: number
  /**
   * Operational effort of one unit, relative across levers. Deferring flexible demand
   * costs a customer something that moving load between feeders does not, and the search
   * has to be able to tell those apart when it decides what to spend.
   */
  effortPerUnit: number
  label: (dose: number) => string
  labelAr: (dose: number) => string
  toActions: (dose: number) => AppliedAction[]
}

export interface CounterfactualEvaluator {
  /** The metric being steered. Lower is better unless `higherIsBetter` is set. */
  measure: (actions: AppliedAction[]) => number
  satisfied: (metric: number) => boolean
  higherIsBetter?: boolean
}

export interface LeverDose {
  key: string
  actionType: string
  assetCode: string
  unit: Lever['unit']
  dose: number
  costSar: number
  durationMin: number
  label: string
  labelAr: string
  /** Metric points this lever removes at its final dose, measured in place. */
  contribution: number
}

export interface CounterfactualResult {
  achieved: boolean
  metricBefore: number
  metricAfter: number
  doses: LeverDose[]
  actions: AppliedAction[]
  totalMw: number
  totalCostSar: number
  totalEffort: number
  maxDurationMin: number
  iterations: number
}

/** Bisection depth. Ten halvings resolve a 40 MW lever to under 40 kW. */
const BISECTION_STEPS = 10

function better(a: number, b: number, higherIsBetter: boolean): boolean {
  return higherIsBetter ? a > b : a < b
}

function combine(doses: Map<string, number>, levers: Lever[]): AppliedAction[] {
  const actions: AppliedAction[] = []
  for (const lever of levers) {
    const dose = doses.get(lever.key) ?? 0
    if (dose <= 0) continue
    actions.push(...lever.toActions(dose))
  }
  return actions
}

/**
 * Smallest dose of `lever`, on top of `base`, that satisfies the target — or null when
 * even the full dose does not.
 */
function smallestSufficientDose(
  lever: Lever,
  base: Map<string, number>,
  levers: Lever[],
  evaluator: CounterfactualEvaluator,
  counter: { evaluations: number },
): number | null {
  const trial = (dose: number) => {
    const doses = new Map(base)
    doses.set(lever.key, dose)
    counter.evaluations += 1
    return evaluator.measure(combine(doses, levers))
  }

  if (!evaluator.satisfied(trial(lever.maxDose))) return null

  let low = 0
  let high = lever.maxDose
  for (let step = 0; step < BISECTION_STEPS; step += 1) {
    const mid = (low + high) / 2
    if (evaluator.satisfied(trial(mid))) high = mid
    else low = mid
  }
  // Round up to the lever's minimum practical dose: a switching order for 0.02 MW is
  // not an order, it is noise.
  return Math.max(lever.minDose, round(high, 2))
}

/**
 * Find the minimum intervention that reaches the target.
 *
 * Levers are ranked by measured efficiency — metric points moved per unit of effort at a
 * probe dose — rather than by a fixed preference order, so the answer adapts to what is
 * actually available around the asset at the time.
 */
export function findMinimalIntervention(
  levers: Lever[],
  evaluator: CounterfactualEvaluator,
  options: { maxLevers?: number } = {},
): CounterfactualResult {
  const higherIsBetter = evaluator.higherIsBetter ?? false
  const maxLevers = options.maxLevers ?? 4
  const counter = { evaluations: 0 }

  const usable = levers.filter((lever) => lever.maxDose > lever.minDose)
  const metricBefore = evaluator.measure([])
  counter.evaluations += 1

  const chosen = new Map<string, number>()

  if (evaluator.satisfied(metricBefore)) {
    return {
      achieved: true,
      metricBefore: round(metricBefore, 2),
      metricAfter: round(metricBefore, 2),
      doses: [],
      actions: [],
      totalMw: 0,
      totalCostSar: 0,
      totalEffort: 0,
      maxDurationMin: 0,
      iterations: counter.evaluations,
    }
  }

  // ── Efficiency probe ─────────────────────────────────────────────────────
  // Half dose, measured in isolation. Half rather than full because several levers
  // saturate near their maximum, and a saturated reading would rank them by ceiling
  // instead of by effectiveness.
  const ranked = usable
    .map((lever) => {
      const probeDose = lever.maxDose / 2
      counter.evaluations += 1
      const metric = evaluator.measure(lever.toActions(probeDose))
      const gain = higherIsBetter ? metric - metricBefore : metricBefore - metric
      const effort = Math.max(0.01, probeDose * lever.effortPerUnit)
      return { lever, efficiency: gain / effort, gain }
    })
    .filter((entry) => entry.gain > 0.01)
    .sort((a, b) => b.efficiency - a.efficiency)

  // ── Greedy accumulation ──────────────────────────────────────────────────
  let achieved = false
  for (const entry of ranked) {
    if (chosen.size >= maxLevers) break
    const dose = smallestSufficientDose(entry.lever, chosen, usable, evaluator, counter)
    if (dose !== null) {
      chosen.set(entry.lever.key, dose)
      achieved = true
      break
    }
    // Not enough on its own: commit the full dose and let the next lever close the gap.
    const withFull = new Map(chosen)
    withFull.set(entry.lever.key, entry.lever.maxDose)
    counter.evaluations += 1
    const metric = evaluator.measure(combine(withFull, usable))
    const current = evaluator.measure(combine(chosen, usable))
    counter.evaluations += 1
    if (better(metric, current, higherIsBetter)) chosen.set(entry.lever.key, entry.lever.maxDose)
  }

  // ── Pruning ──────────────────────────────────────────────────────────────
  // Walk the chosen levers from least to most efficient and shrink each one as far as
  // the target allows. Least efficient first because that is the dose worth losing.
  if (achieved) {
    const order = [...chosen.keys()].sort((a, b) => {
      const ea = ranked.find((r) => r.lever.key === a)?.efficiency ?? 0
      const eb = ranked.find((r) => r.lever.key === b)?.efficiency ?? 0
      return ea - eb
    })
    for (const key of order) {
      const lever = usable.find((l) => l.key === key)
      if (!lever) continue
      const base = new Map(chosen)
      base.delete(key)

      counter.evaluations += 1
      if (evaluator.satisfied(evaluator.measure(combine(base, usable)))) {
        // The target holds without this lever at all.
        chosen.delete(key)
        continue
      }
      const reduced = smallestSufficientDose(lever, base, usable, evaluator, counter)
      if (reduced !== null && reduced < (chosen.get(key) ?? 0)) chosen.set(key, reduced)
    }
  }

  const finalActions = combine(chosen, usable)
  const metricAfter = evaluator.measure(finalActions)
  counter.evaluations += 1

  // ── Attribution ──────────────────────────────────────────────────────────
  // Each lever's contribution is measured by removing it from the final set, which is
  // the only reading that accounts for how the levers interact.
  const doses: LeverDose[] = []
  for (const lever of usable) {
    const dose = chosen.get(lever.key)
    if (!dose) continue
    const without = new Map(chosen)
    without.delete(lever.key)
    counter.evaluations += 1
    const metricWithout = evaluator.measure(combine(without, usable))
    const contribution = higherIsBetter ? metricAfter - metricWithout : metricWithout - metricAfter
    doses.push({
      key: lever.key,
      actionType: lever.actionType,
      assetCode: lever.assetCode,
      unit: lever.unit,
      dose: round(dose, 2),
      costSar: round(dose * lever.costPerUnit, 0),
      durationMin: lever.durationMin,
      label: lever.label(round(dose, 2)),
      labelAr: lever.labelAr(round(dose, 2)),
      contribution: round(Math.max(0, contribution), 2),
    })
  }
  doses.sort((a, b) => b.contribution - a.contribution)

  return {
    achieved: evaluator.satisfied(metricAfter),
    metricBefore: round(metricBefore, 2),
    metricAfter: round(metricAfter, 2),
    doses,
    actions: finalActions,
    totalMw: round(
      doses.filter((d) => d.unit === 'MW').reduce((sum, d) => sum + d.dose, 0),
      2,
    ),
    totalCostSar: round(
      doses.reduce((sum, d) => sum + d.costSar, 0),
      0,
    ),
    totalEffort: round(
      doses.reduce((sum, d) => {
        const lever = usable.find((l) => l.key === d.key)
        return sum + d.dose * (lever?.effortPerUnit ?? 1)
      }, 0),
      3,
    ),
    maxDurationMin: doses.reduce((max, d) => Math.max(max, d.durationMin), 0),
    iterations: counter.evaluations,
  }
}

/**
 * Express a dose as a share of what was available, which is what tells an operator
 * whether the recommendation is comfortable or is spending the last of the headroom.
 */
export function doseUtilisation(dose: LeverDose, lever: Lever): number {
  return round(clamp(lever.maxDose > 0 ? dose.dose / lever.maxDose : 0, 0, 1) * 100, 1)
}
