/**
 * Physics-informed intelligence (§9, §10, §11, §12, §13).
 *
 * The problem this layer exists for: an optimiser that only minimises a risk number will
 * cheerfully recommend moving 40 MW onto a feeder rated for 25, because nothing in the
 * objective function knows what a feeder is. A risk score is a summary; a capacity limit
 * is a fact. This module holds the facts, and every recommendation is measured against
 * them before a human is shown it.
 *
 * Two deliberate choices:
 *
 *   1. **The validator rejects, it does not repair.** A recommendation that breaches a
 *      hard limit comes back marked rejected with the constraint that killed it. Silently
 *      shrinking the dose until it fits would hide the fact that the strategy does not
 *      actually work at the size it needs to be.
 *
 *   2. **The envelope moves.** A transformer's safe ceiling on a 47 °C afternoon is not
 *      the ceiling in its datasheet. The bands returned here are recomputed from ambient
 *      temperature, age, maintenance debt and cooling type, and every penalty is reported
 *      separately so an engineer can see which one took the headroom away (§13).
 *
 * Everything is a pure function of its inputs. No clock reads, no randomness.
 */

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

// ─────────────────────────────────────────────────────────────────────────────
// Constraint vocabulary
// ─────────────────────────────────────────────────────────────────────────────

export const CONSTRAINT_KINDS = [
  'capacity',
  'thermal',
  'voltage',
  'frequency',
  'line',
  'battery',
  'reserve',
  'availability',
  'topology',
] as const
export type ConstraintKind = (typeof CONSTRAINT_KINDS)[number]

export type ConstraintSeverity = 'hard' | 'soft'

export interface PhysicsConstraintDef {
  code: string
  kind: ConstraintKind
  /** `asset_type`, `asset`, `region` or `global`. */
  scopeKind: string
  scopeCode: string
  label: string
  labelAr: string
  comparator: 'lte' | 'gte'
  limitValue: number
  warnValue: number | null
  unit: string
  severity: ConstraintSeverity
}

/**
 * The constraint set this build ships with.
 *
 * These are demo-model figures, not utility data, and they are stated here rather than
 * scattered through components so that the number an operator sees on a validation
 * report is provably the number the validator used (§12: "these values must come from
 * config, not from arbitrary numbers inside the UI").
 */
