///|
/// Scheduling semantics for a household task.
pub(all) enum TaskMode {
  Fixed
  Shiftable
  Interruptible
  Optional
} derive(Debug, Eq, ToJson, FromJson)

///|
pub fn TaskMode::label(self : TaskMode) -> String {
  match self {
    Fixed => "fixed"
    Shiftable => "shiftable"
    Interruptible => "interruptible"
    Optional => "optional"
  }
}

///|
pub fn TaskMode::is_flexible(self : TaskMode) -> Bool {
  match self {
    Fixed => false
    _ => true
  }
}

///|
pub fn TaskMode::may_skip(self : TaskMode) -> Bool {
  match self {
    Optional => true
    _ => false
  }
}

///|
/// Relative importance of a task during normal and outage operation.
pub(all) enum Priority {
  Critical
  High
  Normal
  Low
} derive(Debug, Eq, ToJson, FromJson)

///|
pub fn Priority::rank(self : Priority) -> Int {
  match self {
    Critical => 4
    High => 3
    Normal => 2
    Low => 1
  }
}

///|
pub fn Priority::label(self : Priority) -> String {
  match self {
    Critical => "critical"
    High => "high"
    Normal => "normal"
    Low => "low"
  }
}

///|
/// Policy used when several valid plans are available.
pub(all) enum PlanningPolicy {
  Balanced
  LowestCost
  LowestCarbon
  HighestComfort
  HighestResilience
} derive(Debug, Eq, ToJson, FromJson)

///|
pub fn PlanningPolicy::label(self : PlanningPolicy) -> String {
  match self {
    Balanced => "balanced"
    LowestCost => "lowest-cost"
    LowestCarbon => "lowest-carbon"
    HighestComfort => "highest-comfort"
    HighestResilience => "highest-resilience"
  }
}

///|
/// A regular time grid represented using integer values.
///
/// Values are deliberately unit-agnostic so the same type can carry prices,
/// carbon intensity, solar power, or a capacity limit.
pub(all) struct IntSeries {
  name : String
  unit : String
  slot_minutes : Int
  values : Array[Int]
} derive(Debug, Eq, ToJson, FromJson)

///|
pub fn IntSeries::new(
  name : String,
  unit : String,
  slot_minutes : Int,
  values : Array[Int],
) -> IntSeries {
  { name, unit, slot_minutes, values }
}

///|
pub fn IntSeries::length(self : IntSeries) -> Int {
  self.values.length()
}

///|
pub fn IntSeries::at(self : IntSeries, slot : Int) -> Int {
  if slot < 0 || slot >= self.values.length() {
    0
  } else {
    self.values[slot]
  }
}

///|
pub fn IntSeries::sum(self : IntSeries) -> Int {
  let mut total = 0
  for value in self.values {
    total = total + value
  }
  total
}

///|
pub fn IntSeries::minimum(self : IntSeries) -> Int {
  if self.values.length() == 0 {
    return 0
  }
  let mut value = self.values[0]
  for item in self.values {
    if item < value {
      value = item
    }
  }
  value
}

///|
pub fn IntSeries::maximum(self : IntSeries) -> Int {
  if self.values.length() == 0 {
    return 0
  }
  let mut value = self.values[0]
  for item in self.values {
    if item > value {
      value = item
    }
  }
  value
}

///|
pub fn IntSeries::average(self : IntSeries) -> Int {
  if self.values.length() == 0 {
    0
  } else {
    self.sum() / self.values.length()
  }
}

///|
pub fn IntSeries::copy_values(self : IntSeries) -> Array[Int] {
  self.values.map(value => value)
}

///|
pub fn IntSeries::scale_permille(
  self : IntSeries,
  factor_permille : Int,
) -> IntSeries {
  { ..self, values: self.values.map(value => value * factor_permille / 1000) }
}

///|
pub fn IntSeries::with_value(
  self : IntSeries,
  slot : Int,
  value : Int,
) -> IntSeries {
  let values = self.copy_values()
  if slot >= 0 && slot < values.length() {
    values[slot] = value
  }
  { ..self, values, }
}

///|
/// Battery characteristics use watt-hours, watts, and permille efficiencies.
pub(all) struct BatterySpec {
  name : String
  capacity_wh : Int
  initial_wh : Int
  reserve_wh : Int
  minimum_wh : Int
  maximum_wh : Int
  maximum_charge_w : Int
  maximum_discharge_w : Int
  charge_efficiency_permille : Int
  discharge_efficiency_permille : Int
  cycle_cost_micro_per_kwh : Int
} derive(Debug, Eq, ToJson, FromJson)

