syntax = "proto3";

// Package scrabble.botlink.v1 is the reverse control channel between a remote
// Telegram bot and the gateway. The bot dials the gateway and opens a single
// long-lived Link stream (mTLS); once the stream is open the gateway pushes send
// Commands down it and the bot returns one Ack per command. This keeps the bot
// egress (the Bot API token, getUpdates long-poll and sendMessage) off the main
// host with no inbound port on the bot. See docs/ARCHITECTURE.md.
package scrabble.botlink.v1;

option go_package = "scrabble/pkg/proto/botlink/v1;botlinkv1";

import "telegram/v1/telegram.proto";

// BotLink is the reverse (bot-dials-gateway) control channel. The bot is the gRPC
// client; once the stream is open the gateway (server) sends Commands at will and
// the bot returns one Ack per command. Delivery is best-effort, at-most-once: a
// command lost across a reconnect is not replayed.
service BotLink {
  // Link opens the single bot <-> gateway stream. The first client message is
  // Hello; thereafter the client sends one Ack per received Command.
  rpc Link(stream FromBot) returns (stream ToBot);

  // ResolveChatEligibility answers whether the Telegram user identified by
  // external_id may write in the moderated discussion chat: registered with an
  // account and neither admin-suspended nor chat-muted. The bot calls it over the
  // same mTLS channel when a user joins the chat, to decide whether to grant the
  // write permission. Delivery of the answer is request/response (not best-effort).
  rpc ResolveChatEligibility(ChatEligibilityRequest) returns (ChatEligibilityResponse);

  // ValidatePreCheckout answers whether a Telegram Stars pre_checkout_query may be
  // approved before any star is charged: the order in the invoice payload exists, is
  // still creditable (pending or an honoured-expired order, never one already paid),
  // and its amount and currency match the invoice. The bot calls it on every
  // pre_checkout_query and approves only on ok; a not-ok answer or a channel failure
  // declines the charge (fail-closed). Reusable Stars invoice links make this gate the
  // one place a repeat payment is stopped before money moves. Request/response.
  rpc ValidatePreCheckout(PreCheckoutRequest) returns (PreCheckoutResponse);

  // ForwardPayment delivers a completed Telegram Stars payment from the bot's durable
  // outbox to the gateway for crediting. The bot calls it (retrying until it gets a
  // response) after Telegram confirms the payment; the gateway forwards it to the
  // backend intake, which credits the order once, idempotent on
  // telegram_payment_charge_id. A response means the payment was durably handled
  // (credited, or recorded as unmatched) and the bot may forget the outbox row; a
  // transport error leaves the row for a later retry. Request/response.
  rpc ForwardPayment(ForwardPaymentRequest) returns (ForwardPaymentResponse);
}

// FromBot is a message the bot sends to the gateway: the opening Hello, then one
// Ack per Command and a periodic Health report.
message FromBot {
  oneof msg {
    Hello hello = 1;
    Ack ack = 2;
    Health health = 3;
  }
}

// Health is a periodic report the bot pushes up the stream so the gateway can expose the remote
// bot's Bot-API health as its own metrics — the bot exports no telemetry of its own (otelcol lives
// on the main host, unreachable from the bot), so the bot-link is its only channel out. The three
// counters are DELTAS since the previous Health: the gateway adds them to cumulative counters, so a
// report lost across a reconnect only undercounts, never corrupts. last_ok_unix is the wall-clock
// second of the bot's most recent successful Bot API response (any call, including the getUpdates
// long-poll that returns every poll cycle even when idle) — an absolute liveness stamp the gateway
// surfaces as a gauge, so a silently stalled bot (no errors, no traffic) is still caught.
message Health {
  uint64 connect_failures = 1;  // getUpdates long-poll failures (transport / 5xx) since the last report
  uint64 api_errors = 2;        // other Bot API failures (transport / 5xx) since the last report
  uint64 rate_limited = 3;      // Bot API 429 responses since the last report (should stay ~0)
  int64 last_ok_unix = 4;       // unix seconds of the last successful Bot API response (0 = none yet)
}

