syntax = "proto3";

package nominal.data_connector.v1;

import "buf/validate/validate.proto";
import "google/protobuf/empty.proto";
import "google/protobuf/timestamp.proto";
import "nominal/gen/v1/alias.proto";

option java_multiple_files = true;
option java_package = "io.nominal.data_connector.v1";

// Authentication configuration for OPC-UA server connection
message OpcAuthenticationConfig {
  // Authentication configuration for OPC-UA server connection
  oneof opc_authentication_config {
    option (buf.validate.oneof) = {required: true};

    google.protobuf.Empty anonymous = 1;

    OpcUsernamePasswordAuthentication username_password = 2;

    OpcTokenAuthentication token = 3;
  }
}

// Authentication configuration for OPC-UA server connection with secrets
message OpcAuthenticationConfigSecret {
  oneof opc_authentication_config {
    option (buf.validate.oneof) = {required: true};

    google.protobuf.Empty anonymous = 1;

    OpcUsernamePasswordAuthenticationSecret username_password = 2;

    OpcTokenAuthenticationSecret token = 3;
  }
}

message OpcUsernamePasswordAuthentication {
  string username = 1 [(buf.validate.field).required = true];

  string password = 2 [(buf.validate.field).required = true];
}

message OpcUsernamePasswordAuthenticationSecret {
  string username = 1 [(buf.validate.field).required = true];

  string password = 2 [(nominal.gen.v1.alias) = {java_name: "io.nominal.secrets.api.SecretRid"}];
}

message OpcTokenAuthentication {
  string token = 1 [(buf.validate.field).required = true];
}

message OpcTokenAuthenticationSecret {
  string token = 1 [(nominal.gen.v1.alias) = {java_name: "io.nominal.secrets.api.SecretRid"}];
}

// The identifier field for an OPC-UA node
message OpcIdentifierValue {
  oneof opc_identifier_value {
    option (buf.validate.oneof) = {required: true};

    int32 numeric = 1;
    string string = 2;
  }
}

// An OPC-UA node
message OpcNode {
  optional uint32 namespace = 1;

  OpcIdentifierValue identifier = 2 [(buf.validate.field).required = true];
}

// How do you want to map the names of OPC-UA nodes to Nominal channels?
message OpcUaChannelNamingConvention {
  oneof naming_convention {
    option (buf.validate.oneof) = {required: true};

    // For example, "NodeId{ns=3;i=1010}"
    OpcUaNodeId node_id = 1;
    // The "name" field under the "BrowseName" attribute
    OpcUaBrowseName browse_name = 2;
    // The "text" field under the "DisplayName" attribute
    OpcUaDisplayName display_name = 3;
    // The full path of the node, separated by a delimiter. Use this setting for creating the hierarchical prefix tree.
    OpcUaFullPath full_path = 4;
  }

  message OpcUaNodeId {}
  message OpcUaBrowseName {}
  message OpcUaDisplayName {}

  // Configuration for full path naming convention
  message OpcUaFullPath {
    // We will build path up to any of these nodes, or the Objects folder.
    repeated OpcNode root_nodes = 1;
    // Custom delimiter for path separation. Defaults to "/" if not specified.
    string delimiter = 2;
  }
}

enum OpcUaReferenceExplorationType {
  OPC_UA_REFERENCE_EXPLORATION_TYPE_UNSPECIFIED = 0;
  OPC_UA_REFERENCE_EXPLORATION_TYPE_ORGANIZES = 1;
  OPC_UA_REFERENCE_EXPLORATION_TYPE_HIERARCHICAL_REFERENCES = 2;
}

enum OpcUaUnknownDataTypeHandling {
  OPC_UA_UNKNOWN_DATA_TYPE_HANDLING_UNSPECIFIED = 0;
  // If we encounter an unknown data type, we will error out
  OPC_UA_UNKNOWN_DATA_TYPE_HANDLING_ERROR = 1;
  // If we encounter an unknown data type, we will assume it is a double
  OPC_UA_UNKNOWN_DATA_TYPE_HANDLING_TREAT_AS_DOUBLE = 2;
  // If we encounter an unknown data type, we will assume it is a string
  OPC_UA_UNKNOWN_DATA_TYPE_HANDLING_TREAT_AS_STRING = 3;
}

enum OpcUaFailedMonitorHandling {
  OPC_UA_FAILED_MONITOR_HANDLING_UNSPECIFIED = 0;
  OPC_UA_FAILED_MONITOR_HANDLING_ERROR = 1;
  OPC_UA_FAILED_MONITOR_HANDLING_IGNORE = 2;
}

// Configuration for connecting to an OPC-UA server
message OpcUaConnectorDetails {
  string uri = 1 [(buf.validate.field).required = true];

  OpcAuthenticationConfig authentication_config = 2 [(buf.validate.field).required = true];

  OpcUaScrapingConfig scraping_config = 3 [(buf.validate.field).required = true];
}

// Updated configuration for connecting to an OPC-UA server
message OpcUaConnectorDetailsUpdates {
  optional string uri = 1;

  optional OpcAuthenticationConfig authentication_config = 2;

  optional OpcUaScrapingConfigUpdates scraping_config = 3;
}

