syntax = "proto3";

package nominal.procedures.v1;

import "buf/validate/validate.proto";
import "google/api/annotations.proto";
import "google/protobuf/timestamp.proto";
import "nominal/protos/gen/v1/alias.proto";
import "nominal/protos/gen/v1/error.proto";
// Custom id validation rules
import "nominal/protos/procedures/v1/id_format.proto";
import "nominal/protos/types/time/timestamp_parsers.proto";
import "nominal/protos/types/types.proto";
import "nominal/protos/versioning/v1/versioning.proto";

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

/////////////////////
// Procedure State //
/////////////////////

//////// Graph representations

// ProcedureState is the versioned state of a procedure template.
message ProcedureState {
  // TODO(niwong): global_fields is deprecated, remove in a future version.
  map<string, FormField> global_fields = 1 [(buf.validate.field).map.keys.string.(valid_field_identifier) = true];
  // Global fields are set at the beginning of the procedure execution.
  // These fields can be referenced by any step in the procedure by using {field_id}.
  repeated FormField new_global_fields = 5;
  // A map from node ID to the node.
  map<string, ProcedureNode> nodes = 2 [(buf.validate.field).map.keys.string.(valid_step_identifier) = true];
  // Section edges represent the visual grouping of sections and steps.
  // All keys are IDs for section nodes, and all values are lists of IDs for section or step nodes.
  // { A, [B, C] } means that section A visually groups nodes B and C together.
  map<string, NodeList> section_edges = 3 [(buf.validate.field).map.keys.string.(valid_step_identifier) = true];
  // Step edges represent the dependencies between steps.
  // All keys and values are IDs for step nodes.
  // { A, [B, C] } means that steps B and C depend on step A to complete to be started
  map<string, NodeList> step_edges = 4 [(buf.validate.field).map.keys.string.(valid_step_identifier) = true];
}

// A simpler representation of the procedure graph that is used for display in a UI.
message ProcedureDisplayGraph {
  // Sections or steps that do not belong to any parent sections.
  // This is ordered in a topological ordering of steps.
  repeated string top_level_nodes = 1;
  // Map of section node IDs to their topologically sorted children
  map<string, NodeList> section_to_sorted_children = 2;
}

// A simplified version of procedure state that is currently used for procedure authoring.
// In this form, the graph is a completely linear structure where each step depends solely
// on the preceding step.
message NestedProcedure {
  // Title of the procedure
  string title = 1 [(buf.validate.field).string.min_len = 1];
  // Optional long-form description for the procedure
  optional string description = 2;
  // Steps in the procedure
  repeated NestedProcedureNode steps = 3;
  // TODO(niwong): global_fields is deprecated, remove in a future version.
  map<string, FormField> global_fields = 4 [(buf.validate.field).map.keys.string.(valid_field_identifier) = true];
  // Global fields are set at the beginning of the procedure execution.
  // These fields can be referenced by any step in the procedure by using {field_id}.
  repeated FormField new_global_fields = 5;
}

// A list of node IDs.
message NodeList {
  repeated string node_ids = 1;
}

//////// Node types

// A procedure node is either a section or a step.
message ProcedureNode {
  oneof node {
    option (buf.validate.oneof) = {required: true};
    // A section node provides a visual grouping of other sections and steps.
    ProcedureSectionNode section = 1;
    // A step node represents a step in the procedure.
    // It serves as a "template" for the ExecutionStep that will be created during procedure execution.
    ProcedureStepNode step = 2;
  }
}

// A procedure section node visually groups other sections and steps.
message ProcedureSectionNode {
  // Unique identifier for this node in the procedure graph.
  string id = 1 [(buf.validate.field).string.(valid_step_identifier) = true];
  // Title of this section
  string title = 2 [(buf.validate.field).string.min_len = 1];
  // Optional long-form description for this section
  optional string description = 3;
}

// A procedure step node represents a step in the procedure.
message ProcedureStepNode {
  // Unique identifier for this node in the procedure graph.
  string id = 1 [(buf.validate.field).string.(valid_step_identifier) = true];

  // Title of this step
  string title = 2 [(buf.validate.field).string.min_len = 1];
  // The content of this step.
  ProcedureStepContent content = 3;

  // The below fields are intentionally optional to ensure that we don't inject them
  // into the "NestedProcedure" format.

  reserved 4; // previously output_id, deprecated in favor of id

  // Optional long-form description for this step
  optional string description = 5;

  // is_required defaults to false, meaning that the step may be skipped
  optional bool is_required = 6;

  // Configuration for if and how the step should automatically transition from NOT_STARTED to IN_PROGRESS
  // By default, this will be `all_parents`.
  optional AutoStartConfig auto_start = 7;

  // Configuration for if and how the step should automatically submit the step contents when requirements are met.
  // By default, this will be `disabled`.
  optional AutoProceedConfig initial_auto_proceed_config = 8;

  // Configuration for whether the step will require a condition to be met for the step to be completed
  // By default, this is not set, meaning that the step will not wait on any conditions before completing.
  optional SuccessCondition success_condition = 9;

  // Configuration for the actions to take when the step transitions to a completed state (SUCCEEDED, ERRORED, SKIPPED)
  // By default, there are no actions.
  repeated CompletionActionConfig completion_action_configs = 10;

  // Optional attachments for the step description
  repeated string attachments = 11 [(nominal.gen.v1.alias) = {java_name: "io.nominal.api.rids.AttachmentRid"}];
}

// 2025-09-26: We decided to make the `id` required. Some reasoning and downstream effects:
// - We want roundtrip Procedure -> NestedProcedure -> Procedure to be lossless, ie the section ids don't churn.
// - Now that section ids are visible in the Nested representation, do they namespace field ids?
// - Option 1: field ids look like `{section1}.{section2}.{step}.{field}`. This would be a breaking change to
//     the field id format and requires a migration of existing procedures.
// - Option 2: field ids are still just `{step}.{field}`. This means that step IDs must be globally unique. Relaxing
//     this constraint would actually be really hard.
// - We are going with option 2.

// A section or a step in a NestedProcedure.
message NestedProcedureNode {
  // The unique identifier for this step or section
  string id = 5 [(buf.validate.field).string.(valid_step_identifier) = true];
  // Title of this section or step.
  string title = 1 [(buf.validate.field).string.min_len = 1];
  // Optional long-form description for this section or step.
  optional string description = 2;

  // Below, only one of "section" or "step" should be set.

  /////////////
  // Section //
  /////////////

  // If this node is a section, then it can contain other sections and steps.
  repeated NestedProcedureNode steps = 3;

  //////////
  // Step //
  //////////

  // If this node is a step, then this field contains the step definition.
  optional NestedStepNode step = 4;

  // These fields' docs are identical to those of ProcedureStepNode.
  message NestedStepNode {
    reserved 1; // previously output_id, deprecated in favor of id
    optional bool is_required = 2;
    optional AutoStartConfig auto_start = 3;
    optional AutoProceedConfig initial_auto_proceed_config = 4;
    optional SuccessCondition success_condition = 5;
    repeated CompletionActionConfig completion_action_configs = 6;

    // This should be kept in sync with ProcedureStepContent.content
    oneof content {
      option (buf.validate.oneof) = {required: true};
      FormStep form = 7;
      StartIngestStep start_ingest = 8;
      SelectOrCreateAssetStep select_or_create_asset = 9;
      WaitStep wait = 10;
    }
    reserved 11 to 50; // reserved for future step types

    // Optional attachments for the step description
    repeated string attachments = 51 [(nominal.gen.v1.alias) = {java_name: "io.nominal.api.rids.AttachmentRid"}];
  }
}

//////////////////////////////////
// Template Step Configurations //
// - AutoStartConfig            //
// - AutoProceedConfig          //
// - CompletionCondition        //
// - CompletionActionConfig     //
//////////////////////////////////

