import * as mediasoupClient from 'mediasoup-client'
import type { Device, Transport, Consumer } from 'mediasoup-client/lib/types'
import type * as T from 'mediasoup-client/lib/types'

export type Role = 'teacher' | 'student'

type Handler = (msg: { type: string; data?: unknown }) => void

export class RoomWsClient {
  private ws!: WebSocket
  private pending = new Map<string, { resolve: (v: unknown) => void; reject: (e: Error) => void }>()
  private seq = 0
  private handlers: Handler[] = []
  private device: Device | null = null
  private onWsClosed: (() => void) | null = null
  private voluntaryClose = false
  private signalingEverOpened = false

  constructor(private readonly url: string) {}

  onDisconnect(handler: (() => void) | null) {
    this.onWsClosed = handler
  }

  connect(timeoutMs = 20000): Promise<void> {
    return new Promise((resolve, reject) => {
      let settled = false
      const finish = (fn: () => void) => {
        if (settled) return
        settled = true
        clearTimeout(timer)
        fn()
      }

      this.signalingEverOpened = false
      this.ws = new WebSocket(this.url)

      const timer = setTimeout(() => {
        finish(() => {
          this.voluntaryClose = true
          try {
            this.ws.close()
          } catch {
            /* ignore */
          }
          reject(
            new Error(
              `信令连接超时（${timeoutMs / 1000}s）：请确认教师端已启动 SFU、端口正确；跨设备时「WS 主机」须填教师机局域网 IP，勿使用 127.0.0.1`,
            ),
          )
        })
      }, timeoutMs)

      this.ws.addEventListener('open', () => {
        this.signalingEverOpened = true
        finish(() => resolve())
      })
      this.ws.addEventListener('error', () => {
        finish(() =>
          reject(
            new Error(
              '无法连接信令：请核对「端口」与教师端状态栏里「SFU 已监听」的端口一致；跨设备时「WS 主机」填教师机局域网 IP（勿用 127.0.0.1）；并确认教师已启动 SFU、防火墙放行该端口。',
            ),
          ),
        )
      })
      this.ws.addEventListener('message', ev => this.handleRaw(ev.data))
      this.ws.addEventListener('close', () => this.handleSocketClosed())
    })
  }

  private handleSocketClosed() {
    for (const [, p] of this.pending) {
      p.reject(new Error('WebSocket closed'))
    }
    this.pending.clear()
    const notify = !this.voluntaryClose
    const hadOpened = this.signalingEverOpened
    this.voluntaryClose = false
    this.signalingEverOpened = false
    if (notify && hadOpened) {
      try {
        this.onWsClosed?.()
      } finally {
        this.onWsClosed = null
      }
    } else {
      this.onWsClosed = null
    }
  }

  close() {
    this.voluntaryClose = true
    try {
      this.ws?.close()
    } catch {
      /* ignore */
    }
  }

  subscribe(handler: Handler) {
    this.handlers.push(handler)
    return () => {
      this.handlers = this.handlers.filter(h => h !== handler)
    }
  }

  private emit(msg: { type: string; data?: unknown }) {
    for (const h of this.handlers) h(msg)
  }

  private handleRaw(raw: unknown) {
    let msg: { type: string; requestId?: string; data?: unknown; error?: string }
    try {
      msg = JSON.parse(String(raw))
    } catch {
      return
    }
    if (msg.type === 'response') {
      const p = this.pending.get(msg.requestId || '')
      if (p) {
        this.pending.delete(msg.requestId || '')
        if (msg.error) p.reject(new Error(msg.error))
        else p.resolve(msg.data)
      }
      return
    }
    this.emit(msg as { type: string; data?: unknown })
  }

