///|
/// Supported deterministic cold-chain fault scenarios.
pub(all) enum SimulationScenario {
  StableTransit
  DoorOpen
  CoolingFailure
  FreezerExposure
  SensorDropout
  SensorStuckFault
  PowerCycle
  MixedFaults
} derive(Debug, Eq)

///|
/// Simulation parameters. All timestamps are UTC Unix seconds.
pub(all) struct SimulationConfig {
  scenario : SimulationScenario
  sensor_ids : Array[String]
  started_at : Int64
  duration_seconds : Int64
  interval_seconds : Int64
  baseline_c : Double
  ambient_c : Double
  noise_amplitude_c : Double
  humidity_percent : Double
  battery_percent : Double
  event_start_seconds : Int64
  event_duration_seconds : Int64
  seed : Int64
} derive(Debug, Eq)

///|
/// Result and prose explanation for a generated scenario.
pub(all) struct SimulationResult {
  config : SimulationConfig
  readings : Array[Reading]
  description : String
  diagnostics : Array[Diagnostic]
} derive(Debug, Eq)

///|
/// Minimal standard Park-Miller generator using Int64 arithmetic.
priv struct DeterministicRng {
  mut state : Int64
}

///|
/// Construct a non-zero deterministic random state.
fn new_rng(seed : Int64) -> DeterministicRng {
  let positive = if seed < 0L { -seed } else { seed }
  let normalized = positive % 2147483647L
  { state: if normalized == 0L { 1L } else { normalized } }
}

///|
/// Advance and return the next positive integer.
fn DeterministicRng::next_int(self : DeterministicRng) -> Int64 {
  self.state = self.state * 48271L % 2147483647L
  self.state
}

///|
/// Uniform value in the open interval (0, 1).
fn DeterministicRng::next_unit(self : DeterministicRng) -> Double {
  self.next_int().to_double() / 2147483647.0
}

///|
/// Symmetric random value in [-amplitude, amplitude].
fn DeterministicRng::jitter(
  self : DeterministicRng,
  amplitude : Double,
) -> Double {
  (self.next_unit() * 2.0 - 1.0) * amplitude
}

///|
/// Defaults produce four hours of five-minute readings for two sensors.
pub fn default_simulation_config(
  scenario : SimulationScenario,
) -> SimulationConfig {
  {
    scenario,
    sensor_ids: ["logger-a", "logger-b"],
    started_at: 1785916800L,
    duration_seconds: 14400L,
    interval_seconds: 300L,
    baseline_c: 5.0,
    ambient_c: 25.0,
    noise_amplitude_c: 0.12,
    humidity_percent: 62.0,
    battery_percent: 94.0,
    event_start_seconds: 3600L,
    event_duration_seconds: 1800L,
    seed: 20260805L,
  }
}

///|
/// Validate a simulation request.
pub fn validate_simulation_config(
  config : SimulationConfig,
) -> Array[Diagnostic] {
  let diagnostics : Array[Diagnostic] = []
  if config.sensor_ids.length() == 0 {
    diagnostics.push(
      error_diagnostic(
        "simulate.no_sensors", "at least one sensor identifier is required",
      ),
    )
  }
  for sensor_id in config.sensor_ids {
    if sensor_id.trim().is_empty() {
      diagnostics.push(
        error_diagnostic(
          "simulate.empty_sensor", "sensor identifiers cannot be empty",
        ),
      )
    }
  }
  if config.duration_seconds <= 0L {
    diagnostics.push(
      error_diagnostic(
        "simulate.invalid_duration", "simulation duration must be positive",
      ),
    )
  }
  if config.interval_seconds <= 0L {
    diagnostics.push(
      error_diagnostic(
        "simulate.invalid_interval", "sample interval must be positive",
      ),
    )
  }
  if config.event_start_seconds < 0L {
    diagnostics.push(
      error_diagnostic(
        "simulate.invalid_event_start", "event start cannot be negative",
      ),
    )
  }
  if config.event_duration_seconds < 0L {
    diagnostics.push(
      error_diagnostic(
        "simulate.invalid_event_duration", "event duration cannot be negative",
      ),
    )
  }
  if config.noise_amplitude_c < 0.0 {
    diagnostics.push(
      error_diagnostic(
        "simulate.invalid_noise", "noise amplitude cannot be negative",
      ),
    )
  }
  diagnostics
}