// AutoStartConfig is a configuration that determines whether a step should be
// automatically moved from NOT_STARTED to IN_PROGRESS when some set of criteria are met.
// By default this will be `all_parents`
message AutoStartConfig {
  oneof config {
    option (buf.validate.oneof) = {required: true};
    // The step will start automatically when all parent steps have passed.
    AllParents all_parents = 1;
    // The step will not start automatically.
    Disabled disabled = 2;
  }
  message AllParents {}
  message Disabled {}
}

// AutoProceedConfig is a configuration that determines whether a step should
// automatically proceed (submit the contents and check success_conditions)
// when some set of criteria are met. By default this will be `disabled`.
message AutoProceedConfig {
  oneof config {
    option (buf.validate.oneof) = {required: true};
    // The step will not be automatically completed
    Disabled disabled = 1;
    // The step will automatically submit the step contents once the requirements are met and
    // begin the transition to either PASSED or ERRORED.
    Enabled enabled = 2;
  }
  message Disabled {}
  message Enabled {}
}

// The condition that must be met for a step to succeed after submitting its contents.
message SuccessCondition {
  oneof condition {
    option (buf.validate.oneof) = {required: true};
    AndSuccessCondition and = 1;
    TimerSuccessCondition timer = 2;
    IngestJobSuccessCondition ingest_job = 3;
    ChannelValidationSuccessCondition channel_validation = 4;
    WebhookSuccessCondition webhook = 5;
    // OrSuccessCondition or = 6;
  }
}

// All conditions within must be met for the step to be succeeded.
message AndSuccessCondition {
  repeated SuccessCondition conditions = 1;
}

// A timer condition that will succeed the step after a specified duration.
message TimerSuccessCondition {
  // Duration in seconds to wait before succeeding the step. This value must be non-negative.
  // This duration is a minimum bound; the actual duration may be longer.
  int32 duration_seconds = 1 [(buf.validate.field).int32.gte = 0];
}

// The step will succeed when the associated ingest job succeeds.
// If the ingest job fails, the step will move to ERRORED.
message IngestJobSuccessCondition {
  // The field ID that contains the IngestJobRid of the ingest job to monitor.
  string field_id = 1 [(buf.validate.field).string.(valid_field_reference) = true];
}

// Locates a specific channel within a data source for validation.
// Uses symbolic reference names that are resolved to concrete resources at execution time,
// allowing procedure templates to be defined before the actual data sources exist.
message ChannelLocator {
  // Symbolic name for the data source (e.g., "main", "test_data").
  // This name is resolved to a concrete DataSourceRid at execution time by looking up the name
  // in the data scopes (for assets) or data sources (for runs) specified by asset_or_run.
  // This is NOT a field reference - it's a literal string that must match a data source ref name.
  // Maps to Java type: io.nominal.scout.api.DataSourceRefName
  string data_source_ref = 1 [
    (nominal.gen.v1.alias) = {java_name: "io.nominal.scout.api.DataSourceRefName"},
    (buf.validate.field).string.min_len = 1
  ];

  // The name of the channel within the data source to validate.
  // Maps to Java type: io.nominal.api.Channel
  string channel_name = 2 [
    (nominal.gen.v1.alias) = {java_name: "io.nominal.api.Channel"},
    (buf.validate.field).string.min_len = 1
  ];

  // Optional series tags to filter the channel data.
  // Used to select specific series when multiple series exist for the same channel name.
  // Maps to Java types: io.nominal.api.TagName (key), io.nominal.api.TagValue (value)
  map<string, string> tags = 3 [
    (nominal.gen.v1.alias) = {
      key_java_name: "io.nominal.api.TagName"
      value_java_name: "io.nominal.api.TagValue"
    },
    (buf.validate.field).map.keys.string = {min_len: 1}
  ];

  // Specifies the context for evaluating the channel data.
  // Must specify exactly one of: asset reference (for asset-scoped data) or run reference (for run-scoped data).
  oneof asset_or_run {
    option (buf.validate.oneof) = {required: true};
    AssetReference asset = 4;
    RunReference run = 5;
  }
}

// When in the step lifecycle to capture a channel value.
enum CaptureMoment {
  // Required zero-value enum sentinel. Do not include this in capture_moments.
  CAPTURE_MOMENT_UNSPECIFIED = 0;
  CAPTURE_MOMENT_STEP_START = 1;
  CAPTURE_MOMENT_STEP_COMPLETION = 2;
}

// Shared configuration for capturing a single channel value where the source channel is supplied by the parent config.
//
// At execution time, the backend reads the channel's value at the configured moment(s)
// and writes it into event metadata under dotted keys derived from output_field_id.
message ChannelCaptureConfig {
  // Base field ID used to namespace event property keys (and downstream procedure outputs).
  string output_field_id = 1 [(buf.validate.field).string.(valid_field_identifier) = true];

  // When to capture relative to the step lifecycle.
  // Empty means no capture occurs — at least one moment must be specified.
  repeated CaptureMoment capture_moments = 2 [(buf.validate.field).repeated = {
    unique: true
    items: {
      enum: {
        defined_only: true
        not_in: [0]
      }
    }
  }];
}

// Defines a success condition that compares channel data against a threshold value.
// The step succeeds when the comparison (channel_value COMPARATOR threshold) evaluates to true
// within the timeout period. For example, checking if a channel value is greater than 100.
// Use COMPARATOR_EXISTS for capture-only conditions that succeed as soon as any channel value is observed.
message ChannelValidationSuccessCondition {
  // The channel to validate against the threshold.
  // If the data_source_ref or channel_name cannot be resolved at execution time, the step will
  // transition to ERRORED state immediately.
  ChannelLocator channel = 1 [(buf.validate.field).required = true];

  // Comparison operator to evaluate channel value against threshold.
  // The condition is satisfied when: channel_value COMPARATOR threshold
  // Example: COMPARATOR_GREATER_THAN with threshold 100.0 means channel_value > 100.0
  //
  // Evaluation semantics: The most recent channel value within the timeout period is used for comparison.
  // If the channel has no data, the condition will continuously retry until timeout_millis expires.
  enum COMPARATOR {
    // Unspecified comparator. This is an invalid state and should never be used.
    // Required by proto3 convention as the zero value. Will be rejected at runtime.
    COMPARATOR_UNSPECIFIED = 0;
    COMPARATOR_GREATER_THAN = 1;
    COMPARATOR_GREATER_THAN_OR_EQUAL = 2;
    COMPARATOR_LESS_THAN = 3;
    COMPARATOR_LESS_THAN_OR_EQUAL = 4;
    COMPARATOR_EQUAL = 5;
    COMPARATOR_NOT_EQUAL = 6;
    // Succeeds when any channel value exists. The threshold field is ignored.
    COMPARATOR_EXISTS = 7;
  }

  // The comparison operator to use.
  // COMPARATOR_UNSPECIFIED should never be used and will be rejected at runtime.
  COMPARATOR comparator = 2 [(buf.validate.field).required = true];

  // The threshold value to compare the channel data against.
  // The comparison is: channel_value COMPARATOR threshold
  // Using double for better precision. Note: When using COMPARATOR_EQUAL or COMPARATOR_NOT_EQUAL
  // with floating-point channel data, be aware of potential precision issues inherent to IEEE 754.
  // Ignored when comparator is COMPARATOR_EXISTS.
  double threshold = 3;

  // Maximum time in milliseconds to wait for the condition to be satisfied.
  // If the timeout expires before the comparison evaluates to true, the step moves to ERRORED.
  // Must be greater than zero and at most one hour (3,600,000 milliseconds).
  int32 timeout_millis = 4 [(buf.validate.field).int32 = {
    gt: 0
    lte: 3600000
  }];

  // optionally specify a persistence of individual points
  optional uint32 point_persistence = 5 [(buf.validate.field).uint32 = {gt: 0}];

  // optionally specify a persistence duration in milliseconds (max 5 minutes) for which the condition must hold true
  optional uint32 time_persistence = 6 [(buf.validate.field).uint32 = {
    gt: 0
    lte: 300000
  }];

  // Channel values to capture while evaluating this success condition.
  // These captures use the condition's channel field as their source.
  // Each config produces dot-namespaced event-metadata properties keyed by output_field_id.
  repeated ChannelCaptureConfig channel_captures = 7;
}