///|
pub fn BatterySpec::new(
  name : String,
  capacity_wh : Int,
  initial_wh : Int,
  reserve_wh : Int,
  maximum_charge_w : Int,
  maximum_discharge_w : Int,
) -> BatterySpec {
  {
    name,
    capacity_wh,
    initial_wh,
    reserve_wh,
    minimum_wh: 0,
    maximum_wh: capacity_wh,
    maximum_charge_w,
    maximum_discharge_w,
    charge_efficiency_permille: 950,
    discharge_efficiency_permille: 950,
    cycle_cost_micro_per_kwh: 8000,
  }
}

///|
pub fn BatterySpec::usable_wh(self : BatterySpec) -> Int {
  let lower = if self.reserve_wh > self.minimum_wh {
    self.reserve_wh
  } else {
    self.minimum_wh
  }
  if self.maximum_wh <= lower {
    0
  } else {
    self.maximum_wh - lower
  }
}

///|
pub fn BatterySpec::clamp_state(self : BatterySpec, state_wh : Int) -> Int {
  if state_wh < self.minimum_wh {
    self.minimum_wh
  } else if state_wh > self.maximum_wh {
    self.maximum_wh
  } else {
    state_wh
  }
}

///|
pub fn BatterySpec::state_permille(self : BatterySpec, state_wh : Int) -> Int {
  if self.capacity_wh <= 0 {
    0
  } else {
    self.clamp_state(state_wh) * 1000 / self.capacity_wh
  }
}

///|
/// A schedulable household load.
pub(all) struct LoadTask {
  id : String
  name : String
  mode : TaskMode
  priority : Priority
  power_w : Int
  duration_slots : Int
  earliest_start : Int
  latest_end : Int
  preferred_start : Int
  fixed_start : Int
  minimum_run_slots : Int
  maximum_interruptions : Int
  comfort_penalty_per_slot : Int
  skip_penalty : Int
  tags : Array[String]
} derive(Debug, Eq, ToJson, FromJson)

///|
pub fn LoadTask::shiftable(
  id : String,
  name : String,
  power_w : Int,
  duration_slots : Int,
  earliest_start : Int,
  latest_end : Int,
  preferred_start : Int,
  priority? : Priority = Normal,
) -> LoadTask {
  {
    id,
    name,
    mode: Shiftable,
    priority,
    power_w,
    duration_slots,
    earliest_start,
    latest_end,
    preferred_start,
    fixed_start: -1,
    minimum_run_slots: duration_slots,
    maximum_interruptions: 0,
    comfort_penalty_per_slot: 10,
    skip_penalty: 100000,
    tags: [],
  }
}

///|
pub fn LoadTask::fixed(
  id : String,
  name : String,
  power_w : Int,
  duration_slots : Int,
  fixed_start : Int,
  priority? : Priority = Normal,
) -> LoadTask {
  {
    id,
    name,
    mode: Fixed,
    priority,
    power_w,
    duration_slots,
    earliest_start: fixed_start,
    latest_end: fixed_start + duration_slots,
    preferred_start: fixed_start,
    fixed_start,
    minimum_run_slots: duration_slots,
    maximum_interruptions: 0,
    comfort_penalty_per_slot: 0,
    skip_penalty: 100000,
    tags: [],
  }
}

///|
pub fn LoadTask::interruptible(
  id : String,
  name : String,
  power_w : Int,
  duration_slots : Int,
  earliest_start : Int,
  latest_end : Int,
  preferred_start : Int,
  maximum_interruptions : Int,
  priority? : Priority = Normal,
) -> LoadTask {
  {
    id,
    name,
    mode: Interruptible,
    priority,
    power_w,
    duration_slots,
    earliest_start,
    latest_end,
    preferred_start,
    fixed_start: -1,
    minimum_run_slots: 1,
    maximum_interruptions,
    comfort_penalty_per_slot: 8,
    skip_penalty: 100000,
    tags: [],
  }
}

///|
pub fn LoadTask::optional(
  id : String,
  name : String,
  power_w : Int,
  duration_slots : Int,
  earliest_start : Int,
  latest_end : Int,
  preferred_start : Int,
  priority? : Priority = Low,
) -> LoadTask {
  {
    id,
    name,
    mode: Optional,
    priority,
    power_w,
    duration_slots,
    earliest_start,
    latest_end,
    preferred_start,
    fixed_start: -1,
    minimum_run_slots: duration_slots,
    maximum_interruptions: 0,
    comfort_penalty_per_slot: 5,
    skip_penalty: 800,
    tags: [],
  }
}