// ToBot is a message the gateway sends to the bot. Only Command is carried today.
message ToBot {
  Command command = 1;
}

// Hello registers the bot on connect. instance_id identifies the bot process for
// gateway-side logging and metrics; owns_updates reports whether this bot runs the
// exclusive getUpdates long-poll (exactly one bot must, else Telegram returns 409).
message Hello {
  string instance_id = 1;
  bool owns_updates = 2;
}

// Command is one send instruction addressed by command_id, which the bot echoes in
// its Ack. Exactly one payload is set; the payloads reuse the connector request
// shapes from scrabble.telegram.v1.
message Command {
  string command_id = 1;
  oneof payload {
    scrabble.telegram.v1.NotifyRequest notify = 2;
    scrabble.telegram.v1.SendToUserRequest send_to_user = 3;
    scrabble.telegram.v1.SendToGameChannelRequest send_to_channel = 4;
    ChatGateCommand chat_gate = 5;
    CreateInvoiceCommand create_invoice = 6;
  }
}

// Ack reports the outcome of the Command with command_id. delivered mirrors the
// connector delivery semantics (false when the kind is not rendered out-of-app, the
// user never started the bot, or no channel is configured); error carries an
// unexpected transport/render failure, distinct from a clean not-delivered. result
// carries a command's return value when it has one (the created invoice link for a
// create_invoice command); it is empty otherwise.
message Ack {
  string command_id = 1;
  bool delivered = 2;
  string error = 3;
  string result = 4;
}

// ChatGateCommand sets a Telegram user's write access in the moderated discussion
// chat. external_id is the user's Telegram identity (as in the backend identities
// table); allow grants the right to write when true and revokes it when false. The
// bot applies it only to a user currently in the chat — it guards on getChatMember,
// so a command for an absent user is a no-op. The gateway emits one whenever the
// user's eligibility may have changed: an admin block or unblock, a chat_muted
// grant or revoke, or a temporary block lapsing.
message ChatGateCommand {
  string external_id = 1;
  bool allow = 2;
}

// ChatEligibilityRequest asks whether the Telegram user identified by external_id
// may write in the moderated discussion chat.
message ChatEligibilityRequest {
  string external_id = 1;
}

// ChatEligibilityResponse is the eligibility answer. registered reports whether the
// external_id maps to an account at all; eligible is the final gate the bot acts on
// (registered and neither admin-suspended nor chat-muted).
message ChatEligibilityResponse {
  bool registered = 1;
  bool eligible = 2;
}

// CreateInvoiceCommand asks the bot to mint a Telegram Stars invoice link for a
// pending order (createInvoiceLink in XTR). payload is the order id, which Telegram
// echoes back in the pre_checkout_query and the successful_payment; amount is the
// price in whole stars; title and description are shown on the invoice. The bot
// returns the link in its Ack result.
message CreateInvoiceCommand {
  string title = 1;
  string description = 2;
  string payload = 3;
  int64 amount = 4;
}

// PreCheckoutRequest asks whether a Stars pre_checkout_query for order_id at amount
// (whole stars) in currency may be approved before the charge.
message PreCheckoutRequest {
  string order_id = 1;
  int64 amount = 2;
  string currency = 3;
}

// PreCheckoutResponse is the approval answer. ok approves the charge; reason carries a
// short user-facing decline message when ok is false.
message PreCheckoutResponse {
  bool ok = 1;
  string reason = 2;
}

// ForwardPaymentRequest carries a completed Stars payment: order_id from the invoice
// payload, the Telegram charge id (the idempotency key), the amount in whole stars,
// and the payer's Telegram user id.
message ForwardPaymentRequest {
  string order_id = 1;
  string telegram_payment_charge_id = 2;
  int64 amount = 3;
  int64 telegram_user_id = 4;
}

// ForwardPaymentResponse reports the durable outcome. credited is true when the order
// was credited (or already had been); false means the payment was recorded but could
// not be matched to a creditable order (an operator follows up). Either way the bot
// may forget the outbox row — only a transport error triggers a retry.
message ForwardPaymentResponse {
  bool credited = 1;
}
