syntax = "proto3";
option csharp_namespace = "ZB.MOM.WW.ScadaBridge.Communication.Grpc";
package scadabridge.sitecommand.v1;

import "google/protobuf/timestamp.proto";
import "google/protobuf/duration.proto";
import "google/protobuf/wrappers.proto";

// ─────────────────────────────────────────────────────────────────────────────
// Site command plane (central → site) — the gRPC replacement for the per-site
// ClusterClient command/control channel handled by SiteCommunicationActor.
//
// The 28 migrated commands are grouped into six domain RPCs rather than 28
// individual RPCs, because the grouping is what carries the DEADLINE policy:
// every command inside one group shares a timeout class today (see
// CommunicationOptions), so one RPC per group keeps the deadline choice in one
// place on the client and one dispatch switch on the server. A `oneof` request
// / reply envelope preserves per-command typing inside the group.
//
// EVOLUTION RULE (same as sitestream.proto): field numbers are never reused and
// changes are additive only. New commands take the next free oneof tag; an
// older peer simply sees an unset oneof and answers with a clean error.
//
// The generated C# is CHECKED IN under Communication/SiteCommandGrpc/ because
// protoc segfaults inside the linux_arm64 Docker build image. Regenerate with
// docker/regen-proto.sh (or by hand per the recipe in the .csproj) — never by
// leaving an active <Protobuf> item in the project file.
// ─────────────────────────────────────────────────────────────────────────────

service SiteCommandService {
  // Deployment + instance lifecycle (6 commands).
  rpc ExecuteLifecycle(LifecycleRequest) returns (LifecycleReply);
  // Interactive OPC UA / MxGateway design-time commands (8 commands).
  rpc ExecuteOpcUa(OpcUaRequest) returns (OpcUaReply);
  // Remote read-only queries: event log + debug view (4 commands).
  rpc ExecuteQuery(QueryRequest) returns (QueryReply);
  // Parked store-and-forward message + cached-operation actions (5 commands).
  rpc ExecuteParked(ParkedRequest) returns (ParkedReply);
  // Inbound-API Route.To() relays (4 commands).
  rpc ExecuteRoute(RouteRequest) returns (RouteReply);
  // Operator-initiated manual site-pair failover (1 command).
  rpc TriggerFailover(TriggerSiteFailoverDto) returns (SiteFailoverAckDto);
}

// ─────────────────────────────────────────────────────────────────────────────
// Shared value carrier
// ─────────────────────────────────────────────────────────────────────────────

// Explicit null marker. Needed because a map<string, LooseValue> entry always
// has a value message present, so "the dictionary holds a null for this key"
// cannot be expressed by absence the way a nullable message FIELD can.
message LooseNull {}

message LooseValueList { repeated LooseValue items = 1; }

message LooseValueMap { map<string, LooseValue> entries = 1; }

// Type-tagged carrier for the `object?` members the command plane still uses
// (script parameters and return values, attribute values, tag read/write
// values). The tags cover every CLR type these fields actually carry, so those
// values round-trip with their runtime type intact; anything else falls back to
// `json_value` (see the mapper's documented lossiness note).
//
// Date/time and decimal ride as invariant round-trip STRINGS rather than
// google.protobuf.Timestamp: Timestamp normalises everything to UTC and would
// silently drop DateTime.Kind / DateTimeOffset.Offset, which for an operator's
// tag value is data loss, not normalisation.
message LooseValue {
  oneof kind {
    LooseNull null_value = 1;
    bool bool_value = 2;
    int32 int32_value = 3;
    int64 int64_value = 4;
    double double_value = 5;
    float float_value = 6;
    string string_value = 7;
    string decimal_value = 8;             // invariant-culture round-trip
    string date_time_value = 9;           // DateTime, "O" (preserves Kind)
    string date_time_offset_value = 10;   // DateTimeOffset, "O" (preserves Offset)
    string guid_value = 11;               // "D"
    string time_span_value = 12;          // "c" (constant/round-trip)
    bytes bytes_value = 13;
    LooseValueList list_value = 14;
    LooseValueMap map_value = 15;
    string json_value = 16;               // fallback; decodes to JsonElement
  }
}

