/**
 * df-relay WS 客户端(uni-app 跨端适配)
 *
 * 协议(对齐 crates/df-relay/src/relay.rs handle_connection):
 * 1. 建立 WS 连接到 `/ws/miniapp`
 * 2. 首帧发 Hello{kind:'miniapp', device_id, token} 身份宣告(relay.rs:197-208,10s 超时)
 * 3. 校验通过后进入收发循环;失败 relay 发 Error 帧 + Close
 *
 * 跨端适配:
 * - 微信小程序:uni.connectSocket(微信原生 API,非浏览器 WebSocket)
 * - H5:uni.connectSocket 封装浏览器 WebSocket
 * - 统一经 uni API 封装,业务层不直接 new WebSocket
 *
 * 机制:
 * - 重连:指数退避(baseDelay * 2^n,封顶 maxDelay),握手失败/网络断/服务关闭触发
 * - 心跳:固定间隔发 ControlMessage{control_kind:'ping'},防空闲断开 + 在线状态
 * - JSON 透传:入站解析为 BroadcastMessage,payload 是 AiChatEvent JSON
 *             出站直接发 Command JSON(relay 自动包 payload,见 relay.rs:332-365)
 */

import { getConfig } from '@/config'
import type { BroadcastMessage, Hello, MiniCommand, ControlMessage } from '@/types/relay'

/** WS 连接状态 */
export type WsStatus = 'disconnected' | 'connecting' | 'connected' | 'handshaking' | 'reconnecting'

/** 入站事件回调(Event/Control 消息分派) */
export interface WsHandlers {
  /** 收到 Event 消息(payload 已解为 AiChatEvent JSON,业务层按 type discriminator 处理) */
  onEvent: (msg: BroadcastMessage) => void
  /** 收到 Control 消息(心跳响应 / 在线状态等) */
  onControl: (msg: ControlMessage) => void
  /** 状态变更(连接 / 断开 / 重连中) */
  onStatus: (status: WsStatus, detail?: string) => void
}

/**
 * WS 客户端单例(miniapp 全局一个连接)
 *
 * 设计:单 socket + 单 handlers 回调集合(composable 订阅,业务层不直接持 socket)。
 * 握手/心跳/重连机制内部封装,业务层仅关心 send(Command) + onEvent。
 */
class WsClient {
  private socket: UniApp.SocketTask | null = null
  private handlers: WsHandlers | null = null
  private status: WsStatus = 'disconnected'
  /** 握手是否已完成(收到首个广播或握手成功信号) */
  private handshaked = false
  private heartbeatTimer: ReturnType<typeof setInterval> | null = null
  private reconnectTimer: ReturnType<typeof setTimeout> | null = null
  private reconnectAttempts = 0
  /** 主动关闭标志(用户调用 disconnect 时置 true,不再触发重连) */
  private manualClose = false

  /** 订阅(单订阅,composable 注册一次) */
  subscribe(h: WsHandlers): void {
    this.handlers = h
  }

  /** 读取当前状态 */
  getStatus(): WsStatus {
    return this.status
  }

  /** 主动连接 */
  connect(): void {
    if (this.status === 'connected' || this.status === 'connecting' || this.status === 'handshaking') {
      return
    }
    this.manualClose = false
    this.openSocket()
  }

  /** 主动断开(用户手动,不重连) */
  disconnect(): void {
    this.manualClose = true
    this.cleanup()
    this.setStatus('disconnected', 'manual close')
  }

  /** 若已断开则触发重连(onShow 时调用,小程序后台回前台) */
  resumeIfDisconnected(): void {
    if (this.status === 'disconnected' && !this.manualClose) {
      this.scheduleReconnect()
    }
  }

  /**
   * 发送 Command(miniapp → device)
   *
   * 不包装 BroadcastMessage —— relay.rs:332-365 入站文本帧直接当 payload 包成
   * BroadcastMessage(device_id/kind/source/from 由 relay 填),故客户端仅发业务 JSON。
   */
  send(cmd: MiniCommand): boolean {
    if (!this.socket || !this.handshaked) {
      console.warn('[WsClient] 未连接或握手未完成,丢弃命令', cmd.cmd)
      return false
    }
    try {
      const text = JSON.stringify(cmd)
      this.socket.send({ data: text })
      return true
    } catch (e) {
      console.error('[WsClient] send 失败', e)
      return false
    }
  }

  // ─── 内部实现 ──────────────────────────────────────────

  private openSocket(): void {
    const cfg = getConfig()
    this.setStatus('connecting')
    console.log('[WsClient] 连接', cfg.relayHost)

    try {
      this.socket = uni.connectSocket({
        url: cfg.relayHost,
        complete: () => {
          // complete 回调在 uni-app 仅表示"任务创建完成",非连接完成(onOpen 才是)
        },
      })
    } catch (e) {
      console.error('[WsClient] connectSocket 异常', e)
      this.scheduleReconnect()
      return
    }

    this.socket.onOpen(() => this.handleOpen())
    this.socket.onMessage((res) => this.handleMessage(res))
    this.socket.onClose((res) => this.handleClose(res))
    this.socket.onError((res) => this.handleError(res))
  }