// A webhook success condition that waits for external system approval via HTTP webhook.
// The step succeeds when the external system responds with 2xx status code.
//
// Failure behavior:
// - 4xx responses: Permanent failure, no retry (CLIENT_ERROR)
// - 5xx responses: Retry with exponential backoff until max_retries (SERVER_ERROR_MAX_RETRIES_EXCEEDED)
// - Network errors: Retry until max_retries (NETWORK_ERROR_MAX_RETRIES_EXCEEDED)
// - Timeout: Retry until timeout_seconds exceeded (OVERALL_TIMEOUT_EXCEEDED)
// - Redirects: Followed automatically, fails if redirect loop detected (REDIRECT_LOOP)
message WebhookSuccessCondition {
  // RID reference to a WebhookIntegration (the advanced integration type with HMAC signing)
  string integration_rid = 1 [(buf.validate.field).string.min_len = 1];

  // Webhook delivery configuration. If not provided, server uses documented defaults.
  optional WebhookDeliveryConfig delivery_config = 2;

  // Event type to send (e.g., "procedure.step_waiting")
  string event_type = 3 [(buf.validate.field).string.min_len = 1];

  // JSON payload template using Handlebars syntax (https://handlebarsjs.com/)
  // Supports variable references using {{variable_name}} syntax:
  // - {{execution_rid}} - Procedure execution RID
  // - {{step_id}} - Current step ID
  // - {{step_name}} - Current step name/title
  // - {{procedure_name}} - Procedure name
  // - {{field_id}} - Any global form field value (e.g., {{environment}}, {{approval_level}})
  // - {{asset_rid}} - Asset RID if step has asset context
  // - {{asset_name}} - Asset name if step has asset context
  //
  // Advanced features:
  // - Conditionals: {{#if environment}}production mode{{/if}}
  // - Loops: {{#each tags}}{{this}}{{/each}}
  // - Comparisons: {{#if (eq priority "HIGH")}}urgent{{/if}}
  // - Default values: {{default environment "unknown"}}
  //
  // Example template:
  // {
  //   "event": "approval_required",
  //   "execution_id": "{{execution_rid}}",
  //   "step": "{{step_name}}",
  //   "environment": "{{environment}}",
  //   {{#if asset_name}}
  //   "asset": "{{asset_name}}",
  //   {{/if}}
  //   "is_production": {{#if (eq environment "production")}}true{{else}}false{{/if}}
  // }
  //
  // Variables that cannot be resolved will be replaced with empty string.
  // Invalid JSON after template evaluation will cause the condition to fail with PAYLOAD_ERROR reason.
  string payload_template = 4 [(buf.validate.field).string.min_len = 1];
}

// Configuration for webhook delivery retry logic and timeouts.
message WebhookDeliveryConfig {
  // Maximum delivery attempts (default: 3, max: 10)
  optional int32 max_retries = 1 [(buf.validate.field).int32 = {
    gte: 0
    lte: 10
  }];

  // Initial retry interval seconds (default: 15, min: 5, max: 300)
  optional int64 retry_interval_seconds = 2 [(buf.validate.field).int64 = {
    gte: 5
    lte: 300
  }];

  // Use exponential backoff (default: true)
  optional bool exponential_backoff = 3;

  // Max retry interval seconds (default: 300, max: 3600)
  optional int64 max_retry_interval_seconds = 4 [(buf.validate.field).int64 = {
    gte: 5
    lte: 3600
  }];

  // Overall timeout seconds (default: 600, max: 86400)
  optional int64 timeout_seconds = 5 [(buf.validate.field).int64 = {
    gte: 1
    lte: 86400
  }];
}

// An action that will be taken when a step transitions to a completed state
// (succeeded, errored, or skipped). These actions are executed after the step
// contents are submitted and the PassCondition (if any) is satisfied. These
// actions are executed synchronously with the state transition and generally
// should be quick to complete.
// Almost all actions will be executed only if the step transitions to SUCCEEDED.
// The exception is CreateEvent, which will be executed for all terminal states.
message CompletionActionConfig {
  oneof config {
    option (buf.validate.oneof) = {required: true};
    CreateEventConfig create_event = 1;
    SendNotificationConfig send_notification = 2;
    CreateRunConfig create_run = 3;
    ApplyWorkbookTemplatesConfig apply_workbook_templates = 4;
    ApplyChecklistsConfig apply_checklists = 5;
    UpdateRunConfig update_run = 6;
    // ...
  }
}

// An action to create an event when a step is completed.
// The completion state of the step is mapped to the event's EventType as follows:
// - SUCCEEDED -> SUCCESS
// - ERRORED -> ERROR
// - SKIPPED -> INFO
message CreateEventConfig {
  string name = 1 [(buf.validate.field).string.min_len = 1];
  optional string description = 2;
  repeated string labels = 3 [(nominal.gen.v1.alias) = {java_name: "io.nominal.api.Label"}];

  // Deprecated in favor of property_refs
  // Properties will be overridden by any property_refs with the same key.
  map<string, string> properties = 4 [(nominal.gen.v1.alias) = {
    key_java_name: "io.nominal.api.PropertyName"
    value_java_name: "io.nominal.api.PropertyValue"
  }];

  // Each asset_field_id will be resolved and added as an AssetRid to the created event
  // deprecated in favor of asset_references
  repeated string asset_field_ids = 5;
  // The assets to associate the event with. There must be at least one.
  // asset_references takes precedence over (deprecated) asset_field_ids if both are set.
  repeated AssetReference asset_references = 6;
  // TODO(niwong): re-add after client updates [(buf.validate.field).repeated.min_items = 1];

  // Properties to apply to the event
  repeated PropertyReference property_refs = 7;
}

// An action to send a notification to one or more integrations when a step is completed.
message SendNotificationConfig {
  // The integration(s) to use to send the notification.
  MultiIntegrationReference integrations = 1;
  // The optional title of the notification (length constrained to 130 chars by the IntegrationService).
  optional StringReference title = 2;
  // The message body of the notification.
  StringReference message = 3;
  // We are omitting some optional parameters to the IntegrationService#sendMessage endpoint,
  // such as tags, priority, and opsGenieAlias. For all current integration types as of 2025-09-15
  // (besides OpsGenie), only title and message are used.
}

// An action to create a run when a step is completed.
message CreateRunConfig {
  option (buf.validate.message).cel = {
    id: "create_run_config.time_range_requires_start"
    message: "Literal time range for run creation must include a start time"
    expression: "!has(this.time_range) || !has(this.time_range.literal) || has(this.time_range.literal.start)"
  };

  // If present, the run RID for this step will be available in outputs under this field ID.
  optional string run_output_field_id = 1 [(buf.validate.field).string.(valid_field_identifier) = true];
  // The assets to associate the run with. There must be at least one.
  MultiAssetReference assets = 2;
  // The name of the run.
  StringReference name = 3;
  // Optionally, a description for the run.
  optional StringReference description = 4;
  // The time range for the run. If unset, the run will be created starting at now() with no end time.
  optional TimeRangeReference time_range = 5;
  // Labels to apply on the run.
  optional MultiStringReference labels = 6;
  // Properties to apply on the run.
  repeated Property properties = 7;
  // repeated UrlReference links;
  // repeated AttachmentRidReference attachments; // requires some sort of AttachmentRidField where FE handles upload

  message Property {
    StringReference key = 1;
    StringReference value = 2;
  }
}