///|
pub fn LoadTask::energy_wh(self : LoadTask, slot_minutes : Int) -> Int {
  self.power_w * self.duration_slots * slot_minutes / 60
}

///|
pub fn LoadTask::latest_start(self : LoadTask) -> Int {
  self.latest_end - self.duration_slots
}

///|
pub fn LoadTask::window_slots(self : LoadTask) -> Int {
  if self.latest_end <= self.earliest_start {
    0
  } else {
    self.latest_end - self.earliest_start
  }
}

///|
pub fn LoadTask::is_required(self : LoadTask) -> Bool {
  !self.mode.may_skip()
}

///|
pub fn LoadTask::with_tag(self : LoadTask, tag : String) -> LoadTask {
  let tags = self.tags.map(value => value)
  tags.push(tag)
  { ..self, tags, }
}

///|
/// An interval where the public grid is unavailable or capped.
pub(all) struct OutageEvent {
  id : String
  start_slot : Int
  end_slot : Int
  grid_limit_w : Int
  reserve_override_wh : Int?
  description : String
} derive(Debug, Eq, ToJson, FromJson)

///|
pub fn OutageEvent::blackout(
  id : String,
  start_slot : Int,
  end_slot : Int,
  description? : String = "grid blackout",
) -> OutageEvent {
  {
    id,
    start_slot,
    end_slot,
    grid_limit_w: 0,
    reserve_override_wh: None,
    description,
  }
}

///|
pub fn OutageEvent::contains(self : OutageEvent, slot : Int) -> Bool {
  slot >= self.start_slot && slot < self.end_slot
}

///|
pub fn OutageEvent::duration_slots(self : OutageEvent) -> Int {
  if self.end_slot <= self.start_slot {
    0
  } else {
    self.end_slot - self.start_slot
  }
}

///|
/// Integer weights make scoring deterministic across all MoonBit backends.
pub(all) struct ObjectiveWeights {
  cost : Int
  carbon : Int
  comfort : Int
  resilience : Int
  battery_wear : Int
} derive(Debug, Eq, ToJson, FromJson)

///|
pub fn ObjectiveWeights::balanced() -> ObjectiveWeights {
  { cost: 35, carbon: 20, comfort: 20, resilience: 20, battery_wear: 5 }
}

///|
pub fn ObjectiveWeights::for_policy(
  policy : PlanningPolicy,
) -> ObjectiveWeights {
  match policy {
    Balanced => ObjectiveWeights::balanced()
    LowestCost =>
      { cost: 70, carbon: 10, comfort: 10, resilience: 5, battery_wear: 5 }
    LowestCarbon =>
      { cost: 15, carbon: 65, comfort: 10, resilience: 5, battery_wear: 5 }
    HighestComfort =>
      { cost: 15, carbon: 10, comfort: 65, resilience: 5, battery_wear: 5 }
    HighestResilience =>
      { cost: 10, carbon: 5, comfort: 10, resilience: 70, battery_wear: 5 }
  }
}

///|
pub fn ObjectiveWeights::total(self : ObjectiveWeights) -> Int {
  self.cost + self.carbon + self.comfort + self.resilience + self.battery_wear
}

///|
pub fn ObjectiveWeights::normalized(
  self : ObjectiveWeights,
) -> ObjectiveWeights {
  let total = self.total()
  if total <= 0 {
    ObjectiveWeights::balanced()
  } else {
    {
      cost: self.cost * 100 / total,
      carbon: self.carbon * 100 / total,
      comfort: self.comfort * 100 / total,
      resilience: self.resilience * 100 / total,
      battery_wear: self.battery_wear * 100 / total,
    }
  }
}

///|
/// Complete input contract for the optimizer.
pub(all) struct PlanningInput {
  title : String
  slot_minutes : Int
  horizon_slots : Int
  tariff_micro_per_kwh : IntSeries
  carbon_g_per_kwh : IntSeries
  solar_w : IntSeries
  base_load_w : IntSeries
  grid_limit_w : IntSeries
  tasks : Array[LoadTask]
  battery : BatterySpec?
  outages : Array[OutageEvent]
  weights : ObjectiveWeights
  allow_grid_export : Bool
  export_credit_micro_per_kwh : Int
  random_seed : UInt
} derive(Debug, Eq, ToJson, FromJson)