export const DEFAULT_CONSTRAINTS: PhysicsConstraintDef[] = [
  {
    code: 'PC-XFMR-LOAD',
    kind: 'capacity',
    scopeKind: 'asset_type',
    scopeCode: 'transformer',
    label: 'Transformer loading within nameplate',
    labelAr: 'تحميل المحول ضمن اللوحة الاسمية',
    comparator: 'lte',
    limitValue: 100,
    warnValue: 90,
    unit: '%',
    severity: 'hard',
  },
  {
    code: 'PC-XFMR-TEMP',
    kind: 'thermal',
    scopeKind: 'asset_type',
    scopeCode: 'transformer',
    label: 'Top-oil temperature below thermal limit',
    labelAr: 'حرارة الزيت العلوي دون الحد الحراري',
    comparator: 'lte',
    limitValue: 105,
    warnValue: 95,
    unit: '°C',
    severity: 'hard',
  },
  {
    code: 'PC-LINE-LOAD',
    kind: 'line',
    scopeKind: 'asset_type',
    scopeCode: 'transmission_line',
    label: 'Line loading within thermal rating',
    labelAr: 'تحميل الخط ضمن التصنيف الحراري',
    comparator: 'lte',
    limitValue: 100,
    warnValue: 88,
    unit: '%',
    severity: 'hard',
  },
  {
    code: 'PC-VOLT-DEV',
    kind: 'voltage',
    scopeKind: 'global',
    scopeCode: '',
    label: 'Voltage deviation within ±5 %',
    labelAr: 'انحراف الجهد ضمن ±٥٪',
    comparator: 'lte',
    limitValue: 5,
    warnValue: 3.5,
    unit: '%',
    severity: 'hard',
  },
  {
    code: 'PC-FREQ-DEV',
    kind: 'frequency',
    scopeKind: 'global',
    scopeCode: '',
    label: 'System frequency within ±0.5 Hz',
    labelAr: 'تردد النظام ضمن ±٠٫٥ هرتز',
    comparator: 'lte',
    limitValue: 0.5,
    warnValue: 0.3,
    unit: 'Hz',
    severity: 'hard',
  },
  {
    code: 'PC-BATT-POWER',
    kind: 'battery',
    scopeKind: 'global',
    scopeCode: '',
    label: 'Battery discharge within available power',
    labelAr: 'تفريغ البطارية ضمن القدرة المتاحة',
    comparator: 'lte',
    limitValue: 100,
    warnValue: 85,
    unit: '% of available',
    severity: 'hard',
  },
  {
    code: 'PC-TRANSFER-CAP',
    kind: 'capacity',
    scopeKind: 'global',
    scopeCode: '',
    label: 'Load transfer within neighbouring spare capacity',
    labelAr: 'نقل الحمل ضمن السعة الاحتياطية المجاورة',
    comparator: 'lte',
    limitValue: 100,
    warnValue: 80,
    unit: '% of spare',
    severity: 'hard',
  },
  {
    code: 'PC-FLEX-LOAD',
    kind: 'capacity',
    scopeKind: 'global',
    scopeCode: '',
    label: 'Demand deferral within contracted flexible load',
    labelAr: 'تأجيل الطلب ضمن الحمل المرن المتعاقد عليه',
    comparator: 'lte',
    limitValue: 100,
    warnValue: 80,
    unit: '% of contracted',
    severity: 'hard',
  },
  {
    code: 'PC-RESERVE',
    kind: 'reserve',
    scopeKind: 'global',
    scopeCode: '',
    label: 'Operating reserve margin maintained',
    labelAr: 'الحفاظ على هامش الاحتياطي التشغيلي',
    comparator: 'gte',
    limitValue: 8,
    warnValue: 12,
    unit: '%',
    severity: 'soft',
  },
  {
    code: 'PC-AVAILABILITY',
    kind: 'availability',
    scopeKind: 'global',
    scopeCode: '',
    label: 'Action targets an in-service asset',
    labelAr: 'الإجراء يستهدف أصلًا في الخدمة',
    comparator: 'lte',
    limitValue: 0,
    warnValue: null,
    unit: 'out-of-service targets',
    severity: 'hard',
  },
]

// ─────────────────────────────────────────────────────────────────────────────
// The operating envelope (§12, §13)
// ─────────────────────────────────────────────────────────────────────────────

export type EnvelopeBand = 'normal' | 'recommended' | 'warning' | 'critical' | 'beyond'

export interface EnvelopePenalty {
  key: 'heat' | 'age' | 'maintenance' | 'cooling'
  /** Percentage points removed from the recommended ceiling. Always ≥ 0. */
  points: number
  label: string
  labelAr: string
}

export interface OperatingEnvelope {
  assetCode: string
  currentPct: number
  /** The five bands, ascending. `physicalLimit` is nameplate and never moves. */
  normalMaxPct: number
  recommendedMaxPct: number
  warningPct: number
  criticalPct: number
  physicalLimitPct: number
  /** The recommended ceiling after today's conditions are applied. */
  adjustedMaxPct: number
  penalties: EnvelopePenalty[]
  band: EnvelopeBand
  /** Distance from the current position to the adjusted ceiling. Negative = past it. */
  headroomPct: number
  ambientC: number
}

/** Static band edges, as a share of nameplate. */
const BAND_NORMAL = 80
const BAND_RECOMMENDED = 88
const BAND_WARNING = 90
const BAND_CRITICAL = 95
const BAND_PHYSICAL = 100

/**
 * Above this ambient temperature a transformer's usable loading falls away, because the
 * top-oil rise adds to an already high starting point rather than to a cool one.
 */
const HEAT_KNEE_C = 40
const HEAT_PENALTY_PER_C = 0.55

