// Zone transport layer - gRPC services and messages.
//
// This proto file defines the gRPC services and messages for:
// 1. ZoneTransportService - Internal node-to-node Raft consensus
// 2. ZoneApiService - Client-facing API for zone operations

syntax = "proto3";

package nexus.raft;

import "nexus/raft/commands.proto";

// === Services ===

// ZoneTransportService handles Raft consensus protocol messages.
//
// This service is used for communication between Raft nodes (leaders, followers, witnesses).
service ZoneTransportService {
  // StepMessage forwards a raw raft-rs eraftpb::Message between nodes.
  // The bytes are opaque to the transport layer — serialized using protobuf v2
  // (raft-rs's native format). This is the standard pattern (etcd, tikv) for
  // routing all raft-rs internal message types (~15 types) through a single RPC.
  rpc StepMessage(StepMessageRequest) returns (StepMessageResponse);

  // ReplicateEntries sends EC (eventually consistent) entries to a peer.
  // This is NOT Raft consensus — EC writes bypass Raft and use async replication.
  // Used by the background replication loop (Phase C) for peer data sync.
  // Entries are idempotent — receivers skip already-applied sequence numbers.
  rpc ReplicateEntries(ReplicateEntriesRequest) returns (ReplicateEntriesResponse);
}

// ZoneApiService provides the client-facing API for zone operations.
//
// This service is used by clients (Python, CLI, etc.) to interact with the Raft cluster.
// It differs from ZoneTransportService which is for internal node-to-node communication.
//
// Architecture:
// - Propose: Write operations -> goes through Raft log -> applied to state machine
// - Query: Read operations -> served directly from state machine (any node)
//
// Leader handling:
// - Propose: If not leader, returns leader address for client to redirect
// - Query: Can be served by any node (eventual consistency) or redirected to leader (strong consistency)
service ZoneApiService {
  // Propose submits a command to be replicated through Raft.
  // Only the leader can accept proposals; followers return a redirect.
  rpc Propose(ProposeRequest) returns (ProposeResponse);

  // Query reads from the local state machine without going through Raft.
  // Can be served by any node. Use read_from_leader=true for linearizable reads.
  rpc Query(QueryRequest) returns (QueryResponse);

  // GetClusterInfo returns information about the Raft cluster.
  rpc GetClusterInfo(GetClusterInfoRequest) returns (GetClusterInfoResponse);

  // JoinZone requests that this node add a new node to a zone's Raft group.
  // Must be sent to the leader; followers return a redirect with leader_address.
  // This is a one-time setup operation — after joining, the new node receives
  // a snapshot from the leader and becomes an equal Voter with a local sled replica.
  rpc JoinZone(JoinZoneRequest) returns (JoinZoneResponse);

  // DeleteZone removes this node's local replica for a zone.
  //
  // Internal maintenance RPC used by federation_remove_zone fan-out. This is
  // intentionally local-only: the caller coordinates peer fan-out, while the
  // receiver only shuts down its in-memory raft group and deletes its on-disk
  // replica. It is idempotent so retries and already-cleaned peers are safe.
  rpc DeleteZone(DeleteZoneRequest) returns (DeleteZoneResponse);

  // JoinCluster provisions a new node with cluster TLS certificates.
  // Called by `nexus join` before `nexus serve`. The caller authenticates
  // with the join token password; the server signs a node cert and returns
  // it along with the CA cert. CA key never leaves node-1 (security fix).
  // K3s-style bootstrap: token = shared secret, TLS encrypts the channel.
  rpc JoinCluster(JoinClusterRequest) returns (JoinClusterResponse);

  // GetSearchCapabilities returns the search modes available on a zone (Issue #3147).
  // Used by FederatedSearchDispatcher to route queries to zones that can handle them.
  rpc GetSearchCapabilities(GetSearchCapabilitiesRequest) returns (SearchCapabilities);

  // ReadBlob serves a CAS blob by content hash — driver-to-driver protocol.
  // R20.18.7: inter-node only, co-located with ZoneApiService on the raft
  // port so it inherits the cluster's mTLS + join-token authentication.
  // Used by follower `try_remote_fetch` / scatter-gather chunk fetch when
  // metadata has been replicated ahead of content.
  rpc ReadBlob(ReadBlobRequest) returns (ReadBlobResponse);
}

// === Search Capability Advertisement (Issue #3147) ===

message GetSearchCapabilitiesRequest {
  string zone_id = 1;
}

