import crypto from 'crypto';
import WebSocket from 'ws';

// src/iflytek/IflytekServerAdapter.ts
var IflytekServerAdapter = class {
  constructor(config) {
    this.sessions = /* @__PURE__ */ new Map();
    this.appId = config.appId;
    this.apiKey = config.apiKey;
    this.apiSecret = config.apiSecret;
    this.host = config.host ?? "iat-api.xfyun.cn";
    this.path = config.path ?? "/v2/iat";
    this.debug = config.debug ?? false;
  }
  /**
   * 将适配器绑定到一个 Socket.IO socket 连接上。
   * 自动监听 iflytek:start / iflytek:audio / iflytek:stop / disconnect 事件。
   */
  attach(socket) {
    socket.on("iflytek:start", (payload) => {
      this.handleStart(socket, payload);
    });
    socket.on("iflytek:audio", (frame) => {
      this.handleAudio(socket, frame);
    });
    socket.on("iflytek:stop", (payload) => {
      this.handleStop(socket, payload);
    });
    socket.on("disconnect", () => {
      this.handleDisconnect(socket);
    });
  }
  // ─── 事件处理 ───
  handleStart(socket, payload) {
    const sessionId = payload?.sessionId ?? `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
    if (!this.appId || !this.apiKey || !this.apiSecret) {
      socket.emit("iflytek:error", {
        sessionId,
        message: "\u670D\u52A1\u7AEF\u672A\u914D\u7F6E IFLYTEK_APP_ID / IFLYTEK_API_KEY / IFLYTEK_API_SECRET"
      });
      return;
    }
    const existing = this.sessions.get(socket.id);
    if (existing) {
      if (existing.ended) {
        this.log(socket.id, "replace ended session");
        this.closeUpstream(existing);
        this.sessions.delete(socket.id);
      } else if (existing.ws.readyState === WebSocket.OPEN) {
        this.log(socket.id, "duplicate start ignored (session open)");
        socket.emit("iflytek:ready", { sessionId: existing.sessionId });
        return;
      } else if (existing.ws.readyState === WebSocket.CONNECTING) {
        this.log(socket.id, "duplicate start ignored (session connecting)");
        return;
      } else {
        this.sessions.delete(socket.id);
      }
    }
    const wsUrl = this.buildWsUrl();
    const ws = new WebSocket(wsUrl);
    const state = {
      ws,
      sessionId,
      frameCount: 0,
      firstFrameSent: false,
      ended: false,
      resultText: "",
      resultSegments: []
    };
    this.sessions.set(socket.id, state);
    this.log(socket.id, "start session", { sessionId });
    ws.on("open", () => {
      this.log(socket.id, "upstream open");
      socket.emit("iflytek:ready", { sessionId });
    });
    ws.on("message", (raw) => {
      try {
        const msg = JSON.parse(String(raw));
        this.log(socket.id, "upstream message", {
          code: msg.code,
          status: msg?.data?.status
        });
        if (msg.code !== 0) {
          socket.emit("iflytek:error", {
            sessionId,
            message: msg.message || `\u8BAF\u98DE\u8FD4\u56DE\u9519\u8BEF code=${msg.code}`
          });
          return;
        }
        const current = this.sessions.get(socket.id);
        if (!current || current.ws !== ws) return;
        const result = this.extractResult(msg.data?.result);
        const mergedText = this.mergeResult(current, result);
        const isFinal = msg.data?.status === 2;
        if (mergedText || isFinal) {
          socket.emit("iflytek:result", {
            sessionId,
            text: mergedText,
            isFinal
          });
        }
        if (isFinal) {
          const current2 = this.sessions.get(socket.id);
          if (current2?.ws === ws) {
            this.closeUpstream(current2);
          }
        }
      } catch (e) {
        socket.emit("iflytek:error", { sessionId, message: e.message });
      }
    });
    ws.on("error", (e) => {
      if (e?.message?.includes("before the connection was established")) {
        this.log(socket.id, "ignore early-close error");
        return;
      }
      socket.emit("iflytek:error", {
        sessionId,
        message: e?.message || "\u8BAF\u98DE WebSocket \u8FDE\u63A5\u5931\u8D25"
      });
    });
    ws.on("close", () => {
      this.log(socket.id, "upstream close");
      const current = this.sessions.get(socket.id);
      if (current?.ws === ws) {
        this.sessions.delete(socket.id);
      }
    });
  }
  handleAudio(socket, frame) {
    const state = this.sessions.get(socket.id);
    if (!state || state.ws.readyState !== WebSocket.OPEN) return;
    if (frame.sessionId && frame.sessionId !== state.sessionId) {
      this.log(socket.id, "ignore stale audio frame");
      return;
    }
    if (state.ended && frame.status === 2) {
      this.log(socket.id, "ignore duplicate final frame");
      return;
    }
    if (!state.firstFrameSent && frame.status === 2) {
      this.log(socket.id, "ignore lonely final frame");
      return;
    }
    const isFirst = !state.firstFrameSent && frame.status !== 2;
    const status = isFirst ? 0 : frame.status;
    state.frameCount += 1;
    if (status === 0) state.firstFrameSent = true;
    if (status === 2) state.ended = true;
    this.log(socket.id, "audio frame", {
      status,
      hasAudio: Boolean(frame.audio),
      len: frame.audio?.length ?? 0,
      count: state.frameCount
    });
    const payload = {
      data: { status }
    };
    if (status === 0) {
      payload.common = { app_id: this.appId };
      payload.business = {
        language: frame.language || "zh_cn",
        domain: frame.domain || "iat",
        accent: frame.accent || "mandarin",
        vad_eos: 2e3
      };
      payload.data = {
        status: 0,
        format: "audio/L16;rate=16000",
        encoding: "raw",
        audio: frame.audio || ""
      };
    } else if (status === 1) {
      payload.data = {
        status: 1,
        format: "audio/L16;rate=16000",
        encoding: "raw",
        audio: frame.audio || ""
      };
    } else {
      payload.data = { status: 2 };
    }
    state.ws.send(JSON.stringify(payload));
  }
  handleStop(socket, payload) {
    const state = this.sessions.get(socket.id);
    if (payload?.sessionId && state?.sessionId && payload.sessionId !== state.sessionId) {
      this.log(socket.id, "ignore stale stop signal");
      return;
    }
    this.log(socket.id, "stop signal");
    if (state) {
      this.closeUpstream(state);
      this.sessions.delete(socket.id);
    }
  }
  handleDisconnect(socket) {
    const state = this.sessions.get(socket.id);
    if (state) {
      this.closeUpstream(state);
      this.sessions.delete(socket.id);
    }
  }
  // ─── 工具方法 ───
  buildWsUrl() {
    const date = (/* @__PURE__ */ new Date()).toUTCString();
    const requestLine = `GET ${this.path} HTTP/1.1`;
    const signatureOrigin = `host: ${this.host}
date: ${date}
${requestLine}`;
    const signatureSha = crypto.createHmac("sha256", this.apiSecret).update(signatureOrigin).digest("base64");
    const authOrigin = `api_key="${this.apiKey}", algorithm="hmac-sha256", headers="host date request-line", signature="${signatureSha}"`;
    const authorization = Buffer.from(authOrigin).toString("base64");
    const params = new URLSearchParams({ authorization, date, host: this.host });
    return `wss://${this.host}${this.path}?${params.toString()}`;
  }
  extractText(result) {
    const ws = result?.ws;
    if (!Array.isArray(ws)) return "";
    return ws.map((item) => item?.cw?.[0]?.w ?? "").join("").trim();
  }
  extractResult(result) {
    const text = this.extractText(result);
    const pgs = typeof result?.pgs === "string" ? result.pgs : void 0;
    const rgRaw = Array.isArray(result?.rg) ? result.rg : void 0;
    const rg = rgRaw && rgRaw.length === 2 ? [Number(rgRaw[0]), Number(rgRaw[1])] : void 0;
    const sn = typeof result?.sn === "number" ? result.sn : Number.isFinite(Number(result?.sn)) ? Number(result?.sn) : void 0;
    return { text, pgs, rg, sn };
  }
  mergeResult(state, result) {
    const text = result.text?.trim() ?? "";
    const segments = state.resultSegments;
    if (typeof result.sn === "number") {
      if (result.pgs === "rpl" && result.rg) {
        const [start, end] = result.rg;
        const before = segments.slice(0, start);
        const after = segments.slice(end + 1);
        state.resultSegments = [...before, text, ...after];
      } else {
        segments[result.sn] = text;
        state.resultSegments = segments;
      }
    } else if (result.pgs === "rpl" && result.rg) {
      const [start, end] = result.rg;
      const before = segments.slice(0, start);
      const after = segments.slice(end + 1);
      state.resultSegments = [...before, text, ...after];
    } else if (text) {
      segments.push(text);
      state.resultSegments = segments;
    }
    state.resultText = state.resultSegments.filter(Boolean).join("");
    return state.resultText;
  }
  closeUpstream(state) {
    try {
      state.ws.close();
    } catch {
    }
  }
  log(socketId, msg, extra) {
    if (!this.debug) return;
    if (extra) {
      console.log(`[sa2kit/iflytek][${socketId}] ${msg}`, extra);
    } else {
      console.log(`[sa2kit/iflytek][${socketId}] ${msg}`);
    }
  }
};

export { IflytekServerAdapter };
//# sourceMappingURL=server.mjs.map
//# sourceMappingURL=server.mjs.map