syntax = "proto3";

package flare.common.v1;

import "conversation.proto";
import "errors.proto";
import "event.proto";
import "message.proto";

// =============================================================================
// Sync BC - 会话与消息同步（增量为主，一会话一流）
// =============================================================================
// 对齐 im_sync_design.md / im_sync_best_practice.md：
// - 初始化同步 = Snapshot（状态） + Event（变化） + ACK（可靠性）
// - 事件分级：强一致回放 / 可聚合 / 不参与历史
// - 断线续传：seq 缺口 → 恢复建议（重拉快照或续拉事件）
// - 幂等：客户端丢弃 seq <= lastApplied 的事件
//
// 上行 Sync（kind + payload）；下行 SyncRes（status + payload）。
// user_id 由服务端从认证上下文注入，勿在报文重复传递。
//
// 会话读模型类型见 common/conversation.proto（ConversationSummary、ConversationDetail、ConversationPatch 等）。

// -----------------------------------------------------------------------------
// 事件分级策略（best_practice §3）— 供客户端过滤、服务端文档与监控对齐
// -----------------------------------------------------------------------------

/// 事件在「历史同步 / 断线续传」中的参与方式（与 EventType 正交：同一 Type 也可由服务端标不同策略）
enum SyncEventReplayTier {
  SYNC_EVENT_REPLAY_TIER_UNSPECIFIED = 0;
  /// 必须参与回放：影响最终状态（撤回/编辑/删除/会话变更/置顶标记等）
  SYNC_EVENT_REPLAY_TIER_CRITICAL = 1;
  /// 可不逐条回放：服务端聚合为读模型最终态（已读、反应等）
  SYNC_EVENT_REPLAY_TIER_AGGREGATABLE = 2;
  /// 不参与历史同步：登录后靠订阅（输入中、在线、通话信令等）
  SYNC_EVENT_REPLAY_TIER_EPHEMERAL = 3;
}

/// 拉事件流时的预设，减少客户端漏配 EventType（与 QueryEventsSync.event_types 二选一或叠加由实现约定）
enum EventReplayPreset {
  EVENT_REPLAY_PRESET_UNSPECIFIED = 0;
  /// 仅强一致关键事件（默认安全路径）
  EVENT_REPLAY_PRESET_CRITICAL_ONLY = 1;
  /// 含可聚合类（服务端可返回聚合结果而非全量事件）
  EVENT_REPLAY_PRESET_INCLUDE_AGGREGATABLE = 2;
  /// 全部持久化事件（慎用，体积大）
  EVENT_REPLAY_PRESET_ALL_PERSISTED = 3;
}

/// 服务端对客户端的恢复建议（design §七：seq 过期 / 缺口时重拉快照）
enum SyncRecoveryHint {
  SYNC_RECOVERY_HINT_NONE = 0;
  /// 重新走冷启动快照 + 事件
  SYNC_RECOVERY_HINT_REFETCH_SNAPSHOT = 1;
  /// 仅从当前事件游标续拉（快照仍有效）
  SYNC_RECOVERY_HINT_REPLAY_EVENTS = 2;
  /// 重置单会话同步状态
  SYNC_RECOVERY_HINT_RESYNC_CONVERSATION = 3;
}

// -----------------------------------------------------------------------------
// 会话级双游标：消息可见水位 + 关键事件回放水位（design：快照 vs 事件日志）
// -----------------------------------------------------------------------------

message SyncConversationCheckpoint {
  string conversation_id = 1;
  /// 已应用到本地的最大 conversation_seq（持久化 replay 水位）
  uint64 last_conversation_seq = 2;
  /// 已见的最后一条 message_seq（消息创建排序辅助水位）
  uint64 last_message_seq = 3;
  int64 updated_at = 4;
}

/// seq 缺口 / 快照过期时的结构化上下文（便于客户端埋点与文案）
message SyncStaleContext {
  string conversation_id = 1;
  /// 客户端上报的 conversation_seq
  uint64 client_reported_conversation_seq = 2;
  /// 服务端可提供的最早可用 conversation_seq（若未知则为 0）
  uint64 server_earliest_available_conversation_seq = 3;
  SyncRecoveryHint recommended_action = 4;
}

/// 会话摘要读模型版本提示，用于大群 notify+pull 场景下的摘要差异对账
message ConversationVersion {
  string conversation_id = 1;
  uint64 version = 2;
  uint64 max_conversation_seq = 3;
  int64 updated_at = 4;
}

