// Edge-control-plane schema, authored by ace-ng-edge team, reviewed
// + accepted by Aerodrome 2026-04-22. First publish adds the five
// edge-surface messages (bacnet + commands + edge_events +
// reachability + registry) alongside the existing four.
//
// Corresponds to (in ace_edge.models):
//   BacnetDriverConfig    v0.17.0+ (pre-dated by DriverConfig; renamed)
//   BacnetPointDef        v0.18.0+
//   BacnetNetworkConfig   v0.28.0+ (multi-network-bacnet track)
//
// Storage: edge.config KV under
//   driver.bacnet.{device_id}   → BacnetDriverConfig
//   network.bacnet.{id}          → BacnetNetworkConfig
// Cloud-to-edge delivery: ace.edge.cmd.config.update (see commands.proto).

syntax = "proto3";

package aerodrome.v1;

// Operator-configured logical BACnet network attachment. One edge
// deployment may own multiple — each represents one logical attachment
// the operator defined. The durable identity (network_attachment_id)
// survives DHCP, operator renames of `id`, and hardware replacement
// (as long as the KV record is preserved).
//
// See ace-ng-edge docs/plans/multi-network-bacnet.md §3.1 for the
// rationale behind the id / attachment_id split.
message BacnetNetworkConfig {
  // Operator-facing identifier, unique per deployment. Charset enforced
  // at the Pydantic layer: [a-z0-9_-]+ (no dots, so it's safe as a
  // NATS subject token). Proto can't express that constraint — the
  // edge's KV-parse path rejects invalid values.
  string id = 1;

  // Local IP to bind, e.g. "10.1.10.50".
  string ip_address = 2;

  // CIDR prefix for broadcast derivation. Default 24.
  int32 prefix = 3;

  // BACnet UDP port. Default 47808.
  int32 port = 4;

  // BACnet internetwork number. 0 = local network per spec (NOT
  // "unknown"). Multi-network deployments require distinct non-zero
  // numbers per attachment — bacpypes3's nsap rejects duplicates.
  int32 network_number = 5;

  string description = 6;

  // Durable logical-attachment identity — the cloud correlation key.
  // Immutable once minted. UUID serialized as string to match the
  // style of TsIntervalMessage.
  //
  // Marked `optional` per Aerodrome review (2026-04-22): proto3 can't
  // distinguish "empty string" from "not populated" on bare scalars,
  // and the "optional-on-wire, required-after-load" semantic
  // described in this schema's companion review doc requires that
  // distinction. Edges that haven't minted an attachment yet emit the
  // field unset; consumers branch on presence.
  optional string network_attachment_id = 7;

  // When the attachment was first minted. Persisted so rehydrate is
  // deterministic across edge restarts.
  string created_at = 8;  // RFC 3339, UTC; see note
  // NOTE: created_at is a string rather than google.protobuf.Timestamp
  // because the edge's Pydantic model uses datetime with ISO-8601
  // serialization, and these records live in KV long enough that a
  // round-trip through Timestamp's nanosecond encoding would lose the
  // exact-bytes stability we want for attachment records.
}

// Address payload for one BACnet point. Carried inside PointDef
// (see registry.proto) — common fields (datapoint_id, intervals,
// writable, timeseries_id) live there so per-protocol address
// messages stay narrow.
//
// Companion to ModbusPointAddress (modbus.proto) and
// EthernetIpPointAddress (ethernet_ip.proto).
message BacnetPointAddress {
  // BACnet object type string, e.g. "analog-input", "analog-value",
  // "binary-value". Not an enum because bacpypes3 vendor extensions
  // add types we don't want to pin in the schema.
  string object_type = 1;

  uint32 instance = 2;

  // BACnet property name to read. Defaults to "present-value".
  string property_name = 3;

  // Explicit COV subscription on this point (COV snooping happens
  // regardless; this opts into an explicit SubscribeCOV RPC).
  bool cov_subscribe = 4;
  int32 cov_lifetime = 5;   // COV subscription lifetime in seconds
}