// ─────────────────────────────────────────────────────────────────────────────
// Enums. Every enum reserves 0 for _UNSPECIFIED so a value that is absent on
// the wire is never mistaken for the first CLR member. The mapper translates
// explicitly in both directions (never by ordinal), so reordering a C# enum
// cannot silently re-map the wire.
// ─────────────────────────────────────────────────────────────────────────────

enum DeploymentStatusDto {
  DEPLOYMENT_STATUS_DTO_UNSPECIFIED = 0;
  DEPLOYMENT_STATUS_DTO_PENDING = 1;
  DEPLOYMENT_STATUS_DTO_IN_PROGRESS = 2;
  DEPLOYMENT_STATUS_DTO_SUCCESS = 3;
  DEPLOYMENT_STATUS_DTO_FAILED = 4;
}

enum BrowseNodeClassDto {
  BROWSE_NODE_CLASS_DTO_UNSPECIFIED = 0;
  BROWSE_NODE_CLASS_DTO_OBJECT = 1;
  BROWSE_NODE_CLASS_DTO_VARIABLE = 2;
  BROWSE_NODE_CLASS_DTO_METHOD = 3;
  BROWSE_NODE_CLASS_DTO_OTHER = 4;
}

enum BrowseFailureKindDto {
  BROWSE_FAILURE_KIND_DTO_UNSPECIFIED = 0;
  BROWSE_FAILURE_KIND_DTO_CONNECTION_NOT_FOUND = 1;
  BROWSE_FAILURE_KIND_DTO_CONNECTION_NOT_CONNECTED = 2;
  BROWSE_FAILURE_KIND_DTO_NOT_BROWSABLE = 3;
  BROWSE_FAILURE_KIND_DTO_TIMEOUT = 4;
  BROWSE_FAILURE_KIND_DTO_SERVER_ERROR = 5;
}

enum ReadTagValuesFailureKindDto {
  READ_TAG_VALUES_FAILURE_KIND_DTO_UNSPECIFIED = 0;
  READ_TAG_VALUES_FAILURE_KIND_DTO_CONNECTION_NOT_FOUND = 1;
  READ_TAG_VALUES_FAILURE_KIND_DTO_CONNECTION_NOT_CONNECTED = 2;
  READ_TAG_VALUES_FAILURE_KIND_DTO_TIMEOUT = 3;
  READ_TAG_VALUES_FAILURE_KIND_DTO_SERVER_ERROR = 4;
}

enum VerifyFailureKindDto {
  VERIFY_FAILURE_KIND_DTO_UNSPECIFIED = 0;
  VERIFY_FAILURE_KIND_DTO_UNREACHABLE = 1;
  VERIFY_FAILURE_KIND_DTO_AUTH_FAILED = 2;
  VERIFY_FAILURE_KIND_DTO_UNTRUSTED_CERTIFICATE = 3;
  VERIFY_FAILURE_KIND_DTO_TIMEOUT = 4;
  VERIFY_FAILURE_KIND_DTO_SERVER_ERROR = 5;
}

enum StoreAndForwardCategoryDto {
  STORE_AND_FORWARD_CATEGORY_DTO_UNSPECIFIED = 0;
  STORE_AND_FORWARD_CATEGORY_DTO_EXTERNAL_SYSTEM = 1;
  STORE_AND_FORWARD_CATEGORY_DTO_NOTIFICATION = 2;
  STORE_AND_FORWARD_CATEGORY_DTO_CACHED_DB_WRITE = 3;
}

enum AlarmStateDto {
  ALARM_STATE_DTO_UNSPECIFIED = 0;
  ALARM_STATE_DTO_ACTIVE = 1;
  ALARM_STATE_DTO_NORMAL = 2;
}

enum AlarmLevelDto {
  ALARM_LEVEL_DTO_UNSPECIFIED = 0;
  ALARM_LEVEL_DTO_NONE = 1;
  ALARM_LEVEL_DTO_LOW = 2;
  ALARM_LEVEL_DTO_LOW_LOW = 3;
  ALARM_LEVEL_DTO_HIGH = 4;
  ALARM_LEVEL_DTO_HIGH_HIGH = 5;
}

