/**
 * Preventive action engine (§7, §24).
 *
 * Candidate actions are generated from the physical resources actually available around
 * the at-risk asset (neighbouring spare capacity, battery energy, deferrable demand),
 * and each one's benefit is measured by *re-running the digital twin with the action
 * applied* — not by looking up a stored percentage. That is what makes "78% → 9%"
 * reproducible and what the SIMULATE button actually does.
 */

import type { ActionType } from '../domain/enums'
import type { EnvironmentProvider } from './prediction'
import { assessRisk, type RiskWeights } from './risk'
import {
  type AppliedAction,
  type AssetProfile,
  type Perturbation,
  NO_PERTURBATION,
  sampleAsset,
} from './telemetry'
import { clamp, round } from './rng'

const MS_PER_MIN = 60_000

/** Resources the operator can actually call on for this asset. */
export interface ActionResources {
  /** Spare MW available on adjacent feeders that can absorb transferred load. */
  transferCapacityMw: number
  /** Battery power the region can discharge for the duration of the event. */
  batteryPowerMw: number
  /** Deferrable demand behind this asset (industrial, EV, thermal storage). */
  flexibleLoadMw: number
  /** Whether forced cooling can still be stepped up. */
  coolingHeadroom: boolean
  /** Whether an alternative network configuration exists. */
  reconfigurable: boolean
}

export interface ActionCandidate {
  key: string
  actionType: ActionType
  title: string
  titleAr: string
  description: string
  descriptionAr: string
  magnitudeMw: number
  durationMin: number
  costEstimateSar: number
  requiresRole: string
  applied: AppliedAction[]
}

export interface EvaluatedAction extends ActionCandidate {
  riskBefore: number
  riskAfter: number
  /** Absolute drop in risk points attributable to this action alone. */
  riskReduction: number
  loadAfterPct: number
  tempAfterC: number
}

export interface StrategyEvaluation {
  riskBefore: number
  riskAfter: number
  riskReduction: number
  loadBeforePct: number
  loadAfterPct: number
  tempBeforeC: number
  tempAfterC: number
  actions: AppliedAction[]
  totalMw: number
  totalCostSar: number
  maxDurationMin: number
}

export interface ActionContext {
  profile: AssetProfile
  ts: number
  envAt: EnvironmentProvider
  perturbation?: Perturbation
  dnaSimilarity?: number
  neighbourStress?: number
  weights?: RiskWeights
  resources: ActionResources
  /** When the benefit is measured — normally the predicted time of failure. */
  evaluateAtMin?: number
}

/** Risk of the asset at the evaluation horizon with a given set of actions applied. */
function riskWithActions(ctx: ActionContext, actions: AppliedAction[]) {
  const horizonMin = ctx.evaluateAtMin ?? 45
  const ts = ctx.ts + horizonMin * MS_PER_MIN
  const env = ctx.envAt(ts)
  const perturbation: Perturbation = {
    ...(ctx.perturbation ?? NO_PERTURBATION),
    actions: [...(ctx.perturbation?.actions ?? []), ...actions],
  }
  const sample = sampleAsset(ctx.profile, ts, env, perturbation)
  const risk = assessRisk({
    profile: ctx.profile,
    sample,
    env,
    // A relieved asset no longer resembles the historical failure signature; the DNA
    // prior is scaled by how much of the loading excursion has been removed.
    dnaSimilarity: scaleDnaPrior(ctx, sample.loadPct, sample.tempC),
    neighbourStress: ctx.neighbourStress ?? 0,
    weights: ctx.weights,
  })
  return { sample, risk }
}

/**
 * A Failure-DNA match is a statement about the *current* signature. Once loading and
 * temperature come down, the signature no longer matches, so the prior must fall with
 * them rather than staying pinned at its pre-action value.
 */