/**
 * Short-term emergency loading, as a multiple of nameplate. Plant is routinely rated to
 * carry this for a bounded period; a prediction inside it is a forecast of an overload,
 * not a broken model.
 */
const EMERGENCY_RATING_FACTOR = 1.3

/** Hot-spot excursion over rated temperature that a failing unit can genuinely reach. */
const THERMAL_EXCURSION_C = 55

/** Forced-cooled plant loses less to ambient heat than a naturally cooled unit. */
const COOLING_RELIEF: Record<string, number> = {
  oil_forced: 0.55,
  oil_directed: 0.5,
  forced_air: 0.7,
  oil_natural: 1,
  natural: 1,
}

export interface EnvelopeInputs {
  profile: AssetProfile
  sample: TelemetrySample
  /** Years since installation. */
  ageYears: number
  /** 0–1, where 1 means maintenance is fully overdue. */
  maintenanceDebt: number
}

/**
 * The band structure for one asset under today's conditions.
 *
 * The penalties are returned individually rather than pre-summed because "your ceiling
 * is 86 %" is not actionable, and "your ceiling is 86 % — 4 points of that is the heat
 * and 1.5 is overdue maintenance" is: one of those an operator can do something about
 * this week.
 */
export function assetEnvelope(input: EnvelopeInputs): OperatingEnvelope {
  const { profile, sample, ageYears, maintenanceDebt } = input
  const relief = COOLING_RELIEF[profile.coolingType] ?? 1

  const penalties: EnvelopePenalty[] = []

  const overHeat = Math.max(0, sample.ambientC - HEAT_KNEE_C)
  const heat = round(overHeat * HEAT_PENALTY_PER_C * relief, 2)
  if (heat > 0) {
    penalties.push({
      key: 'heat',
      points: heat,
      label: `Ambient ${round(sample.ambientC, 1)} °C is ${round(overHeat, 1)} °C above the derating knee`,
      labelAr: `الحرارة المحيطة ${round(sample.ambientC, 1)}° أعلى بـ${round(overHeat, 1)}° من نقطة الخفض`,
    })
  }

  // Insulation ages. Twenty-five years of service is worth about three points of
  // headroom in this model — enough to matter, not enough to dominate.
  const age = round(clamp(ageYears / 25, 0, 1.4) * 3, 2)
  if (age > 0.05) {
    penalties.push({
      key: 'age',
      points: age,
      label: `${Math.round(ageYears)} years in service`,
      labelAr: `${Math.round(ageYears)} سنة في الخدمة`,
    })
  }

  const maint = round(clamp(maintenanceDebt, 0, 1) * 2.5, 2)
  if (maint > 0.05) {
    penalties.push({
      key: 'maintenance',
      points: maint,
      label: `Maintenance ${Math.round(maintenanceDebt * 100)} % overdue`,
      labelAr: `الصيانة متأخرة بنسبة ${Math.round(maintenanceDebt * 100)}٪`,
    })
  }

  // A unit already running hot for its own load has less cooling in hand than the
  // nameplate assumes. Measured as excess over the rated temperature, not over ambient.
  const thermalExcess = Math.max(0, sample.tempC - profile.ratedTempC)
  const cooling = round(clamp(thermalExcess / 10, 0, 1) * 2, 2)
  if (cooling > 0.05) {
    penalties.push({
      key: 'cooling',
      points: cooling,
      label: `Running ${round(thermalExcess, 1)} °C over rated temperature`,
      labelAr: `يعمل بزيادة ${round(thermalExcess, 1)}° عن الحرارة المقننة`,
    })
  }

  const total = penalties.reduce((sum, p) => sum + p.points, 0)
  // The ceiling can be pushed down but never below a floor — a derated transformer is
  // still a transformer, and an envelope that collapses to nothing is not a useful
  // instrument.
  const adjustedMaxPct = round(Math.max(60, BAND_RECOMMENDED - total), 1)

  const currentPct = round(Math.abs(sample.loadPct), 1)
  const band: EnvelopeBand =
    currentPct >= BAND_PHYSICAL
      ? 'beyond'
      : currentPct >= BAND_CRITICAL
        ? 'critical'
        : currentPct >= BAND_WARNING
          ? 'warning'
          : currentPct >= adjustedMaxPct
            ? 'recommended'
            : 'normal'

  return {
    assetCode: profile.code,
    currentPct,
    normalMaxPct: BAND_NORMAL,
    recommendedMaxPct: BAND_RECOMMENDED,
    warningPct: BAND_WARNING,
    criticalPct: BAND_CRITICAL,
    physicalLimitPct: BAND_PHYSICAL,
    adjustedMaxPct,
    penalties,
    band,
    headroomPct: round(adjustedMaxPct - currentPct, 1),
    ambientC: round(sample.ambientC, 1),
  }
}

