///|
/// Semantic version of the stable report model.
pub const REPORT_SCHEMA : String = "thermo-trail.report/v1"

///|
/// Origin of a temperature sample.
pub(all) enum SampleOrigin {
  Recorded
  Interpolated
  Simulated
} derive(Debug, Eq)

///|
/// Quality flags are additive: one sample may carry several independent issues.
pub(all) enum QualityFlag {
  DuplicateTimestamp
  MissingField
  OutOfPhysicalRange
  GapBefore
  Calibrated
  SensorStuck
  SensorNoisy
  SensorDrifting
  SensorOffline
  SensorConflict
  UserExcluded
} derive(Debug, Eq)

///|
/// Domain-level diagnostic severity.
pub(all) enum DiagnosticLevel {
  Info
  Warning
  Error
} derive(Debug, Eq)

///|
/// A diagnostic always points to a stable code and may include an input line.
pub(all) struct Diagnostic {
  level : DiagnosticLevel
  code : String
  message : String
  line : Int?
  sensor_id : String?
} derive(Debug, Eq)

///|
/// Create an informational diagnostic.
pub fn info_diagnostic(code : String, message : String) -> Diagnostic {
  { level: Info, code, message, line: None, sensor_id: None }
}

///|
/// Create a warning diagnostic.
pub fn warning_diagnostic(code : String, message : String) -> Diagnostic {
  { level: Warning, code, message, line: None, sensor_id: None }
}

///|
/// Create an error diagnostic.
pub fn error_diagnostic(code : String, message : String) -> Diagnostic {
  { level: Error, code, message, line: None, sensor_id: None }
}

///|
/// Attach an input line to a diagnostic.
pub fn Diagnostic::at_line(self : Diagnostic, line : Int) -> Diagnostic {
  { ..self, line: Some(line) }
}

///|
/// Attach a sensor identifier to a diagnostic.
pub fn Diagnostic::for_sensor(
  self : Diagnostic,
  sensor_id : String,
) -> Diagnostic {
  { ..self, sensor_id: Some(sensor_id) }
}

///|
/// A normalized sensor reading. Timestamp is UTC Unix seconds.
pub(all) struct Reading {
  timestamp : Int64
  sensor_id : String
  temperature_c : Double
  humidity_percent : Double?
  battery_percent : Double?
  status : String
  origin : SampleOrigin
  flags : Array[QualityFlag]
  source_line : Int?
} derive(Debug, Eq)

///|
/// Construct the smallest valid reading.
pub fn reading(
  timestamp : Int64,
  sensor_id : String,
  temperature_c : Double,
) -> Reading {
  {
    timestamp,
    sensor_id,
    temperature_c,
    humidity_percent: None,
    battery_percent: None,
    status: "ok",
    origin: Recorded,
    flags: [],
    source_line: None,
  }
}

///|
/// Add a quality flag once while preserving the existing order.
pub fn Reading::add_flag(self : Reading, flag : QualityFlag) -> Reading {
  let next = self.flags.copy()
  let mut found = false
  for existing in next {
    if existing == flag {
      found = true
    }
  }
  if !found {
    next.push(flag)
  }
  { ..self, flags: next }
}

///|
/// Whether the reading contains a particular quality flag.
pub fn Reading::has_flag(self : Reading, flag : QualityFlag) -> Bool {
  for existing in self.flags {
    if existing == flag {
      return true
    }
  }
  false
}

///|
/// True when a reading should participate in primary metrics.
pub fn Reading::is_usable(self : Reading) -> Bool {
  !self.has_flag(UserExcluded) && !self.has_flag(OutOfPhysicalRange)
}

///|
/// Limits used by the excursion state machine.
pub(all) struct TemperaturePolicy {
  lower_c : Double
  upper_c : Double
  hysteresis_c : Double
  grace_seconds : Int64
  minimum_event_seconds : Int64
  maximum_gap_seconds : Int64
} derive(Debug, Eq)

///|
/// A conservative policy suitable for a 2–8 °C demonstration data set.
pub fn default_temperature_policy() -> TemperaturePolicy {
  {
    lower_c: 2.0,
    upper_c: 8.0,
    hysteresis_c: 0.3,
    grace_seconds: 300L,
    minimum_event_seconds: 120L,
    maximum_gap_seconds: 900L,
  }
}