/// 下行通用提示：恢复策略、国际化、分批渲染建议（best_practice §六 batchApply）
message SyncSessionHints {
  SyncRecoveryHint recovery_hint = 1;
  string localization_key = 2;
  map<string, string> localization_params = 3;
  /// 建议每批 apply 事件条数（如 50），0 表示由客户端默认
  int32 suggested_event_apply_batch_size = 4;
  /// 会话内持久化 replay 水位（与服务端读模型对账）
  uint64 server_max_conversation_seq = 5;
  /// 事件流水位（与 EventEnvelope.max_conversation_seq 对齐，供客户端检测缺口）
  uint64 server_replay_max_conversation_seq = 6;
  /// 用户级可见状态版本；客户端相等时可跳过跨会话拉取。
  uint64 server_user_version = 7;
  /// 当客户端 user_version 落后时，服务端可下发需要二级会话同步的 conversation_id。
  repeated string changed_conversation_ids = 8;
  /// true 表示变更索引窗口已截断，客户端应回退到全量/快照同步。
  bool user_version_index_truncated = 9;
  /// 服务端检测到比客户端已知版本更新的会话摘要。
  repeated ConversationVersion conversation_versions = 10;
}

// -----------------------------------------------------------------------------
// 事件流 ACK（design §四 ACK + EventEnvelope.window_id）
// -----------------------------------------------------------------------------

message EventStreamAckSync {
  string conversation_id = 1;
  /// 对应 EventEnvelope.window_id，确认本批已落盘/可丢弃重传
  string window_id = 2;
  /// 已应用到本地的最大 conversation_seq（幂等：后续仅处理 > 此值的事件）
  uint64 applied_through_conversation_seq = 3;
  string device_id = 4;
}

message EventStreamAckSyncRes {}

// -----------------------------------------------------------------------------
// 编排快照 / 会话水位（原 sync.proto 独立 RPC 收敛至此，便于网关 ExecuteSync 直转）
// -----------------------------------------------------------------------------

message SnapshotConversationRow {
  string conversation_id = 1;
  repeated Message messages = 2;
  uint64 last_conversation_seq = 3;
  int64 last_message_at = 4;
  int32 unread_count = 5;
  /// 当前用户在会话内的已读 seq（来自 conversation_participants.last_read_seq）
  uint64 last_read_seq = 6;
  /// 完整会话摘要（冷启 bootstrap bundle：一次 RPC 同时携带摘要+首页消息，免二次拉取）
  ConversationSummary summary = 7;
}

message SyncSnapshotSync {
  repeated string conversation_ids = 1;
  int32 messages_per_conversation = 2;
  bool include_deleted = 3;
  bool include_conversations = 4;
  string snapshot_cursor = 5;
  /// true=按活跃度降序（冷启首屏 top-N 先到）；false=按 (ms,cid) 升序（增量续拉稳定翻页）
  bool newest_first = 6;
  /// 每页返回的会话行数（与 messages_per_conversation 正交；0=服务端默认）
  int32 conversation_page_limit = 7;
}

message SyncSnapshotSyncRes {
  repeated SnapshotConversationRow conversations = 1;
  uint64 snapshot_version = 2;
  int64 snapshot_at = 3;
  string next_cursor = 4;
  bool has_more = 5;
}

message ConversationMaxSeqSync {
  string conversation_id = 1;
}

message ConversationMaxSeqSyncRes {
  uint64 max_conversation_seq = 1;
  int64 last_event_at = 2;
  string last_message_id = 3;
}

message Sync {
  string device_id = 2;
  oneof payload {
    SingleConversationSync single_conversation = 10;
    MultiConversationSync multi_conversation = 11;
    ConversationsIncrementalSync conversations_incremental = 12;
    ConversationsAllSync conversations_all = 13;
    ConversationDetailSync conversation_detail = 14;
    QueryEventsSync query_events = 16;
    GetSyncCursorSync get_sync_cursor = 17;
    UpdateSyncCursorSync update_sync_cursor = 18;
    EventStreamAckSync event_stream_ack = 19;
    SyncSnapshotSync sync_snapshot = 20;
    ConversationMaxSeqSync conversation_max_seq = 21;
    ConversationsSync conversations = 22;
    ConversationParticipantsSync conversation_participants = 23;
    ConversationUserSettingsSync conversation_user_settings = 24;
    // 显式建群：客户端在 get_group_by_user_ids 时一次性把成员表交给服务端建群，
    // 之后消息不再携带整张成员表（避免超大群建群首条消息超 NATS 单消息上限）。
    EnsureConversationSync ensure_conversation = 25;
  }
}