enum AlarmKindDto {
  ALARM_KIND_DTO_UNSPECIFIED = 0;
  ALARM_KIND_DTO_COMPUTED = 1;
  ALARM_KIND_DTO_NATIVE_OPC_UA = 2;
  ALARM_KIND_DTO_NATIVE_MX_ACCESS = 3;
}

enum AlarmShelveStateDto {
  ALARM_SHELVE_STATE_DTO_UNSPECIFIED = 0;
  ALARM_SHELVE_STATE_DTO_UNSHELVED = 1;
  ALARM_SHELVE_STATE_DTO_ONE_SHOT_SHELVED = 2;
  ALARM_SHELVE_STATE_DTO_TIMED_SHELVED = 3;
  ALARM_SHELVE_STATE_DTO_PERMANENT_SHELVED = 4;
}

// ─────────────────────────────────────────────────────────────────────────────
// RPC 1 — ExecuteLifecycle
// ─────────────────────────────────────────────────────────────────────────────

message RefreshDeploymentCommandDto {
  string deployment_id = 1;
  string instance_unique_name = 2;
  string revision_hash = 3;
  string deployed_by = 4;
  google.protobuf.Timestamp timestamp = 5;
  string central_fetch_base_url = 6;
  string fetch_token = 7;
}

message EnableInstanceCommandDto {
  string command_id = 1;
  string instance_unique_name = 2;
  google.protobuf.Timestamp timestamp = 3;
}

message DisableInstanceCommandDto {
  string command_id = 1;
  string instance_unique_name = 2;
  google.protobuf.Timestamp timestamp = 3;
}

message DeleteInstanceCommandDto {
  string command_id = 1;
  string instance_unique_name = 2;
  google.protobuf.Timestamp timestamp = 3;
}

message DeploymentStateQueryRequestDto {
  string correlation_id = 1;
  string instance_unique_name = 2;
  google.protobuf.Timestamp timestamp = 3;
}

message SharedScriptArtifactDto {
  string name = 1;
  string code = 2;
  string parameter_definitions = 3;   // empty string represents null
  string return_definition = 4;       // empty string represents null
}

message ExternalSystemArtifactDto {
  string name = 1;
  string endpoint_url = 2;
  string auth_type = 3;
  string auth_configuration = 4;        // empty string represents null
  string method_definitions_json = 5;   // empty string represents null
  int32 timeout_seconds = 6;
}

message DatabaseConnectionArtifactDto {
  string name = 1;
  string connection_string = 2;
  int32 max_retries = 3;
  google.protobuf.Duration retry_delay = 4;
}

message NotificationListArtifactDto {
  string name = 1;
  repeated string recipient_emails = 2;
}

message DataConnectionArtifactDto {
  string name = 1;
  string protocol = 2;
  string primary_configuration_json = 3;   // empty string represents null
  string backup_configuration_json = 4;    // empty string represents null
  int32 failover_retry_count = 5;
}

message SmtpConfigurationArtifactDto {
  string name = 1;
  string server = 2;
  int32 port = 3;
  string auth_mode = 4;
  string from_address = 5;
  string username = 6;      // empty string represents null
  string password = 7;      // empty string represents null
  string oauth_config = 8;  // empty string represents null
}

// Each artifact collection on DeployArtifactsCommand is a NULLABLE list, and
// proto3 `repeated` cannot distinguish null from empty. Wrapping each in its
// own message makes the null/empty distinction a message-presence question,
// which proto3 does model — the same silent-data-loss class the transport
// round-trip guard caught in PLAN-05 T8.
message SharedScriptArtifactListDto { repeated SharedScriptArtifactDto items = 1; }
message ExternalSystemArtifactListDto { repeated ExternalSystemArtifactDto items = 1; }
message DatabaseConnectionArtifactListDto { repeated DatabaseConnectionArtifactDto items = 1; }
message NotificationListArtifactListDto { repeated NotificationListArtifactDto items = 1; }
message DataConnectionArtifactListDto { repeated DataConnectionArtifactDto items = 1; }
message SmtpConfigurationArtifactListDto { repeated SmtpConfigurationArtifactDto items = 1; }