///|
/// Validate policy relationships and ranges.
pub fn validate_temperature_policy(
  policy : TemperaturePolicy,
) -> Array[Diagnostic] {
  let issues : Array[Diagnostic] = []
  if policy.lower_c >= policy.upper_c {
    issues.push(
      error_diagnostic(
        "policy.invalid_range", "lower temperature must be below upper temperature",
      ),
    )
  }
  if policy.hysteresis_c < 0.0 {
    issues.push(
      error_diagnostic(
        "policy.negative_hysteresis", "hysteresis cannot be negative",
      ),
    )
  }
  if policy.hysteresis_c * 2.0 >= policy.upper_c - policy.lower_c {
    issues.push(
      error_diagnostic(
        "policy.excessive_hysteresis", "hysteresis consumes the accepted temperature band",
      ),
    )
  }
  if policy.grace_seconds < 0L {
    issues.push(
      error_diagnostic(
        "policy.negative_grace", "grace period cannot be negative",
      ),
    )
  }
  if policy.minimum_event_seconds < 0L {
    issues.push(
      error_diagnostic(
        "policy.negative_minimum_event", "minimum event duration cannot be negative",
      ),
    )
  }
  if policy.maximum_gap_seconds <= 0L {
    issues.push(
      error_diagnostic(
        "policy.invalid_gap", "maximum sampling gap must be positive",
      ),
    )
  }
  issues
}

///|
/// Calibration and physical plausibility settings for one sensor.
pub(all) struct SensorProfile {
  sensor_id : String
  calibration_offset_c : Double
  physical_min_c : Double
  physical_max_c : Double
  expected_interval_seconds : Int64
  stuck_tolerance_c : Double
  stuck_minimum_samples : Int
  noise_step_c : Double
  drift_window_samples : Int
  drift_threshold_c : Double
} derive(Debug, Eq)

///|
/// Default profile for common electronic temperature loggers.
pub fn default_sensor_profile(sensor_id : String) -> SensorProfile {
  {
    sensor_id,
    calibration_offset_c: 0.0,
    physical_min_c: -100.0,
    physical_max_c: 100.0,
    expected_interval_seconds: 300L,
    stuck_tolerance_c: 0.01,
    stuck_minimum_samples: 6,
    noise_step_c: 5.0,
    drift_window_samples: 6,
    drift_threshold_c: 1.5,
  }
}

///|
/// Validate one sensor profile.
pub fn validate_sensor_profile(profile : SensorProfile) -> Array[Diagnostic] {
  let issues : Array[Diagnostic] = []
  if profile.sensor_id.trim().is_empty() {
    issues.push(
      error_diagnostic("sensor.empty_id", "sensor identifier cannot be empty"),
    )
  }
  if profile.physical_min_c >= profile.physical_max_c {
    issues.push(
      error_diagnostic(
        "sensor.invalid_physical_range", "physical minimum must be below maximum",
      ).for_sensor(profile.sensor_id),
    )
  }
  if profile.expected_interval_seconds <= 0L {
    issues.push(
      error_diagnostic(
        "sensor.invalid_interval", "expected sample interval must be positive",
      ).for_sensor(profile.sensor_id),
    )
  }
  if profile.stuck_tolerance_c < 0.0 {
    issues.push(
      error_diagnostic(
        "sensor.invalid_stuck_tolerance", "stuck tolerance cannot be negative",
      ).for_sensor(profile.sensor_id),
    )
  }
  if profile.stuck_minimum_samples < 2 {
    issues.push(
      error_diagnostic(
        "sensor.invalid_stuck_samples", "stuck detection needs at least two samples",
      ).for_sensor(profile.sensor_id),
    )
  }
  if profile.noise_step_c <= 0.0 {
    issues.push(
      error_diagnostic(
        "sensor.invalid_noise_step", "noise step threshold must be positive",
      ).for_sensor(profile.sensor_id),
    )
  }
  if profile.drift_window_samples < 2 {
    issues.push(
      error_diagnostic(
        "sensor.invalid_drift_window", "drift window needs at least two samples",
      ).for_sensor(profile.sensor_id),
    )
  }
  issues
}

///|
/// Overall analysis behavior.
pub(all) struct AnalysisConfig {
  policy : TemperaturePolicy
  profiles : Array[SensorProfile]
  default_profile : SensorProfile
  merge_duplicate_average : Bool
  interpolate_short_gaps : Bool
  interpolation_limit_seconds : Int64
  conflict_threshold_c : Double
  include_flagged_in_metrics : Bool
} derive(Debug, Eq)