///|
/// Stable sinusoidal compressor cycle plus sensor-specific phase.
fn baseline_temperature(
  config : SimulationConfig,
  elapsed : Int64,
  sensor_index : Int,
) -> Double {
  let phase = elapsed.to_double() / 1800.0 * @math.PI * 2.0 +
    sensor_index.to_double() * 0.7
  config.baseline_c + @math.sin(phase) * 0.25 + sensor_index.to_double() * 0.08
}

///|
/// Whether elapsed time lies in the configured event interval.
fn inside_event(config : SimulationConfig, elapsed : Int64) -> Bool {
  elapsed >= config.event_start_seconds &&
  elapsed < config.event_start_seconds + config.event_duration_seconds
}

///|
/// Smooth linear progress through the configured event.
fn event_progress(config : SimulationConfig, elapsed : Int64) -> Double {
  if config.event_duration_seconds <= 0L {
    0.0
  } else {
    clamp_double(
      (elapsed - config.event_start_seconds).to_double() /
      config.event_duration_seconds.to_double(),
      0.0,
      1.0,
    )
  }
}

///|
/// Door opening approaches ambient temperature and then recovers gradually.
fn door_open_temperature(
  config : SimulationConfig,
  elapsed : Int64,
  baseline : Double,
) -> Double {
  if inside_event(config, elapsed) {
    let progress = event_progress(config, elapsed)
    baseline + (config.ambient_c - baseline) * progress * 0.45
  } else {
    let after = elapsed -
      (config.event_start_seconds + config.event_duration_seconds)
    if after >= 0L && after < config.event_duration_seconds {
      let recovery = 1.0 -
        after.to_double() / config.event_duration_seconds.to_double()
      baseline + (config.ambient_c - baseline) * recovery * 0.18
    } else {
      baseline
    }
  }
}

///|
/// Cooling failure produces a continued temperature rise.
fn cooling_failure_temperature(
  config : SimulationConfig,
  elapsed : Int64,
  baseline : Double,
) -> Double {
  if elapsed < config.event_start_seconds {
    baseline
  } else {
    let failure_elapsed = elapsed - config.event_start_seconds
    let rise = failure_elapsed.to_double() / 3600.0 * 3.2
    clamp_double(baseline + rise, baseline, config.ambient_c)
  }
}

///|
/// Accidental freezer exposure drops and then recovers.
fn freezer_exposure_temperature(
  config : SimulationConfig,
  elapsed : Int64,
  baseline : Double,
) -> Double {
  if inside_event(config, elapsed) {
    baseline - event_progress(config, elapsed) * 8.0
  } else {
    let after = elapsed -
      (config.event_start_seconds + config.event_duration_seconds)
    if after >= 0L && after < 1800L {
      baseline - (1.0 - after.to_double() / 1800.0) * 4.0
    } else {
      baseline
    }
  }
}

///|
/// Scenario-specific temperature transformation.
fn scenario_temperature(
  config : SimulationConfig,
  elapsed : Int64,
  baseline : Double,
) -> Double {
  match config.scenario {
    StableTransit | SensorDropout | PowerCycle => baseline
    DoorOpen => door_open_temperature(config, elapsed, baseline)
    CoolingFailure => cooling_failure_temperature(config, elapsed, baseline)
    FreezerExposure => freezer_exposure_temperature(config, elapsed, baseline)
    SensorStuckFault =>
      if elapsed >= config.event_start_seconds {
        config.baseline_c + 0.4
      } else {
        baseline
      }
    MixedFaults => {
      let heated = door_open_temperature(config, elapsed, baseline)
      if elapsed > config.event_start_seconds + config.event_duration_seconds {
        cooling_failure_temperature(config, elapsed, heated)
      } else {
        heated
      }
    }
  }
}

///|
/// Scenario can intentionally omit samples.
fn should_emit_sample(
  config : SimulationConfig,
  elapsed : Int64,
  sensor_index : Int,
) -> Bool {
  match config.scenario {
    SensorDropout => !(sensor_index == 0 && inside_event(config, elapsed))
    MixedFaults =>
      !(sensor_index == 1 &&
      inside_event(config, elapsed - config.event_duration_seconds))
    _ => true
  }
}

///|
/// Scenario can expose explicit device status.
fn simulated_status(config : SimulationConfig, elapsed : Int64) -> String {
  match config.scenario {
    PowerCycle => if inside_event(config, elapsed) { "offline" } else { "ok" }
    MixedFaults =>
      if elapsed >=
        config.event_start_seconds + config.event_duration_seconds * 2L &&
        elapsed <
        config.event_start_seconds + config.event_duration_seconds * 2L + 600L {
        "offline"
      } else {
        "ok"
      }
    _ => "ok"
  }
}

