/**
 * The battery dispatch simulator (5.0 §19).
 *
 * **Nothing here commands a battery.** §19 is explicit, and the shape of this file follows
 * it: these are pure functions from a battery's state and a request to a *trajectory of
 * what would happen*. There is no client, no connector and no action type — the output is
 * a curve to look at, not an instruction to send.
 *
 * The simulator answers the three questions an operator actually has when a strategy says
 * "discharge 45 MW": can this unit deliver that at all, how long can it hold it, and where
 * does the state of charge end up. A recommendation that assumed a battery could hold any
 * power for any duration would be recommending a battery that does not exist.
 *
 * Where the platform has no stored figure — and it has no energy rating for its storage
 * assets, only a power rating — the missing number is supplied as a **declared assumption**
 * carried on the result, never as a silent constant. §29's discipline applies to energy
 * exactly as it applies to customer counts.
 */

import { round } from './rng'

/**
 * Default storage duration, in hours, when no energy rating is on record.
 *
 * Two hours is the common utility-scale configuration for a grid-support battery, and it
 * is stated on every result that uses it so a reader can see it is an assumption rather
 * than a nameplate.
 */
export const ASSUMED_DURATION_H = 2

/** Round-trip efficiency for a modern lithium-ion installation. An assumption, declared. */
export const ASSUMED_ROUND_TRIP = 0.88

/** The operating window. Discharging below or charging above these damages the cells. */
export const MIN_SOC_PCT = 10
export const MAX_SOC_PCT = 95

export interface BatterySpec {
  code: string
  /** Rated power, MW. From the asset record. */
  ratedPowerMw: number
  /**
   * Usable energy, MWh. Null when the platform has no rating on record, in which case the
   * simulator derives one and says so.
   */
  ratedEnergyMwh: number | null
  /** Present state of charge, per cent. */
  socPct: number
  /** Round-trip efficiency, 0–1. Null falls back to the declared assumption. */
  roundTripEfficiency: number | null
  isOutage: boolean
}

export interface DispatchStep {
  /** Minutes from the start of the dispatch. */
  minute: number
  /** Power actually delivered in this step, MW. Positive discharges, negative charges. */
  powerMw: number
  socPct: number
  energyMwh: number
  /** True once the request can no longer be met in full. */
  limited: boolean
  /** Which limit bound this step, when one did. */
  limitedBy: 'none' | 'power' | 'energy' | 'soc_floor' | 'soc_ceiling' | 'outage'
}

export interface DispatchAssumption {
  key: 'energy_rating' | 'round_trip' | 'soc_window' | 'no_degradation'
  text: string
  textAr: string
}

export interface DispatchResult {
  batteryCode: string
  /** Always true. The result is a projection; nothing was sent anywhere. */
  simulatedOnly: true
  requestMw: number
  durationMin: number
  /** What the unit could actually deliver, averaged over the window. */
  deliveredMwAvg: number
  /** The request minus what could be delivered, averaged. Zero when the request is met. */
  shortfallMwAvg: number
  /** Minutes the full request could be held before any limit bound. */
  sustainedMin: number
  /** True when the request was met in full for the whole window. */
  meetsRequest: boolean
  socStartPct: number
  socEndPct: number
  energyDeliveredMwh: number
  energyDrawnMwh: number
  /** Equivalent full cycles this dispatch represents. Wear, in the unit engineers use. */
  equivalentCycles: number
  usableEnergyMwh: number
  steps: DispatchStep[]
  assumptions: DispatchAssumption[]
  note: string
  noteAr: string
}

