///|
/// Runtime event categories exposed by the integration facade.
pub enum RuntimeEventKind {
  RuntimeStarted
  StationRegisteredRuntime
  TelemetryAcceptedRuntime
  TelemetryRejectedRuntime
  CommandSubmittedRuntime
  CommandDispatchedRuntime
  CommandCompletedRuntime
  FileSessionRuntime
  RedundancyRuntime
  RuntimeDiagnostic
} derive(Eq, Debug)

///|
pub struct RuntimeEvent {
  sequence : Int
  timestamp : Int
  kind : RuntimeEventKind
  station : CommonAddress?
  object : InformationAddress?
  correlation : Int
  message : String
} derive(Eq, Debug)

///|
pub fn RuntimeEvent::sequence(self : RuntimeEvent) -> Int {
  self.sequence
}

///|
pub fn RuntimeEvent::timestamp(self : RuntimeEvent) -> Int {
  self.timestamp
}

///|
pub fn RuntimeEvent::kind(self : RuntimeEvent) -> RuntimeEventKind {
  self.kind
}

///|
pub fn RuntimeEvent::station(self : RuntimeEvent) -> CommonAddress? {
  self.station
}

///|
pub fn RuntimeEvent::object(self : RuntimeEvent) -> InformationAddress? {
  self.object
}

///|
pub fn RuntimeEvent::correlation(self : RuntimeEvent) -> Int {
  self.correlation
}

///|
pub fn RuntimeEvent::message(self : RuntimeEvent) -> String {
  self.message
}

///|
/// Runtime-wide limits and policy defaults.
pub struct RuntimeConfig {
  event_limit : Int
  audit_limit : Int
  scheduler_audit_limit : Int
  file_history_limit : Int
  file_capacity : Int
  file_maximum_size : Int
  telemetry_alarm_limit : Int
  redundancy_policy : RedundancyPolicy
} derive(Debug)

///|
pub fn RuntimeConfig::default() -> RuntimeConfig {
  {
    event_limit: 8192,
    audit_limit: 4096,
    scheduler_audit_limit: 4096,
    file_history_limit: 1024,
    file_capacity: 128,
    file_maximum_size: 1048576,
    telemetry_alarm_limit: 4096,
    redundancy_policy: RedundancyPolicy::new().unwrap(),
  }
}

///|
pub fn RuntimeConfig::new(
  event_limit? : Int = 8192,
  audit_limit? : Int = 4096,
  scheduler_audit_limit? : Int = 4096,
  file_history_limit? : Int = 1024,
  file_capacity? : Int = 128,
  file_maximum_size? : Int = 1048576,
  telemetry_alarm_limit? : Int = 4096,
  redundancy_policy? : RedundancyPolicy = RedundancyPolicy::new().unwrap(),
) -> Result[RuntimeConfig, String] {
  if event_limit < 1 ||
    audit_limit < 1 ||
    scheduler_audit_limit < 1 ||
    file_history_limit < 1 ||
    file_capacity < 1 ||
    file_maximum_size < 1 ||
    telemetry_alarm_limit < 1 {
    Err("runtime resource limits must be positive")
  } else {
    Ok({
      event_limit,
      audit_limit,
      scheduler_audit_limit,
      file_history_limit,
      file_capacity,
      file_maximum_size,
      telemetry_alarm_limit,
      redundancy_policy,
    })
  }
}

///|
pub fn RuntimeConfig::event_limit(self : RuntimeConfig) -> Int {
  self.event_limit
}

///|
pub fn RuntimeConfig::audit_limit(self : RuntimeConfig) -> Int {
  self.audit_limit
}

///|
pub fn RuntimeConfig::scheduler_audit_limit(self : RuntimeConfig) -> Int {
  self.scheduler_audit_limit
}

///|
pub fn RuntimeConfig::file_history_limit(self : RuntimeConfig) -> Int {
  self.file_history_limit
}

///|
pub fn RuntimeConfig::file_capacity(self : RuntimeConfig) -> Int {
  self.file_capacity
}