function scaleDnaPrior(ctx: ActionContext, loadPct: number, tempC: number): number {
  const base = ctx.dnaSimilarity ?? 0
  if (base <= 0) return 0
  const baselineTs = ctx.ts + (ctx.evaluateAtMin ?? 45) * MS_PER_MIN
  const baseline = sampleAsset(
    ctx.profile,
    baselineTs,
    ctx.envAt(baselineTs),
    ctx.perturbation ?? NO_PERTURBATION,
  )
  const loadRelief = clamp(
    (baseline.loadPct - loadPct) / Math.max(baseline.loadPct * 0.35, 1),
    0,
    1,
  )
  const tempRelief = clamp((baseline.tempC - tempC) / Math.max(baseline.tempC * 0.25, 1), 0, 1)
  const relief = clamp(0.65 * loadRelief + 0.35 * tempRelief, 0, 1)
  return round(base * (1 - relief), 1)
}

/**
 * Generate the candidate actions that are physically possible for this asset.
 * Magnitudes are sized to the actual excess loading, capped by available resources.
 */
export function generateCandidates(ctx: ActionContext): ActionCandidate[] {
  const { profile, resources } = ctx
  const horizonMin = ctx.evaluateAtMin ?? 45
  const ts = ctx.ts + horizonMin * MS_PER_MIN
  const env = ctx.envAt(ts)
  const sample = sampleAsset(profile, ts, env, ctx.perturbation ?? NO_PERTURBATION)

  // Target: bring loading back under the stress threshold with a safety margin.
  const targetPct = 62
  const excessMw = Math.max(0, ((sample.loadPct - targetPct) / 100) * profile.capacityMw)
  const candidates: ActionCandidate[] = []

  if (excessMw > 0.4 && resources.transferCapacityMw > 0.4) {
    const mw = round(Math.min(excessMw * 0.55, resources.transferCapacityMw), 1)
    candidates.push({
      key: 'load_transfer',
      actionType: 'load_transfer',
      title: `Transfer ${mw} MW to the adjacent feeder`,
      titleAr: `تحويل ${mw} ميجاواط إلى المغذي المجاور`,
      description:
        'Switch part of the load onto neighbouring circuits that still have thermal headroom, cutting the loading on the at-risk unit without shedding any customer.',
      descriptionAr:
        'تحويل جزء من الحمل إلى الدوائر المجاورة التي ما زالت تملك هامشًا حراريًا، مما يخفض التحميل على الوحدة المهددة دون فصل أي مشترك.',
      magnitudeMw: mw,
      durationMin: 12,
      costEstimateSar: round(mw * 950, 0),
      requiresRole: 'control_operator',
      applied: [{ type: 'load_transfer', assetCode: profile.code, magnitudeMw: mw }],
    })
  }

  if (resources.batteryPowerMw > 0.4 && excessMw > 0.2) {
    const mw = round(Math.min(excessMw * 0.4, resources.batteryPowerMw), 1)
    candidates.push({
      key: 'battery_discharge',
      actionType: 'battery_discharge',
      title: `Discharge ${mw} MW from battery storage`,
      titleAr: `تفريغ ${mw} ميجاواط من تخزين البطاريات`,
      description:
        'Dispatch regional battery storage behind the constraint so stored energy — not the transformer — serves the peak.',
      descriptionAr:
        'تشغيل تخزين البطاريات الإقليمي خلف نقطة الاختناق بحيث تخدم الطاقة المخزنة الذروة بدلًا من المحول.',
      magnitudeMw: mw,
      durationMin: 45,
      costEstimateSar: round(mw * 45 * 12, 0),
      requiresRole: 'control_operator',
      applied: [{ type: 'battery_discharge', assetCode: profile.code, magnitudeMw: mw }],
    })
  }

  if (resources.flexibleLoadMw > 0.3 && excessMw > 0.2) {
    const mw = round(Math.min(excessMw * 0.35, resources.flexibleLoadMw), 1)
    candidates.push({
      key: 'demand_response',
      actionType: 'demand_response',
      title: `Defer ${mw} MW of flexible demand`,
      titleAr: `تأجيل ${mw} ميجاواط من الأحمال المرنة`,
      description:
        'Call the pre-contracted demand-response block — industrial process load and EV charging — for the duration of the peak window.',
      descriptionAr:
        'تفعيل كتلة الاستجابة للطلب المتعاقد عليها مسبقًا — أحمال العمليات الصناعية وشحن المركبات الكهربائية — طوال نافذة الذروة.',
      magnitudeMw: mw,
      durationMin: 60,
      costEstimateSar: round(mw * 1_400, 0),
      requiresRole: 'control_operator',
      applied: [{ type: 'demand_response', assetCode: profile.code, magnitudeMw: mw }],
    })
  }

  if (resources.coolingHeadroom && sample.tempC > profile.ratedTempC - 25) {
    candidates.push({
      key: 'cooling_boost',
      actionType: 'cooling_boost',
      title: 'Force cooling to maximum',
      titleAr: 'رفع التبريد القسري إلى الحد الأقصى',
      description:
        'Bring every cooling stage online immediately instead of waiting for the automatic thermostat step, buying roughly 18 % of the temperature rise back.',
      descriptionAr:
        'تشغيل جميع مراحل التبريد فورًا بدلًا من انتظار خطوة الثرموستات التلقائية، مما يستعيد نحو ١٨٪ من الارتفاع الحراري.',
      magnitudeMw: 0,
      durationMin: 5,
      costEstimateSar: 800,
      requiresRole: 'engineer',
      applied: [{ type: 'cooling_boost', assetCode: profile.code, magnitudeMw: 0 }],
    })
  }

  if (resources.reconfigurable && excessMw > 1) {
    const mw = round(Math.min(excessMw * 0.25, resources.transferCapacityMw * 0.5), 1)
    if (mw > 0.3) {
      candidates.push({
        key: 'network_reconfigure',
        actionType: 'network_reconfigure',
        title: `Reconfigure the ring to offload ${mw} MW`,
        titleAr: `إعادة تشكيل الحلقة لتخفيف ${mw} ميجاواط`,
        description:
          'Open and close the ring-main switches so the substation is fed from two directions, spreading the load across both transformers.',
        descriptionAr:
          'فتح وإغلاق مفاتيح الحلقة بحيث تُغذى المحطة من اتجاهين، مما يوزع الحمل على المحولين معًا.',
        magnitudeMw: mw,
        durationMin: 20,
        costEstimateSar: round(mw * 600, 0),
        requiresRole: 'control_operator',
        applied: [{ type: 'network_reconfigure', assetCode: profile.code, magnitudeMw: mw }],
      })
    }
  }

  candidates.push({
    key: 'maintenance_dispatch',
    actionType: 'maintenance_dispatch',
    title: 'Dispatch a condition inspection crew',
    titleAr: 'إرسال فريق فحص حالة الأصل',
    description:
      'Send a crew for thermographic and dissolved-gas checks. This does not reduce risk within the prediction window, but it closes the loop on the underlying condition.',
    descriptionAr:
      'إرسال فريق لفحص التصوير الحراري وتحليل الغازات الذائبة. لا يخفض هذا الخطر داخل نافذة التنبؤ، لكنه يعالج السبب الجذري لحالة الأصل.',
    magnitudeMw: 0,
    durationMin: 120,
    costEstimateSar: 4_500,
    requiresRole: 'maintenance_engineer',
    applied: [],
  })

  return candidates
}