message SingleConversationSync {
  string conversation_id = 1;
  uint64 after_conversation_seq = 2;
  string cursor = 3;
  int32 limit = 4;
  /// 历史回溯：>0 时返回严格小于该 seq 的最近 limit 条（服务端按 seq 降序取页、升序返回）。
  /// 与 after_conversation_seq / cursor 互斥；方向显式化，替代曾经的 "before:" 字符串游标约定。
  uint64 before_conversation_seq = 5;
}

message MultiConversationSync {
  repeated string conversation_ids = 1;
  map<string, uint64> last_conversation_seq_per_conversation = 2;
  int32 limit_per_conversation = 3;
}

message ConversationsIncrementalSync {
  string cursor = 1;
  int32 limit = 2;
}

message ConversationsAllSync {
  ConversationSyncAllOptions sync_options = 1;
}

message ConversationDetailSync {
  string conversation_id = 1;
}

/// 会话列表快照同步（编排层走 SyncSnapshot；与增量补丁 ConversationsIncrementalSync 并存）
message ConversationsSync {
  string cursor = 1;
  int32 limit = 2;
  bool include_deleted = 3;
}

message ConversationParticipantsSync {
  string conversation_id = 1;
  string cursor = 2;
  int32 limit = 3;
  bool include_removed = 4;
  /// 与 ConversationSummary.participant_version 相等且 cursor 为空时可返回空 participants（304 语义）
  uint64 known_participant_version = 5;
}

message QueryEventsSync {
  string conversation_id = 1; // 为空表示由实现约定（全量或拒绝）
  uint64 after_conversation_seq = 2;
  uint64 before_conversation_seq = 3;
  int32 limit = 4;
  repeated EventType event_types = 5;
  bool include_deleted = 6;
  /// 事件回放预设（与 event_types 并存时：以服务端合并策略为准）
  EventReplayPreset replay_preset = 7;
  /// 客户端自报的「已应用 conversation_seq」，服务端可返回 REFETCH_SNAPSHOT（design 幂等与缺口）
  uint64 client_last_applied_conversation_seq = 8;
}

message GetSyncCursorSync {
  string device_id = 2;
  string conversation_id = 3;
  repeated ConversationVersion known_conversation_versions = 4;
}

message UpdateSyncCursorSync { MultiDeviceCursor cursor = 1; }

message ConversationUserSettingsSync {
  string conversation_id = 1;
  optional bool is_pinned = 2;
  optional bool is_muted = 3;
  optional bool is_archived = 4;
  optional string draft = 5;
  uint64 base_settings_version = 6;
}

// -----------------------------------------------------------------------------
// 下行：SyncRes
// -----------------------------------------------------------------------------

message SyncRes {
  oneof payload {
    SingleConversationSyncRes single_conversation = 10;
    MultiConversationSyncRes multi_conversation = 11;
    ConversationsIncrementalSyncRes conversations_incremental = 12;
    ConversationsAllSyncRes conversations_all = 13;
    ConversationDetailSyncRes conversation_detail = 14;
    QueryEventsSyncRes query_events = 16;
    GetSyncCursorSyncRes get_sync_cursor = 17;
    UpdateSyncCursorSyncRes update_sync_cursor = 18;
    EventStreamAckSyncRes event_stream_ack_res = 19;
    SyncSnapshotSyncRes sync_snapshot_res = 20;
    ConversationMaxSeqSyncRes conversation_max_seq_res = 21;
    ConversationsSyncRes conversations = 22;
    ConversationParticipantsSyncRes conversation_participants = 23;
    ConversationUserSettingsSyncRes conversation_user_settings = 24;
    EnsureConversationSyncRes ensure_conversation = 25;
  }
}

message SingleConversationSyncRes {
  string conversation_id = 1;
  repeated SyncSliceItem items = 2;
  uint64 max_conversation_seq = 3;
  string next_cursor = 4;
  bool has_more = 5;
  SyncSessionHints hints = 6;
  SyncStaleContext stale = 7;
}

