import { API_BASE_URL } from "./config";
import { HttpError } from "./errors";
import { serializeQuery } from "./query";
import { createRequestId } from "./requestId";
import type {
  ApiSuccessResponse,
  BlobResult,
  JsonResult,
  QueryParams,
} from "./types";
import { publishAuthExpired } from "../auth/authEvents";
import { tokenStore } from "../auth/tokenStore";

type HttpMethod = "GET" | "POST" | "PUT" | "DELETE";
type FetchImplementation = typeof fetch;

export interface RequestOptions {
  query?: QueryParams;
  headers?: HeadersInit;
  signal?: AbortSignal;
  auth?: boolean;
}

export interface BodyRequestOptions extends RequestOptions {
  body?: unknown | FormData;
}

export interface HttpClientOptions {
  baseUrl?: string;
  fetchImpl?: FetchImplementation;
  tokenProvider?: () => string | null;
  requestIdFactory?: () => string;
}

function joinUrl(baseUrl: string, path: string, query?: QueryParams): string {
  const normalizedBase = baseUrl.replace(/\/+$/, "");
  const normalizedPath = path.replace(/^\/+/, "");
  const url = normalizedPath ? `${normalizedBase}/${normalizedPath}` : normalizedBase;
  const serialized = serializeQuery(query);
  if (!serialized) return url;
  return `${url}${url.includes("?") ? "&" : "?"}${serialized}`;
}

function isRecord(value: unknown): value is Record<string, unknown> {
  return !!value && typeof value === "object";
}

function parseJson(text: string): unknown {
  if (!text.trim()) return undefined;
  try {
    return JSON.parse(text) as unknown;
  } catch {
    return undefined;
  }
}

function responseRequestId(
  response: Response,
  body: unknown,
  sentRequestId: string,
): string {
  const headerRequestId = response.headers.get("X-Request-Id");
  if (headerRequestId) return headerRequestId;
  if (isRecord(body) && typeof body.requestId === "string") return body.requestId;
  return sentRequestId;
}

function errorFromResponse(
  response: Response,
  body: unknown,
  rawText: string,
  sentRequestId: string,
): HttpError {
  const requestId = responseRequestId(response, body, sentRequestId);
  if (
    isRecord(body) &&
    typeof body.code === "string" &&
    body.code !== "OK" &&
    typeof body.message === "string"
  ) {
    return new HttpError({
      httpStatus: response.status,
      code: body.code,
      message: body.message,
      details: "details" in body ? body.details : null,
      requestId,
    });
  }

  const plainMessage = rawText.trim();
  const fallbackByStatus: Record<number, string> = {
    413: "文件或请求体过大",
    415: "文件类型或媒体格式不受支持",
  };
  return new HttpError({
    httpStatus: response.status,
    code: "INTERNAL_ERROR",
    message: plainMessage || fallbackByStatus[response.status] || `HTTP请求失败（${response.status}）`,
    details: null,
    requestId,
  });
}

function handleUnauthorized(requestId: string): void {
  tokenStore.clearAccessToken();
  publishAuthExpired({ requestId });
}

export class HttpClient {
  private readonly baseUrl: string;
  private readonly fetchImpl: FetchImplementation;
  private readonly tokenProvider: () => string | null;
  private readonly requestIdFactory: () => string;

  constructor(options: HttpClientOptions = {}) {
    this.baseUrl = options.baseUrl ?? API_BASE_URL;
    this.fetchImpl = options.fetchImpl ?? globalThis.fetch.bind(globalThis);
    this.tokenProvider = options.tokenProvider ?? (() => tokenStore.getAccessToken());
    this.requestIdFactory = options.requestIdFactory ?? createRequestId;
  }

  get<T>(path: string, options?: RequestOptions): Promise<JsonResult<T>> {
    return this.requestJson<T>("GET", path, options);
  }

  post<T>(
    path: string,
    body?: unknown | FormData,
    options?: RequestOptions,
  ): Promise<JsonResult<T>> {
    return this.requestJson<T>("POST", path, { ...options, body });
  }

  put<T>(
    path: string,
    body?: unknown | FormData,
    options?: RequestOptions,
  ): Promise<JsonResult<T>> {
    return this.requestJson<T>("PUT", path, { ...options, body });
  }