// An action to update an existing run when a step is completed (e.g. set end time to previous step end time).
message UpdateRunConfig {
  // option (buf.validate.message).cel = {
  //   id: "update_run_config.at_least_one_update"
  //   message: "At least one update field (name, description, time_range, labels, or properties) must be set"
  //   expression: "has(this.name) || has(this.description) || has(this.time_range) || has(this.labels) || this.properties.size() > 0"
  // };

  // The run(s) to update. Typically but not limited to a reference to a run created earlier in the procedure
  MultiRunReference runs = 1 [(buf.validate.field).required = true];

  // Optional PATCH-style updates. Only set fields that should change.
  // If unset, the run will not be updated.
  optional StringReference name = 2;
  optional StringReference description = 3;
  optional TimeRangeReference time_range = 4;
  optional MultiStringReference labels = 5; // TODO(ryanyost): support patch-behavior for labels
  repeated PropertyReference properties = 6; // TODO(ryanyost): support patch-behavior for properties

  // When true, time fields overwrite existing values. When false, start only moves earlier, end only moves later (aligns with runs API strictOverwrite).
  optional bool strict_overwrite = 7;
}

message PropertyReference {
  StringReference key = 1;
  StringReference value = 2;
}

// An action to apply one or more workbook templates to one or more runs when a step is completed.
message ApplyWorkbookTemplatesConfig {
  // The workbook templates to apply.
  MultiWorkbookTemplateReference workbook_templates = 1;
  // The runs to apply the workbook templates to. There must be at least one.
  MultiRunReference runs = 2;
}

// An action to apply one or more checklists to one or more runs when a step is completed.
message ApplyChecklistsConfig {
  // The checklists to apply.
  MultiChecklistReference checklists = 1;
  // The runs to apply the checklists to. There must be at least one.
  MultiRunReference runs = 2;
}

///////////////////////////////
// Step Content Types        //
// - FormStep                //
// - StartIngestStep         //
// - SelectOrCreateAssetStep //
// - WaitStep                //
///////////////////////////////

// The content of a procedure step. This typically involves some form of user interaction/input.
message ProcedureStepContent {
  // This should be kept in sync with NestedProcedureNode.NestedStepNode.content
  oneof content {
    option (buf.validate.oneof) = {required: true};
    FormStep form = 1;
    StartIngestStep start_ingest = 2;
    SelectOrCreateAssetStep select_or_create_asset = 3;
    WaitStep wait = 4;
    // ...
  }
}

message WaitStep {}

// A form step consists of a set of fields to be filled out by the user.
message FormStep {
  repeated FormField fields = 1;
}

// The user will be prompted to start an ingest job in the frontend
// and the output of the step will be the rid of the created ingest job.
message StartIngestStep {
  // The asset to ingest data into.
  AssetReference asset = 1;
  // The data scope within the asset to ingest data into.
  StringReference ref_name = 2;
  // Restrict and pre-configure the type of ingest that can be started.
  IngestTypeConfig ingest_type_config = 3;
  // If present, the ingest job RID for this step will be available in outputs under this field ID.
  optional string ingest_job_output_field_id = 4 [(buf.validate.field).string.(valid_field_identifier) = true];

  message IngestTypeConfig {
    oneof config {
      option (buf.validate.oneof) = {required: true};
      // The ingest job must be a containerized extractor ingest.
      ContainerizedExtractorIngestConfig containerized_extractor = 1;
      DataflashIngestConfig dataflash = 2;
      CsvIngestConfig csv = 3;
      ParquetIngestConfig parquet = 4;
    }

    message ContainerizedExtractorIngestConfig {
      // The ingest job must use this specific containerized extractor.
      string rid = 1 [(nominal.gen.v1.alias) = {java_name: "io.nominal.ingest.api.ContainerizedExtractorRid"}];
      // The step will prompt the user to upload a file for each extractor input that doesn't have a FileInputBinding.
      repeated FileInputBinding file_input_bindings = 2;
      // String parameter bindings to pass to the containerized extractor as environment variables.
      repeated EnvironmentInputBinding environment_input_bindings = 3;
      // Additional file tags to apply to containerized extractor outputs.
      repeated AdditionalFileTagBinding additional_file_tag_bindings = 4;
    }

    message DataflashIngestConfig {
      // File to ingest into the dataset. Should be a dataflash `.bin` file.
      // Uses tags configured on the asset data scope as additional file tags
      optional FileReference file_input = 1;
    }

    message CsvIngestConfig {
      // header for timestamp column
      optional StringReference timestamp_series_name = 1;

      TimestampTypeParameter timestamp_type = 2;

      // File to ingest into the dataset. Should be a `.csv` or `.csv.gz` file.
      // Uses tags configured on the asset data scope as additional file tags
      optional FileReference file_input = 3;
    }

    message ParquetIngestConfig {
      // header for timestamp column
      optional StringReference timestamp_series_name = 1;

      TimestampTypeParameter timestamp_type = 2;

      // File to ingest into the dataset. Should be a `.parquet` or `.parquet.gz` file or archive file.
      // Uses tags configured on the asset data scope as additional file tags
      optional FileReference file_input = 3;
    }
  }
}

message FileInputBinding {
  // The input key. For containerized extractors, this is the environment variable name.
  string environment_variable = 1;
  // The specific type of file
  FileReference file_reference = 2;
}

message FileReference {
  oneof option {
    // Reference a field from a prior step
    string field_id = 1;
  }
}

message EnvironmentInputBinding {
  // The input key. For containerized extractors, this is the environment variable name.
  string environment_variable = 1;
  // The string value to pass to the extractor.
  StringReference value = 2;
}

message AdditionalFileTagBinding {
  // The additional file tag name.
  string tag_name = 1;

  oneof value {
    option (buf.validate.oneof) = {required: true};
    // A string value to apply as the tag value.
    StringReference string_value = 2;
    // An asset RID value to apply as the tag value.
    AssetReference asset_rid = 3;
  }
}

message TimestampTypeParameter {
  oneof option {
    option (buf.validate.oneof) = {required: true};
    // Timestamp type to always use for ingest.
    nominal.types.time.TimestampType constant = 1;
    // If specified, allows user to enter timestamp metadata based on input options.
    UserInputOptions user_input = 2;
  }

  message UserInputOptions {
    // Can allow for more configuration
  }
}