message DeployArtifactsCommandDto {
  string deployment_id = 1;
  SharedScriptArtifactListDto shared_scripts = 2;              // absent => null
  ExternalSystemArtifactListDto external_systems = 3;          // absent => null
  DatabaseConnectionArtifactListDto database_connections = 4;  // absent => null
  NotificationListArtifactListDto notification_lists = 5;      // absent => null
  DataConnectionArtifactListDto data_connections = 6;          // absent => null
  SmtpConfigurationArtifactListDto smtp_configurations = 7;    // absent => null
  google.protobuf.Timestamp timestamp = 8;
}

message DeploymentStatusResponseDto {
  string deployment_id = 1;
  string instance_unique_name = 2;
  DeploymentStatusDto status = 3;
  string error_message = 4;   // empty string represents null
  google.protobuf.Timestamp timestamp = 5;
}

message InstanceLifecycleResponseDto {
  string command_id = 1;
  string instance_unique_name = 2;
  bool success = 3;
  string error_message = 4;   // empty string represents null
  google.protobuf.Timestamp timestamp = 5;
}

message DeploymentStateQueryResponseDto {
  string correlation_id = 1;
  string instance_unique_name = 2;
  bool is_deployed = 3;
  string applied_deployment_id = 4;   // empty string represents null
  string applied_revision_hash = 5;   // empty string represents null
  google.protobuf.Timestamp timestamp = 6;
}

message ArtifactDeploymentResponseDto {
  string deployment_id = 1;
  string site_id = 2;
  bool success = 3;
  string error_message = 4;   // empty string represents null
  google.protobuf.Timestamp timestamp = 5;
}

message LifecycleRequest {
  oneof command {
    RefreshDeploymentCommandDto refresh_deployment = 1;
    EnableInstanceCommandDto enable_instance = 2;
    DisableInstanceCommandDto disable_instance = 3;
    DeleteInstanceCommandDto delete_instance = 4;
    DeploymentStateQueryRequestDto deployment_state_query = 5;
    DeployArtifactsCommandDto deploy_artifacts = 6;
  }
}

message LifecycleReply {
  oneof reply {
    DeploymentStatusResponseDto deployment_status = 1;
    InstanceLifecycleResponseDto instance_lifecycle = 2;
    DeploymentStateQueryResponseDto deployment_state_query = 3;
    ArtifactDeploymentResponseDto artifact_deployment = 4;
  }
}

// ─────────────────────────────────────────────────────────────────────────────
// RPC 2 — ExecuteOpcUa
// ─────────────────────────────────────────────────────────────────────────────

message BrowseNodeCommandDto {
  string connection_name = 1;
  string parent_node_id = 2;      // empty string represents null (browse root)
  string continuation_token = 3;  // empty string represents null (first page)
  string site_identifier = 4;     // empty string represents null
}

message BrowseNodeDto {
  string node_id = 1;
  string display_name = 2;
  BrowseNodeClassDto node_class = 3;
  bool has_children = 4;
  string data_type = 5;                       // empty string represents null
  google.protobuf.Int32Value value_rank = 6;  // absent => null
  google.protobuf.BoolValue writable = 7;     // absent => null
}

message BrowseFailureDto {
  BrowseFailureKindDto kind = 1;
  string message = 2;
}

message BrowseNodeResultDto {
  repeated BrowseNodeDto children = 1;
  bool truncated = 2;
  BrowseFailureDto failure = 3;   // absent => null (success)
  string continuation_token = 4;  // empty string represents null (final page)
}

message SearchAddressSpaceCommandDto {
  string connection_name = 1;
  string query = 2;
  int32 max_depth = 3;
  int32 max_results = 4;
  string site_identifier = 5;   // empty string represents null
}

message AddressSpaceMatchDto {
  BrowseNodeDto node = 1;
  string path = 2;
}

