///|
/// Severity used by the independent result auditor.
pub(all) enum AuditSeverity {
  AuditInfo
  AuditWarning
  AuditError
} derive(Debug, Eq, ToJson, FromJson)

///|
pub fn AuditSeverity::label(self : AuditSeverity) -> String {
  match self {
    AuditInfo => "info"
    AuditWarning => "warning"
    AuditError => "error"
  }
}

///|
/// A stable, machine-readable audit result. Codes are intentionally kept
/// separate from prose so downstream applications do not parse messages.
pub(all) struct AuditFinding {
  code : String
  severity : AuditSeverity
  subject : String
  message : String
  slot : Int?
  expected : Int?
  actual : Int?
} derive(Debug, Eq, ToJson, FromJson)

///|
pub fn AuditFinding::new(
  code : String,
  severity : AuditSeverity,
  subject : String,
  message : String,
  slot? : Int,
  expected? : Int,
  actual? : Int,
) -> AuditFinding {
  { code, severity, subject, message, slot, expected, actual }
}

///|
/// Per-slot reconstruction of supply, demand, storage, and residual energy.
pub(all) struct EnergyBalanceRow {
  slot : Int
  demand_w : Int
  served_demand_w : Int
  grid_w : Int
  solar_w : Int
  battery_charge_w : Int
  battery_discharge_w : Int
  unserved_w : Int
  supply_w : Int
  accounted_use_w : Int
  residual_w : Int
} derive(Debug, Eq, ToJson, FromJson)

///|
pub fn EnergyBalanceRow::is_balanced(
  self : EnergyBalanceRow,
  tolerance_w? : Int = 2,
) -> Bool {
  analytics_abs(self.residual_w) <= tolerance_w
}

///|
pub(all) struct AuditSummary {
  passed : Bool
  error_count : Int
  warning_count : Int
  info_count : Int
  maximum_balance_residual_w : Int
  maximum_grid_excess_w : Int
  minimum_battery_state_wh : Int
  maximum_battery_state_wh : Int
  findings : Array[AuditFinding]
  balances : Array[EnergyBalanceRow]
} derive(Debug, Eq, ToJson, FromJson)

///|
pub fn AuditSummary::has_code(self : AuditSummary, code : String) -> Bool {
  for finding in self.findings {
    if finding.code == code {
      return true
    }
  }
  false
}

///|
pub fn AuditSummary::to_json_string(self : AuditSummary) -> String {
  self.to_json().stringify(indent=2)
}

///|
/// A compact comparison between two valid optimizer outputs.
pub(all) struct PlanDelta {
  baseline_title : String
  candidate_title : String
  cost_delta_micro : Int
  carbon_delta_g : Int
  peak_delta_w : Int
  unserved_delta_wh : Int
  critical_unserved_delta_wh : Int
  resilience_delta_permille : Int
  comfort_delta : Int
  completed_task_delta : Int
  candidate_dominates : Bool
  baseline_dominates : Bool
  tradeoff_count : Int
  summary : Array[String]
} derive(Debug, Eq, ToJson, FromJson)

///|
pub fn PlanDelta::to_json_string(self : PlanDelta) -> String {
  self.to_json().stringify(indent=2)
}

///|
/// Capacity and outage adequacy indicators computed before optimization.
pub(all) struct AdequacyProfile {
  total_demand_wh : Int
  total_solar_wh : Int
  usable_battery_wh : Int
  outage_demand_wh : Int
  outage_solar_wh : Int
  outage_storage_margin_wh : Int
  peak_requested_w : Int
  weakest_supply_margin_w : Int
  weakest_supply_slot : Int
  critical_task_energy_wh : Int
  flexible_task_energy_wh : Int
  risk_codes : Array[String]
} derive(Debug, Eq, ToJson, FromJson)

///|
pub fn AdequacyProfile::to_json_string(self : AdequacyProfile) -> String {
  self.to_json().stringify(indent=2)
}

///|
fn analytics_abs(value : Int) -> Int {
  if value < 0 {
    -value
  } else {
    value
  }
}

