// Edge-control-plane schema, authored by ace-ng-edge team, reviewed
// + accepted by Aerodrome 2026-04-22.
//
// Cloud-to-edge command payloads. Published by Aerodrome on the
// COMMANDS JetStream (subject root: ace.edge.cmd.>), consumed via
// durable consumers per agent.
//
// Per ace-ng-edge docs/plans/ot-network-monitoring.md Phase 1 and
// src/ace_edge/config/agent.py, ConfigUpdate / ConfigDelete are
// currently ad-hoc JSON dicts — formalizing them here so the cloud-
// to-edge contract gets the same schema-CI treatment as everything
// else.

syntax = "proto3";

package aerodrome.v1;

import "google/protobuf/struct.proto";

// Trigger a network scan on the edge. Consumed by ace-scan on
// ace.edge.cmd.scan.start.
//
// Matches ScanCommand in src/ace_edge/models.py. The scan type is an
// enum rather than a free string because only two values are
// meaningful today — a broken value is an operator error we want to
// surface at schema validation time, not at runtime.
message ScanCommand {
  enum ScanType {
    SCAN_TYPE_UNSPECIFIED = 0;
    SCAN_TYPE_FULL = 1;        // rediscover every device_id in range
    SCAN_TYPE_INCREMENTAL = 2; // diff against the latest snapshot
  }

  ScanType scan_type = 1;

  // Device-id range. Absent = default (0..4_194_303, the full BACnet
  // space). Setting either bound narrows the WhoIs.
  optional int32 low_limit = 2;
  optional int32 high_limit = 3;

  // Adaptive range walk (grasshopper pattern): start wide, step
  // dense when devices respond, step sparse when they don't.
  // false = single global WhoIs (manual/TUI scans that want a
  // predictable one-shot shape).
  bool adaptive = 4;
}

// Update a single config KV entry. Consumed by ace-config on
// ace.edge.cmd.config.update.
//
// Two payload shapes are supported during the multi-protocol
// parity transition (see Aerodrome's
// `multi-protocol-config-parity.md` and
// `SPEC-aceiot-models-proto-updates.md`):
//
//   - Legacy: `value_json` — JSON-encoded payload of the target
//     model as a bare string. Edges parse against the matching
//     Pydantic model on the value-shape side.
//   - New: `value` — google.protobuf.Struct carrying the same
//     structured payload natively. Easier to validate cross-
//     language, schema-CI catches shape regressions.
//
// Producers should populate ONE of the two during the transition.
// Aerodrome populates `value_json` today; will switch to `value`
// after every consumer reads it.
message ConfigUpdateCommand {
  // Fully-qualified KV key. Examples:
  //   driver.bacnet.01HX2K3M9PQRS7VYZ8N4WGFTBA
  //   registry.01HX2K3M9PQRS7VYZ8N4WGFTBA
  //   driver.modbus.01HX2K3M9PQRS7VYZ8N4WGFTBA
  //   driver.ethernet_ip.01HX2K3M9PQRS7VYZ8N4WGFTBA
  //   network.bacnet.vlan10
  // (Per the multi-protocol parity plan, device-id KV tokens are
  // ULID strings; legacy int-device-id keys are no longer minted.)
  // The edge rejects wildcard / prefix keys (ending in . or
  // containing * or >) to prevent bulk operations.
  string key = 1;

  // LEGACY: JSON-serialized config value. Schema depends on the
  // key prefix — the edge deserializes against the matching
  // Pydantic model and fails the command (without applying) if
  // validation errors. Deprecated by `value` (field 4); will be
  // dropped in the next major schema bump.
  string value_json = 2 [deprecated = true];

  // Optional idempotency / correlation id — operator UIs can set
  // this to retry safely without worrying about double-apply side
  // effects. The edge logs it on apply; no semantic guarantees
  // beyond that.
  optional string correlation_id = 3;

  // Structured config value. Same semantics as `value_json` but
  // self-describing — every proto runtime decodes Struct natively
  // (Rust prost-types, Python google.protobuf.struct_pb2,
  // ts-proto's @bufbuild/protobuf). Producers populate either
  // this field OR `value_json`, not both; edges prefer `value`
  // when both are present.
  optional google.protobuf.Struct value = 4;
}

// Delete a single config KV entry. Consumed by ace-config on
// ace.edge.cmd.config.delete.
//
// The edge refuses to delete keys that look like prefixes (ending
// with . or containing * / >). Cloud senders should always target a
// single fully-qualified key.
message ConfigDeleteCommand {
  // Fully-qualified KV key. Same rules as ConfigUpdateCommand.key.
  string key = 1;

  // Optional idempotency / correlation id.
  optional string correlation_id = 2;
}