// ─────────────────────────────────────────────────────────────────────────────
// The validator (§10, §11)
// ─────────────────────────────────────────────────────────────────────────────

export interface PhysicsCheck {
  constraintCode: string
  kind: ConstraintKind
  assetCode: string
  label: string
  labelAr: string
  observedValue: number
  limitValue: number
  unit: string
  /** Signed distance to the limit in the safe direction. Negative means breached. */
  headroom: number
  passed: boolean
  severity: ConstraintSeverity
  reason: string
  reasonAr: string
}

export type PhysicsVerdict = 'valid' | 'valid_with_warnings' | 'rejected'

export interface PhysicsValidation {
  verdict: PhysicsVerdict
  checks: PhysicsCheck[]
  failed: PhysicsCheck[]
  warnings: PhysicsCheck[]
  /** 0–100. How much room the tightest binding constraint still has. */
  confidence: number
  summary: string
  summaryAr: string
}

export interface ValidationSubject {
  /** The asset the strategy is aimed at. */
  profile: AssetProfile
  /** Its state once the actions are applied. */
  after: TelemetrySample
  actions: AppliedAction[]
  /** Headroom the actions are drawing on. */
  resources: {
    transferCapacityMw: number
    batteryPowerMw: number
    flexibleLoadMw: number
  }
  /** Region-level reserve margin, per cent, after the actions. */
  reserveMarginPct: number
  /** System frequency deviation from nominal, in Hz. */
  frequencyDeviationHz: number
  /** Assets the actions target that are out of service. */
  unavailableTargets: string[]
  /** Loading on the receiving feeders after a transfer, per cent of their rating. */
  neighbourLoadingPct?: number
  constraints?: PhysicsConstraintDef[]
}

function applicable(def: PhysicsConstraintDef, profile: AssetProfile): boolean {
  if (def.scopeKind === 'global') return true
  if (def.scopeKind === 'asset_type') return def.scopeCode === profile.assetTypeKey
  if (def.scopeKind === 'asset') return def.scopeCode === profile.code
  if (def.scopeKind === 'region') return def.scopeCode === profile.regionCode
  return false
}

/** The measured value each constraint is tested against, or null when it does not apply. */
function observedFor(def: PhysicsConstraintDef, subject: ValidationSubject): number | null {
  const { after, actions, resources } = subject
  const sumBy = (type: string) =>
    actions.filter((a) => a.type === type).reduce((s, a) => s + Math.abs(a.magnitudeMw), 0)

  switch (def.code) {
    case 'PC-XFMR-LOAD':
      return Math.abs(after.loadPct)
    case 'PC-XFMR-TEMP':
      return after.tempC
    case 'PC-LINE-LOAD':
      return subject.neighbourLoadingPct ?? after.lineLoadingPct
    case 'PC-VOLT-DEV':
      return Math.abs(after.voltageDeviationPct)
    case 'PC-FREQ-DEV':
      return Math.abs(subject.frequencyDeviationHz)
    case 'PC-BATT-POWER': {
      const used = sumBy('battery_discharge')
      if (used === 0) return null
      // Guard the division: a region with no battery cannot host a battery action at
      // all, which is a breach of 100 % rather than an arithmetic error.
      return resources.batteryPowerMw > 0 ? (used / resources.batteryPowerMw) * 100 : 1000
    }
    case 'PC-TRANSFER-CAP': {
      const used = sumBy('load_transfer')
      if (used === 0) return null
      return resources.transferCapacityMw > 0 ? (used / resources.transferCapacityMw) * 100 : 1000
    }
    case 'PC-FLEX-LOAD': {
      const used = sumBy('demand_response')
      if (used === 0) return null
      return resources.flexibleLoadMw > 0 ? (used / resources.flexibleLoadMw) * 100 : 1000
    }
    case 'PC-RESERVE':
      return subject.reserveMarginPct
    case 'PC-AVAILABILITY':
      return subject.unavailableTargets.length
    default:
      return null
  }
}