message MultiConversationSyncRes {
  repeated ConversationSyncSlice slices = 1;
  map<string, uint64> max_conversation_seq_per_conversation = 2;
  bool has_more = 3;
  SyncSessionHints hints = 4;
}

message ConversationsIncrementalSyncRes {
  repeated ConversationPatch patches = 1;
  string next_cursor = 2;
  bool has_more = 3;
  SyncSessionHints hints = 4;
}

message ConversationsAllSyncRes {
  repeated ConversationSummary conversations = 1;
  string next_cursor = 2;
  uint64 server_max_conversation_seq = 3;
  map<string, string> attributes = 4;
  SyncSessionHints hints = 5;
}

message ConversationDetailSyncRes {
  ConversationDetail detail = 1;
  map<string, string> attributes = 2;
  SyncSessionHints hints = 3;
}

message QueryEventsSyncRes {
  EventEnvelope envelope = 1;
  SyncSessionHints hints = 2;
  SyncStaleContext stale = 3;
}

message GetSyncCursorSyncRes {
  MultiDeviceCursor cursor = 1;
  SyncSessionHints hints = 2;
}

message UpdateSyncCursorSyncRes {
  MultiDeviceCursor cursor = 1;
  SyncSessionHints hints = 2;
}

message ConversationUserSettingsSyncRes {
  ConversationUserSettings settings = 1;
}

/// 显式建群请求：携带完整成员表，服务端据此创建/确保会话存在（幂等）。
message EnsureConversationSync {
  string conversation_id = 1;
  int32 conversation_type = 2;
  string business_type = 3;
  string channel_id = 4;
  repeated string member_ids = 5;
}
message EnsureConversationSyncRes {
  string conversation_id = 1;
  bool ok = 2;
  string error = 3;
  uint64 member_count = 4;
}

message ConversationsSyncRes {
  repeated ConversationSummary conversations = 1;
  string next_cursor = 2;
  bool has_more = 3;
  SyncSessionHints hints = 4;
}

message ConversationParticipantsSyncRes {
  string conversation_id = 1;
  repeated ConversationParticipant participants = 2;
  string next_cursor = 3;
  bool has_more = 4;
  uint64 participant_version = 5;
  int32 member_count = 6;
}

// -----------------------------------------------------------------------------
// 下行结构复用
// -----------------------------------------------------------------------------

message SyncSkipItem {
  string reason = 1;
}

message SyncTombstoneItem {
  string tombstone_id = 1;
  optional string reason = 2;
}

message SyncSliceItem {
  uint64 conversation_seq = 1;
  int64 created_at = 2;
  oneof payload {
    Message message = 10;
    Event event = 11;
    SyncSkipItem skip = 12;
    SyncTombstoneItem tombstone = 13;
  }
}

message ConversationSyncSlice {
  string conversation_id = 1;
  repeated SyncSliceItem items = 2;
  uint64 max_conversation_seq = 3;
  string next_cursor = 4;
  bool has_more = 5;
}

enum ConversationPatchType {
  CONVERSATION_PATCH_UNSPECIFIED = 0;
  CONVERSATION_PATCH_LIGHT = 1;
  CONVERSATION_PATCH_SUMMARY = 2;
  CONVERSATION_PATCH_REMOVED = 3;
  CONVERSATION_PATCH_CREATED = 4;
  CONVERSATION_PATCH_DETAIL_REQUIRED = 5;
}

message ConversationPatch {
  string conversation_id = 1;
  ConversationPatchType patch_type = 2;
  ConversationSummary summary = 11;  // LIGHT / SUMMARY 均用本字段，LIGHT 可只填子集
  int64 patched_at = 13;
}

message ConversationSyncAllOptions {
  bool include_archived = 1;
  bool include_deleted = 2;
  int32 max_batch_size = 3;
}

// -----------------------------------------------------------------------------
// 多端游标：消息同步 seq + 关键事件回放 seq（与双游标设计对齐）
// -----------------------------------------------------------------------------

message MultiDeviceCursor {
  string device_id = 2;
  string conversation_id = 3;
  /// 已应用到本地的最大 conversation_seq
  uint64 last_conversation_seq = 4;
  int64 last_sync_at = 5;
  uint64 last_read_seq = 6;
  /// 已见的最后一条 message_seq（消息创建排序辅助水位）
  uint64 last_message_seq = 7;
}