  /** WS 已建立连接 → 立即发 Hello 握手帧 */
  private handleOpen(): void {
    this.setStatus('handshaking')
    const cfg = getConfig()
    const hello: Hello = {
      kind: 'miniapp',
      device_id: cfg.deviceId,
      token: cfg.token,
    }
    try {
      this.socket?.send({ data: JSON.stringify(hello) })
    } catch (e) {
      console.error('[WsClient] 发送 Hello 失败', e)
      this.scheduleReconnect()
    }
  }

  /** 入站消息分派:握手阶段首帧=控制(握手成功/失败),握手后=BroadcastMessage */
  private handleMessage(res: { data: string | ArrayBuffer }): void {
    const text = typeof res.data === 'string' ? res.data : ''
    if (!text) return

    // 握手阶段可能收到 relay 发的错误控制帧(relay.rs:200-238)
    if (!this.handshaked) {
      this.handleHandshakeResponse(text)
      return
    }

    // 解析 BroadcastMessage(device 事件经 relay 包成完整 BroadcastMessage 下发)
    let msg: BroadcastMessage
    try {
      msg = JSON.parse(text) as BroadcastMessage
    } catch (e) {
      console.warn('[WsClient] 入站消息 JSON 解析失败', e, text.slice(0, 200))
      return
    }

    // 按 kind 分派:Event → 业务层;Control → 心跳响应等
    if (msg.kind === 'event') {
      this.handlers?.onEvent(msg)
    } else if (msg.kind === 'control') {
      this.handlers?.onControl(msg.payload as ControlMessage)
    }
    // command 是 device→miniapp 反向(不应在此端收到,忽略)
  }

  /** 处理握手响应(relay 握手通过后无显式 ack,首条业务消息即视为握手成功;
   *  失败时 relay 发 {"kind":"control","error":"..."} + 关连接) */
  private handleHandshakeResponse(text: string): void {
    // relay 握手失败帧(relay.rs:200-238):{"kind":"control","error":"handshake_failed|kind_mismatch|auth_failed"}
    if (text.includes('"error"')) {
      console.error('[WsClient] 握手失败', text)
      this.setStatus('disconnected', `handshake failed: ${text}`)
      // 握手失败不重连(token 错则重连也错),等用户修配置后手动 connect
      this.cleanup()
      return
    }
    // 首条非错误消息即视为握手通过(实际握手后 relay 会路由 device 的 Event 过来)
    this.handshaked = true
    this.reconnectAttempts = 0
    this.setStatus('connected')
    this.startHeartbeat()
    // 该首消息也是业务消息,转 handleMessage 处理(此时 handshaked=true,走 BroadcastMessage 解析路径)
    this.handleMessage({ data: text })
  }

  private handleClose(res: { code: number; reason: string }): void {
    console.log('[WsClient] 连接关闭', res.code, res.reason)
    this.cleanup()
    if (!this.manualClose) {
      this.scheduleReconnect()
    } else {
      this.setStatus('disconnected', 'manual close')
    }
  }

  private handleError(res: { errMsg: string }): void {
    console.error('[WsClient] socket 错误', res.errMsg)
    // 不直接 scheduleReconnect:onClose 会跟着触发,在 onClose 统一处理重连
  }

  /** 启动心跳(固定间隔发 ControlMessage{control_kind:'heartbeat'}) */
  private startHeartbeat(): void {
    this.stopHeartbeat()
    const cfg = getConfig()
    this.heartbeatTimer = setInterval(() => {
      const ping: ControlMessage = { control_kind: 'ping' }
      // 直接发 ControlMessage JSON(relay 透传,device 可回 pong)
      try {
        this.socket?.send({ data: JSON.stringify(ping) })
      } catch (e) {
        console.warn('[WsClient] 心跳发送失败', e)
      }
      // 注:cfg.heartbeatInterval 读但不显式用(已通过 setInterval 传入),保 cfg 引用
      void cfg
    }, cfg.heartbeatInterval)
  }

  private stopHeartbeat(): void {
    if (this.heartbeatTimer) {
      clearInterval(this.heartbeatTimer)
      this.heartbeatTimer = null
    }
  }

  /** 指数退避重连(baseDelay * 2^attempts,封顶 maxDelay) */
  private scheduleReconnect(): void {
    if (this.reconnectTimer) return
    const cfg = getConfig()
    const attempt = this.reconnectAttempts++
    const delay = Math.min(cfg.reconnectBaseDelay * Math.pow(2, attempt), cfg.reconnectMaxDelay)
    this.setStatus('reconnecting', `attempt ${attempt + 1}, delay ${delay}ms`)
    console.log(`[WsClient] ${delay}ms 后重连(attempt ${attempt + 1})`)
    this.reconnectTimer = setTimeout(() => {
      this.reconnectTimer = null
      this.openSocket()
    }, delay)
  }

  /** 清理 socket + 定时器(连接关闭时调) */
  private cleanup(): void {
    this.stopHeartbeat()
    if (this.reconnectTimer) {
      clearTimeout(this.reconnectTimer)
      this.reconnectTimer = null
    }
    if (this.socket) {
      try {
        this.socket.close({})
      } catch {
        // 已关闭的 socket close 抛错忽略
      }
      this.socket = null
    }
    this.handshaked = false
  }

  private setStatus(status: WsStatus, detail?: string): void {
    this.status = status
    this.handlers?.onStatus(status, detail)
  }
}

/** 全局单例(miniapp 进程内一个连接) */
export const wsClient = new WsClient()