/** The usable energy and the assumptions that produced it. */
function usableEnergy(battery: BatterySpec): { mwh: number; assumptions: DispatchAssumption[] } {
  const assumptions: DispatchAssumption[] = []

  let rated = battery.ratedEnergyMwh
  if (rated === null) {
    rated = battery.ratedPowerMw * ASSUMED_DURATION_H
    assumptions.push({
      key: 'energy_rating',
      text: `No energy rating is on record for ${battery.code}. Assumed ${ASSUMED_DURATION_H} h at rated power, giving ${round(rated, 1)} MWh.`,
      textAr: `لا يوجد تصنيف طاقة مسجل لـ${battery.code}. افتُرضت ${ASSUMED_DURATION_H} ساعة عند القدرة المقننة، أي ${round(rated, 1)} م.و.س.`,
    })
  }

  assumptions.push({
    key: 'soc_window',
    text: `Operating window ${MIN_SOC_PCT}–${MAX_SOC_PCT} % state of charge. Energy outside it exists but is not dispatchable without damaging the cells.`,
    textAr: `نافذة التشغيل ${MIN_SOC_PCT}–${MAX_SOC_PCT} ٪ من الشحن. الطاقة خارجها موجودة لكنها غير قابلة للسحب دون إضرار بالخلايا.`,
  })

  if (battery.roundTripEfficiency === null) {
    assumptions.push({
      key: 'round_trip',
      text: `Round-trip efficiency assumed at ${Math.round(ASSUMED_ROUND_TRIP * 100)} %.`,
      textAr: `افتُرضت كفاءة الدورة الكاملة عند ${Math.round(ASSUMED_ROUND_TRIP * 100)} ٪.`,
    })
  }

  assumptions.push({
    key: 'no_degradation',
    text: 'Capacity fade within a single dispatch is not modelled. Over one window it is far smaller than the measurement error on state of charge.',
    textAr: 'لا يُنمذَج تدهور السعة داخل عملية تفريغ واحدة؛ فهو أصغر بكثير من خطأ قياس حالة الشحن.',
  })

  return { mwh: rated, assumptions }
}

/**
 * Walk the dispatch forward, minute by minute.
 *
 * Each step delivers the smallest of what was asked, what the inverter can pass, and what
 * the remaining energy allows — and records which of the three bound it. A simulator that
 * returned only the average would hide the moment the battery ran out, which is the moment
 * an operator is planning around.
 *
 * `requestMw` is positive to discharge and negative to charge; the sign is preserved
 * throughout, because a battery that is absorbing is not a battery that is delivering.
 */
