/**
 * df-relay 协议类型镜像(对齐 Rust crates/df-relay/src/)
 *
 * 类型证据:
 * - ClientKind / MessageKind:crates/df-relay/src/broadcast.rs:21-41
 *   (#[serde(rename_all = "snake_case")] → 'device'/'miniapp', 'event'/'command'/'control')
 * - BroadcastMessage:broadcast.rs:48-62(device_id/kind/source/from/payload/ts)
 * - Hello 握手帧:crates/df-relay/src/relay.rs:44-53(kind/device_id/token)
 *   kind 用 ClientKindWire(relay.rs:56-61),序列化为 'device'/'miniapp'
 * - 路由端点:relay.rs:177-183 `/ws/miniapp`(小程序专用端点)
 *
 * 关键协议点(relay.rs:332-365 handle_inbound_text):
 *   relay 不解析 payload —— miniapp 入站文本帧直接当 payload 包成 BroadcastMessage 路由给 device。
 *   故 miniapp 发送时,直接序列化 Command JSON(relay 自动包 payload),不需客户端构造 BroadcastMessage。
 *   接收时,device 事件经 relay 包成完整 BroadcastMessage 序列化下发,miniapp 需先解 BroadcastMessage
 *   再从 payload 取 AiChatEvent。
 */

/** 客户端类型(对齐 Rust ClientKind,snake_case 序列化,broadcast.rs:21-28) */
export type ClientKind = 'device' | 'miniapp'

/** 消息语义分类(对齐 Rust MessageKind,snake_case 序列化,broadcast.rs:35-41) */
export type MessageKind = 'event' | 'command' | 'control'

/** 在线状态(对齐 Rust PresenceState,snake_case 序列化,broadcast.rs:119-126) */
export type PresenceState = 'online' | 'offline'

/** ConnId 是 Rust u64(对齐 conn.rs ConnId struct),TS 用 number */
export type ConnId = number

/**
 * 中继路由消息骨架(对齐 Rust BroadcastMessage,broadcast.rs:48-62)
 *
 * miniapp 接收时 payload 为 AiChatEvent JSON(relay 透传不解析)。
 */
export interface BroadcastMessage {
  /** 配对绑定的设备 ID */
  device_id: string
  /** 消息语义 */
  kind: MessageKind
  /** 消息来源连接 ID(relay 分配,u64) */
  source: ConnId
  /** 来源客户端类型 */
  from: ClientKind
  /** 载荷(原始 JSON,relay 透传不解析 —— Event 时为 AiChatEvent) */
  payload: unknown
  /** 时间戳(ms) */
  ts: number
}

/**
 * 客户端首消息:身份宣告(对齐 Rust Hello,relay.rs:44-53)
 *
 * 小程序连 `/ws/miniapp` 后首帧必须发此结构,否则 relay 关连接(relay.rs:197-208)。
 * kind 恒为 'miniapp'(与端点匹配,relay.rs:212-225 校验,不匹配则关连接)。
 */
export interface Hello {
  /** 客户端类型,小程序恒 'miniapp'(对齐 ClientKindWire) */
  kind: 'miniapp' | 'device'
  /** 配对绑定的设备 ID(用户首次配置时生成) */
  device_id: string
  /** 配对 token(env DF_RELAY_TOKEN,fallback "devflow-relay-default-token") */
  token: string
}

/**
 * 控制面消息(对齐 Rust ControlMessage,broadcast.rs:132-145,tag = "control_kind")
 *
 * miniapp 主要发 Heartbeat 心跳 + Ping;Pair 在首次绑定时用(本批未实现绑定流程)。
 */
export type ControlMessage =
  | { control_kind: 'pair'; device_id: string; token: string }
  | { control_kind: 'heartbeat'; device_id: string }
  | { control_kind: 'presence'; device_id: string; state: PresenceState }
  | { control_kind: 'ping' }
  | { control_kind: 'pong' }

/**
 * 小程序 → 桌面端 Command 协议(miniapp 发起操作)
 *
 * 类型为对齐 Tauri command 名,relay 透传 device_id 路由到桌面端,桌面端 invoke 对应 command。
 * 字段:
 * - cmd:命令名(对齐 src-tauri command 函数名)
 * - args:命令参数(对齐各 command 的参数)
 *
 * 路由表全覆盖(14 条):
 * - send_message / stop / switch_conversation / regenerate(MVP 已发)
 * - approve / authorize_dir / continue_loop / stop_loop(阶段3 补齐,见 useAiChat.ts)
 * - list_conversations / load_messages(F-#95 扩展,会话列表/历史消息读取)
 * - list_skills / list_entities(F-#95 联想扩展,技能/项目·任务·灵感列表,见 useAiChat.ts)
 * - rename_conversation(会话重命名,2026-06-23)/ sync_pending(重连审批恢复,2026-06-23)
 */