// The user will be prompted to select an existing asset or create a new asset.
message SelectOrCreateAssetStep {
  // If present, the AssetRid for this step will be available in outputs under this field ID.
  optional string asset_output_field_id = 1 [(buf.validate.field).string.(valid_field_identifier) = true];
  // If present, the user will be given the option to create a new asset restricted by these parameters.
  optional CreateAssetParameters create_asset_parameters = 2;
  // The asset selection options for this step.
  // This can be omitted, in which case the asset can be chosen freely from all assets.
  oneof options {
    // The asset must be chosen from preset options.
    PresetAssetFieldOptions preset_options = 3;
    // The asset must be chosen from a saved view.
    // SavedViewRid saved_view = 4;
  }

  // When creating an new asset, the title is always provided by the user and the
  // frontend should prompt for it. Other parameters for asset creation can be
  // specified here as either user-prompted or preset.
  message CreateAssetParameters {
    // If set, the new asset will be created with this description.
    optional DescriptionParameter description = 2;
    // If set, the new asset will be created with these labels.
    optional LabelsParameter labels = 3;
    // If set, the new asset will be created with these properties.
    optional PropertiesParameter properties = 4;
    // Data scopes for the new asset. The keys are the ref names of the data scopes.
    map<string, DataScopeParameter> data_scopes = 5;

    message DescriptionParameter {
      oneof option {
        // The description is always a constant string.
        string constant = 1;
        // Can allow for free-form / guardrailed (templated?) user input.
      }
    }

    message LabelsParameter {
      // Labels to always apply on the new asset.
      repeated string constant = 1 [(nominal.gen.v1.alias) = {java_name: "io.nominal.api.Label"}];
      // If allowing for user input, the frontend should leverage ResourceMetadataService#listPropertiesAndLabels
      // to provide the user with existing labels to choose from.
      optional UserInputOptions user_input = 2;

      message UserInputOptions {
        // Can allow for more configuration - e.g. default set of starting labels? (which can be overrideen)
      }
    }

    message PropertiesParameter {
      // Properties to always apply on the new asset.
      map<string, string> constant = 1 [(nominal.gen.v1.alias) = {
        key_java_name: "io.nominal.api.PropertyName"
        value_java_name: "io.nominal.api.PropertyValue"
      }];
      // If allowing for user input, the frontend should leverage ResourceMetadataService#listPropertiesAndLabels
      // to provide the user with existing property keys/values to choose from.
      optional UserInputOptions user_input = 2;

      message UserInputOptions {
        // Each property key here must have a value provided by the user for asset creation.
        repeated string required_keys = 1;
        // Each property key here will be suggested to the user but is not required for asset creation
        repeated string suggested_keys = 2;
        // Can allow for more configuration - e.g. required k-v pairs, default k-v pairs?
      }
    }

    message DataScopeParameter {
      oneof data_source {
        option (buf.validate.oneof) = {required: true};
        // Creates a new empty dataset.
        NewDataset new_dataset = 1;
        // Use existing dataset
        ExistingDataset existing_dataset = 2;
      }

      reserved 3 to 20; // reserved for more data scope types

      TagsParameter series_tags = 21;

      // Other CreateAssetDataScope options not included here but could be added: offset

      message NewDataset {
        // The Dataset will be automatically named "{asset}-{ref_name}".
        // Can later allow for this to be specified.
        // Other CreateDataset options exist but are omitted for now
      }

      message ExistingDataset {
        // The dataset options for this field.
        // This can be omitted, in which case the dataset can be chosen freely.
        oneof options {
          // The dataset must be chosen from preset options.
          PresetDatasetFieldOptions preset_options = 1;
        }
      }
    }
  }
}

// A limited set of options to choose from, with an optional default option.
message PresetDatasetFieldOptions {
  // The list of options to choose from.
  repeated DatasetReference options = 1;
  // If set, this option will be selected by default, if it is present in the options.
  // The user can still select a different option.
  optional DatasetReference default_option = 2;
}

// A reference to a single dataset
message DatasetReference {
  oneof option {
    option (buf.validate.oneof) = {required: true};
    // a constant DatasetRid
    string rid = 1 [(nominal.gen.v1.alias) = {java_name: "io.nominal.api.rids.DatasetRid"}];
    // The field reference that will contain a DatasetRid value. The execution will fail if the field is not set
    // but the value is accessed.
    string field_id = 2 [(buf.validate.field).string.(valid_field_reference) = true];
  }
}

message TagsParameter {
  // Tags to always filter to on the dataset or asset data scope.
  map<string, string> constant = 1 [(nominal.gen.v1.alias) = {
    key_java_name: "io.nominal.api.TagName"
    value_java_name: "io.nominal.api.TagValue"
  }];
  // If allowing for user input, the frontend should leverage DataSourceService#getAvailableTagKeys/Values
  // to provide the user with existing series tag keys/values to choose from.
  optional UserInputOptions user_input = 2;

  message UserInputOptions {
    // Each tag key here must have a value provided by the user
    repeated string required_keys = 1;
    // Each tag key here will be suggested to the user but is not required
    repeated string suggested_keys = 2;
    // Can allow for more configuration - e.g. required k-v pairs, default k-v pairs?
  }
}

//////////////////////////////////
// FooReferences: ways to refer //
// to values of type Foo        //
//////////////////////////////////

// A reference to multiple string values
message MultiStringReference {
  oneof option {
    option (buf.validate.oneof) = {required: true};
    // The value is taken from a field in the current or previous steps.
    string field_id = 1 [(buf.validate.field).string.(valid_field_reference) = true];
  }
}

// A reference to a single string value
message StringReference {
  oneof option {
    option (buf.validate.oneof) = {required: true};
    // The value is a constant string
    string constant = 1;
    // The value is taken from a field in the current or previous steps.
    string field_id = 2 [(buf.validate.field).string.(valid_field_reference) = true];
    // TODO(WB-1728): support string templating.
    // string template = 3;
  }
}

// A reference to multiple assets
message MultiAssetReference {
  oneof option {
    option (buf.validate.oneof) = {required: true};
    AssetReferenceList list = 1;
    // TODO: add support for a field that contains a list of asset RIDs
  }

  message AssetReferenceList {
    repeated AssetReference references = 1 [(buf.validate.field).repeated.min_items = 1];
  }
}

// A reference to a single asset
message AssetReference {
  oneof option {
    option (buf.validate.oneof) = {required: true};
    // a constant AssetRid
    string rid = 1 [(nominal.gen.v1.alias) = {java_name: "io.nominal.scout.rids.api.AssetRid"}];
    // The field reference that will contain an AssetRid value. The execution will fail if the field is not set
    // but the value is accessed.
    string field_id = 2 [(buf.validate.field).string.(valid_field_reference) = true];
  }
}

// A reference to a time range
message TimeRangeReference {
  oneof option {
    option (buf.validate.oneof) = {required: true};
    // The time range is the union of the time ranges ingested by all the specified ingest jobs.
    IngestJobList from_ingest_jobs = 1;
    RangeLiteral literal = 2;
  }

  message IngestJobList {
    // Field ids that contain IngestJobRids.
    repeated string field_ids = 1 [(buf.validate.field).repeated = {
      min_items: 1
      items: {
        string: {[nominal.procedures.v1.valid_field_reference]: true}
      }
    }];
  }

  message RangeLiteral {
    // If absent, the run's start time will not be set or modified.
    // For CreateRunConfig, a start time is required (see message-level CEL constraint).
    optional TimestampReference start = 1;
    optional TimestampReference end = 2;
  }
}

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

    // A field reference that resolves to a timestamp from a step's execution state.
    // Supported field names: started_at, succeeded_at, submitted_at, skipped_at, errored_at.
    // Aliases: start_time (= started_at), end_time (= succeeded_at).
    string field_id = 1 [(buf.validate.field).string.(valid_field_reference) = true];

    // A literal/constant timestamp
    google.protobuf.Timestamp constant = 2;
  }
}

// A reference to multiple runs
message MultiRunReference {
  oneof option {
    option (buf.validate.oneof) = {required: true};
    RunReferenceList list = 1;
    // TODO: add support for a field that contains a list of run RIDs
  }

  message RunReferenceList {
    repeated RunReference references = 1 [(buf.validate.field).repeated.min_items = 1];
  }
}

// A reference to a single run
message RunReference {
  oneof option {
    option (buf.validate.oneof) = {required: true};
    // The field id that will contain a RunRid value. The execution will fail if the field is not set properly.
    string field_id = 1 [(buf.validate.field).string.(valid_field_reference) = true];
  }
}

// A reference to multiple workbook templates
message MultiWorkbookTemplateReference {
  oneof option {
    option (buf.validate.oneof) = {required: true};
    WorkbookTemplateReferenceList list = 1;
    // TODO: add support for a field that contains a list of workbook template RIDs
  }
  message WorkbookTemplateReferenceList {
    repeated WorkbookTemplateReference references = 1 [(buf.validate.field).repeated.min_items = 1];
  }
}

// A reference to a single workbook template
message WorkbookTemplateReference {
  oneof option {
    option (buf.validate.oneof) = {required: true};
    // A constant workbook template RID.
    string rid = 1 [(nominal.gen.v1.alias) = {java_name: "io.nominal.scout.rids.api.TemplateRid"}];
    // string field_id = 2;
  }
}

