// Package pangolin.agent.v1 is the frozen control-plane <-> node-agent contract.
//
// CONNECTION DIRECTION: the node agent ALWAYS dials the control plane (never the
// reverse). Every RPC except Enroll requires a verified mTLS client certificate
// whose Subject CN equals the node UUID (see server/internal/mtls). Enroll is the
// single bootstrap exception, authenticated by a one-time bootstrap token instead.
//
// DATA-PLANE RED LINE (doc/06 §3, doc/04 §2): messages on this contract carry ONLY
// the opaque data-plane credential id (dp_uuid) and aggregate counters. They MUST
// NOT carry user_id, email, device id, destination address, SNI or any DNS query.
//
// NOTE ON CODE GENERATION: this repository has no protoc/buf toolchain wired yet
// (Makefile `generate` is a stub). Until task #5's proto pipeline lands, the Go
// types and gRPC stubs in server/internal/pb/agentv1 are hand-written to mirror
// this file 1:1 and are wire-encoded with a JSON gRPC codec. When protoc is
// introduced this file is the source of truth; field numbers are reserved here so
// the generated code stays compatible.
syntax = "proto3";

package pangolin.agent.v1;

option go_package = "github.com/wangjia/pangolin/server/internal/pb/agentv1;agentv1";

// AgentService is implemented by the control plane and consumed by node agents.
service AgentService {
  // Enroll exchanges a one-time bootstrap token + CSR for a node client
  // certificate (CN = node_uuid, 90d). Exempt from mTLS — this is the only RPC an
  // un-enrolled agent may call. Idempotent only within the token TTL.
  rpc Enroll(EnrollRequest) returns (EnrollResponse);

  // Register is called after (re)connecting over mTLS. It returns the authoritative
  // full ConfigSnapshot so the agent can converge local state.
  rpc Register(RegisterRequest) returns (ConfigSnapshot);

  // Heartbeat reports liveness + load every ~30s and the agent's local
  // config_version. The control plane flags need_full_resync when it detects drift.
  rpc Heartbeat(HeartbeatRequest) returns (HeartbeatResponse);

  // Subscribe is a server-streaming feed of Commands. The agent passes the last
  // command_id it durably processed so the control plane can resume without gaps
  // (at-least-once; commands are idempotent and Ack'd individually).
  rpc Subscribe(SubscribeRequest) returns (stream Command);

  // Ack confirms a command was applied. Safe to retry.
  rpc Ack(AckRequest) returns (AckResponse);

  // ReportUsage uploads per-dp_uuid aggregate byte/minute counters. No identities.
  rpc ReportUsage(UsageReport) returns (UsageAck);
}

// ─── enroll ──────────────────────────────────────────────────────────────────

message EnrollRequest {
  string bootstrap_token = 1; // one-time, injected via cloud-init
  bytes csr_pem = 2;          // PKCS#10 CSR, agent-generated key never leaves node
  string agent_version = 3;
}

message EnrollResponse {
  string node_uuid = 1;  // authoritative; becomes the cert CN
  bytes cert_pem = 2;    // signed leaf, 90d, EKU=clientAuth
  bytes ca_pem = 3;      // CA to pin for server validation
  int64 not_after_unix = 4;
}

// ─── register / config ─────────────────────────────────────────────────────────

message RegisterRequest {
  string node_uuid = 1;
  string agent_version = 2;
  int64 local_config_version = 3; // 0 on a fresh node
}

// Protocol selects which inbound(s) a credential is provisioned on.
enum Protocol {
  PROTOCOL_UNSPECIFIED = 0;
  PROTOCOL_REALITY = 1; // VLESS+REALITY inbound, user = {uuid, flow}
  PROTOCOL_HY2 = 2;     // Hysteria2 inbound, user = {password}
  PROTOCOL_BOTH = 3;    // both inbounds share the same dp_uuid
}

