import axios from "axios";
import type { AxiosError, AxiosRequestConfig } from "axios";
import {
  clearAuthTokens,
  getAccessToken,
  getRefreshToken,
  isAccessTokenExpired,
  storeAuthTokens,
} from "@/constants/token";
import type { OAuthTokenResponse } from "@/constants/token";
import { readStoredLocale } from "@/i18n/storage";
import { defaultLocale } from "@/i18n/types";
import type {
  ApiResponse,
  NormalizedError,
  NormalizedErrorKind,
  RequestContext,
  RequestOptions,
} from "@/shared/request-types";

function resolveBaseURL(): string {
  const baseURL = (import.meta.env as Record<string, unknown>)
    .VITE_API_BASE_URL;
  if (typeof baseURL !== "string" || baseURL.trim() === "") {
    throw new Error("Missing VITE_API_BASE_URL");
  }
  return baseURL;
}

function resolveTimeout(): number {
  const raw = (import.meta.env as Record<string, unknown>).VITE_API_TIMEOUT_MS;
  if (typeof raw !== "string" || raw.trim() === "") {
    return 15000;
  }
  const parsed = Number.parseInt(raw, 10);
  return Number.isFinite(parsed) ? parsed : 15000;
}

const http = axios.create({
  baseURL: resolveBaseURL(),
  timeout: resolveTimeout(),
});

const OAUTH_TOKEN_URL = "/auth/oauth2/token";
const OAUTH_BASIC_AUTHORIZATION = "Basic Y3VzdG9tOmN1c3RvbQ==";
const AUTH_REFRESH_ERROR_STATUSES = new Set([400, 401, 403]);

let context: RequestContext = {
  token: getAccessToken(),
  clientId: import.meta.env.VITE_CLIENT_ID ?? null,
  latitude: null,
  longitude: null,
  language: null,
};

let refreshAccessTokenPromise: Promise<string> | null = null;

export function setAuthToken(token: string | null): void {
  context = { ...context, token };
}

export function setClientId(clientId: string | null): void {
  context = { ...context, clientId };
}

export function setLocation(
  latitude: number | null,
  longitude: number | null,
): void {
  context = { ...context, latitude, longitude };
}

export function setLanguage(language: string | null): void {
  context = { ...context, language };
}

export function getRequestContext(): RequestContext {
  return { ...context };
}

class RequestError extends Error implements NormalizedError {
  kind: NormalizedErrorKind;
  code?: number;
  httpStatus?: number;
  response?: unknown;

  constructor(params: {
    message: string;
    kind: NormalizedErrorKind;
    code?: number;
    httpStatus?: number;
    response?: unknown;
  }) {
    super(params.message);
    this.name = "RequestError";
    this.kind = params.kind;
    this.code = params.code;
    this.httpStatus = params.httpStatus;
    this.response = params.response;
  }
}

function resolveRequestLanguage(): string | null {
  if (context.language) {
    return context.language;
  }
  return readStoredLocale() ?? defaultLocale;
}

function buildContextHeaders(options?: RequestOptions): Record<string, string> {
  const headers: Record<string, string> = {};

  const token = context.token ?? getAccessToken();
  if (!options?.skipAuth && token) {
    headers.Authorization = `Bearer ${token}`;
  }

  if (context.clientId) {
    headers.clientId = context.clientId;
  }

  if (context.latitude !== null && Number.isFinite(context.latitude)) {
    headers["X-Latitude"] = String(context.latitude);
  }

  if (context.longitude !== null && Number.isFinite(context.longitude)) {
    headers["X-Longitude"] = String(context.longitude);
  }

  const language = resolveRequestLanguage();
  if (language) {
    headers.language = language;
  }

  return headers;
}

function buildRefreshTokenParams(refreshToken: string): URLSearchParams {
  const params = new URLSearchParams();
  params.append("grant_type", "refresh_token");
  params.append("refresh_token", refreshToken);
  return params;
}

async function refreshAccessToken(): Promise<string> {
  if (refreshAccessTokenPromise) {
    return refreshAccessTokenPromise;
  }

  refreshAccessTokenPromise = (async () => {
    const refreshToken = getRefreshToken();
    if (!refreshToken) {
      clearAuthTokens();
      setAuthToken(null);
      throw new RequestError({
        kind: "http",
        httpStatus: 401,
        message: "Missing refresh token",
      });
    }

    try {
      const res = await http.request<OAuthTokenResponse>({
        url: OAUTH_TOKEN_URL,
        method: "post",
        data: buildRefreshTokenParams(refreshToken),
        headers: {
          ...buildContextHeaders({ skipAuth: true }),
          "Content-Type": "application/x-www-form-urlencoded",
          Authorization: OAUTH_BASIC_AUTHORIZATION,
        },
      });
      const tokens = storeAuthTokens(res.data);
      setAuthToken(tokens.accessToken);
      return tokens.accessToken;
    } catch (e: unknown) {
      if (
        axios.isAxiosError(e) &&
        e.response &&
        AUTH_REFRESH_ERROR_STATUSES.has(e.response.status)
      ) {
        clearAuthTokens();
        setAuthToken(null);
      }
      throw e;
    }
  })().finally(() => {
    refreshAccessTokenPromise = null;
  });

  return refreshAccessTokenPromise;
}