///|
pub fn PlanningInput::empty(title : String, slots : Int) -> PlanningInput {
  let values = Array::make(slots, 0)
  {
    title,
    slot_minutes: 60,
    horizon_slots: slots,
    tariff_micro_per_kwh: IntSeries::new(
      "tariff",
      "micro/kWh",
      60,
      Array::make(slots, 100000),
    ),
    carbon_g_per_kwh: IntSeries::new(
      "carbon",
      "g/kWh",
      60,
      Array::make(slots, 500),
    ),
    solar_w: IntSeries::new("solar", "W", 60, values.copy()),
    base_load_w: IntSeries::new("base-load", "W", 60, values.copy()),
    grid_limit_w: IntSeries::new(
      "grid-limit",
      "W",
      60,
      Array::make(slots, 10000),
    ),
    tasks: [],
    battery: None,
    outages: [],
    weights: ObjectiveWeights::balanced(),
    allow_grid_export: false,
    export_credit_micro_per_kwh: 0,
    random_seed: 1U,
  }
}

///|
pub fn PlanningInput::with_task(
  self : PlanningInput,
  task : LoadTask,
) -> PlanningInput {
  let tasks = self.tasks.map(value => value)
  tasks.push(task)
  { ..self, tasks, }
}

///|
pub fn PlanningInput::with_outage(
  self : PlanningInput,
  outage : OutageEvent,
) -> PlanningInput {
  let outages = self.outages.map(value => value)
  outages.push(outage)
  { ..self, outages, }
}

///|
pub fn PlanningInput::grid_limit_at(self : PlanningInput, slot : Int) -> Int {
  let mut limit = self.grid_limit_w.at(slot)
  for outage in self.outages {
    if outage.contains(slot) && outage.grid_limit_w < limit {
      limit = outage.grid_limit_w
    }
  }
  limit
}

///|
pub fn PlanningInput::has_outage_at(self : PlanningInput, slot : Int) -> Bool {
  for outage in self.outages {
    if outage.contains(slot) && outage.grid_limit_w == 0 {
      return true
    }
  }
  false
}

///|
pub fn PlanningInput::total_task_energy_wh(self : PlanningInput) -> Int {
  let mut total = 0
  for task in self.tasks {
    total = total + task.energy_wh(self.slot_minutes)
  }
  total
}

///|
pub fn PlanningInput::total_solar_energy_wh(self : PlanningInput) -> Int {
  self.solar_w.sum() * self.slot_minutes / 60
}

///|
/// A chosen execution interval. Interruptible tasks may have several entries.
pub(all) struct ScheduleEntry {
  task_id : String
  task_name : String
  start_slot : Int
  end_slot : Int
  power_w : Int
  energy_wh : Int
  priority : Priority
  reason : String
} derive(Debug, Eq, ToJson, FromJson)

///|
pub fn ScheduleEntry::duration_slots(self : ScheduleEntry) -> Int {
  self.end_slot - self.start_slot
}

///|
pub fn ScheduleEntry::contains(self : ScheduleEntry, slot : Int) -> Bool {
  slot >= self.start_slot && slot < self.end_slot
}

///|
/// Positive battery power means charging; negative means discharging.
pub(all) struct BatteryStep {
  slot : Int
  state_before_wh : Int
  power_w : Int
  state_after_wh : Int
  source : String
  reason : String
} derive(Debug, Eq, ToJson, FromJson)

///|
pub fn BatteryStep::charged_wh(self : BatteryStep) -> Int {
  if self.state_after_wh > self.state_before_wh {
    self.state_after_wh - self.state_before_wh
  } else {
    0
  }
}

///|
pub fn BatteryStep::discharged_wh(self : BatteryStep) -> Int {
  if self.state_before_wh > self.state_after_wh {
    self.state_before_wh - self.state_after_wh
  } else {
    0
  }
}

///|
/// Aggregate objective values. Money is stored in micro currency units.
pub(all) struct PlanMetrics {
  imported_energy_wh : Int
  exported_energy_wh : Int
  solar_used_wh : Int
  solar_curtailed_wh : Int
  battery_charged_wh : Int
  battery_discharged_wh : Int
  cost_micro : Int
  export_credit_micro : Int
  carbon_g : Int
  comfort_penalty : Int
  unserved_energy_wh : Int
  critical_unserved_wh : Int
  completed_tasks : Int
  skipped_tasks : Int
  peak_grid_w : Int
  resilience_permille : Int
  score : Int64
} derive(Debug, Eq, ToJson, FromJson)