///|
/// Battery discharge model with faster loss during a power cycle.
fn simulated_battery(config : SimulationConfig, elapsed : Int64) -> Double {
  let normal_loss = elapsed.to_double() / 86400.0 * 2.0
  let fault_loss = match config.scenario {
    PowerCycle | MixedFaults =>
      if elapsed >= config.event_start_seconds {
        12.0
      } else {
        0.0
      }
    _ => 0.0
  }
  clamp_double(config.battery_percent - normal_loss - fault_loss, 0.0, 100.0)
}

///|
/// Human-readable scenario description.
pub fn simulation_scenario_description(scenario : SimulationScenario) -> String {
  match scenario {
    StableTransit => "stable cold-chain transit with compressor cycling"
    DoorOpen => "temporary door opening followed by gradual recovery"
    CoolingFailure => "progressive warming after refrigeration failure"
    FreezerExposure => "accidental sub-zero exposure followed by recovery"
    SensorDropout => "one logger stops reporting during the fault interval"
    SensorStuckFault => "one logger becomes stuck at a constant value"
    PowerCycle => "logger reports an explicit offline power-cycle interval"
    MixedFaults => "door opening, dropout, warming and power-cycle faults"
  }
}

///|
/// Parse a CLI-friendly scenario name.
pub fn parse_simulation_scenario(name : String) -> SimulationScenario? {
  match name.trim().to_owned().to_lower() {
    "stable" | "stable-transit" => Some(StableTransit)
    "door" | "door-open" => Some(DoorOpen)
    "cooling" | "cooling-failure" => Some(CoolingFailure)
    "freezer" | "freezer-exposure" => Some(FreezerExposure)
    "dropout" | "sensor-dropout" => Some(SensorDropout)
    "stuck" | "sensor-stuck" => Some(SensorStuckFault)
    "power" | "power-cycle" => Some(PowerCycle)
    "mixed" | "mixed-faults" => Some(MixedFaults)
    _ => None
  }
}

///|
/// Generate a complete deterministic simulation.
pub fn simulate(config : SimulationConfig) -> SimulationResult {
  let diagnostics = validate_simulation_config(config)
  if diagnostics_have_errors(diagnostics) {
    return {
      config,
      readings: [],
      description: simulation_scenario_description(config.scenario),
      diagnostics,
    }
  }
  let readings : Array[Reading] = []
  let rng = new_rng(config.seed)
  let mut elapsed = 0L
  while elapsed <= config.duration_seconds {
    for sensor_index = 0
        sensor_index < config.sensor_ids.length()
        sensor_index = sensor_index + 1 {
      if !should_emit_sample(config, elapsed, sensor_index) {
        continue
      }
      let baseline = baseline_temperature(config, elapsed, sensor_index)
      let transformed = scenario_temperature(config, elapsed, baseline)
      let noise = if config.scenario == SensorStuckFault &&
        sensor_index == 0 &&
        elapsed >= config.event_start_seconds {
        0.0
      } else {
        rng.jitter(config.noise_amplitude_c)
      }
      readings.push({
        timestamp: config.started_at + elapsed,
        sensor_id: config.sensor_ids[sensor_index],
        temperature_c: transformed + noise,
        humidity_percent: Some(
          clamp_double(config.humidity_percent + rng.jitter(1.5), 0.0, 100.0),
        ),
        battery_percent: Some(simulated_battery(config, elapsed)),
        status: simulated_status(config, elapsed),
        origin: Simulated,
        flags: [],
        source_line: None,
      })
    }
    elapsed = elapsed + config.interval_seconds
  }
  {
    config,
    readings: sort_readings(readings),
    description: simulation_scenario_description(config.scenario),
    diagnostics,
  }
}

///|
/// Convenience simulation used by examples and smoke tests.
pub fn simulate_named(name : String) -> SimulationResult {
  match parse_simulation_scenario(name) {
    Some(scenario) => simulate(default_simulation_config(scenario))
    None => {
      let config = default_simulation_config(StableTransit)
      {
        config,
        readings: [],
        description: "unknown simulation scenario",
        diagnostics: [
          error_diagnostic(
            "simulate.unknown_scenario",
            "unknown scenario: \{name}",
          ),
        ],
      }
    }
  }
}