///|
fn analytics_min(a : Int, b : Int) -> Int {
  if a < b {
    a
  } else {
    b
  }
}

///|
fn analytics_max(a : Int, b : Int) -> Int {
  if a > b {
    a
  } else {
    b
  }
}

///|
fn array_value(values : Array[Int], index : Int) -> Int {
  if index >= 0 && index < values.length() {
    values[index]
  } else {
    0
  }
}

///|
fn battery_power_at(result : PlanResult, slot : Int) -> Int {
  for step in result.battery_steps {
    if step.slot == slot {
      return step.power_w
    }
  }
  0
}

///|
fn find_task(input : PlanningInput, task_id : String) -> LoadTask? {
  for task in input.tasks {
    if task.id == task_id {
      return Some(task)
    }
  }
  None
}

///|
fn append_length_finding(
  findings : Array[AuditFinding],
  name : String,
  actual : Int,
  expected : Int,
) -> Unit {
  if actual != expected {
    findings.push(
      AuditFinding::new(
        "AUDIT-SHAPE-001",
        AuditError,
        name,
        "Result series length does not match the planning horizon",
        expected~,
        actual~,
      ),
    )
  }
}

///|
fn audit_series_shapes(
  result : PlanResult,
  findings : Array[AuditFinding],
) -> Unit {
  append_length_finding(
    findings,
    "load_w",
    result.load_w.length(),
    result.horizon_slots,
  )
  append_length_finding(
    findings,
    "grid_w",
    result.grid_w.length(),
    result.horizon_slots,
  )
  append_length_finding(
    findings,
    "solar_used_w",
    result.solar_used_w.length(),
    result.horizon_slots,
  )
  append_length_finding(
    findings,
    "unserved_w",
    result.unserved_w.length(),
    result.horizon_slots,
  )
  let expected_battery_states = result.horizon_slots + 1
  append_length_finding(
    findings,
    "battery_state_wh",
    result.battery_state_wh.length(),
    expected_battery_states,
  )
}

///|
pub fn reconstruct_energy_balance(
  result : PlanResult,
) -> Array[EnergyBalanceRow] {
  let rows : Array[EnergyBalanceRow] = []
  for slot = 0; slot < result.horizon_slots; slot = slot + 1 {
    let demand = array_value(result.load_w, slot)
    let unserved = array_value(result.unserved_w, slot)
    let grid = array_value(result.grid_w, slot)
    let solar = array_value(result.solar_used_w, slot)
    let battery_power = battery_power_at(result, slot)
    let charge = analytics_max(0, battery_power)
    let discharge = analytics_max(0, -battery_power)
    let served = analytics_max(0, demand - unserved)
    let supply = grid + solar + discharge
    let accounted_use = served + charge
    rows.push({
      slot,
      demand_w: demand,
      served_demand_w: served,
      grid_w: grid,
      solar_w: solar,
      battery_charge_w: charge,
      battery_discharge_w: discharge,
      unserved_w: unserved,
      supply_w: supply,
      accounted_use_w: accounted_use,
      residual_w: supply - accounted_use,
    })
  }
  rows
}

///|
fn audit_nonnegative_series(
  result : PlanResult,
  findings : Array[AuditFinding],
) -> Unit {
  for slot = 0; slot < result.horizon_slots; slot = slot + 1 {
    let values = [
      ("load_w", array_value(result.load_w, slot)),
      ("grid_w", array_value(result.grid_w, slot)),
      ("solar_used_w", array_value(result.solar_used_w, slot)),
      ("unserved_w", array_value(result.unserved_w, slot)),
    ]
    for pair in values {
      let (name, value) = pair
      if value < 0 {
        findings.push(
          AuditFinding::new(
            "AUDIT-RANGE-001",
            AuditError,
            name,
            "Power series contains a negative value",
            slot~,
            expected=0,
            actual=value,
          ),
        )
      }
    }
    let unserved = array_value(result.unserved_w, slot)
    let load = array_value(result.load_w, slot)
    if unserved > load {
      findings.push(
        AuditFinding::new(
          "AUDIT-RANGE-002",
          AuditError,
          "unserved_w",
          "Unserved power is greater than requested load",
          slot~,
          expected=load,
          actual=unserved,
        ),
      )
    }
  }
}

