/**
 * 聊天 WebSocket 处理器
 * 
 * 事件：
 *   - onConnected(userId)
 *   - onTyping(isTyping)
 *   - onSentence(index, text, total, done)  ← AI 逐句推送
 *   - onVoiceReady(index, audioUrl)
 *   - onError(errorMessage)
 */
import { getApiConfig } from "@/core/net/config/api_config";
import { AppEnvUtil, Logger } from "@/utils";

const log = new Logger("ChatWebSocket");
const RECONNECT_DELAY_MS = 3000;

export interface SentencePayload {
  index: number;
  text: string;
  total: number;
  done: boolean;
}

export interface PaywallStatusPayload {
  locked: boolean;
  showContent: boolean;
  showUpgrade: boolean;
  reason: string | null;
  hint: string | null;
  detail: Record<string, unknown> | null;
}

export class ChatWebSocket {
  private ws: WebSocket | null = null;
  private reconnectTimer: ReturnType<typeof setTimeout> | null = null;
  private connectionUrl: string | null = null;
  private disposed = false;

  onConnected: ((userId: string) => void) | null = null;
  onTyping: ((isTyping: boolean) => void) | null = null;
  onSentence: ((payload: SentencePayload) => void) | null = null;
  onVoiceReady: ((index: number, audioUrl: string) => void) | null = null;
  onImage: ((url: string) => void) | null = null;
  onPaywallStatus: ((payload: PaywallStatusPayload) => void) | null = null;
  onError: ((errorMessage: string) => void) | null = null;

  constructor(
    private readonly serverUrl: string,
    private readonly token: string,
  ) {}

  get isConnected(): boolean {
    return this.ws?.readyState === WebSocket.OPEN;
  }

  connect(): void {
    if (this.disposed) return;
    if (this.ws && this.ws.readyState !== WebSocket.CLOSED) return;
    try {
      const url = buildChatWebSocketUrl(this.serverUrl, this.token);
      this.connectionUrl = url;
      logWebSocketDebug(`↔ WS CONNECT ${redactWebSocketUrl(url)}`);
      this.ws = new WebSocket(url);
      this.ws.onopen = () => {
        logWebSocketDebug(`↔ WS OPEN ${redactWebSocketUrl(url)}`);
        // 连接成功；userId 由后端在第一条消息中下发
      };
      this.ws.onmessage = (e) => {
        logWebSocketFrame("← WS MESSAGE", url, e.data);
        this.handleMessage(e.data);
      };
      this.ws.onerror = () => {
        logWebSocketError(`✕ WS ERROR ${redactWebSocketUrl(url)}`);
        this.onError?.("WebSocket error");
      };
      this.ws.onclose = (event) => {
        logWebSocketDebug(`↔ WS CLOSE ${redactWebSocketUrl(url)}`, {
          code: event.code,
          reason: event.reason,
          wasClean: event.wasClean,
          reconnectInMs: this.disposed ? null : RECONNECT_DELAY_MS,
        });
        if (!this.disposed) {
          this.reconnectTimer = setTimeout(
            () => this.connect(),
            RECONNECT_DELAY_MS,
          );
        }
      };
    } catch (e) {
      logWebSocketError("✕ WS CONNECT FAILED", e);
      this.onError?.(e instanceof Error ? e.message : String(e));
    }
  }

  sendMessage(content: string): boolean {
    const payload = { type: "message", content };
    if (!this.isConnected) {
      logWebSocketWarn("→ WS SEND SKIPPED: socket is not open", payload);
      return false;
    }
    logWebSocketFrame(
      "→ WS SEND",
      this.connectionUrl ?? this.serverUrl,
      payload,
    );
    this.ws?.send(JSON.stringify(payload));
    return true;
  }

  disconnect(): void {
    this.disposed = true;
    if (this.reconnectTimer) {
      clearTimeout(this.reconnectTimer);
      this.reconnectTimer = null;
    }
    this.ws?.close();
    this.ws = null;
    this.connectionUrl = null;
  }