// A reference to multiple checklists
message MultiChecklistReference {
  oneof option {
    option (buf.validate.oneof) = {required: true};
    ChecklistReferenceList list = 1;
    // TODO: add support for a field that contains a list of checklist RIDs
  }
  message ChecklistReferenceList {
    repeated ChecklistReference references = 1 [(buf.validate.field).repeated.min_items = 1];
  }
}

// A reference to a single checklist
message ChecklistReference {
  oneof option {
    option (buf.validate.oneof) = {required: true};
    // A constant checklist RID.
    string rid = 1 [(nominal.gen.v1.alias) = {java_name: "io.nominal.scout.rids.api.ChecklistRid"}];
    // string field_id = 2;
  }
}

// A reference to multiple notification Integrations
message MultiIntegrationReference {
  oneof option {
    option (buf.validate.oneof) = {required: true};
    IntegrationReferenceList list = 1;
  }

  message IntegrationReferenceList {
    repeated IntegrationReference references = 1 [(buf.validate.field).repeated.min_items = 1];
  }
}

// A reference to a notification Integration
message IntegrationReference {
  oneof option {
    option (buf.validate.oneof) = {required: true};
    // A constant integration RID.
    string rid = 1 [(nominal.gen.v1.alias) = {java_name: "io.nominal.scout.integrations.api.IntegrationRid"}];
    // string field_id = 2;
  }
}

//////////////////////////
// FormField: a way to  //
// collect user input   //
//////////////////////////

// A field to be filled out by the user.
message FormField {
  // This field's id can be used by downstream steps to refer to the value.
  string id = 1 [(buf.validate.field).string.(valid_field_identifier) = true];
  oneof field {
    option (buf.validate.oneof) = {required: true};
    AssetField asset = 2;
    CheckboxField checkbox = 3;
    TextField text = 4;
    IntField int = 5;
    DoubleField double = 6;
    SingleEnumField single_enum = 7;
    MultiEnumField multi_enum = 8;
    FileUploadField file_upload = 9;
    MultiFileUploadField multi_file_upload = 10;
  }
  // UI label for the field component. Overrides any label defined in the specific FormField type.
  optional string label = 11 [(buf.validate.field).string.min_len = 1];
  // Help text describing how to use this input field.
  optional string description = 12 [(buf.validate.field).string.min_len = 1];
}

// A limited set of options to choose from, with an optional default option.
message PresetAssetFieldOptions {
  // The list of options to choose from.
  repeated AssetReference options = 1;
  // If set, this option will be selected by default, if it is present in the options.
  // The user can still select a different option.
  optional AssetReference default_option = 2;

  // TODO(pchen): we could add a strict mode to switch behavior for having
  // the selected option be required to be present in the options.
}

// A field to select a single asset.
message AssetField {
  // Helper text to guide selection of the asset
  // deprecated in favor of label directly on FormField
  string label = 1 [(buf.validate.field).string.min_len = 1];
  // Whether this field is required to be set complete the step.
  bool is_required = 2;
  // The asset options for this field.
  // This can be omitted, in which case the asset can be chosen freely from all assets.
  oneof options {
    PresetAssetFieldOptions preset_options = 3;
    // The asset must be chosen from a saved view.
    // SavedViewRid saved_view = 5;
    // TODO(pchen): do we want to provide single string types from AssetFieldOption to simplify the Nested
    // or do we want to explicitly modify the types Nested spec uses?
  }
}

// A field to capture a boolean value.
message CheckboxField {
  // Label to show for this checkbox field
  // deprecated in favor of label directly on FormField
  string label = 1 [(buf.validate.field).string.min_len = 1];
  // If set, the checkbox value must be checked (`true`) to complete the step.
  bool is_required = 2;
}

message TextFieldSimpleInputType {}

message TextFieldMarkdownInputType {}

// A field to capture a string value.
message TextField {
  // Label to show for this text field
  // deprecated in favor of label directly on FormField
  string label = 1 [(buf.validate.field).string.min_len = 1];
  // type of form input to render. Defaults to SIMPLE.
  oneof input_type {
    option (buf.validate.oneof) = {required: true};
    // renders a simple html text input field
    TextFieldSimpleInputType simple = 2;
    // renders a Markdown editor for longer inputs with formatting capabilities
    TextFieldMarkdownInputType markdown = 3;
  }
  // if set, validate that input is at least this many characters
  // `is_required` can be achieved by setting `min_length >= 1`
  optional uint32 min_length = 4 [(buf.validate.field).uint32.gte = 0];
  // if set, validate that input is at most this many characters
  optional uint32 max_length = 5 [(buf.validate.field).uint32.gte = 0];
  // if set, validate that input matches this regular expression pattern
  optional string pattern = 6 [(buf.validate.field).string.min_len = 1];
  // if set, show this message on pattern match failure instead of a generic error
  optional string pattern_description = 7 [(buf.validate.field).string.min_len = 1];
  // only allow pattern_description if pattern is set
  option (buf.validate.message).cel = {
    id: "text_field.pattern_description_requires_pattern"
    message: "pattern must be present if pattern_description is present"
    expression: "!has(this.pattern_description) || has(this.pattern)"
  };
}

// A field to capture an int value.
message IntField {
  // Label to show for this int field
  // deprecated in favor of label directly on FormField
  string label = 1 [(buf.validate.field).string.min_len = 1];
  // Whether this field is required to be set complete the step.
  bool is_required = 2;
  // if set, validate that input is at least this value
  optional int64 gte_value = 3;
  // if set, validate that input is at most this value
  optional int64 lte_value = 4;
}

// A field to capture a double value.
message DoubleField {
  // Label to show for this double field
  // deprecated in favor of label directly on FormField
  string label = 1 [(buf.validate.field).string.min_len = 1];
  // Whether this field is required to be set complete the step.
  bool is_required = 2;
  // if set, a validation on the lower bound of the input value
  oneof lower_bound {
    // greater than
    double gt_value = 3;
    // greater than or equal to
    double gte_value = 4;
  }
  // if set, a validation on the upper bound of the input value
  oneof upper_bound {
    // less than
    double lt_value = 5;
    // less than or equal to
    double lte_value = 6;
  }
}

message EnumFieldButtonsInputType {}

message EnumFieldMenuInputType {}

// A field to capture one of several options.
message SingleEnumField {
  // Label to show for this options field
  // deprecated in favor of label directly on FormField
  string label = 1 [(buf.validate.field).string.min_len = 1];
  // List of predefined options that can be selected by the user
  repeated string options = 2;
  // type of form input to render.
  oneof input_type {
    option (buf.validate.oneof) = {required: true};
    // renders a radio button component
    EnumFieldButtonsInputType buttons = 3;
    // renders a typical drop down menu
    EnumFieldMenuInputType dropdown = 4;
  }
  // Whether a user can create and specify a new custom option not in the list of options.
  bool allow_custom = 5;
  // Whether a selection is required
  bool is_required = 6;
}

// A field to capture one or more of several options.
message MultiEnumField {
  // Label to show for this options field
  // deprecated in favor of label directly on FormField
  string label = 1 [(buf.validate.field).string.min_len = 1];
  // List of predefined options that can be selected by the user
  repeated string options = 2;
  // type of form input to render.
  oneof input_type {
    option (buf.validate.oneof) = {required: true};
    // renders a collection of checkboxes
    EnumFieldButtonsInputType buttons = 3;
    // renders a typical drop down menu
    EnumFieldMenuInputType dropdown = 4;
  }
  // Whether a user can create and specify a new custom option not in the list of options.
  bool allow_custom = 6;
  // If set, the user must select at least this many options
  // `is_required` behavior can be achieved by setting `min_count` = 1
  optional uint32 min_count = 7;
  // If set, the user can select at most this many options
  optional uint32 max_count = 8;
}

