import { Logger } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import {
  ConnectedSocket,
  MessageBody,
  OnGatewayConnection,
  OnGatewayDisconnect,
  SubscribeMessage,
  WebSocketGateway,
} from '@nestjs/websockets';
import type { Socket } from 'socket.io';
import { RealtimeCoachContextService } from './realtime-coach-context.service';
import { DashScopeRealtimeProxy } from './dashscope-realtime.proxy';
import { GeminiLiveProxy } from './gemini-live.proxy';
import type { CoachProxyHandle, RealtimeProvider } from './realtime-provider.types';
import type {
  AudioFrameMsg,
  CoachErrorMsg,
  CoachReadyMsg,
  CoachStartMsg,
  CoachScope,
  TextDeltaMsg,
  TextDoneMsg,
} from './dto/coach-messages';

/**
 * CoachGateway — 实时教练 WS 网关(socket.io)。
 *
 * 链路:浏览器麦克风音频 → 本网关 → DashScope 代理 → Qwen-Omni-Realtime → 文字回推浏览器。
 * 鉴权:握手 query.token 走现有 JWT(WS 无法带自定义 header,与 SSE 流同思路)。
 * 每个 socket 一条独立 DashScope 连接(instructions 隔离 + 断开即收)。
 */
@WebSocketGateway({
  namespace: 'pac/v1/realtime/coach',
  cors: { origin: true, credentials: false },
})
export class CoachGateway implements OnGatewayConnection, OnGatewayDisconnect {
  private readonly logger = new Logger(CoachGateway.name);
  private readonly proxies = new Map<string, CoachProxyHandle>();

  constructor(
    private readonly jwt: JwtService,
    private readonly contextService: RealtimeCoachContextService,
    private readonly qwenProxy: DashScopeRealtimeProxy,
    private readonly geminiProxy: GeminiLiveProxy,
  ) {}

  private pickProvider(key: 'qwen' | 'gemini' | undefined): RealtimeProvider {
    return key === 'gemini' ? this.geminiProxy : this.qwenProxy;
  }

  handleConnection(client: Socket): void {
    const token = (client.handshake.query.token as string | undefined) ?? '';
    try {
      const payload = this.jwt.verify<{ sub: string; tenantId: string; hostId: string }>(token);
      const scope: CoachScope = {
        tenantId: payload.tenantId,
        hostId: payload.hostId,
        userId: payload.sub,
      };
      client.data.scope = scope;
    } catch {
      client.emit('coach:error', { message: '鉴权失败' } satisfies CoachErrorMsg);
      client.disconnect();
    }
  }

  handleDisconnect(client: Socket): void {
    this.proxies.get(client.id)?.close();
    this.proxies.delete(client.id);
  }

  @SubscribeMessage('coach:start')
  async onStart(
    @MessageBody() msg: CoachStartMsg,
    @ConnectedSocket() client: Socket,
  ): Promise<void> {
    if (!client.data.scope) return;
    if (this.proxies.has(client.id)) return; // 幂等

    try {
      const { instructions, skills } = await this.contextService.buildInstructions(msg.planId);
      client.emit('coach:ready', { skills } satisfies CoachReadyMsg);

      const provider = this.pickProvider(msg.provider);
      const handle = await provider.open(instructions, {
        onDelta: (text) => client.emit('text:delta', { text } satisfies TextDeltaMsg),
        onDone: (fullText) => client.emit('text:done', { fullText } satisfies TextDoneMsg),
        onError: (message) => client.emit('coach:error', { message } satisfies CoachErrorMsg),
      });
      this.proxies.set(client.id, handle);
      this.logger.log(`coach started: plan=${msg.planId} skills=${skills.length}`);
    } catch (err) {
      const message = err instanceof Error ? err.message : String(err);
      client.emit('coach:error', { message } satisfies CoachErrorMsg);
    }
  }

  @SubscribeMessage('audio:frame')
  onAudioFrame(@MessageBody() msg: AudioFrameMsg, @ConnectedSocket() client: Socket): void {
    this.proxies.get(client.id)?.appendAudio(msg.frame);
  }

  @SubscribeMessage('coach:stop')
  onStop(@ConnectedSocket() client: Socket): void {
    this.proxies.get(client.id)?.close();
    this.proxies.delete(client.id);
  }
}