///|
pub fn RuntimeConfig::file_maximum_size(self : RuntimeConfig) -> Int {
  self.file_maximum_size
}

///|
pub fn RuntimeConfig::telemetry_alarm_limit(self : RuntimeConfig) -> Int {
  self.telemetry_alarm_limit
}

///|
pub fn RuntimeConfig::redundancy_policy(
  self : RuntimeConfig,
) -> RedundancyPolicy {
  self.redundancy_policy
}

///|
/// Runtime health summary for control-room endpoints.
pub struct RuntimeHealth {
  timestamp : Int
  revision : Int
  stations : Int
  points : Int
  pending_commands : Int
  active_alarms : Int
  active_sessions : Int
  redundancy_owners : Int
  event_count : Int
  policy_revision : Int
  healthy : Bool
  message : String
} derive(Eq, Debug)

///|
pub fn RuntimeHealth::timestamp(self : RuntimeHealth) -> Int {
  self.timestamp
}

///|
pub fn RuntimeHealth::revision(self : RuntimeHealth) -> Int {
  self.revision
}

///|
pub fn RuntimeHealth::stations(self : RuntimeHealth) -> Int {
  self.stations
}

///|
pub fn RuntimeHealth::points(self : RuntimeHealth) -> Int {
  self.points
}

///|
pub fn RuntimeHealth::pending_commands(self : RuntimeHealth) -> Int {
  self.pending_commands
}

///|
pub fn RuntimeHealth::active_alarms(self : RuntimeHealth) -> Int {
  self.active_alarms
}

///|
pub fn RuntimeHealth::active_sessions(self : RuntimeHealth) -> Int {
  self.active_sessions
}

///|
pub fn RuntimeHealth::redundancy_owners(self : RuntimeHealth) -> Int {
  self.redundancy_owners
}

///|
pub fn RuntimeHealth::event_count(self : RuntimeHealth) -> Int {
  self.event_count
}

///|
pub fn RuntimeHealth::policy_revision(self : RuntimeHealth) -> Int {
  self.policy_revision
}

///|
pub fn RuntimeHealth::healthy(self : RuntimeHealth) -> Bool {
  self.healthy
}

///|
pub fn RuntimeHealth::message(self : RuntimeHealth) -> String {
  self.message
}

///|
/// Aggregated runtime counters.
pub struct RuntimeMetrics {
  mut telemetry_received : Int
  mut telemetry_accepted : Int
  mut telemetry_rejected : Int
  mut commands_submitted : Int
  mut commands_dispatched : Int
  mut commands_completed : Int
  mut diagnostics : Int
} derive(Eq, Debug)

///|
pub fn RuntimeMetrics::new() -> RuntimeMetrics {
  {
    telemetry_received: 0,
    telemetry_accepted: 0,
    telemetry_rejected: 0,
    commands_submitted: 0,
    commands_dispatched: 0,
    commands_completed: 0,
    diagnostics: 0,
  }
}

///|
pub fn RuntimeMetrics::telemetry_received(self : RuntimeMetrics) -> Int {
  self.telemetry_received
}

///|
pub fn RuntimeMetrics::telemetry_accepted(self : RuntimeMetrics) -> Int {
  self.telemetry_accepted
}

///|
pub fn RuntimeMetrics::telemetry_rejected(self : RuntimeMetrics) -> Int {
  self.telemetry_rejected
}

///|
pub fn RuntimeMetrics::commands_submitted(self : RuntimeMetrics) -> Int {
  self.commands_submitted
}

///|
pub fn RuntimeMetrics::commands_dispatched(self : RuntimeMetrics) -> Int {
  self.commands_dispatched
}

///|
pub fn RuntimeMetrics::commands_completed(self : RuntimeMetrics) -> Int {
  self.commands_completed
}

///|
pub fn RuntimeMetrics::diagnostics(self : RuntimeMetrics) -> Int {
  self.diagnostics
}