// A field to process the uploading of a file
message FileUploadField {
  // If a file is required
  bool is_required = 1;
  // Optional list of case insensitive suffixes to filter files. (ie "csv", "parquet", or "json")
  repeated string suffix_filters = 2 [(buf.validate.field).repeated.items.string.min_len = 1];
}

// A field to process the uploading of a dynamic set of files
message MultiFileUploadField {
  // Count of required files
  optional uint32 min_count = 1;
  // File count limit
  optional uint32 max_count = 2;
  // Optional list of case insensitive suffixes to filter files. (ie "csv", "parquet", or "json")
  repeated string suffix_filters = 3 [(buf.validate.field).repeated.items.string.min_len = 1];
}

///////////////////////////////
// Top-level Procedure Types //
///////////////////////////////

// Metadata about a procedure that is not part of the versioned state.
message ProcedureMetadata {
  string rid = 1 [(nominal.gen.v1.alias) = {java_name: "io.nominal.api.rids.ProcedureRid"}];
  string title = 2 [(buf.validate.field).string.min_len = 1];
  optional string description = 3;
  repeated string labels = 4 [(nominal.gen.v1.alias) = {java_name: "io.nominal.api.Label"}];
  map<string, string> properties = 5 [(nominal.gen.v1.alias) = {
    key_java_name: "io.nominal.api.PropertyName"
    value_java_name: "io.nominal.api.PropertyValue"
  }];
  bool is_archived = 6;
  bool is_published = 7;
  google.protobuf.Timestamp created_at = 8;
  string created_by = 9 [(nominal.gen.v1.alias) = {java_name: "io.nominal.scout.rids.api.UserRid"}];
  google.protobuf.Timestamp updated_at = 10;
  string updated_by = 11 [(nominal.gen.v1.alias) = {java_name: "io.nominal.scout.rids.api.UserRid"}];
  string workspace = 12 [(nominal.gen.v1.alias) = {java_name: "io.nominal.api.rids.WorkspaceRid"}];
  // Number of executions created from this procedure. Only populated by search.
  optional int64 execution_count = 13;
}

// A procedure describes a series of steps to be performed, typically in a specific order,
message Procedure {
  // The RID of the procedure.
  string rid = 1 [(nominal.gen.v1.alias) = {java_name: "io.nominal.api.rids.ProcedureRid"}];
  // The commit ID of this version of the procedure.
  string commit = 2 [(nominal.gen.v1.alias) = {java_name: "io.nominal.scout.versioning.api.CommitId"}];
  // Metadata about the procedure.
  ProcedureMetadata metadata = 3;
  // The versioned state of the procedure.
  ProcedureState state = 4;
}

/////////////////////
// Req/Res Objects //
/////////////////////

message CreateProcedureRequest {
  string title = 1 [(buf.validate.field).string.min_len = 1];
  optional string description = 2;
  repeated string labels = 3 [(nominal.gen.v1.alias) = {java_name: "io.nominal.api.Label"}];
  map<string, string> properties = 4 [(nominal.gen.v1.alias) = {
    key_java_name: "io.nominal.api.PropertyName"
    value_java_name: "io.nominal.api.PropertyValue"
  }];
  optional ProcedureState state = 5;
  optional bool is_published = 6;
  string workspace = 7 [(nominal.gen.v1.alias) = {java_name: "io.nominal.api.rids.WorkspaceRid"}];
  string commit_message = 8;
  optional string initial_branch_name = 9;
}

message CreateProcedureResponse {
  Procedure procedure = 1;
  optional string branch_name = 2;
}

message GetProcedureRequest {
  string rid = 1 [(nominal.gen.v1.alias) = {java_name: "io.nominal.api.rids.ProcedureRid"}];
  nominal.versioning.v1.BranchOrCommit branch_or_commit = 2;

  // If true, the display graph will be included in the response.
  // Default is false.
  bool include_display_graph = 3;
}

message GetProcedureResponse {
  Procedure procedure = 1;

  // Included if include_display_graph was set to true
  optional ProcedureDisplayGraph display_graph = 2;
}

message BatchGetProcedureMetadataRequest {
  repeated string procedure_rids = 1 [(nominal.gen.v1.alias) = {java_name: "io.nominal.api.rids.ProcedureRid"}];
}

message BatchGetProcedureMetadataResponse {
  repeated ProcedureMetadata procedure_metadatas = 1;
}

message UpdateProcedureMetadataRequest {
  string rid = 1 [(nominal.gen.v1.alias) = {java_name: "io.nominal.api.rids.ProcedureRid"}];
  optional string title = 2;
  optional string description = 3;
  optional nominal.types.LabelUpdateWrapper labels = 4;
  optional nominal.types.PropertyUpdateWrapper properties = 5;
  optional bool is_archived = 6;
  optional bool is_published = 7;
}

message UpdateProcedureMetadataResponse {
  ProcedureMetadata procedure_metadata = 1 [(buf.validate.field) = {required: true}];
}

message ParseNestedProcedureRequest {
  NestedProcedure nested_procedure = 1;
  // If true, the display graph will be included in the response.
  // Default is false.
  bool include_display_graph = 3;
}

message ParseNestedProcedureResponse {
  Procedure procedure = 1;
  // Included if include_display_graph was set to true
  optional ProcedureDisplayGraph display_graph = 2;
}

message GetProcedureAsNestedRequest {
  string rid = 1 [(nominal.gen.v1.alias) = {java_name: "io.nominal.api.rids.ProcedureRid"}];
  nominal.versioning.v1.BranchOrCommit branch_or_commit = 2;
}

message GetProcedureAsNestedResponse {
  NestedProcedure nested_procedure = 1;
}

message MergeToMainRequest {
  string rid = 1 [(nominal.gen.v1.alias) = {java_name: "io.nominal.api.rids.ProcedureRid"}];
  string branch = 2 [(nominal.gen.v1.alias) = {java_name: "io.nominal.scout.versioning.api.BranchName"}];
  // If present, this commit ID must match the latest commit on main for the merge to succeed.
  optional string latest_commit_on_main = 3 [(nominal.gen.v1.alias) = {java_name: "io.nominal.scout.versioning.api.CommitId"}];
  string message = 4 [(buf.validate.field) = {required: true}];
}

message MergeToMainResponse {
  Procedure procedure = 1;
}

message SaveWorkingStateRequest {
  string rid = 1 [(nominal.gen.v1.alias) = {java_name: "io.nominal.api.rids.ProcedureRid"}];
  optional string branch = 2 [(nominal.gen.v1.alias) = {java_name: "io.nominal.scout.versioning.api.BranchName"}];
  string message = 3;
  // If present, this commit ID must match the latest commit on the branch for the save to succeed.
  optional string latest_commit_on_branch = 4 [(nominal.gen.v1.alias) = {java_name: "io.nominal.scout.versioning.api.CommitId"}];
  ProcedureState state = 5;
}

message SaveWorkingStateResponse {
  Procedure procedure = 1;
}

message CommitRequest {
  string rid = 1 [(nominal.gen.v1.alias) = {java_name: "io.nominal.api.rids.ProcedureRid"}];
  optional string branch = 2 [(nominal.gen.v1.alias) = {java_name: "io.nominal.scout.versioning.api.BranchName"}];
  // If present, this commit ID must match the latest commit on the branch for the commit to succeed.
  optional string latest_commit_on_branch = 3 [(nominal.gen.v1.alias) = {java_name: "io.nominal.scout.versioning.api.CommitId"}];
  string message = 4;
  ProcedureState state = 5;
}

message CommitResponse {
  Procedure procedure = 1;
}

// This should be kept in sync with the procedure search query defined in procedures-saved-view-api.yml,
// as saved views are defined in conjure.