///|
/// Create a configuration for an arbitrary set of sensor identifiers.
pub fn default_analysis_config() -> AnalysisConfig {
  {
    policy: default_temperature_policy(),
    profiles: [],
    default_profile: default_sensor_profile("*"),
    merge_duplicate_average: true,
    interpolate_short_gaps: false,
    interpolation_limit_seconds: 600L,
    conflict_threshold_c: 1.5,
    include_flagged_in_metrics: false,
  }
}

///|
/// Resolve a sensor-specific profile or clone the wildcard defaults.
pub fn AnalysisConfig::profile_for(
  self : AnalysisConfig,
  sensor_id : String,
) -> SensorProfile {
  for profile in self.profiles {
    if profile.sensor_id == sensor_id {
      return profile
    }
  }
  { ..self.default_profile, sensor_id, }
}

///|
/// Validate the complete analysis configuration.
pub fn validate_analysis_config(config : AnalysisConfig) -> Array[Diagnostic] {
  let issues = validate_temperature_policy(config.policy)
  issues.append(validate_sensor_profile(config.default_profile))
  for profile in config.profiles {
    issues.append(validate_sensor_profile(profile))
  }
  if config.interpolation_limit_seconds < 0L {
    issues.push(
      error_diagnostic(
        "config.invalid_interpolation_limit", "interpolation limit cannot be negative",
      ),
    )
  }
  if config.conflict_threshold_c <= 0.0 {
    issues.push(
      error_diagnostic(
        "config.invalid_conflict_threshold", "sensor conflict threshold must be positive",
      ),
    )
  }
  issues
}

///|
/// Classification of an excursion.
pub(all) enum ExcursionKind {
  LowTemperature
  HighTemperature
  DataGap
  SensorFailure
} derive(Debug, Eq)

///|
/// Life-cycle status retained for auditability.
pub(all) enum EventStatus {
  Candidate
  Confirmed
  Suppressed
  Closed
} derive(Debug, Eq)

///|
/// One confirmed or suppressed excursion segment.
pub(all) struct ExcursionEvent {
  event_id : String
  sensor_id : String
  kind : ExcursionKind
  status : EventStatus
  started_at : Int64
  ended_at : Int64
  duration_seconds : Int64
  sample_count : Int
  minimum_c : Double?
  maximum_c : Double?
  mean_c : Double?
  degree_seconds : Double
  peak_deviation_c : Double
  reason : String
} derive(Debug, Eq)

///|
/// Empty event helper used internally by state machines and tests.
pub fn empty_excursion(
  event_id : String,
  sensor_id : String,
  kind : ExcursionKind,
  started_at : Int64,
) -> ExcursionEvent {
  {
    event_id,
    sensor_id,
    kind,
    status: Candidate,
    started_at,
    ended_at: started_at,
    duration_seconds: 0L,
    sample_count: 0,
    minimum_c: None,
    maximum_c: None,
    mean_c: None,
    degree_seconds: 0.0,
    peak_deviation_c: 0.0,
    reason: "",
  }
}

///|
/// Describes one missing interval in an otherwise ordered sequence.
pub(all) struct SamplingGap {
  sensor_id : String
  previous_timestamp : Int64
  next_timestamp : Int64
  duration_seconds : Int64
  estimated_missing_samples : Int
} derive(Debug, Eq)

///|
/// Summary statistics for one sensor.
pub(all) struct SensorStatistics {
  sensor_id : String
  sample_count : Int
  usable_count : Int
  first_timestamp : Int64?
  last_timestamp : Int64?
  minimum_c : Double?
  maximum_c : Double?
  mean_c : Double?
  time_weighted_mean_c : Double?
  mean_kinetic_temperature_c : Double?
  standard_deviation_c : Double?
  median_c : Double?
  p05_c : Double?
  p95_c : Double?
  in_range_seconds : Int64
  low_seconds : Int64
  high_seconds : Int64
  unknown_seconds : Int64
  low_degree_seconds : Double
  high_degree_seconds : Double
} derive(Debug, Eq)

///|
/// One fixed-width time-window aggregate.
pub(all) struct WindowStatistic {
  sensor_id : String
  started_at : Int64
  ended_at : Int64
  sample_count : Int
  minimum_c : Double?
  maximum_c : Double?
  mean_c : Double?
  low_degree_seconds : Double
  high_degree_seconds : Double
} derive(Debug, Eq)

///|
/// Health assessment for a sensor stream.
pub(all) struct SensorHealth {
  sensor_id : String
  score : Int
  stuck_runs : Int
  noisy_steps : Int
  drift_windows : Int
  gaps : Int
  conflicts : Int
  low_battery_samples : Int
  flagged_samples : Int
  observations : Array[String]
} derive(Debug, Eq)