// Configuration for connecting to an OPC-UA server with secrets
message OpcUaConnectorDetailsSecret {
  string uri = 1;

  OpcAuthenticationConfigSecret authentication_config = 2;

  optional OpcUaScrapingConfig scraping_config = 3;
}

message OpcUaTraversalConfig {
  // Root nodes to subscribe to. For each node specified here, we will
  // subscribe to that node and all of its children.
  repeated OpcNode root_nodes = 1 [(buf.validate.field).repeated.min_items = 1];

  // We will not subscribe to any of these nodes.
  repeated OpcNode skip_nodes = 2;

  // The type of references that we will follow when traversing the root nodes in the OPC UA server.
  OpcUaReferenceExplorationType reference_exploration_type = 3 [(buf.validate.field).required = true];
}

message OpcUaDirectNodeSubscription {
  repeated OpcNode nodes = 1 [(buf.validate.field).repeated.min_items = 1];
}

message OpcUaNodeExplorationConfig {
  oneof opc_ua_node_exploration_config {
    option (buf.validate.oneof) = {required: true};

    OpcUaTraversalConfig opc_ua_traversal_config = 1;
    OpcUaDirectNodeSubscription opc_ua_direct_node_subscription = 2;
  }
}

message OpcUaTimestampHandling {
  oneof timestamp_type {
    option (buf.validate.oneof) = {required: true};

    OpcUaServerTime server = 1;
    OpcUaSourceTime source = 2;
    OpcUaRelativeTimestamp relative = 3;
  }
  // Use the server timestamp https://reference.opcfoundation.org/Core/Part4/v104/docs/7.7.4
  message OpcUaServerTime {}
  // Use the source timestamp https://reference.opcfoundation.org/Core/Part4/v104/docs/7.7.3
  message OpcUaSourceTime {}

  // Use the server timestamp, but add an optional offset. By default, the offset will be the time now.
  // This is useful for when the server starts at an epoch time of 0.
  message OpcUaRelativeTimestamp {
    optional google.protobuf.Timestamp offset = 1;
  }
}

enum OpcUaDataChangeTrigger {
  OPC_UA_DATA_CHANGE_TRIGGER_UNSPECIFIED = 0;
  OPC_UA_DATA_CHANGE_TRIGGER_STATUS_ONLY = 1;
  OPC_UA_DATA_CHANGE_TRIGGER_STATUS_VALUE = 2;
  OPC_UA_DATA_CHANGE_TRIGGER_STATUS_VALUE_TIMESTAMP = 3;
}

enum OpcUaDeadbandType {
  OPC_UA_DEADBAND_TYPE_UNSPECIFIED = 0;
  OPC_UA_DEADBAND_TYPE_NONE = 1;
  OPC_UA_DEADBAND_TYPE_ABSOLUTE = 2;
  OPC_UA_DEADBAND_TYPE_PERCENT = 3;
}

message OpcUaDataChangeFilter {
  // The trigger for the data change filter.
  OpcUaDataChangeTrigger trigger = 1;
  OpcUaDeadbandType deadband_type = 2;
  double deadband_value = 3;
}

// Configuration for determining how data is discovered and handled
message OpcUaScrapingConfig {
  OpcUaNodeExplorationConfig node_exploration_config = 1 [(buf.validate.field).required = true];

  // The browse name of the node under which the unit is stored for the nodes we scrape.
  string unit_node_name = 2 [(buf.validate.field).required = true];

  OpcUaChannelNamingConvention channel_naming_convention = 3 [(buf.validate.field).required = true];

  // If true, we will override the host and port of the OPC UA server with the host and port of the original server URL.
  bool override_host = 4 [(buf.validate.field).required = true];

  // How we will handle unknown data types when we read them from the server
  OpcUaUnknownDataTypeHandling unknown_data_type_handling = 5 [(buf.validate.field).required = true];

  // How to handle situations where there are failed monitors.
  OpcUaFailedMonitorHandling failed_monitor_handling = 6 [(buf.validate.field).required = true];

  // What timestamps should we write for the channels?
  OpcUaTimestampHandling timestamp_handling = 7 [(buf.validate.field).required = true];

  // The data change filter to be applied for the monitored items in the subscription.
  OpcUaDataChangeFilter data_change_filter = 8 [(buf.validate.field).required = true];
}

// Updated configuration for determining how data is discovered and handled
message OpcUaScrapingConfigUpdates {
  optional OpcUaNodeExplorationConfig node_exploration_config = 1;

  // The browse name of the node under which the unit is stored for the nodes we scrape.
  optional string unit_node_name = 2;

  optional OpcUaChannelNamingConvention channel_naming_convention = 3;

  // If true, we will override the host and port of the OPC UA server with the host and port of the original server URL.
  optional bool override_host = 4;

  // How we will handle unknown data types when we read them from the server
  optional OpcUaUnknownDataTypeHandling unknown_data_type_handling = 5;

  // How to handle situations where there are failed monitors.
  optional OpcUaFailedMonitorHandling failed_monitor_handling = 6;

  // What timestamps should we write for the channels?
  optional OpcUaTimestampHandling timestamp_handling = 7;

  // The data change filter to be applied for the monitored items in the subscription.
  optional OpcUaDataChangeFilter data_change_filter = 8;
}