function reasonFor(def: PhysicsConstraintDef, observed: number, passed: boolean): [string, string] {
  const v = round(observed, 1)
  const lim = round(def.limitValue, 1)
  if (passed) {
    return [
      `${v}${def.unit === '%' ? ' %' : ` ${def.unit}`} against a limit of ${lim}`,
      `${v}${def.unit === '%' ? '٪' : ` ${def.unit}`} مقابل حد ${lim}`,
    ]
  }
  const dir = def.comparator === 'lte' ? 'exceeds' : 'falls below'
  const dirAr = def.comparator === 'lte' ? 'يتجاوز' : 'يقل عن'
  return [
    `${v}${def.unit === '%' ? ' %' : ` ${def.unit}`} ${dir} the limit of ${lim}`,
    `${v}${def.unit === '%' ? '٪' : ` ${def.unit}`} ${dirAr} الحد ${lim}`,
  ]
}

/**
 * Run every applicable constraint against a proposed end state.
 *
 * A constraint that has nothing to say about this strategy — a battery limit when no
 * battery is being used — is skipped rather than passed. Reporting nine green checks
 * when only four were actually evaluated would make the validation look more thorough
 * than it was.
 */
export function validatePhysics(subject: ValidationSubject): PhysicsValidation {
  const defs = subject.constraints ?? DEFAULT_CONSTRAINTS
  const checks: PhysicsCheck[] = []

  for (const def of defs) {
    if (!applicable(def, subject.profile)) continue
    const observed = observedFor(def, subject)
    if (observed === null) continue

    const passed =
      def.comparator === 'lte' ? observed <= def.limitValue : observed >= def.limitValue
    const headroom =
      def.comparator === 'lte' ? def.limitValue - observed : observed - def.limitValue
    const [reason, reasonAr] = reasonFor(def, observed, passed)

    checks.push({
      constraintCode: def.code,
      kind: def.kind,
      assetCode: subject.profile.code,
      label: def.label,
      labelAr: def.labelAr,
      observedValue: round(observed, 2),
      limitValue: def.limitValue,
      unit: def.unit,
      headroom: round(headroom, 2),
      passed,
      severity: def.severity,
      reason,
      reasonAr,
    })
  }

  const failed = checks.filter((c) => !c.passed && c.severity === 'hard')
  const warnings = checks.filter((c) => !c.passed && c.severity === 'soft')

  const verdict: PhysicsVerdict =
    failed.length > 0 ? 'rejected' : warnings.length > 0 ? 'valid_with_warnings' : 'valid'

  // Confidence is driven by the tightest passing constraint: a strategy that clears
  // every limit by a hair is technically valid and practically fragile, and the number
  // should say so.
  // Count-style constraints ("no out-of-service targets", limit 0) are pass/fail rather
  // than margins: their headroom is zero even when they pass comfortably, and leaving
  // them in would peg every confidence at zero.
  const passing = checks.filter((c) => c.passed && Math.abs(c.limitValue) > 1e-9)
  const tightest = passing.reduce(
    (min, c) => Math.min(min, normalisedHeadroom(c)),
    passing.length ? 1 : 0.5,
  )
  const confidence = failed.length ? 0 : round(clamp(tightest, 0, 1) * 100, 1)

  const first = failed[0] ?? warnings[0]
  const summary = failed.length
    ? `Rejected: ${first.label.toLowerCase()} — ${first.reason}.`
    : warnings.length
      ? `Valid with ${warnings.length} warning${warnings.length > 1 ? 's' : ''}: ${first.reason}.`
      : `All ${checks.length} applicable constraints satisfied.`
  const summaryAr = failed.length
    ? `مرفوض: ${first.labelAr} — ${first.reasonAr}.`
    : warnings.length
      ? `صالح مع ${warnings.length} تنبيه: ${first.reasonAr}.`
      : `تم استيفاء جميع القيود المطبّقة (${checks.length}).`

  return { verdict, checks, failed, warnings, confidence, summary, summaryAr }
}