/** Measure each candidate on its own, by re-running the twin with only that action. */
export function evaluateCandidates(
  ctx: ActionContext,
  candidates: ActionCandidate[],
): EvaluatedAction[] {
  const before = riskWithActions(ctx, [])
  return candidates
    .map((candidate) => {
      const after = riskWithActions(ctx, candidate.applied)
      return {
        ...candidate,
        riskBefore: before.risk.score,
        riskAfter: after.risk.score,
        riskReduction: round(before.risk.score - after.risk.score, 1),
        loadAfterPct: after.sample.loadPct,
        tempAfterC: after.sample.tempC,
      }
    })
    .sort((a, b) => b.riskReduction - a.riskReduction)
}

/** Measure a chosen combination of actions together (§7's "Combined Strategy"). */
export function evaluateStrategy(
  ctx: ActionContext,
  actions: AppliedAction[],
  meta: { costSar?: number; durationMin?: number } = {},
): StrategyEvaluation {
  const before = riskWithActions(ctx, [])
  const after = riskWithActions(ctx, actions)
  return {
    riskBefore: before.risk.score,
    riskAfter: after.risk.score,
    riskReduction: round(before.risk.score - after.risk.score, 1),
    loadBeforePct: before.sample.loadPct,
    loadAfterPct: after.sample.loadPct,
    tempBeforeC: before.sample.tempC,
    tempAfterC: after.sample.tempC,
    actions,
    totalMw: round(
      actions.reduce((sum, a) => sum + a.magnitudeMw, 0),
      1,
    ),
    totalCostSar: meta.costSar ?? 0,
    maxDurationMin: meta.durationMin ?? 0,
  }
}