///|
fn audit_grid_constraints(
  input : PlanningInput,
  result : PlanResult,
  findings : Array[AuditFinding],
) -> Int {
  let mut maximum_excess = 0
  for slot = 0; slot < result.horizon_slots; slot = slot + 1 {
    let grid = array_value(result.grid_w, slot)
    let limit = input.grid_limit_at(slot)
    let excess = analytics_max(0, grid - limit)
    maximum_excess = analytics_max(maximum_excess, excess)
    if excess > 0 {
      findings.push(
        AuditFinding::new(
          "AUDIT-GRID-001",
          AuditError,
          "grid_w",
          "Grid import exceeds the effective grid or outage limit",
          slot~,
          expected=limit,
          actual=grid,
        ),
      )
    }
    if input.has_outage_at(slot) && grid != 0 {
      findings.push(
        AuditFinding::new(
          "AUDIT-OUTAGE-001",
          AuditError,
          "grid_w",
          "Grid power must be zero during a full outage",
          slot~,
          expected=0,
          actual=grid,
        ),
      )
    }
  }
  maximum_excess
}

///|
fn audit_battery_constraints(
  input : PlanningInput,
  result : PlanResult,
  findings : Array[AuditFinding],
) -> (Int, Int) {
  let mut minimum_state = 0
  let mut maximum_state = 0
  match input.battery {
    None =>
      for step in result.battery_steps {
        if step.power_w != 0 {
          findings.push(
            AuditFinding::new(
              "AUDIT-BATTERY-001",
              AuditError,
              "battery_steps",
              "Plan dispatches storage although the input has no battery",
              slot=step.slot,
              expected=0,
              actual=step.power_w,
            ),
          )
        }
      }
    Some(spec) => {
      minimum_state = spec.maximum_wh
      maximum_state = spec.minimum_wh
      for index = 0; index < result.battery_state_wh.length(); index = index + 1 {
        let state = result.battery_state_wh[index]
        minimum_state = analytics_min(minimum_state, state)
        maximum_state = analytics_max(maximum_state, state)
        if state < spec.minimum_wh || state > spec.maximum_wh {
          findings.push(
            AuditFinding::new(
              "AUDIT-BATTERY-002",
              AuditError,
              "battery_state_wh",
              "Battery state is outside its configured safety interval",
              slot=analytics_min(index, result.horizon_slots - 1),
              expected=spec.maximum_wh,
              actual=state,
            ),
          )
        }
      }
      for step in result.battery_steps {
        if step.power_w > spec.maximum_charge_w {
          findings.push(
            AuditFinding::new(
              "AUDIT-BATTERY-003",
              AuditError,
              "battery_steps",
              "Charge power exceeds the battery limit",
              slot=step.slot,
              expected=spec.maximum_charge_w,
              actual=step.power_w,
            ),
          )
        }
        if -step.power_w > spec.maximum_discharge_w {
          findings.push(
            AuditFinding::new(
              "AUDIT-BATTERY-004",
              AuditError,
              "battery_steps",
              "Discharge power exceeds the battery limit",
              slot=step.slot,
              expected=spec.maximum_discharge_w,
              actual=-step.power_w,
            ),
          )
        }
      }
    }
  }
  (minimum_state, maximum_state)
}