  delete<T>(
    path: string,
    options?: BodyRequestOptions,
  ): Promise<JsonResult<T>> {
    return this.requestJson<T>("DELETE", path, options);
  }

  async requestJson<T>(
    method: HttpMethod,
    path: string,
    options: BodyRequestOptions = {},
  ): Promise<JsonResult<T>> {
    const { response, sentRequestId } = await this.send(method, path, options);
    const rawText = await response.text();
    const body = parseJson(rawText);
    const requestId = responseRequestId(response, body, sentRequestId);

    if (!response.ok) {
      if (response.status === 401 && options.auth !== false) {
        handleUnauthorized(requestId);
      }
      throw errorFromResponse(response, body, rawText, sentRequestId);
    }

    if (
      !isRecord(body) ||
      body.code !== "OK" ||
      typeof body.message !== "string" ||
      !("data" in body)
    ) {
      if (isRecord(body) && typeof body.code === "string" && body.code !== "OK") {
        throw errorFromResponse(response, body, rawText, sentRequestId);
      }
      throw new HttpError({
        httpStatus: response.status,
        code: "INTERNAL_ERROR",
        message: "后端成功响应不符合接口契约",
        details: null,
        requestId,
      });
    }

    const success = body as unknown as ApiSuccessResponse<T>;
    return {
      data: success.data,
      message: success.message,
      requestId,
      httpStatus: response.status,
      headers: response.headers,
    };
  }

  async getBlob(
    path: string,
    options: RequestOptions = {},
  ): Promise<BlobResult> {
    const { response, sentRequestId } = await this.send("GET", path, options);
    const contentType = response.headers.get("Content-Type");
    const isJson = contentType?.toLowerCase().includes("application/json") ?? false;

    if (!response.ok || isJson) {
      const rawText = await response.text();
      const body = parseJson(rawText);
      const requestId = responseRequestId(response, body, sentRequestId);
      if (response.status === 401 && options.auth !== false) {
        handleUnauthorized(requestId);
      }
      if (!response.ok || (isRecord(body) && body.code !== "OK")) {
        throw errorFromResponse(response, body, rawText, sentRequestId);
      }
      throw new HttpError({
        httpStatus: response.status,
        code: "INTERNAL_ERROR",
        message: "文件接口返回了非Blob成功响应",
        details: null,
        requestId,
      });
    }

    const requestId = responseRequestId(response, undefined, sentRequestId);
    return {
      blob: await response.blob(),
      contentType,
      contentDisposition: response.headers.get("Content-Disposition"),
      requestId,
      httpStatus: response.status,
      headers: response.headers,
    };
  }

  private async send(
    method: HttpMethod,
    path: string,
    options: BodyRequestOptions,
  ): Promise<{ response: Response; sentRequestId: string }> {
    const headers = new Headers(options.headers);
    const sentRequestId =
      headers.get("X-Request-Id") ?? this.requestIdFactory();
    headers.set("X-Request-Id", sentRequestId);

    if (options.auth !== false && !headers.has("Authorization")) {
      const accessToken = this.tokenProvider();
      if (accessToken) headers.set("Authorization", `Bearer ${accessToken}`);
    }

    let body: BodyInit | undefined;
    if (options.body instanceof FormData) {
      body = options.body;
      headers.delete("Content-Type");
    } else if (options.body !== undefined) {
      body = JSON.stringify(options.body);
      if (!headers.has("Content-Type")) {
        headers.set("Content-Type", "application/json");
      }
    }

    try {
      const response = await this.fetchImpl(
        joinUrl(this.baseUrl, path, options.query),
        {
          method,
          headers,
          body,
          signal: options.signal,
        },
      );
      return { response, sentRequestId };
    } catch (cause) {
      throw new HttpError({
        httpStatus: 0,
        code: "INTERNAL_ERROR",
        message:
          cause instanceof DOMException && cause.name === "AbortError"
            ? "请求已取消"
            : "网络请求失败",
        details: null,
        requestId: sentRequestId,
        cause,
      });
    }
  }
}

export const httpClient = new HttpClient();