///|
/// Integration facade connecting data, control, policy and redundancy layers.
pub struct Iec104Runtime {
  database : StationDatabase
  telemetry : TelemetryPipeline
  schedulers : Map[Int, CommandScheduler]
  redundancy : Map[Int, RedundancyManager]
  policy : OperationalPolicy
  redundancy_policy : RedundancyPolicy
  files : FileTransferManager
  clock : VirtualClock
  events : Array[RuntimeEvent]
  event_limit : Int
  mut next_event : Int
  metrics : RuntimeMetrics
} derive(Debug)

///|
pub fn Iec104Runtime::new(
  config? : RuntimeConfig = RuntimeConfig::default(),
) -> Result[Iec104Runtime, String] {
  match StationDatabase::new(event_limit=config.event_limit()) {
    Err(error) => Err(error)
    Ok(database) =>
      match TelemetryPipeline::new(alarm_limit=config.telemetry_alarm_limit()) {
        Err(error) => Err(error)
        Ok(telemetry) =>
          match OperationalPolicy::new(audit_limit=config.audit_limit()) {
            Err(error) => Err(error)
            Ok(policy) =>
              match
                FileStore::new(
                  capacity=config.file_capacity(),
                  maximum_size=config.file_maximum_size(),
                ) {
                Err(error) => Err(error)
                Ok(store) =>
                  match
                    FileTransferManager::new(
                      store,
                      history_limit=config.file_history_limit(),
                    ) {
                    Err(error) => Err(error)
                    Ok(files) =>
                      match VirtualClock::new() {
                        Err(error) => Err(error)
                        Ok(clock) =>
                          Ok({
                            database,
                            telemetry,
                            schedulers: {},
                            redundancy: {},
                            policy,
                            redundancy_policy: config.redundancy_policy(),
                            files,
                            clock,
                            events: [],
                            event_limit: config.event_limit(),
                            next_event: 1,
                            metrics: RuntimeMetrics::new(),
                          })
                      }
                  }
              }
          }
      }
  }
}

///|
pub fn Iec104Runtime::now(self : Iec104Runtime) -> Int {
  self.clock.now()
}

///|
pub fn Iec104Runtime::database(self : Iec104Runtime) -> StationDatabase {
  self.database
}

///|
pub fn Iec104Runtime::telemetry(self : Iec104Runtime) -> TelemetryPipeline {
  self.telemetry
}

///|
pub fn Iec104Runtime::policy(self : Iec104Runtime) -> OperationalPolicy {
  self.policy
}

///|
pub fn Iec104Runtime::files(self : Iec104Runtime) -> FileTransferManager {
  self.files
}

///|
pub fn Iec104Runtime::metrics(self : Iec104Runtime) -> RuntimeMetrics {
  self.metrics
}

///|
pub fn Iec104Runtime::events(self : Iec104Runtime) -> Array[RuntimeEvent] {
  self.events.copy()
}

///|
fn Iec104Runtime::event(
  self : Iec104Runtime,
  timestamp : Int,
  kind : RuntimeEventKind,
  station : CommonAddress?,
  object : InformationAddress?,
  correlation : Int,
  message : String,
) -> RuntimeEvent {
  let value = {
    sequence: self.next_event,
    timestamp,
    kind,
    station,
    object,
    correlation,
    message,
  }
  self.next_event += 1
  self.events.push(value)
  while self.events.length() > self.event_limit {
    ignore(self.events.remove(0))
  }
  value
}

///|
pub fn Iec104Runtime::register_station(
  self : Iec104Runtime,
  config : StationConfig,
) -> Result[StationSummary, String] {
  let timestamp = self.now()
  match self.database.register(config, timestamp) {
    Err(error) => Err(error)
    Ok(summary) =>
      match CommandScheduler::new(audit_limit=self.event_limit) {
        Err(error) => Err(error)
        Ok(scheduler) => {
          self.schedulers[config.common_address().number()] = scheduler
          match
            RedundancyManager::new(
              config.common_address(),
              policy=self.default_redundancy_policy(),
            ) {
            Err(error) => Err(error)
            Ok(manager) => {
              self.redundancy[config.common_address().number()] = manager
              ignore(
                self.event(
                  timestamp,
                  StationRegisteredRuntime,
                  Some(config.common_address()),
                  None,
                  0,
                  "station registered at runtime",
                ),
              )
              Ok(summary)
            }
          }
        }
      }
  }
}