export interface MiniCommand {
  /** Tauri command 名 */
  cmd: string
  /** 命令参数(原始对象,对齐各 command 参数签名) */
  args: Record<string, unknown>
}

/** send_message 命令参数(对齐 src-tauri ai_chat_send command) */
export interface SendMessageArgs {
  message: string
  conversation_id?: string | null
  model_override?: string | null
  /**
   * 选中技能名(`/<skill>` 联想选中后透传,对齐 ai_chat_send 的 skill 参数)。
   * null/undefined=无技能(普通对话);非空=后端注入对应 SKILL.md 全文到 prompt。
   */
  skill?: string | null
  /**
   * @ mention 区间元数据(用户选中 @项目/@任务/@灵感 后透传,
   * 对齐 ai_chat_send 的 mention_spans 参数,后端 resolve 投影成 Augmentation 注入)。
   * null/undefined/空=无 mention(纯文本消息)。
   */
  mention_spans?: MentionSpan[] | null
}

/**
 * 用户消息内 mention 区间的元数据
 * (对齐后端 MentionSpanDto,crates/df-types/src/augmentation.rs:235)。
 *
 * 字段名对齐后端 serde:refId(camelCase,后端 serde rename)。
 * 前端按 {start, length} 在原文中切出区间替换为 chip。
 */
export interface MentionSpan {
  /** 区间起点(字符偏移,前端约定,后端仅透传不解释) */
  start: number
  /** 区间长度 */
  length: number
  /** kind 标签:project / task / idea / skill(对齐后端 MentionKind serde snake_case) */
  kind: 'project' | 'task' | 'idea' | 'skill'
  /** 引用稳定标识(Project/Task/Idea 为 id,Skill 为 name),前端用于反查 */
  refId: string
  /** chip 展示文本(项目名/任务标题/技能名) */
  label: string
}

/**
 * 技能信息(对齐桌面端 src/api/types.ts:614 SkillInfo + 后端 SkillInfo,
 * src-tauri/src/commands/ai/skills.rs:19-34)。
 *
 * 供 `/` 联想浮层渲染 + 选中后 name 透传 SendMessageArgs.skill。
 * duplicates 可选:后端 #[serde(skip_serializing_if=Option::is_none)] 无冲突不序列化,
 * miniapp 加 `?` 兜底防 prod 反序列化宽松报错。
 */
export interface SkillInfo {
  /** 技能名(联想浮层展示 + 选中后透传 skill 参数) */
  name: string
  /** 技能描述(浮层副标题) */
  description: string
  /** 参数提示(可选,如 "<url>") */
  argument_hint?: string
  /** skill | command | plugin(来源分类) */
  source: string
  /** SKILL.md 绝对路径(后端注入时读全文) */
  path: string
  /** 同名冲突的其他来源路径(可选,无冲突不序列化) */
  duplicates?: string[]
}

/** switch_conversation 命令参数 */
export interface SwitchConversationArgs {
  conversation_id: string
}

/** stop 命令参数(对齐 ai_chat_stop) */
export interface StopArgs {
  conversation_id?: string | null
}

/** approve 命令参数(对齐 src-tauri ai_approve,Phase3 §2.2 路由表) */
export interface ApproveArgs {
  /** 待审批的工具调用 id(对应 AiApprovalRequired 事件的 id) */
  tool_call_id: string
  /** 是否批准(true=批准执行,false=拒绝) */
  approved: boolean
}

/**
 * authorize_dir 命令参数(对齐 src-tauri ai_authorize_dir,Phase3 §2.2 路由表)
 *
 * decision 取值:对齐 src-tauri ai_authorize_dir 的 decision 参数
 * - 'once':本次授权(下次同路径仍需问)
 * - 'always':永久授权(写入会话级信任目录)
 * - 'deny':拒绝(本次不授权)
 */
export interface AuthorizeDirArgs {
  /** 待授权的工具调用 id(对应 AiDirAuthRequired 事件的 id) */
  tool_call_id: string
  /** 授权决策('once' / 'always' / 'deny') */
  decision: 'once' | 'always' | 'deny'
}

/** continue_loop 命令参数(对齐 src-tauri ai_continue_loop,Phase3 §2.2 路由表) */
export interface ContinueLoopArgs {
  /** 需继续运行的会话 id */
  conversation_id: string
}

/** stop_loop 命令参数(对齐 src-tauri ai_stop_loop,Phase3 §2.2 路由表) */
export interface StopLoopArgs {
  /** 霷停止循环的会话 id */
  conversation_id: string
}