message SearchCapabilities {
  string zone_id = 1;
  string device_tier = 2;            // "phone", "laptop", "server"
  repeated string search_modes = 3;  // "keyword", "semantic", "hybrid"
  string embedding_model = 4;
  int32 embedding_dimensions = 5;
  bool has_graph = 6;
}

// === Client Messages ===

// ProposeRequest submits a command to be replicated through Raft.
message ProposeRequest {
  RaftCommand command = 1;
  string request_id = 2;
  // Zone ID for multi-zone routing. Required in multi-zone mode.
  string zone_id = 3;
  // Internal forwarding: bincode-serialized Command bytes.
  // When set, the server deserializes directly (skipping proto→internal conversion).
  // Used by ZoneConsensus::propose() leader forwarding — avoids double serialization.
  bytes raw_command = 4;
  // Set by follower when forwarding. Prevents infinite forwarding loops
  // (if leader_hint is stale and target is also a follower).
  bool forwarded = 5;
}

// ProposeResponse is the response to a Propose RPC.
message ProposeResponse {
  bool success = 1;
  optional string error = 2;
  optional string leader_address = 3;
  optional RaftResponse result = 4;
  uint64 applied_index = 5;
}

// QueryRequest reads from the local state machine.
message QueryRequest {
  RaftQuery query = 1;
  bool read_from_leader = 2;
  // Zone ID for multi-zone routing. Required in multi-zone mode.
  string zone_id = 3;
}

// QueryResponse is the response to a Query RPC.
message QueryResponse {
  bool success = 1;
  optional string error = 2;
  optional string leader_address = 3;
  optional RaftQueryResponse result = 4;
}

// GetClusterInfoRequest requests cluster information.
message GetClusterInfoRequest {
  // Zone ID for multi-zone routing. Required in multi-zone mode.
  string zone_id = 1;
}

// GetClusterInfoResponse returns cluster information.
message GetClusterInfoResponse {
  uint64 node_id = 1;
  uint64 leader_id = 2;
  uint64 term = 3;
  ClusterConfig config = 4;
  bool is_leader = 5;
  optional string leader_address = 6;
}

// JoinZoneRequest is sent by a new node to an existing zone member.
// The leader will propose a ConfChange(AddVoter) for the joining node,
// or ConfChange(AddLearnerNode) when as_learner is true.
message JoinZoneRequest {
  // Zone to join.
  string zone_id = 1;
  // The joining node's Raft ID.
  uint64 node_id = 2;
  // The joining node's gRPC address (e.g., "http://10.0.0.3:2126").
  string node_address = 3;
  // When true, leader proposes AddLearnerNode instead of AddNode.
  // Learners receive log replication but do not count toward quorum.
  bool as_learner = 4;
}

// JoinZoneResponse is returned after the ConfChange is committed.
message JoinZoneResponse {
  bool success = 1;
  optional string error = 2;
  // If not leader, redirect to leader.
  optional string leader_address = 3;
  // Current cluster config so the joiner knows all peers.
  optional ClusterConfig config = 4;
}

// DeleteZoneRequest asks a peer to remove its local zone replica.
message DeleteZoneRequest {
  string zone_id = 1;
  // Reserved for future policy checks. The current maintenance RPC is local
  // and idempotent; high-level mounted-zone safety is enforced by the caller.
  bool force = 2;
}

// DeleteZoneResponse reports local replica deletion.
message DeleteZoneResponse {
  bool success = 1;
  optional string error = 2;
}

// JoinClusterRequest is sent during TLS bootstrap to provision certificates.
// The caller authenticates with a join token password (K3s-style).
// The server signs a node certificate — CA key never leaves the leader.
message JoinClusterRequest {
  // Join token password (64-char hex string).
  string password = 1;
  // The joining node's Raft ID.
  uint64 node_id = 2;
  // The joining node's advertise address (e.g., "http://10.0.0.3:2126").
  string node_address = 3;
  // Zone ID for the node certificate CN (e.g., "root").
  string zone_id = 4;
  // Reserved: was peers_bootstrap (removed — only token-based auth supported).
  reserved 5;
  reserved "peers_bootstrap";
}

// JoinClusterResponse returns cluster TLS material on success.
// The server signs the node cert — CA key never leaves node-1.
message JoinClusterResponse {
  bool success = 1;
  optional string error = 2;
  // Cluster CA certificate (PEM-encoded). Used to verify peer certs.
  bytes ca_pem = 3;
  // Reserved: was ca_key_pem — removed for security (CA key stays on node-1).
  reserved 4;
  reserved "ca_key_pem";
  // Server-signed node certificate (PEM-encoded).
  bytes node_cert_pem = 5;
  // Node private key (PEM-encoded). Generated server-side for the joiner.
  bytes node_key_pem = 6;
}