message SearchAddressSpaceResultDto {
  repeated AddressSpaceMatchDto matches = 1;
  bool cap_reached = 2;
  BrowseFailureDto failure = 3;   // absent => null (success)
}

message ReadTagValuesCommandDto {
  string connection_name = 1;
  repeated string tag_paths = 2;
}

message TagReadOutcomeDto {
  string tag_path = 1;
  bool success = 2;
  LooseValue value = 3;   // absent => null
  string quality = 4;
  google.protobuf.Timestamp timestamp = 5;
  string error_message = 6;   // empty string represents null
}

message ReadTagValuesFailureDto {
  ReadTagValuesFailureKindDto kind = 1;
  string message = 2;
}

message ReadTagValuesResultDto {
  repeated TagReadOutcomeDto outcomes = 1;
  ReadTagValuesFailureDto failure = 2;   // absent => null (success)
}

message VerifyEndpointCommandDto {
  string connection_name = 1;
  string protocol = 2;
  string config_json = 3;
  string site_identifier = 4;   // empty string represents null
}

message ServerCertInfoDto {
  string thumbprint = 1;
  string subject = 2;
  string issuer = 3;
  google.protobuf.Timestamp not_before_utc = 4;
  google.protobuf.Timestamp not_after_utc = 5;
  string der_base64 = 6;
}

// failure_kind is a NULLABLE enum on the CLR side, and proto3 enums have no
// presence, so it rides inside a one-field message exactly like Int32Value.
message VerifyFailureKindValue { VerifyFailureKindDto value = 1; }

message VerifyEndpointResultDto {
  bool success = 1;
  VerifyFailureKindValue failure_kind = 2;   // absent => null (success)
  string error = 3;                          // empty string represents null
  ServerCertInfoDto cert = 4;                // absent => null
}

message TrustServerCertCommandDto {
  string connection_name = 1;
  string der_base64 = 2;
  string thumbprint = 3;
  string site_identifier = 4;   // empty string represents null
}

message ListServerCertsCommandDto {
  string site_identifier = 1;   // empty string represents null
}

message RemoveServerCertCommandDto {
  string thumbprint = 1;
  string site_identifier = 2;   // empty string represents null
}

message TrustedCertInfoDto {
  string thumbprint = 1;
  string subject = 2;
  string issuer = 3;
  google.protobuf.Timestamp not_before_utc = 4;
  google.protobuf.Timestamp not_after_utc = 5;
  bool rejected = 6;
}

// CertTrustResult.Certs is a nullable list (null for trust/remove, populated
// for list) — same null-vs-empty problem as the artifact collections.
message TrustedCertInfoListDto { repeated TrustedCertInfoDto items = 1; }

message CertTrustResultDto {
  bool success = 1;
  string error = 2;                 // empty string represents null
  TrustedCertInfoListDto certs = 3; // absent => null
}

message WriteTagRequestDto {
  string correlation_id = 1;
  string connection_name = 2;
  string tag_path = 3;
  LooseValue value = 4;   // absent => null
  google.protobuf.Timestamp timestamp = 5;
}

message WriteTagResponseDto {
  string correlation_id = 1;
  bool success = 2;
  string error_message = 3;   // empty string represents null
  google.protobuf.Timestamp timestamp = 4;
}

message OpcUaRequest {
  oneof command {
    BrowseNodeCommandDto browse_node = 1;
    SearchAddressSpaceCommandDto search_address_space = 2;
    ReadTagValuesCommandDto read_tag_values = 3;
    VerifyEndpointCommandDto verify_endpoint = 4;
    TrustServerCertCommandDto trust_server_cert = 5;
    ListServerCertsCommandDto list_server_certs = 6;
    RemoveServerCertCommandDto remove_server_cert = 7;
    WriteTagRequestDto write_tag = 8;
  }
}

message OpcUaReply {
  oneof reply {
    BrowseNodeResultDto browse_node = 1;
    SearchAddressSpaceResultDto search_address_space = 2;
    ReadTagValuesResultDto read_tag_values = 3;
    VerifyEndpointResultDto verify_endpoint = 4;
    CertTrustResultDto cert_trust = 5;
    WriteTagResponseDto write_tag = 6;
  }
}