///|
fn audit_schedule(
  input : PlanningInput,
  result : PlanResult,
  findings : Array[AuditFinding],
) -> Unit {
  for entry in result.schedule {
    match find_task(input, entry.task_id) {
      None =>
        findings.push(
          AuditFinding::new(
            "AUDIT-TASK-001",
            AuditError,
            entry.task_id,
            "Schedule references a task that is not present in the input",
          ),
        )
      Some(task) => {
        if entry.start_slot < task.earliest_start ||
          entry.end_slot > task.latest_end ||
          entry.start_slot >= entry.end_slot {
          findings.push(
            AuditFinding::new(
              "AUDIT-TASK-002",
              AuditError,
              entry.task_id,
              "Scheduled interval is outside the task window",
              slot=entry.start_slot,
            ),
          )
        }
        if entry.power_w != task.power_w {
          findings.push(
            AuditFinding::new(
              "AUDIT-TASK-003",
              AuditError,
              entry.task_id,
              "Scheduled power differs from the task contract",
              slot=entry.start_slot,
              expected=task.power_w,
              actual=entry.power_w,
            ),
          )
        }
      }
    }
  }
}

///|
fn audit_balances(
  balances : Array[EnergyBalanceRow],
  findings : Array[AuditFinding],
  tolerance_w : Int,
) -> Int {
  let mut maximum_residual = 0
  for row in balances {
    maximum_residual = analytics_max(
      maximum_residual,
      analytics_abs(row.residual_w),
    )
    if !row.is_balanced(tolerance_w~) {
      findings.push(
        AuditFinding::new(
          "AUDIT-BALANCE-001",
          AuditWarning,
          "energy-balance",
          "Supply and accounted use differ; the residual can represent export or rounding",
          slot=row.slot,
          expected=row.accounted_use_w,
          actual=row.supply_w,
        ),
      )
    }
  }
  maximum_residual
}

///|
/// Independently audit a plan against its original input. The auditor does not
/// call the optimizer and therefore also catches regression defects in solve().
pub fn audit_plan(
  input : PlanningInput,
  result : PlanResult,
  balance_tolerance_w? : Int = 2,
) -> AuditSummary {
  let findings : Array[AuditFinding] = []
  audit_series_shapes(result, findings)
  audit_nonnegative_series(result, findings)
  let maximum_grid_excess = audit_grid_constraints(input, result, findings)
  let (minimum_state, maximum_state) = audit_battery_constraints(
    input, result, findings,
  )
  audit_schedule(input, result, findings)
  let balances = reconstruct_energy_balance(result)
  let maximum_residual = audit_balances(balances, findings, balance_tolerance_w)
  let mut errors = 0
  let mut warnings = 0
  let mut info = 0
  for finding in findings {
    match finding.severity {
      AuditError => errors = errors + 1
      AuditWarning => warnings = warnings + 1
      AuditInfo => info = info + 1
    }
  }
  {
    passed: errors == 0,
    error_count: errors,
    warning_count: warnings,
    info_count: info,
    maximum_balance_residual_w: maximum_residual,
    maximum_grid_excess_w: maximum_grid_excess,
    minimum_battery_state_wh: minimum_state,
    maximum_battery_state_wh: maximum_state,
    findings,
    balances,
  }
}

///|
fn improvement_text(name : String, delta : Int, unit : String) -> String {
  if delta < 0 {
    name + " improves by " + (-delta).to_string() + " " + unit
  } else if delta > 0 {
    name + " worsens by " + delta.to_string() + " " + unit
  } else {
    name + " is unchanged"
  }
}

///|
fn no_worse_for_minimize(delta : Int) -> Bool {
  delta <= 0
}

///|
fn no_worse_for_maximize(delta : Int) -> Bool {
  delta >= 0
}