// Credential is the ONLY per-subscriber object a node ever sees.
message Credential {
  string dp_uuid = 1;          // opaque data-plane id; REALITY user uuid
  Protocol protocol = 2;
  string flow = 3;             // e.g. "xtls-rprx-vision"
  int64 expires_at_unix = 4;   // agent-side TTL; 0 = no expiry
}

// ConfigSnapshot is the full authoritative node state.
message ConfigSnapshot {
  int64 config_version = 1;
  repeated Credential credentials = 2;

  // Inbound parameters (node-wide, not per-user).
  RealityInbound reality = 3;
  Hy2Inbound hy2 = 4;

  // Baseline command id already reflected in this snapshot; the agent resumes
  // Subscribe from here.
  int64 last_command_id = 5;
}

message RealityInbound {
  int32 listen_port = 1;
  string private_key = 2; // REALITY server private key (base64)
  string short_id = 3;
  string server_name = 4; // masquerade SNI handshake target
  string handshake_server = 5;
  int32 handshake_port = 6;
}

message Hy2Inbound {
  int32 listen_port = 1;
  string masquerade = 2;
  string cert_path = 3;
  string key_path = 4;
}

// ─── heartbeat ───────────────────────────────────────────────────────────────

message HeartbeatRequest {
  string node_uuid = 1;
  int64 config_version = 2;
  int32 online_peers = 3;
  int64 bandwidth_up_bps = 4;
  int64 bandwidth_down_bps = 5;
  double cpu_percent = 6;
  int64 timestamp_unix = 7;
}

message HeartbeatResponse {
  bool need_full_resync = 1; // agent must Register + overwrite local state
  int64 server_time_unix = 2;
}

// ─── command stream ──────────────────────────────────────────────────────────

enum CommandType {
  COMMAND_TYPE_UNSPECIFIED = 0;
  COMMAND_TYPE_UPSERT = 1;            // add/update a credential
  COMMAND_TYPE_REVOKE = 2;            // remove a credential
  COMMAND_TYPE_ROTATE_CREDENTIAL = 3; // new replaces old, old kept until grace
  COMMAND_TYPE_APPLY_CONFIG = 4;      // replace inbound params / full snapshot
  COMMAND_TYPE_LIFECYCLE = 5;         // drain / resume / shutdown
}

enum LifecycleAction {
  LIFECYCLE_ACTION_UNSPECIFIED = 0;
  LIFECYCLE_ACTION_DRAIN = 1;
  LIFECYCLE_ACTION_RESUME = 2;
  LIFECYCLE_ACTION_SHUTDOWN = 3;
}

message Command {
  int64 command_id = 1; // monotonically increasing per node
  CommandType type = 2;

  UpsertPayload upsert = 3;
  RevokePayload revoke = 4;
  RotatePayload rotate = 5;
  ConfigSnapshot apply_config = 6;
  LifecyclePayload lifecycle = 7;
}

message UpsertPayload {
  Credential credential = 1;
}

message RevokePayload {
  string dp_uuid = 1;
}

message RotatePayload {
  string old_dp_uuid = 1;
  Credential new_credential = 2;
  int64 grace_until_unix = 3; // old credential kept active until this time
}

message LifecyclePayload {
  LifecycleAction action = 1;
}

message SubscribeRequest {
  string node_uuid = 1;
  int64 last_command_id = 2; // resume point; 0 = from the beginning
}

message AckRequest {
  string node_uuid = 1;
  int64 command_id = 2;
}

message AckResponse {}

// ─── usage ───────────────────────────────────────────────────────────────────

message UsageEntry {
  string dp_uuid = 1; // never a user id
  int64 bytes_up = 2;
  int64 bytes_down = 3;
  int64 session_minutes = 4;
}

message UsageReport {
  string node_uuid = 1;
  int64 window_start_unix = 2;
  int64 window_end_unix = 3;
  repeated UsageEntry entries = 4;
}

message UsageAck {}