// ─────────────────────────────────────────────────────────────────────────────
// RPC 3 — ExecuteQuery
// ─────────────────────────────────────────────────────────────────────────────

message EventLogQueryRequestDto {
  string correlation_id = 1;
  string site_id = 2;
  google.protobuf.Timestamp from = 3;   // absent => null
  google.protobuf.Timestamp to = 4;     // absent => null
  string event_type = 5;                // empty string represents null
  string severity = 6;                  // empty string represents null
  string instance_id = 7;               // empty string represents null
  string keyword_filter = 8;            // empty string represents null
  string continuation_token = 9;        // empty string represents null
  int32 page_size = 10;
  google.protobuf.Timestamp timestamp = 11;
}

message EventLogEntryDto {
  string id = 1;
  google.protobuf.Timestamp timestamp = 2;
  string event_type = 3;
  string severity = 4;
  string instance_id = 5;   // empty string represents null
  string source = 6;
  string message = 7;
  string details = 8;       // empty string represents null
}

message EventLogQueryResponseDto {
  string correlation_id = 1;
  string site_id = 2;
  repeated EventLogEntryDto entries = 3;
  string continuation_token = 4;   // empty string represents null
  bool has_more = 5;
  bool success = 6;
  string error_message = 7;        // empty string represents null
  google.protobuf.Timestamp timestamp = 8;
}

message DebugSnapshotRequestDto {
  string instance_unique_name = 1;
  string correlation_id = 2;
  // Alarms-only projection (WP2.3 wire efficiency): when true the site builds
  // and returns ONLY the alarm rows of the debug snapshot, leaving
  // attribute_values empty. Used by the central per-site live alarm cache,
  // whose seed/reconcile fan-out discards every attribute row anyway — a full
  // snapshot ships the whole attribute surface of every enabled instance once
  // per reconcile for nothing. proto3 defaults it to false, so an older
  // central that never sets it keeps the full-snapshot behaviour. Additive-only.
  bool alarms_only = 3;
}

message SubscribeDebugViewRequestDto {
  string instance_unique_name = 1;
  string correlation_id = 2;
}

message UnsubscribeDebugViewRequestDto {
  string instance_unique_name = 1;
  string correlation_id = 2;
}

// UnsubscribeDebugView is a Tell today (CommunicationService.UnsubscribeDebugView
// fires and forgets). A unary RPC must still answer something, so the site sends
// this empty ack; the central transport ignores it to keep the caller-visible
// fire-and-forget semantics identical.
message UnsubscribeDebugViewAckDto {}

message AlarmConditionStateDto {
  bool active = 1;
  bool acknowledged = 2;
  google.protobuf.BoolValue confirmed = 3;   // absent => null (not confirmable)
  AlarmShelveStateDto shelve = 4;
  bool suppressed = 5;
  int32 severity = 6;
}

message DebugAttributeValueDto {
  string instance_unique_name = 1;
  string attribute_path = 2;
  string attribute_name = 3;
  LooseValue value = 4;   // absent => null
  string quality = 5;
  google.protobuf.Timestamp timestamp = 6;
}

// Full-fidelity projection of Commons AlarmStateChanged. Deliberately NOT the
// sitestream AlarmStateUpdate: that one flattens the value to a display string
// and the shelve state to free text, which is right for a live stream but would
// lose data on a snapshot the central UI renders as authoritative state.
message DebugAlarmStateDto {
  string instance_unique_name = 1;
  string alarm_name = 2;
  AlarmStateDto state = 3;
  int32 priority = 4;
  google.protobuf.Timestamp timestamp = 5;
  AlarmLevelDto level = 6;
  string message = 7;
  AlarmKindDto kind = 8;
  // Absent means "not explicitly set" — the CLR record then derives the
  // computed default from state + priority. See the mapper's note on why the
  // encoder omits a condition that already equals that derived default.
  AlarmConditionStateDto condition = 9;
  string source_reference = 10;
  string alarm_type_name = 11;
  string category = 12;
  string operator_user = 13;
  string operator_comment = 14;
  google.protobuf.Timestamp original_raise_time = 15;   // absent => null
  string current_value = 16;
  string limit_value = 17;
  string native_source_canonical_name = 18;
  bool is_configured_placeholder = 19;
}