///|
fn Iec104Runtime::default_redundancy_policy(
  self : Iec104Runtime,
) -> RedundancyPolicy {
  self.redundancy_policy
}

///|
pub fn Iec104Runtime::add_peer(
  self : Iec104Runtime,
  peer : RedundantPeer,
) -> Result[Unit, String] {
  match self.redundancy.get(peer.station().number()) {
    None => Err("station has no redundancy manager")
    Some(manager) => manager.add_peer(peer)
  }
}

///|
pub fn Iec104Runtime::ingest(
  self : Iec104Runtime,
  station : CommonAddress,
  object : ApplicationObject,
  source? : String = "protocol",
) -> Result[PipelineResult, Diagnostic] {
  let timestamp = self.now()
  self.metrics.telemetry_received += 1
  match self.telemetry.ingest(station, object, timestamp, source~) {
    Err(error) => {
      self.metrics.telemetry_rejected += 1
      self.metrics.diagnostics += 1
      ignore(
        self.event(
          timestamp,
          TelemetryRejectedRuntime,
          Some(station),
          Some(object.address()),
          0,
          error.to_line(),
        ),
      )
      Err(error)
    }
    Ok(result) =>
      if result.accepted() {
        match self.database.write(station, object, timestamp, source~) {
          Err(error) => {
            self.metrics.telemetry_rejected += 1
            self.metrics.diagnostics += 1
            Err(error)
          }
          Ok(_) => {
            self.metrics.telemetry_accepted += 1
            ignore(
              self.event(
                timestamp,
                TelemetryAcceptedRuntime,
                Some(station),
                Some(object.address()),
                result.sample().sequence(),
                result.message(),
              ),
            )
            Ok(result)
          }
        }
      } else {
        self.metrics.telemetry_rejected += 1
        Ok(result)
      }
  }
}

///|
pub fn Iec104Runtime::heartbeat(
  self : Iec104Runtime,
  station : CommonAddress,
  peer_id : Int,
) -> Result[Unit, String] {
  let timestamp = self.now()
  match self.database.heartbeat(station, timestamp) {
    Err(error) => Err(error)
    Ok(_) =>
      match self.redundancy.get(station.number()) {
        None => Ok(())
        Some(manager) =>
          match manager.heartbeat(peer_id, timestamp) {
            Err(error) => Err(error)
            Ok(_) => {
              ignore(
                self.event(
                  timestamp,
                  RedundancyRuntime,
                  Some(station),
                  None,
                  peer_id,
                  "redundant heartbeat accepted",
                ),
              )
              Ok(())
            }
          }
      }
  }
}

///|
pub fn Iec104Runtime::submit_command(
  self : Iec104Runtime,
  operator_id : String,
  station : CommonAddress,
  object : ApplicationObject,
  priority : CommandPriority,
  deadline : Int,
  idempotency_key : String,
) -> Result[CommandRequest, Diagnostic] {
  let decision = self.policy.authorize(
    operator_id,
    SubmitCommands,
    station,
    Some(object.type_id()),
    self.now(),
  )
  if !decision.allowed() {
    self.metrics.diagnostics += 1
    Err(Diagnostic::new(InvalidQualifier, decision.reason()))
  } else {
    match self.schedulers.get(station.number()) {
      None =>
        Err(Diagnostic::new(InvalidAddress, "station has no command scheduler"))
      Some(scheduler) =>
        match
          scheduler.submit(
            station,
            object,
            priority,
            self.now(),
            deadline,
            idempotency_key,
          ) {
          Err(error) => {
            self.metrics.diagnostics += 1
            Err(error)
          }
          Ok(request) => {
            self.metrics.commands_submitted += 1
            ignore(
              self.event(
                self.now(),
                CommandSubmittedRuntime,
                Some(station),
                Some(object.address()),
                request.ticket(),
                "command submitted",
              ),
            )
            Ok(request)
          }
        }
    }
  }
}