///|
/// Compare plans using decision-facing deltas rather than their weighted score,
/// because scores from different objective policies are not directly comparable.
pub fn compare_plans(
  baseline : PlanResult,
  candidate : PlanResult,
) -> PlanDelta {
  let cost_delta = candidate.metrics.net_cost_micro() -
    baseline.metrics.net_cost_micro()
  let carbon_delta = candidate.metrics.carbon_g - baseline.metrics.carbon_g
  let peak_delta = candidate.metrics.peak_grid_w - baseline.metrics.peak_grid_w
  let unserved_delta = candidate.metrics.unserved_energy_wh -
    baseline.metrics.unserved_energy_wh
  let critical_delta = candidate.metrics.critical_unserved_wh -
    baseline.metrics.critical_unserved_wh
  let resilience_delta = candidate.metrics.resilience_permille -
    baseline.metrics.resilience_permille
  let comfort_delta = candidate.metrics.comfort_penalty -
    baseline.metrics.comfort_penalty
  let completed_delta = candidate.metrics.completed_tasks -
    baseline.metrics.completed_tasks
  let candidate_no_worse = no_worse_for_minimize(cost_delta) &&
    no_worse_for_minimize(carbon_delta) &&
    no_worse_for_minimize(peak_delta) &&
    no_worse_for_minimize(unserved_delta) &&
    no_worse_for_minimize(critical_delta) &&
    no_worse_for_minimize(comfort_delta) &&
    no_worse_for_maximize(resilience_delta) &&
    no_worse_for_maximize(completed_delta)
  let baseline_no_worse = cost_delta >= 0 &&
    carbon_delta >= 0 &&
    peak_delta >= 0 &&
    unserved_delta >= 0 &&
    critical_delta >= 0 &&
    comfort_delta >= 0 &&
    resilience_delta <= 0 &&
    completed_delta <= 0
  let candidate_strict = cost_delta < 0 ||
    carbon_delta < 0 ||
    peak_delta < 0 ||
    unserved_delta < 0 ||
    critical_delta < 0 ||
    comfort_delta < 0 ||
    resilience_delta > 0 ||
    completed_delta > 0
  let baseline_strict = cost_delta > 0 ||
    carbon_delta > 0 ||
    peak_delta > 0 ||
    unserved_delta > 0 ||
    critical_delta > 0 ||
    comfort_delta > 0 ||
    resilience_delta < 0 ||
    completed_delta < 0
  let summary = [
    improvement_text("net cost", cost_delta, "micro"),
    improvement_text("carbon", carbon_delta, "g"),
    improvement_text("peak grid power", peak_delta, "W"),
    improvement_text("unserved energy", unserved_delta, "Wh"),
    improvement_text("critical unserved energy", critical_delta, "Wh"),
  ]
  let mut improvements = 0
  let mut regressions = 0
  for
    delta in [
      cost_delta, carbon_delta, peak_delta, unserved_delta, critical_delta, comfort_delta,
    ] {
    if delta < 0 {
      improvements = improvements + 1
    } else if delta > 0 {
      regressions = regressions + 1
    }
  }
  if resilience_delta > 0 {
    improvements = improvements + 1
  }
  if resilience_delta < 0 {
    regressions = regressions + 1
  }
  if completed_delta > 0 {
    improvements = improvements + 1
  }
  if completed_delta < 0 {
    regressions = regressions + 1
  }
  {
    baseline_title: baseline.title,
    candidate_title: candidate.title,
    cost_delta_micro: cost_delta,
    carbon_delta_g: carbon_delta,
    peak_delta_w: peak_delta,
    unserved_delta_wh: unserved_delta,
    critical_unserved_delta_wh: critical_delta,
    resilience_delta_permille: resilience_delta,
    comfort_delta,
    completed_task_delta: completed_delta,
    candidate_dominates: candidate_no_worse && candidate_strict,
    baseline_dominates: baseline_no_worse && baseline_strict,
    tradeoff_count: analytics_min(improvements, regressions),
    summary,
  }
}

///|
fn task_overlaps_outage(input : PlanningInput, task : LoadTask) -> Bool {
  for outage in input.outages {
    if task.earliest_start < outage.end_slot &&
      task.latest_end > outage.start_slot {
      return true
    }
  }
  false
}