message DebugViewSnapshotDto {
  string instance_unique_name = 1;
  repeated DebugAttributeValueDto attribute_values = 2;
  repeated DebugAlarmStateDto alarm_states = 3;
  google.protobuf.Timestamp snapshot_timestamp = 4;
  bool instance_not_found = 5;
}

message QueryRequest {
  oneof command {
    EventLogQueryRequestDto event_log_query = 1;
    DebugSnapshotRequestDto debug_snapshot = 2;
    SubscribeDebugViewRequestDto subscribe_debug_view = 3;
    UnsubscribeDebugViewRequestDto unsubscribe_debug_view = 4;
  }
}

message QueryReply {
  oneof reply {
    EventLogQueryResponseDto event_log_query = 1;
    DebugViewSnapshotDto debug_view_snapshot = 2;
    UnsubscribeDebugViewAckDto unsubscribe_debug_view = 3;
  }
}

// ─────────────────────────────────────────────────────────────────────────────
// RPC 4 — ExecuteParked
// ─────────────────────────────────────────────────────────────────────────────

message ParkedMessageQueryRequestDto {
  string correlation_id = 1;
  string site_id = 2;
  int32 page_number = 3;
  int32 page_size = 4;
  google.protobuf.Timestamp timestamp = 5;
}

message ParkedMessageEntryDto {
  string message_id = 1;
  string target_system = 2;
  string method_name = 3;
  string error_message = 4;
  int32 attempt_count = 5;
  google.protobuf.Timestamp original_timestamp = 6;
  google.protobuf.Timestamp last_attempt_timestamp = 7;
  int32 max_attempts = 8;
  StoreAndForwardCategoryDto category = 9;
  string origin_instance = 10;   // empty string represents null
}

message ParkedMessageQueryResponseDto {
  string correlation_id = 1;
  string site_id = 2;
  repeated ParkedMessageEntryDto messages = 3;
  int32 total_count = 4;
  int32 page_number = 5;
  int32 page_size = 6;
  bool success = 7;
  string error_message = 8;   // empty string represents null
  google.protobuf.Timestamp timestamp = 9;
}

message ParkedMessageRetryRequestDto {
  string correlation_id = 1;
  string site_id = 2;
  string message_id = 3;
  google.protobuf.Timestamp timestamp = 4;
}

message ParkedMessageRetryResponseDto {
  string correlation_id = 1;
  bool success = 2;
  string error_message = 3;   // empty string represents null
}

message ParkedMessageDiscardRequestDto {
  string correlation_id = 1;
  string site_id = 2;
  string message_id = 3;
  google.protobuf.Timestamp timestamp = 4;
}

message ParkedMessageDiscardResponseDto {
  string correlation_id = 1;
  bool success = 2;
  string error_message = 3;   // empty string represents null
}

message RetryParkedOperationDto {
  string correlation_id = 1;
  string tracked_operation_id = 2;   // TrackedOperationId GUID, "D" format
}

message DiscardParkedOperationDto {
  string correlation_id = 1;
  string tracked_operation_id = 2;   // TrackedOperationId GUID, "D" format
}

message ParkedOperationActionAckDto {
  string correlation_id = 1;
  bool applied = 2;
  string error_message = 3;   // empty string represents null
}

message ParkedRequest {
  oneof command {
    ParkedMessageQueryRequestDto parked_message_query = 1;
    ParkedMessageRetryRequestDto parked_message_retry = 2;
    ParkedMessageDiscardRequestDto parked_message_discard = 3;
    RetryParkedOperationDto retry_parked_operation = 4;
    DiscardParkedOperationDto discard_parked_operation = 5;
  }
}