  private handleMessage(data: unknown): void {
    if (typeof data !== "string") {
      logWebSocketWarn("← WS MESSAGE IGNORED: non-string payload", data);
      return;
    }
    let payload: {
      type?: string;
      userId?: string;
      isTyping?: boolean;
      index?: number;
      text?: string;
      total?: number;
      done?: boolean;
      audioUrl?: string;
      error?: string;
      data?: {
        image?: {
          type?: string | null;
          url?: string | null;
        };
        lockDetail?: Partial<PaywallStatusPayload>;
      };
    };
    try {
      payload = JSON.parse(data);
    } catch (e) {
      logWebSocketError("← WS MESSAGE PARSE FAILED", e);
      return;
    }
    switch (payload.type) {
      case "connected":
        if (payload.userId) this.onConnected?.(payload.userId);
        break;
      case "typing":
        this.onTyping?.(payload.isTyping ?? false);
        break;
      case "sentence":
        this.onSentence?.({
          index: payload.index ?? 0,
          text: payload.text ?? "",
          total: payload.total ?? 0,
          done: payload.done ?? false,
        });
        break;
      case "voice_ready":
        this.onVoiceReady?.(payload.index ?? 0, payload.audioUrl ?? "");
        break;
      case "image": {
        const url = payload.data?.image?.url;
        if (url) this.onImage?.(url);
        break;
      }
      case "paywall_status": {
        const lockDetail = payload.data?.lockDetail;
        this.onPaywallStatus?.({
          locked: lockDetail?.locked ?? false,
          showContent: lockDetail?.showContent ?? true,
          showUpgrade: lockDetail?.showUpgrade ?? false,
          reason: lockDetail?.reason ?? null,
          hint: lockDetail?.hint ?? null,
          detail: lockDetail?.detail ?? null,
        });
        break;
      }
      case "error":
        this.onError?.(payload.error ?? "Unknown error");
        break;
    }
  }
}

function buildChatWebSocketUrl(serverUrl: string, token: string): string {
  if (AppEnvUtil.isDevelopment()) return serverUrl;
  return `${serverUrl}?token=${encodeURIComponent(token)}`;
}

function redactWebSocketUrl(url: string): string {
  try {
    const parsed = new URL(url);
    if (parsed.searchParams.has("token")) {
      parsed.searchParams.set("token", "<redacted>");
    }
    return parsed.toString();
  } catch {
    return url.replace(/([?&]token=)[^&]+/i, "$1<redacted>");
  }
}

function logWebSocketFrame(
  label: string,
  url: string,
  payload: unknown,
): void {
  if (AppEnvUtil.isProduction()) return;
  const body = formatWebSocketLogValue(payload);
  const bodyLabel = label.startsWith("→") ? "request body" : "response body";
  log.debug(
    `${label} ${redactWebSocketUrl(url)}${body ? `\n${bodyLabel}:\n${body}` : ""}`,
  );
}

function logWebSocketDebug(message: string, payload?: unknown): void {
  if (AppEnvUtil.isProduction()) return;
  const body = formatWebSocketLogValue(payload);
  log.debug(`${message}${body ? `\nbody:\n${body}` : ""}`);
}

function logWebSocketWarn(message: string, payload?: unknown): void {
  if (AppEnvUtil.isProduction()) return;
  const body = formatWebSocketLogValue(payload);
  log.warn(`${message}${body ? `\nbody:\n${body}` : ""}`);
}

function logWebSocketError(message: string, payload?: unknown): void {
  if (AppEnvUtil.isProduction()) return;
  const body = formatWebSocketLogValue(payload);
  log.error(`${message}${body ? `\nbody:\n${body}` : ""}`);
}

function formatWebSocketLogValue(payload: unknown): string {
  if (payload instanceof Error) {
    return Logger.formatValue({
      message: payload.message,
      stack: payload.stack,
    });
  }
  return Logger.formatValue(payload);
}

/** 默认创建实例：从 env 读取 WS URL。 */
export function createChatWebSocket(token: string): ChatWebSocket {
  const base = getApiConfig().wsUrl;
  return new ChatWebSocket(base, token);
}