///|
/// Estimate resource adequacy before scheduling. This deliberately reports a
/// conservative envelope rather than claiming that aggregate energy guarantees
/// a feasible schedule.
pub fn assess_adequacy(input : PlanningInput) -> AdequacyProfile {
  let mut critical_energy = 0
  let mut flexible_energy = 0
  let mut peak_requested = 0
  for task in input.tasks {
    let energy = task.energy_wh(input.slot_minutes)
    if task.priority == Critical {
      critical_energy = critical_energy + energy
    }
    if task.mode.is_flexible() {
      flexible_energy = flexible_energy + energy
    }
    peak_requested = peak_requested + task.power_w
  }
  peak_requested = peak_requested + input.base_load_w.maximum()
  let mut outage_demand = 0
  let mut outage_solar = 0
  for slot = 0; slot < input.horizon_slots; slot = slot + 1 {
    if input.has_outage_at(slot) {
      outage_demand = outage_demand +
        input.base_load_w.at(slot) * input.slot_minutes / 60
      outage_solar = outage_solar +
        input.solar_w.at(slot) * input.slot_minutes / 60
      for task in input.tasks {
        if task.priority == Critical && task_overlaps_outage(input, task) {
          outage_demand = outage_demand + task.power_w * input.slot_minutes / 60
        }
      }
    }
  }
  let usable_battery = match input.battery {
    None => 0
    Some(battery) => analytics_max(0, battery.initial_wh - battery.reserve_wh)
  }
  let outage_margin = outage_solar + usable_battery - outage_demand
  let mut weakest_margin = 2147483647
  let mut weakest_slot = 0
  for slot = 0; slot < input.horizon_slots; slot = slot + 1 {
    let battery_support = match input.battery {
      Some(battery) => battery.maximum_discharge_w
      None => 0
    }
    let supply = input.grid_limit_at(slot) +
      input.solar_w.at(slot) +
      battery_support
    let demand = input.base_load_w.at(slot)
    let margin = supply - demand
    if margin < weakest_margin {
      weakest_margin = margin
      weakest_slot = slot
    }
  }
  if input.horizon_slots == 0 {
    weakest_margin = 0
  }
  let risks : Array[String] = []
  if outage_margin < 0 {
    risks.push("ADEQUACY-OUTAGE-ENERGY")
  }
  if weakest_margin < 0 {
    risks.push("ADEQUACY-SLOT-POWER")
  }
  if input.total_solar_energy_wh() >
    input.total_task_energy_wh() +
    input.base_load_w.sum() * input.slot_minutes / 60 {
    risks.push("ADEQUACY-SOLAR-SURPLUS")
  }
  if flexible_energy == 0 {
    risks.push("ADEQUACY-NO-FLEXIBILITY")
  }
  {
    total_demand_wh: input.total_task_energy_wh() +
    input.base_load_w.sum() * input.slot_minutes / 60,
    total_solar_wh: input.total_solar_energy_wh(),
    usable_battery_wh: usable_battery,
    outage_demand_wh: outage_demand,
    outage_solar_wh: outage_solar,
    outage_storage_margin_wh: outage_margin,
    peak_requested_w: peak_requested,
    weakest_supply_margin_w: weakest_margin,
    weakest_supply_slot: weakest_slot,
    critical_task_energy_wh: critical_energy,
    flexible_task_energy_wh: flexible_energy,
    risk_codes: risks,
  }
}

///|
pub fn audit_markdown(summary : AuditSummary) -> String {
  let buffer = StringBuilder()
  let status = if summary.passed { "PASS" } else { "FAIL" }
  buffer <+ "# Plan audit\n\n"
  buffer <+ "- Status: **\{status}**\n"
  buffer <+ "- Errors: \{summary.error_count}\n"
  buffer <+ "- Warnings: \{summary.warning_count}\n"
  buffer <+ "- Maximum grid excess: \{summary.maximum_grid_excess_w} W\n"
  buffer <+
    "- Maximum balance residual: \{summary.maximum_balance_residual_w} W\n\n"
  buffer <+ "| Severity | Code | Subject | Slot | Message |\n"
  buffer <+ "|---|---|---|---:|---|\n"
  for finding in summary.findings {
    let slot = match finding.slot {
      Some(value) => value.to_string()
      None => "-"
    }
    buffer <+
      "| \{finding.severity.label()} | \{finding.code} | \{finding.subject} | \{slot} | \{finding.message} |\n"
  }
  buffer.to_string()
}