export function simulateDispatch(input: {
  battery: BatterySpec
  requestMw: number
  durationMin: number
  /** Step size in minutes. One minute unless a longer horizon needs fewer points. */
  stepMin?: number
}): DispatchResult {
  const { battery } = input
  const stepMin = Math.max(1, Math.round(input.stepMin ?? 1))
  const durationMin = Math.max(stepMin, Math.round(input.durationMin))
  const { mwh: ratedEnergyMwh, assumptions } = usableEnergy(battery)
  const efficiency = battery.roundTripEfficiency ?? ASSUMED_ROUND_TRIP

  const usableMwh = round((ratedEnergyMwh * (MAX_SOC_PCT - MIN_SOC_PCT)) / 100, 3)
  const requested = input.requestMw

  const steps: DispatchStep[] = []
  // Clamped to the physical 0–100, not to the operating window: a unit sitting above the
  // ceiling holds that energy and can discharge it. What the ceiling forbids is charging
  // it further, and the charge branch below is where that is enforced.
  let soc = Math.min(100, Math.max(0, battery.socPct))
  let energyDelivered = 0
  let energyDrawn = 0
  let sustainedMin = 0
  let stillSustaining = true

  for (let minute = stepMin; minute <= durationMin; minute += stepMin) {
    const hours = stepMin / 60

    if (battery.isOutage) {
      steps.push({ minute, powerMw: 0, socPct: round(soc, 2), energyMwh: 0, limited: true, limitedBy: 'outage' })
      stillSustaining = false
      continue
    }

    // Three limits, applied in the order they physically bind: the inverter first, then
    // the energy left inside the operating window.
    const byPower = Math.sign(requested) * Math.min(Math.abs(requested), battery.ratedPowerMw)
    let limitedBy: DispatchStep['limitedBy'] =
      Math.abs(byPower) < Math.abs(requested) - 1e-9 ? 'power' : 'none'

    let powerMw = byPower
    if (byPower > 0) {
      // Discharging: bounded by the energy above the floor.
      const availableMwh = ((soc - MIN_SOC_PCT) / 100) * ratedEnergyMwh
      const maxMw = availableMwh / hours
      if (maxMw < byPower) {
        powerMw = Math.max(0, round(maxMw, 4))
        limitedBy = powerMw <= 0 ? 'soc_floor' : 'energy'
      }
      // Losses come out of the store: delivering 1 MWh drains rather more than 1 MWh.
      const drawnMwh = (powerMw * hours) / Math.sqrt(efficiency)
      soc -= (drawnMwh / ratedEnergyMwh) * 100
      energyDelivered += powerMw * hours
    } else if (byPower < 0) {
      // Charging: bounded by the headroom below the ceiling.
      const headroomMwh = Math.max(0, ((MAX_SOC_PCT - soc) / 100) * ratedEnergyMwh)
      const maxMw = headroomMwh / hours
      if (maxMw < Math.abs(byPower)) {
        powerMw = -Math.max(0, round(maxMw, 4))
        limitedBy = powerMw >= 0 ? 'soc_ceiling' : 'energy'
      }
      const storedMwh = Math.abs(powerMw) * hours * Math.sqrt(efficiency)
      soc += (storedMwh / ratedEnergyMwh) * 100
      energyDrawn += Math.abs(powerMw) * hours
    }

    soc = Math.min(100, Math.max(0, soc))
    const met = Math.abs(Math.abs(powerMw) - Math.abs(requested)) < 1e-6
    if (stillSustaining && met) sustainedMin = minute
    else stillSustaining = false

    steps.push({
      minute,
      powerMw: round(powerMw, 3),
      socPct: round(soc, 2),
      energyMwh: round(Math.abs(powerMw) * hours, 4),
      limited: !met,
      limitedBy,
    })
  }

  const deliveredAvg =
    steps.length === 0 ? 0 : steps.reduce((sum, step) => sum + step.powerMw, 0) / steps.length
  const shortfallAvg = Math.max(0, Math.abs(requested) - Math.abs(deliveredAvg))
  const meetsRequest = steps.length > 0 && steps.every((step) => !step.limited)

  return {
    batteryCode: battery.code,
    simulatedOnly: true,
    requestMw: round(requested, 3),
    durationMin,
    deliveredMwAvg: round(deliveredAvg, 3),
    shortfallMwAvg: round(shortfallAvg, 3),
    sustainedMin,
    meetsRequest,
    socStartPct: round(battery.socPct, 2),
    socEndPct: round(soc, 2),
    energyDeliveredMwh: round(energyDelivered, 3),
    energyDrawnMwh: round(energyDrawn, 3),
    equivalentCycles:
      ratedEnergyMwh > 0 ? round((energyDelivered + energyDrawn) / ratedEnergyMwh, 4) : 0,
    usableEnergyMwh: usableMwh,
    steps,
    assumptions,
    note: battery.isOutage
      ? 'This unit is out of service. Nothing can be dispatched from it, and the strategy that asked for it needs another source.'
      : meetsRequest
        ? 'The unit can hold this request for the whole window. Simulated — no command was sent to the battery.'
        : `The request cannot be held for the whole window. Simulated — no command was sent to the battery.`,
    noteAr: battery.isOutage
      ? 'هذه الوحدة خارج الخدمة. لا يمكن سحب أي قدرة منها.'
      : meetsRequest
        ? 'تستطيع الوحدة تلبية الطلب طوال النافذة. محاكاة — لم يُرسل أي أمر إلى البطارية.'
        : 'لا يمكن تلبية الطلب طوال النافذة. محاكاة — لم يُرسل أي أمر إلى البطارية.',
  }
}

/**
 * The longest a unit could hold a given power, in minutes.
 *
 * A convenience over `simulateDispatch` for the common question "how long have we got?",
 * computed by the same walk so the two can never disagree.
 */
export function sustainableMinutes(battery: BatterySpec, powerMw: number): number {
  if (powerMw <= 0 || battery.isOutage) return 0
  return simulateDispatch({ battery, requestMw: powerMw, durationMin: 24 * 60, stepMin: 1 }).sustainedMin
}