///|
/// Overall risk bands are deliberately non-regulatory.
pub(all) enum RiskBand {
  Minimal
  Low
  Moderate
  High
  Critical
  Indeterminate
} derive(Debug, Eq)

///|
/// Explainable risk score with component contributions.
pub(all) struct RiskAssessment {
  score : Int
  band : RiskBand
  temperature_component : Int
  duration_component : Int
  data_quality_component : Int
  sensor_health_component : Int
  confidence_percent : Int
  reasons : Array[String]
} derive(Debug, Eq)

///|
/// Complete immutable result of one analysis run.
pub(all) struct AnalysisReport {
  schema : String
  shipment_id : String
  generated_at : Int64
  source_name : String
  sensor_ids : Array[String]
  readings : Array[Reading]
  gaps : Array[SamplingGap]
  events : Array[ExcursionEvent]
  statistics : Array[SensorStatistics]
  health : Array[SensorHealth]
  risk : RiskAssessment
  diagnostics : Array[Diagnostic]
} derive(Debug, Eq)

///|
/// Result of parsing or normalizing readings.
pub(all) struct ReadingBatch {
  readings : Array[Reading]
  diagnostics : Array[Diagnostic]
} derive(Debug, Eq)

///|
/// Create an empty batch.
pub fn empty_reading_batch() -> ReadingBatch {
  { readings: [], diagnostics: [] }
}

///|
/// Count diagnostics at or above error level.
pub fn diagnostics_have_errors(diagnostics : Array[Diagnostic]) -> Bool {
  for diagnostic in diagnostics {
    if diagnostic.level == Error {
      return true
    }
  }
  false
}

///|
/// Count diagnostics of one level.
pub fn count_diagnostics(
  diagnostics : Array[Diagnostic],
  level : DiagnosticLevel,
) -> Int {
  let mut count = 0
  for diagnostic in diagnostics {
    if diagnostic.level == level {
      count = count + 1
    }
  }
  count
}

///|
/// Stable lowercase name for a diagnostic level.
pub fn diagnostic_level_name(level : DiagnosticLevel) -> String {
  match level {
    Info => "info"
    Warning => "warning"
    Error => "error"
  }
}

///|
/// Stable lowercase name for a quality flag.
pub fn quality_flag_name(flag : QualityFlag) -> String {
  match flag {
    DuplicateTimestamp => "duplicate_timestamp"
    MissingField => "missing_field"
    OutOfPhysicalRange => "out_of_physical_range"
    GapBefore => "gap_before"
    Calibrated => "calibrated"
    SensorStuck => "sensor_stuck"
    SensorNoisy => "sensor_noisy"
    SensorDrifting => "sensor_drifting"
    SensorOffline => "sensor_offline"
    SensorConflict => "sensor_conflict"
    UserExcluded => "user_excluded"
  }
}

///|
/// Stable lowercase name for an event kind.
pub fn excursion_kind_name(kind : ExcursionKind) -> String {
  match kind {
    LowTemperature => "low_temperature"
    HighTemperature => "high_temperature"
    DataGap => "data_gap"
    SensorFailure => "sensor_failure"
  }
}

///|
/// Stable lowercase name for a risk band.
pub fn risk_band_name(band : RiskBand) -> String {
  match band {
    Minimal => "minimal"
    Low => "low"
    Moderate => "moderate"
    High => "high"
    Critical => "critical"
    Indeterminate => "indeterminate"
  }
}

///|
/// Clamp an integer to an inclusive range.
pub fn clamp_int(value : Int, minimum : Int, maximum : Int) -> Int {
  if value < minimum {
    minimum
  } else if value > maximum {
    maximum
  } else {
    value
  }
}

///|
/// Clamp a double to an inclusive range.
pub fn clamp_double(
  value : Double,
  minimum : Double,
  maximum : Double,
) -> Double {
  if value < minimum {
    minimum
  } else if value > maximum {
    maximum
  } else {
    value
  }
}

///|
/// Absolute value without relying on target-specific math bindings.
pub fn abs_double(value : Double) -> Double {
  if value < 0.0 {
    -value
  } else {
    value
  }
}

///|
/// Copy an array so APIs do not share mutable array storage accidentally.
pub fn[T] copy_array(source : Array[T]) -> Array[T] {
  let result : Array[T] = []
  for item in source {
    result.push(item)
  }
  result
}