///|
pub fn Iec104Runtime::dispatch_command(
  self : Iec104Runtime,
  station : CommonAddress,
  policy : CommandPolicy,
) -> Result[CommandDispatch?, Diagnostic] {
  match self.schedulers.get(station.number()) {
    None =>
      Err(Diagnostic::new(InvalidAddress, "station has no command scheduler"))
    Some(scheduler) =>
      match scheduler.dispatch(policy, self.now()) {
        Err(error) => Err(error)
        Ok(dispatch) => {
          match dispatch {
            Some(value) => {
              self.metrics.commands_dispatched += 1
              ignore(
                self.event(
                  self.now(),
                  CommandDispatchedRuntime,
                  Some(station),
                  Some(value.object().address()),
                  value.ticket(),
                  "command dispatched",
                ),
              )
            }
            None => ()
          }
          Ok(dispatch)
        }
      }
  }
}

///|
pub fn Iec104Runtime::confirm_command(
  self : Iec104Runtime,
  station : CommonAddress,
  ticket : Int,
  accepted : Bool,
  message? : String = "",
) -> Result[CommandRequest, Diagnostic] {
  match self.schedulers.get(station.number()) {
    None =>
      Err(Diagnostic::new(InvalidAddress, "station has no command scheduler"))
    Some(scheduler) =>
      match scheduler.confirm(ticket, self.now(), accepted, message~) {
        Err(error) => Err(error)
        Ok(request) => {
          if accepted {
            self.metrics.commands_completed += 1
            ignore(
              self.event(
                self.now(),
                CommandCompletedRuntime,
                Some(station),
                Some(request.object().address()),
                ticket,
                message,
              ),
            )
          }
          Ok(request)
        }
      }
  }
}

///|
pub fn Iec104Runtime::tick(
  self : Iec104Runtime,
  seconds : Int,
) -> Result[Unit, String] {
  match self.clock.advance(seconds) {
    Err(error) => Err(error)
    Ok(now) => {
      for _, scheduler in self.schedulers {
        ignore(scheduler.expire(now))
      }
      for _, manager in self.redundancy {
        ignore(manager.tick(now))
        ignore(manager.elect(now))
      }
      Ok(())
    }
  }
}

///|
pub fn Iec104Runtime::health(self : Iec104Runtime) -> RuntimeHealth {
  let points = self.database.query(
    StationQuery::all().with_disabled(true),
    self.now(),
  )
  let mut pending = 0
  let mut owners = 0
  for _, scheduler in self.schedulers {
    pending += scheduler.pending_count()
  }
  for _, manager in self.redundancy {
    if manager.owner() is Some(_) {
      owners += 1
    }
  }
  let alarms = self.telemetry.alarm_history().length()
  let sessions = self.files.session_count()
  {
    timestamp: self.now(),
    revision: self.database.revision(),
    stations: self.database.station_count(),
    points: points.length(),
    pending_commands: pending,
    active_alarms: alarms,
    active_sessions: sessions,
    redundancy_owners: owners,
    event_count: self.events.length(),
    policy_revision: self.policy.revision(),
    healthy: self.metrics.diagnostics == 0,
    message: if self.metrics.diagnostics == 0 {
      "runtime healthy"
    } else {
      "runtime has diagnostics"
    },
  }
}

///|
pub fn runtime_event_kind_examples() -> Array[RuntimeEventKind] {
  [
    RuntimeStarted,
    StationRegisteredRuntime,
    TelemetryAcceptedRuntime,
    TelemetryRejectedRuntime,
    CommandSubmittedRuntime,
    CommandDispatchedRuntime,
    CommandCompletedRuntime,
    FileSessionRuntime,
    RedundancyRuntime,
    RuntimeDiagnostic,
  ]
}