/** Headroom as a 0–1 share of the limit, so constraints in different units compare. */
function normalisedHeadroom(check: PhysicsCheck): number {
  const scale = Math.abs(check.limitValue) || 1
  return clamp(check.headroom / scale, 0, 1)
}

/**
 * Physics confidence for a *situation* rather than a strategy (§14).
 *
 * Answers "is the trajectory the model is predicting physically coherent?" — a
 * prediction that a transformer will reach 130 % of nameplate is not a forecast, it is
 * a modelling error, and the confidence attached to it should collapse.
 */
export function physicsPlausibility(input: {
  envelope: OperatingEnvelope
  predictedLoadPct: number
  predictedTempC: number
  ratedTempC: number
}): { score: number; reason: string; reasonAr: string } {
  const { envelope, predictedLoadPct, predictedTempC, ratedTempC } = input

  // An overload is exactly what this platform predicts, so predicting one must not be
  // treated as a modelling error. Transformers genuinely run past nameplate — that is
  // what an emergency rating is — and only well beyond it does the trajectory stop
  // describing anything the plant can do. The knee therefore sits at the emergency
  // rating, not at nameplate, and the penalty ramps over the range between there and
  // the point where the model is plainly extrapolating.
  const emergencyCeiling = envelope.physicalLimitPct * EMERGENCY_RATING_FACTOR
  const loadOver = Math.max(0, predictedLoadPct - emergencyCeiling)
  const loadPenalty = clamp(loadOver / 25, 0, 1) * 55

  // Likewise for temperature: a hot-spot 40 °C over rated is a failure in progress, not
  // an impossible reading. Implausibility begins where the insulation would already have
  // failed outright.
  const tempOver = Math.max(0, predictedTempC - (ratedTempC + THERMAL_EXCURSION_C))
  const tempPenalty = clamp(tempOver / 30, 0, 1) * 40

  // A trajectory that runs backwards through the bands — predicting a lower load while
  // conditions worsen — is internally inconsistent.
  const regression = predictedLoadPct < envelope.currentPct - 25 ? 12 : 0

  const score = round(clamp(100 - loadPenalty - tempPenalty - regression, 0, 100), 1)

  if (loadPenalty > 0) {
    return {
      score,
      reason: `Predicted loading of ${round(predictedLoadPct, 1)} % is ${round(loadOver, 1)} points beyond the emergency rating of ${round(emergencyCeiling, 0)} %`,
      reasonAr: `التحميل المتوقع ${round(predictedLoadPct, 1)}٪ يتجاوز التصنيف الطارئ ${round(emergencyCeiling, 0)}٪ بـ${round(loadOver, 1)} نقطة`,
    }
  }
  if (tempPenalty > 0) {
    return {
      score,
      reason: `Predicted temperature of ${round(predictedTempC, 1)} °C is far beyond the rated point`,
      reasonAr: `الحرارة المتوقعة ${round(predictedTempC, 1)}° تتجاوز النقطة المقننة بكثير`,
    }
  }
  return {
    score,
    reason: 'Trajectory is within the physical operating range of the asset',
    reasonAr: 'المسار ضمن النطاق التشغيلي الفيزيائي للأصل',
  }
}