  request<T>(type: string, data: Record<string, unknown> = {}): Promise<T> {
    if (this.ws.readyState !== WebSocket.OPEN) {
      return Promise.reject(new Error('WebSocket is not open'))
    }
    const requestId = `r${++this.seq}`
    return new Promise((resolve, reject) => {
      this.pending.set(requestId, { resolve: resolve as (v: unknown) => void, reject })
      try {
        this.ws.send(JSON.stringify({ type, requestId, data }))
      } catch (e) {
        this.pending.delete(requestId)
        reject(e instanceof Error ? e : new Error(String(e)))
      }
    })
  }

  async join(roomId: string, role: Role, displayName: string) {
    return (await this.request('join', { roomId, role, displayName })) as {
      peerId: string
      roomId: string
      role: Role
      rtpCapabilities: T.RtpCapabilities
      existingProducers: {
        peerId: string
        producerId: string
        kind: T.MediaKind
        appData: Record<string, unknown>
      }[]
    }
  }

  async loadDevice(rtpCapabilities: T.RtpCapabilities) {
    this.device = new mediasoupClient.Device()
    await this.device.load({ routerRtpCapabilities: rtpCapabilities })
    return this.device
  }

  getDevice(): Device {
    if (!this.device) throw new Error('Device not loaded')
    return this.device
  }

  async createWebRtcTransport(direction: 'send' | 'recv') {
    return (await this.request('createWebRtcTransport', { direction })) as {
      id: string
      iceParameters: T.IceParameters
      iceCandidates: T.IceCandidate[]
      dtlsParameters: T.DtlsParameters
    }
  }

  async connectWebRtcTransport(transportId: string, dtlsParameters: T.DtlsParameters) {
    await this.request('connectWebRtcTransport', { transportId, dtlsParameters })
  }

  async createSendTransport(): Promise<Transport> {
    const device = this.getDevice()
    const opt = await this.createWebRtcTransport('send')
    const transport = device.createSendTransport(opt)
    transport.on(
      'connect',
      (
        { dtlsParameters }: { dtlsParameters: T.DtlsParameters },
        cb: () => void,
        err: (e: Error) => void,
      ) => {
        this.connectWebRtcTransport(transport.id, dtlsParameters)
          .then(() => cb())
          .catch(err)
      },
    )
    transport.on(
      'produce',
      async (
        {
          kind,
          rtpParameters,
          appData,
        }: {
          kind: T.MediaKind
          rtpParameters: T.RtpParameters
          appData?: Record<string, unknown>
        },
        cb: (args: { id: string }) => void,
        err: (e: Error) => void,
      ) => {
        try {
          const { id } = (await this.request('produce', {
            transportId: transport.id,
            kind,
            rtpParameters,
            appData: appData || {},
          })) as { id: string }
          cb({ id })
        } catch (e) {
          err(e instanceof Error ? e : new Error(String(e)))
        }
      },
    )
    return transport
  }

  async createRecvTransport(): Promise<Transport> {
    const device = this.getDevice()
    const opt = await this.createWebRtcTransport('recv')
    const transport = device.createRecvTransport(opt)
    transport.on(
      'connect',
      (
        { dtlsParameters }: { dtlsParameters: T.DtlsParameters },
        cb: () => void,
        err: (e: Error) => void,
      ) => {
        this.connectWebRtcTransport(transport.id, dtlsParameters)
          .then(() => cb())
          .catch(err)
      },
    )
    return transport
  }

  async consume(recvTransport: Transport, producerId: string): Promise<Consumer> {
    const device = this.getDevice()
    const { id, kind, rtpParameters, type } = (await this.request('consume', {
      transportId: recvTransport.id,
      producerId,
      rtpCapabilities: device.rtpCapabilities,
    })) as {
      id: string
      kind: T.MediaKind
      rtpParameters: T.RtpParameters
      type: T.ConsumerType
    }
    const consumer = await recvTransport.consume({ id, producerId, kind, rtpParameters, type })
    await this.request('resumeConsumer', { consumerId: consumer.id })
    return consumer
  }

  async pauseConsumer(consumerId: string) {
    await this.request('pauseConsumer', { consumerId })
  }

  async resumeConsumer(consumerId: string) {
    await this.request('resumeConsumer', { consumerId })
  }
}