/**
 * The recommended strategy: greedily add the action with the best marginal benefit until
 * risk falls into the "watch" band or no candidate helps by more than a point. Greedy is
 * the right choice here because the actions interact (each one shrinks the excess the
 * next has to work on), so the marginal value must be recomputed at every step.
 */
export function buildStrategy(
  ctx: ActionContext,
  candidates: EvaluatedAction[],
  targetRisk = 30,
): { chosen: EvaluatedAction[]; evaluation: StrategyEvaluation } {
  const chosen: EvaluatedAction[] = []
  const pool = candidates.filter((c) => c.applied.length > 0)
  let applied: AppliedAction[] = []
  let current = riskWithActions(ctx, applied).risk.score

  while (pool.length > 0 && current > targetRisk) {
    let best: { candidate: EvaluatedAction; score: number } | null = null
    for (const candidate of pool) {
      const trial = riskWithActions(ctx, [...applied, ...candidate.applied]).risk.score
      if (!best || trial < best.score) best = { candidate, score: trial }
    }
    if (!best || current - best.score < 1) break
    chosen.push(best.candidate)
    applied = [...applied, ...best.candidate.applied]
    current = best.score
    pool.splice(pool.indexOf(best.candidate), 1)
  }

  const evaluation = evaluateStrategy(ctx, applied, {
    costSar: chosen.reduce((sum, c) => sum + c.costEstimateSar, 0),
    durationMin: chosen.reduce((max, c) => Math.max(max, c.durationMin), 0),
  })

  return { chosen, evaluation }
}

/**
 * Scenario comparison (§24): what happens with no action, with a single manual action,
 * and with the full AI strategy.
 */
export interface ComparisonRow {
  key: 'no_action' | 'manual_action' | 'ai_strategy'
  label: string
  labelAr: string
  risk: number
  loadPct: number
  tempC: number
  actions: AppliedAction[]
}

export function compareStrategies(
  ctx: ActionContext,
  manual: EvaluatedAction | null,
  aiActions: AppliedAction[],
): ComparisonRow[] {
  const none = riskWithActions(ctx, [])
  const manualResult = manual ? riskWithActions(ctx, manual.applied) : none
  const ai = riskWithActions(ctx, aiActions)

  return [
    {
      key: 'no_action',
      label: 'No action',
      labelAr: 'بدون إجراء',
      risk: none.risk.score,
      loadPct: none.sample.loadPct,
      tempC: none.sample.tempC,
      actions: [],
    },
    {
      key: 'manual_action',
      label: manual ? manual.title : 'Single manual action',
      labelAr: manual ? manual.titleAr : 'إجراء يدوي منفرد',
      risk: manualResult.risk.score,
      loadPct: manualResult.sample.loadPct,
      tempC: manualResult.sample.tempC,
      actions: manual?.applied ?? [],
    },
    {
      key: 'ai_strategy',
      label: 'NABDH combined strategy',
      labelAr: 'استراتيجية نَبْض المدمجة',
      risk: ai.risk.score,
      loadPct: ai.sample.loadPct,
      tempC: ai.sample.tempC,
      actions: aiActions,
    },
  ]
}