// LEGACY: A BACnet point on a device — one object,property tuple.
// Mirrors BacnetPointDef (BasePointDef + object_type/instance/
// property_name).
//
// Deprecated by the multi-protocol parity sweep — use PointDef
// (registry.proto) with `address.bacnet` (BacnetPointAddress) for
// new code. Kept here as a deprecated message for in-flight
// consumers; the registry.proto migration leaves field #2 free
// for new payloads.
message BacnetPointDef {
  option deprecated = true;
  // Which device owns this point.
  int32 device_id = 1;

  // BACnet object type string, e.g. "analog-input", "analog-value",
  // "binary-value". Not an enum because bacpypes3 vendor extensions
  // add types we don't want to pin in the schema.
  string object_type = 2;

  int32 instance = 3;

  // BACnet property name to read. Defaults to "present-value".
  string property_name = 4;

  // Whether the edge accepts writes on this point.
  bool writable = 5;

  // Aerodrome timeseries UUID, set by the config bridge when the
  // point is mapped to a cloud series. Absent = not mapped, don't
  // publish telemetry.
  optional string timeseries_id = 6;

  // Temporal dedup settings (all seconds, float).
  double min_interval = 7;   // rate-limit publishes
  double max_interval = 8;   // heartbeat — publish at least this often
  double max_age = 9;        // cached-poll freshness threshold

  // Explicit COV subscription on this point (COV snooping happens
  // regardless; this opts into an explicit SubscribeCOV RPC).
  bool cov_subscribe = 10;
  int32 cov_lifetime = 11;   // COV subscription lifetime in seconds
}

// Per-device BACnet driver configuration. One row in edge.config
// under driver.bacnet.{device_id}. The driver entrypoint loads all
// keys with that prefix on startup and hot-reloads on changes.
message BacnetDriverConfig {
  // ULID device handle (26-char Crockford-base32). Replaces the
  // pre-parity int BACnet-device-instance overload — uniform with
  // Modbus + EtherNet/IP, decoupled from the BACnet bus's native
  // numbering. Operators reach a device by its BACnet instance
  // through `bacnet_device_instance` (field 22) plus the
  // network_attachment_id; the wire / KV key uses this string.
  // Wire-breaking change vs pre-v0.18 schema — see
  // adr-device-id-handle-vs-identity.md.
  string device_id = 1;

  // Human-readable label (rendered in TUI / UI).
  string device_name = 2;

  // "ip:port" for BACnet/IP devices; "net:mac" for routed MSTP devices.
  string device_address = 3;

  // Whether to use ReadPropertyMultiple for batched reads.
  bool use_read_multiple = 4;

  // Broadcast address override — defaults to 255.255.255.255 when unset.
  // Marked `optional` for symmetry with `router` (field 8) per
  // Aerodrome review consistency note.
  optional string broadcast_address = 5;

  // Bind port. 0 = auto (pick an ephemeral).
  int32 bacnet_port = 6;

  int32 bacnet_prefix = 7;

  // Upstream router for routed devices, formatted "ip:port". Absent
  // means the device is directly reachable on the edge's local subnet.
  optional string router = 8;

  // Discovery cache lifetime + jitter (seconds, float).
  double discovery_cache_ttl = 9;
  double discovery_jitter = 10;

  // OT monitoring eligibility. Absent or 0 = not monitored. Positive
  // int = synthetic probe cadence in seconds.
  optional int32 monitoring_interval = 11;

  // Shared timeout for reads/writes (seconds, int).
  int32 timeout_seconds = 12;

  // Operator handle into BacnetNetworkConfig.id. Defaults to "default"
  // for zero-config deployments — ace-driver mints a "default"
  // attachment on first boot per multi-network-bacnet.md §6.
  string network_id = 20;

  // Durable cloud correlation UUID, resolved from BacnetNetworkConfig
  // at config-bridge write time (v0.35.0+) or at driver-load time for
  // legacy records (v0.32.0+). MUST be populated before the driver
  // emits any reachability event — proto can't encode that, but
  // ace_edge.entrypoints.driver._load_configs will emit a
  // DriverConfigError with reason="unknown_network" if it isn't
  // resolvable at load time.
  //
  // Unset on pre-v0.28 legacy records; the edge's in-memory resolver
  // fills it during load.
  optional string network_attachment_id = 21;

  // driver_type is implicit from the message type itself; no field
  // needed here (modbus, ethernet_ip get their own messages). The
  // Pydantic BaseDriverConfig also carries a DriverType enum for the
  // polymorphic KV codepath — that's a Python-side dispatch concern
  // that doesn't belong in the wire schema.

  // BACnet device-instance number (1..=4_194_303 per spec).
  // Aerodrome stores this in canonical_device.locator_metadata
  // and surfaces it on the UI for operators searching by their
  // native BACnet identifier. Distinct from `device_id` above,
  // which is the cloud-allocated ULID handle.
  optional uint32 bacnet_device_instance = 22;
}