///|
pub fn PlanMetrics::empty() -> PlanMetrics {
  {
    imported_energy_wh: 0,
    exported_energy_wh: 0,
    solar_used_wh: 0,
    solar_curtailed_wh: 0,
    battery_charged_wh: 0,
    battery_discharged_wh: 0,
    cost_micro: 0,
    export_credit_micro: 0,
    carbon_g: 0,
    comfort_penalty: 0,
    unserved_energy_wh: 0,
    critical_unserved_wh: 0,
    completed_tasks: 0,
    skipped_tasks: 0,
    peak_grid_w: 0,
    resilience_permille: 1000,
    score: 0L,
  }
}

///|
pub fn PlanMetrics::net_cost_micro(self : PlanMetrics) -> Int {
  self.cost_micro - self.export_credit_micro
}

///|
pub fn PlanMetrics::served_energy_wh(self : PlanMetrics) -> Int {
  self.imported_energy_wh +
  self.solar_used_wh +
  self.battery_discharged_wh -
  self.exported_energy_wh -
  self.battery_charged_wh
}

///|
/// A machine-readable explanation tied to a plan decision.
pub(all) struct Explanation {
  code : String
  severity : String
  subject : String
  message : String
  slot : Int?
  evidence : Array[String]
} derive(Debug, Eq, ToJson, FromJson)

///|
pub fn Explanation::info(
  code : String,
  subject : String,
  message : String,
  slot? : Int,
) -> Explanation {
  { code, severity: "info", subject, message, slot, evidence: [] }
}

///|
pub fn Explanation::warning(
  code : String,
  subject : String,
  message : String,
  slot? : Int,
) -> Explanation {
  { code, severity: "warning", subject, message, slot, evidence: [] }
}

///|
pub(all) enum PlanStatus {
  Feasible
  FeasibleWithCurtailment
  Infeasible
} derive(Debug, Eq, ToJson, FromJson)

///|
pub fn PlanStatus::label(self : PlanStatus) -> String {
  match self {
    Feasible => "feasible"
    FeasibleWithCurtailment => "feasible-with-curtailment"
    Infeasible => "infeasible"
  }
}

///|
/// Full optimizer output used by the CLI, web demo, and JSON API.
pub(all) struct PlanResult {
  title : String
  status : PlanStatus
  slot_minutes : Int
  horizon_slots : Int
  schedule : Array[ScheduleEntry]
  battery_steps : Array[BatteryStep]
  load_w : Array[Int]
  grid_w : Array[Int]
  solar_used_w : Array[Int]
  unserved_w : Array[Int]
  battery_state_wh : Array[Int]
  skipped_task_ids : Array[String]
  metrics : PlanMetrics
  explanations : Array[Explanation]
} derive(Debug, Eq, ToJson, FromJson)

///|
pub fn PlanResult::empty(
  title : String,
  slots : Int,
  slot_minutes : Int,
) -> PlanResult {
  {
    title,
    status: Infeasible,
    slot_minutes,
    horizon_slots: slots,
    schedule: [],
    battery_steps: [],
    load_w: Array::make(slots, 0),
    grid_w: Array::make(slots, 0),
    solar_used_w: Array::make(slots, 0),
    unserved_w: Array::make(slots, 0),
    battery_state_wh: Array::make(slots + 1, 0),
    skipped_task_ids: [],
    metrics: PlanMetrics::empty(),
    explanations: [],
  }
}

///|
pub fn PlanResult::entry_for(
  self : PlanResult,
  task_id : String,
) -> ScheduleEntry? {
  for entry in self.schedule {
    if entry.task_id == task_id {
      return Some(entry)
    }
  }
  None
}

///|
pub fn PlanResult::is_task_scheduled(
  self : PlanResult,
  task_id : String,
) -> Bool {
  self.entry_for(task_id) is Some(_)
}

///|
pub fn PlanResult::grid_peak_slot(self : PlanResult) -> Int {
  let mut peak_slot = 0
  let mut peak = -1
  for index, value in self.grid_w {
    if value > peak {
      peak = value
      peak_slot = index
    }
  }
  peak_slot
}

///|
pub fn PlanResult::total_unserved_wh(self : PlanResult) -> Int {
  self.unserved_w.fold(init=0, fn(total, value) { total + value }) *
  self.slot_minutes /
  60
}

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

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

///|
pub fn planning_input_from_json(source : String) -> PlanningInput raise {
  let json = @json.parse(source)
  @json.from_json(json)
}

///|
pub fn plan_result_from_json(source : String) -> PlanResult raise {
  let json = @json.parse(source)
  @json.from_json(json)
}