// === Cluster Configuration ===

// ClusterConfig describes the cluster membership.
message ClusterConfig {
  repeated NodeInfo voters = 1;
  repeated NodeInfo learners = 2;
  repeated NodeInfo witnesses = 3;
}

// NodeInfo describes a node in the cluster.
message NodeInfo {
  uint64 id = 1;
  string address = 2;
  NodeRole role = 3;
}

// === Enums ===

// NodeRole indicates the role of a node.
enum NodeRole {
  ROLE_VOTER = 0;
  ROLE_LEARNER = 1;
  ROLE_WITNESS = 2;
}

// === StepMessage (Raw Raft Message Transport) ===

// StepMessageRequest carries a serialized eraftpb::Message.
// The bytes are opaque to the transport layer.
message StepMessageRequest {
  bytes message = 1;
  // Zone ID for multi-zone routing. Required in multi-zone mode.
  string zone_id = 2;
  // Sender's advertise address (e.g., "http://10.0.0.3:2126").  The
  // receiver updates its transport peer map with
  // `(message.from_id -> sender_address)` so the network is the SSOT
  // for routing — restart, DHCP changes, and post-failover leaders
  // converge naturally as messages arrive.  Optional: empty string
  // is treated as "sender did not advertise" and the receiver falls
  // back to whatever peer-map entry already exists.
  string sender_address = 3;
}

// StepMessageResponse acknowledges receipt.
message StepMessageResponse {
  bool success = 1;
  optional string error = 2;
}

// === EC Replication Messages (Phase C) ===

// ECReplicationEntry is a single entry from the EC replication WAL.
// Sent from the writer node to peers for async replication.
message ECReplicationEntry {
  // Sequence number in the writer's replication log (WriteToken).
  uint64 seq = 1;
  // Serialized Command bytes (same format as Raft log entries).
  bytes command = 2;
  // Wall clock timestamp (Unix seconds) for LWW conflict resolution (Phase D).
  uint64 timestamp = 3;
  // Writer node ID for LWW tie-breaking (Phase D).
  uint64 node_id = 4;
}

// ReplicateEntriesRequest sends a batch of EC entries to a peer.
message ReplicateEntriesRequest {
  // Zone this replication belongs to.
  string zone_id = 1;
  // Batch of EC entries to replicate.
  repeated ECReplicationEntry entries = 2;
  // The sender's node ID.
  uint64 sender_node_id = 3;
}

// ReplicateEntriesResponse acknowledges receipt and application.
message ReplicateEntriesResponse {
  bool success = 1;
  optional string error = 2;
  // Highest sequence number successfully applied by the receiver.
  uint64 applied_up_to = 3;
}

// === ReadBlob (R20.18.7 — driver-to-driver CAS fetch) ===

// ReadBlobRequest asks a peer for the blob bytes.
//
// Two addressing modes:
//   - `content_hash` set, `path` empty → CAS lookup by hash on the peer's
//     local backends. Used by chunk fetch and CAS-backed federation.
//   - `path` set → drive the peer's VFSRouter at that global path, so PAS
//     mounts (PathLocalBackend, S3Backend, …) can serve cross-node reads.
//     `content_hash` is ignored when `path` is non-empty.
//
// Internal RPC; authentication is the transport's mTLS (client cert signed
// by the cluster CA), so no explicit auth field is carried in the body.
message ReadBlobRequest {
  // Reserved: pre-store-and-forward dual-mode shape (content_hash + path).
  // Removed in favor of a single ``content_id`` so the kernel never has to
  // pick CAS-vs-PAS dispatch — peer self-routes through its own
  // ``VFSRouter`` just like a local ``sys_read``.
  reserved 1, 2;
  reserved "content_hash", "path";

  // Backend-defined opaque content identifier — the same string the peer
  // would have stored in its local ``FileMetadata.content_id`` slot at
  // write time. For federation reads this carries a global VFS path
  // (``/<zone>/<...>``); the peer's ``BlobFetcher::read`` impl drives its
  // local kernel through the standard read path so each backend interprets
  // ``content_id`` in its own way (CAS=hash, PAS=backend_path, etc.).
  // The caller's kernel never inspects this field.
  string content_id = 3;
}

// ReadBlobResponse returns either the blob bytes or a non-empty `error`.
message ReadBlobResponse {
  bytes content = 1;
  string error = 2;
}
