import { ApiError, isStructuredApiErrorPayload, toApiError } from "./errors";

const API_ROOT = "/api";

function buildUrl(path: string): string {
  const normalizedPath = path.startsWith("/") ? path : `/${path}`;
  return `${API_ROOT}${normalizedPath}`;
}

async function parseResponseBody(response: Response): Promise<unknown> {
  const text = await response.text();
  if (!text.trim()) {
    return null;
  }

  const contentType = response.headers.get("content-type") ?? "";
  if (contentType.includes("application/json")) {
    try {
      return JSON.parse(text) as unknown;
    } catch {
      return null;
    }
  }

  const trimmed = text.trim();
  if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
    try {
      return JSON.parse(text) as unknown;
    } catch {
      return null;
    }
  }

  return null;
}

const GATEWAY_RETRY_STATUSES = new Set([502, 503, 504]);

/** Повтор при «пустом» 404: часто бывает при нескольких инстансах/прокси до полного деплоя. */
function shouldRetryIdempotentError(status: number, payload: unknown): boolean {
  if (GATEWAY_RETRY_STATUSES.has(status)) {
    return true;
  }
  return status === 404 && !isStructuredApiErrorPayload(payload);
}

function delay(ms: number): Promise<void> {
  return new Promise((resolve) => {
    setTimeout(resolve, ms);
  });
}

export async function requestJson<T>(path: string, init?: RequestInit): Promise<T> {
  const method = (init?.method ?? "GET").toUpperCase();
  const idempotent = method === "GET" || method === "HEAD";
  const maxAttempts = idempotent ? 3 : 1;

  for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
    try {
      const defaultHeaders: Record<string, string> = {};
      if (method !== "GET" && method !== "HEAD") {
        defaultHeaders["Content-Type"] = "application/json";
      }

      const response = await fetch(buildUrl(path), {
        ...init,
        headers: {
          ...defaultHeaders,
          ...(init?.headers ?? {}),
        },
      });

      if (response.status === 204) {
        return undefined as T;
      }

      const payload = await parseResponseBody(response);

      if (!response.ok) {
        const retryable = idempotent && attempt < maxAttempts && shouldRetryIdempotentError(response.status, payload);
        if (retryable) {
          await delay(80 * attempt);
          continue;
        }
        throw toApiError(response.status, payload);
      }

      return payload as T;
    } catch (error) {
      if (error instanceof ApiError) {
        throw error;
      }
      if (error instanceof DOMException && error.name === "AbortError") {
        throw error;
      }
      const retryable = idempotent && attempt < maxAttempts;
      if (retryable) {
        await delay(80 * attempt);
        continue;
      }
      throw new ApiError({
        code: "network_error",
        status: null,
        message: "Не удалось связаться с сервером.",
      });
    }
  }

  throw new ApiError({
    code: "network_error",
    status: null,
    message: "Не удалось связаться с сервером.",
  });
}