async function prepareAuthToken(options: RequestOptions): Promise<void> {
  if (options.skipAuth) {
    return;
  }

  const accessToken = getAccessToken();
  if (!accessToken) {
    setAuthToken(null);
    if (getRefreshToken()) {
      await refreshAccessToken();
    }
    return;
  }

  setAuthToken(accessToken);
  if (isAccessTokenExpired()) {
    await refreshAccessToken();
  }
}

function shouldRetryUnauthorized(
  error: unknown,
  options: RequestOptions,
): boolean {
  return (
    !options.skipAuth &&
    axios.isAxiosError(error) &&
    error.response?.status === 401 &&
    Boolean(getRefreshToken())
  );
}

async function sendHttpRequest<T>(
  config: AxiosRequestConfig,
  options: RequestOptions,
  allowAuthRetry = true,
) {
  try {
    await prepareAuthToken(options);
    const ctxHeaders = buildContextHeaders(options);
    const mergedHeaders: Record<string, string> = {
      ...ctxHeaders,
      ...((config.headers ?? {}) as Record<string, string>),
      ...(options.headers ?? {}),
    };

    return await http.request<T>({
      ...config,
      signal: options.signal ?? config.signal,
      headers: mergedHeaders,
    });
  } catch (e: unknown) {
    if (allowAuthRetry && shouldRetryUnauthorized(e, options)) {
      await refreshAccessToken();
      return sendHttpRequest<T>(config, options, false);
    }
    throw e;
  }
}

export async function ensureValidAuthToken(): Promise<string | null> {
  await prepareAuthToken({});
  return context.token ?? getAccessToken();
}

function isApiResponse(value: unknown): value is ApiResponse<unknown> {
  if (!value || typeof value !== "object") {
    return false;
  }
  const v = value as Record<string, unknown>;
  return typeof v.code === "number" && "data" in v;
}

function normalizeAxiosError(error: AxiosError): RequestError {
  if (error.response) {
    const httpStatus = error.response.status;
    const message = error.message || `HTTP ${httpStatus}`;
    return new RequestError({
      kind: "http",
      httpStatus,
      message,
      response: error.response.data,
    });
  }

  return new RequestError({
    kind: "network",
    message: error.message || "Network Error",
  });
}

export async function request<T>(
  config: AxiosRequestConfig,
  options: RequestOptions = {},
): Promise<ApiResponse<T>> {
  try {
    const res = await sendHttpRequest<ApiResponse<T>>(config, options);
    const body = res.data as unknown;
    if (!isApiResponse(body)) {
      throw new RequestError({
        kind: "unknown",
        message: "Unexpected response shape",
        response: body,
      });
    }

    if (body.code !== 0) {
      throw new RequestError({
        kind: "business",
        code: body.code,
        message: body.message || "Business Error",
        response: body,
      });
    }

    return body as ApiResponse<T>;
  } catch (e: unknown) {
    if (e instanceof RequestError) {
      throw e;
    }

    if (axios.isAxiosError(e)) {
      throw normalizeAxiosError(e);
    }

    const err = e instanceof Error ? e : new Error(String(e));
    throw new RequestError({
      kind: "unknown",
      message: err.message || "Unknown Error",
      response: err,
    });
  }
}

export async function requestRaw<T>(
  config: AxiosRequestConfig,
  options: RequestOptions = {},
): Promise<T> {
  try {
    const res = await sendHttpRequest<T>(config, options);
    return res.data;
  } catch (e: unknown) {
    if (e instanceof RequestError) {
      throw e;
    }

    if (axios.isAxiosError(e)) {
      throw normalizeAxiosError(e);
    }

    const err = e instanceof Error ? e : new Error(String(e));
    throw new RequestError({
      kind: "unknown",
      message: err.message || "Unknown Error",
      response: err,
    });
  }
}

export async function requestData<T>(
  config: AxiosRequestConfig,
  options: RequestOptions = {},
): Promise<T> {
  const res = await request<T>(config, options);
  return res.data;
}