message ParkedReply {
  oneof reply {
    ParkedMessageQueryResponseDto parked_message_query = 1;
    ParkedMessageRetryResponseDto parked_message_retry = 2;
    ParkedMessageDiscardResponseDto parked_message_discard = 3;
    ParkedOperationActionAckDto parked_operation_action = 4;
  }
}

// ─────────────────────────────────────────────────────────────────────────────
// RPC 5 — ExecuteRoute
// ─────────────────────────────────────────────────────────────────────────────

message RouteToCallRequestDto {
  string correlation_id = 1;
  string instance_unique_name = 2;
  string script_name = 3;
  LooseValueMap parameters = 4;   // absent => null (distinct from empty)
  google.protobuf.Timestamp timestamp = 5;
  string parent_execution_id = 6; // empty string represents null
}

message RouteToCallResponseDto {
  string correlation_id = 1;
  bool success = 2;
  LooseValue return_value = 3;   // absent => null
  string error_message = 4;      // empty string represents null
  google.protobuf.Timestamp timestamp = 5;
}

message RouteToGetAttributesRequestDto {
  string correlation_id = 1;
  string instance_unique_name = 2;
  repeated string attribute_names = 3;
  google.protobuf.Timestamp timestamp = 4;
  string parent_execution_id = 5;   // empty string represents null
}

message RouteToGetAttributesResponseDto {
  string correlation_id = 1;
  LooseValueMap values = 2;   // non-nullable on the CLR side; absent => empty
  bool success = 3;
  string error_message = 4;   // empty string represents null
  google.protobuf.Timestamp timestamp = 5;
}

message RouteToSetAttributesRequestDto {
  string correlation_id = 1;
  string instance_unique_name = 2;
  map<string, string> attribute_values = 3;
  google.protobuf.Timestamp timestamp = 4;
  string parent_execution_id = 5;   // empty string represents null
}

message RouteToSetAttributesResponseDto {
  string correlation_id = 1;
  bool success = 2;
  string error_message = 3;   // empty string represents null
  google.protobuf.Timestamp timestamp = 4;
}

message RouteToWaitForAttributeRequestDto {
  string correlation_id = 1;
  string instance_unique_name = 2;
  string attribute_name = 3;
  // NOT the empty-string-means-null convention: "wait for this attribute to
  // become the empty string" is a legitimate target that must stay distinct
  // from "no target supplied", so this one nullable string rides a wrapper.
  google.protobuf.StringValue target_value_encoded = 4;
  google.protobuf.Duration timeout = 5;
  google.protobuf.Timestamp timestamp = 6;
  string parent_execution_id = 7;    // empty string represents null
  bool require_good_quality = 8;
}

message RouteToWaitForAttributeResponseDto {
  string correlation_id = 1;
  bool matched = 2;
  LooseValue value = 3;   // absent => null
  string quality = 4;     // empty string represents null
  bool timed_out = 5;
  bool success = 6;
  string error_message = 7;   // empty string represents null
  google.protobuf.Timestamp timestamp = 8;
}

message RouteRequest {
  oneof command {
    RouteToCallRequestDto route_to_call = 1;
    RouteToGetAttributesRequestDto route_to_get_attributes = 2;
    RouteToSetAttributesRequestDto route_to_set_attributes = 3;
    RouteToWaitForAttributeRequestDto route_to_wait_for_attribute = 4;
  }
}

message RouteReply {
  oneof reply {
    RouteToCallResponseDto route_to_call = 1;
    RouteToGetAttributesResponseDto route_to_get_attributes = 2;
    RouteToSetAttributesResponseDto route_to_set_attributes = 3;
    RouteToWaitForAttributeResponseDto route_to_wait_for_attribute = 4;
  }
}

// ─────────────────────────────────────────────────────────────────────────────
// RPC 6 — TriggerFailover
// ─────────────────────────────────────────────────────────────────────────────

message TriggerSiteFailoverDto {
  string correlation_id = 1;
  string site_id = 2;
}

message SiteFailoverAckDto {
  string correlation_id = 1;
  bool accepted = 2;
  string target_address = 3;   // empty string represents null
  string error_message = 4;    // empty string represents null
}