message ProcedureSearchQuery {
  oneof query {
    option (buf.validate.oneof) = {required: true};
    string search_text = 1;
    string label = 2 [(nominal.gen.v1.alias) = {java_name: "io.nominal.api.Label"}];
    nominal.types.Property property = 3;
    ProcedureSearchAndQuery and = 4;
    ProcedureSearchOrQuery or = 5;
    string workspace = 6 [(nominal.gen.v1.alias) = {java_name: "io.nominal.api.rids.WorkspaceRid"}];
    string created_by = 7 [(nominal.gen.v1.alias) = {java_name: "io.nominal.scout.rids.api.UserRid"}];
    bool is_archived = 8;
  }

  message ProcedureSearchAndQuery {
    repeated ProcedureSearchQuery queries = 1;
  }

  message ProcedureSearchOrQuery {
    repeated ProcedureSearchQuery queries = 1;
  }
}

// This should be kept in sync with the procedure search sort options defined in
// procedures-saved-view-api.yml, as saved views are defined in conjure.

message SearchProceduresSortOptions {
  // Defaults to true (descending)
  optional bool is_descending = 1;
  // Defaults to CREATED_AT
  optional SearchProceduresSortField sort_field = 2;
}

enum SearchProceduresSortField {
  SEARCH_PROCEDURES_SORT_FIELD_UNSPECIFIED = 0;
  SEARCH_PROCEDURES_SORT_FIELD_NAME = 1;
  SEARCH_PROCEDURES_SORT_FIELD_CREATED_AT = 2;
  SEARCH_PROCEDURES_SORT_FIELD_UPDATED_AT = 3;
}

message SearchProceduresRequest {
  ProcedureSearchQuery query = 1;
  // If not set, default is to sort by created_at descending
  optional SearchProceduresSortOptions sort_options = 2;
  // Page sizes greater than 1_000 will be rejected. Default pageSize is 100.
  optional int32 page_size = 3 [
    (buf.validate.field).int32.gte = 1,
    (buf.validate.field).int32.lte = 1000
  ];
  optional string next_page_token = 4 [(nominal.gen.v1.alias) = {java_name: "io.nominal.api.Token"}];
}

message SearchProceduresResponse {
  repeated ProcedureMetadata procedure_metadata = 1;
  optional string next_page_token = 2 [(nominal.gen.v1.alias) = {java_name: "io.nominal.api.Token"}];
}

message ArchiveProceduresRequest {
  repeated string procedure_rids = 1 [(nominal.gen.v1.alias) = {java_name: "io.nominal.api.rids.ProcedureRid"}];
}

message ArchiveProceduresResponse {
  repeated string procedure_rids = 1 [(nominal.gen.v1.alias) = {java_name: "io.nominal.api.rids.ProcedureRid"}];
}

message UnarchiveProceduresRequest {
  repeated string procedure_rids = 1 [(nominal.gen.v1.alias) = {java_name: "io.nominal.api.rids.ProcedureRid"}];
}

message UnarchiveProceduresResponse {
  repeated string procedure_rids = 1 [(nominal.gen.v1.alias) = {java_name: "io.nominal.api.rids.ProcedureRid"}];
}

////////////
// Errors //
////////////

enum ProceduresServiceError {
  PROCEDURES_SERVICE_ERROR_UNSPECIFIED = 0;
  PROCEDURES_SERVICE_ERROR_NOT_FOUND = 1 [(nominal.gen.v1.error) = {
    code: "NOT_FOUND"
    message: "Procedure not found"
  }];
  PROCEDURES_SERVICE_ERROR_COMMIT_NOT_FOUND = 2 [(nominal.gen.v1.error) = {
    code: "NOT_FOUND"
    message: "Procedure commit not found"
  }];
  PROCEDURES_SERVICE_ERROR_CANNOT_MERGE_MAIN = 3 [(nominal.gen.v1.error) = {
    code: "INVALID_ARGUMENT"
    message: "Cannot merge to main from main"
  }];
  PROCEDURES_SERVICE_ERROR_CANNOT_COMMIT_TO_ARCHIVED_PROCEDURE = 4 [(nominal.gen.v1.error) = {
    code: "INVALID_ARGUMENT"
    message: "Cannot commit to archived procedure"
  }];
  PROCEDURES_SERVICE_ERROR_INVALID_GRAPH = 5 [(nominal.gen.v1.error) = {
    code: "INVALID_ARGUMENT"
    message: "Invalid procedure graph"
  }];
  PROCEDURES_SERVICE_ERROR_INVALID_SEARCH_TOKEN = 6 [(nominal.gen.v1.error) = {
    code: "INVALID_ARGUMENT"
    message: "Invalid search token"
  }];
}

// Service for creating and managing procedure templates, which are versioned collections
// of steps to be performed, typically in a specific order.
service ProceduresService {
  // Create a new procedure
  rpc CreateProcedure(CreateProcedureRequest) returns (CreateProcedureResponse) {
    option (google.api.http) = {
      post: "/procedures/v1/procedures"
      body: "*"
    };
  }

  // Get a procedure by its RID
  rpc GetProcedure(GetProcedureRequest) returns (GetProcedureResponse) {
    option (google.api.http) = {get: "/procedures/v1/procedures/{rid}"};
  }

  // Get metadata for multiple procedure by RIDs.
  // Unauthorized RIDs will be omitted from the response.
  // Order is not guaranteed to match the request.
  rpc BatchGetProcedureMetadata(BatchGetProcedureMetadataRequest) returns (BatchGetProcedureMetadataResponse) {
    option (google.api.http) = {
      post: "/procedures/v1/procedures/batch-get-metadata"
      body: "*"
    };
  }

  // Update the metadata of a procedure
  rpc UpdateProcedureMetadata(UpdateProcedureMetadataRequest) returns (UpdateProcedureMetadataResponse) {
    option (google.api.http) = {
      put: "/procedures/v1/procedures/{rid}/metadata"
      body: "*"
    };
  }

  // Parse a procedure from a nested procedure structure
  rpc ParseNestedProcedure(ParseNestedProcedureRequest) returns (ParseNestedProcedureResponse) {
    option (google.api.http) = {
      post: "/procedures/v1/procedures/parse-nested-procedure"
      body: "*"
    };
  }

  // Get procedure as nested procedure
  // TODO(pchen): in the future, not all graphs will be representable as nested procedures
  rpc GetProcedureAsNested(GetProcedureAsNestedRequest) returns (GetProcedureAsNestedResponse) {
    option (google.api.http) = {get: "/procedures/v1/procedures/nested/{rid}"};
  }

  // Merge a branch of a procedure to main
  rpc MergeToMain(MergeToMainRequest) returns (MergeToMainResponse) {
    option (google.api.http) = {
      post: "/procedures/v1/procedures/{rid}/merge-to-main"
      body: "*"
    };
  }

  // Save the working state of a procedure
  rpc SaveWorkingState(SaveWorkingStateRequest) returns (SaveWorkingStateResponse) {
    option (google.api.http) = {
      post: "/procedures/v1/procedures/{rid}/save-working-state"
      body: "*"
    };
  }

  // Commit a procedure to a branch
  rpc Commit(CommitRequest) returns (CommitResponse) {
    option (google.api.http) = {
      post: "/procedures/v1/procedures/{rid}/commit"
      body: "*"
    };
  }

  // Searches all procedures, returning paged results
  rpc SearchProcedures(SearchProceduresRequest) returns (SearchProceduresResponse) {
    option (google.api.http) = {
      post: "/procedures/v1/procedures/search"
      body: "*"
    };
  }

  // Archive procedures
  rpc ArchiveProcedures(ArchiveProceduresRequest) returns (ArchiveProceduresResponse) {
    option (google.api.http) = {
      post: "/procedures/v1/procedures/archive"
      body: "*"
    };
  }

  // Unarchive procedures
  rpc UnarchiveProcedures(UnarchiveProceduresRequest) returns (UnarchiveProceduresResponse) {
    option (google.api.http) = {
      post: "/procedures/v1/procedures/unarchive"
      body: "*"
    };
  }
}
