/**
 * 千问 / 聊天补全流式接口（SSE）。
 *
 * 使用 `@microsoft/fetch-event-source`，不走 axios，因此需在此自行组装请求头
 *（含 `Authorization`，与登录后 `localStorage` 中的 `accessToken` 一致）。
 */
import { fetchEventSource } from "@microsoft/fetch-event-source";
import {
  ACCESS_TOKEN_KEY,
  REFRESH_TOKEN_KEY,
  invalidateSessionAndGoLogin,
  persistTokens,
  refreshTokens,
  SessionExpiredError,
} from "./auth";
import { ApiPath } from "./paths";

/** 携带 HTTP 状态码，便于流式 `onopen` 与外层续签逻辑区分 */
class StreamHttpError extends Error {
  readonly status: number;
  constructor(status: number, message: string) {
    super(message);
    this.name = "StreamHttpError";
    this.status = status;
  }
}

export type ChatRole = "user" | "assistant" | "system";

export interface ChatMessagePayload {
  role: ChatRole;
  content: string;
}

export interface StreamOptions {
  messages: ChatMessagePayload[];
  /** OpenAI 兼容字段；不传则由服务端默认模型 */
  model?: string;
  /** 与会话绑定流式补全时传入（若后端支持） */
  sessionId?: string;
  /** 是否启用联网搜索（OpenAPI: `enableWebSearch`） */
  enableWebSearch?: boolean;
  /** 是否启用深度思考（OpenAPI: `enableThinking`） */
  enableThinking?: boolean;
  onToken: (token: string) => void;
  signal?: AbortSignal;
  /** 超时后中止请求（与业务 `signal` 合并） */
  timeoutMs?: number;
}

const API_BASE_URL = import.meta.env.VITE_API_BASE_URL ?? "";

/** 流式 POST 的请求头：JSON + 可选 Bearer */
function streamRequestHeaders(): Record<string, string> {
  const headers: Record<string, string> = {
    "Content-Type": "application/json",
  };
  const accessToken = window.localStorage.getItem(ACCESS_TOKEN_KEY);
  if (accessToken) {
    headers.Authorization = `Bearer ${accessToken}`;
  }
  return headers;
}

/**
 * 从 SSE 单条 JSON 中解析增量文本（兼容多种后端字段）。
 */
function pickTokenFromJson(payload: Record<string, unknown>): string {
  const directDelta = payload.delta;
  if (typeof directDelta === "string") return directDelta;

  const directContent = payload.content;
  if (typeof directContent === "string") return directContent;

  const text = payload.text;
  if (typeof text === "string") return text;

  const choices = payload.choices;
  if (Array.isArray(choices) && choices.length > 0) {
    const first = choices[0] as Record<string, unknown>;
    const delta = first.delta as Record<string, unknown> | undefined;
    if (delta && typeof delta.content === "string") {
      return delta.content;
    }
    const message = first.message as Record<string, unknown> | undefined;
    if (message && typeof message.content === "string") {
      return message.content;
    }
  }

  return "";
}

/** 判断是否为用户/超时类中止，便于重试逻辑区分 */
export function isAbortLikeError(error: unknown): boolean {
  if (!error) return false;
  if (error instanceof DOMException && error.name === "AbortError") return true;
  if (error instanceof Error) {
    const message = error.message.toLowerCase();
    return message.includes("aborted") || message.includes("aborterror");
  }
  return false;
}

function createTimeoutAbortSignal(timeoutMs: number): AbortSignal {
  const controller = new AbortController();
  window.setTimeout(
    () => controller.abort(new DOMException("Request timeout", "AbortError")),
    timeoutMs,
  );
  return controller.signal;
}

function mergeAbortSignals(signals: Array<AbortSignal | undefined>): AbortSignal {
  const controller = new AbortController();
  const onAbort = (event: Event) => {
    const source = event.target as AbortSignal;
    if (!controller.signal.aborted) controller.abort(source.reason);
  };
  for (const signal of signals) {
    if (!signal) continue;
    if (signal.aborted) {
      controller.abort(signal.reason);
      break;
    }
    signal.addEventListener("abort", onAbort, { once: true });
  }
  return controller.signal;
}

function isStreamUnauthorized(error: unknown): boolean {
  return error instanceof StreamHttpError && error.status === 401;
}

/**
 * 建立 SSE 流式聊天连接，在 `onToken` 中持续收到增量文本。
 *
 * **401 续签**：首次 `onopen` 为 401 时，用 `refreshToken` 调刷新接口，写入新令牌后**自动重试一次**；
 * 若仍失败或无 `refreshToken`，则抛出原错误。
 */
export async function streamQwenChat(options: StreamOptions): Promise<void> {
  const connectOnce = async () => {
    const timeoutSignal = createTimeoutAbortSignal(options.timeoutMs ?? 90000);
    const mergedSignal = mergeAbortSignals([options.signal, timeoutSignal]);

    await fetchEventSource(`${API_BASE_URL}${ApiPath.chatCompletionsStream}`, {
      method: "POST",
      headers: streamRequestHeaders(),
      body: JSON.stringify({
        messages: options.messages,
        ...(options.model ? { model: options.model } : {}),
        ...(options.sessionId ? { sessionId: options.sessionId } : {}),
        enableWebSearch: !!options.enableWebSearch,
        enableThinking: !!options.enableThinking,
      }),
      signal: mergedSignal,
      openWhenHidden: true,
      async onopen(response) {
        if (!response.ok) {
          const errorText = await response.text();
          throw new StreamHttpError(
            response.status,
            errorText || `Stream request failed with status ${response.status}`,
          );
        }
      },
      onmessage(event) {
        const raw = event.data?.trim();
        if (!raw || raw === "[DONE]") return;

        try {
          const parsed = JSON.parse(raw) as Record<string, unknown>;
          const token = pickTokenFromJson(parsed);
          if (token) options.onToken(token);
        } catch {
          options.onToken(raw);
        }
      },
      onerror(error) {
        throw error;
      },
    });
  };

  try {
    await connectOnce();
  } catch (error) {
    if (!isStreamUnauthorized(error)) throw error;
    const storedRefresh = window.localStorage.getItem(REFRESH_TOKEN_KEY);
    if (!storedRefresh) {
      invalidateSessionAndGoLogin();
      throw new SessionExpiredError();
    }
    try {
      const refreshed = await refreshTokens(storedRefresh);
      persistTokens(refreshed.accessToken, refreshed.refreshToken);
      await connectOnce();
    } catch {
      invalidateSessionAndGoLogin();
      throw new SessionExpiredError();
    }
  }
}
