import {
  AxiosError,
  AxiosResponse,
  InternalAxiosRequestConfig,
} from "axios";
import Cookies from "js-cookie";
import { toast } from "react-toastify";
import { getLogoutCallback } from "../contexts/User.ctx";
import { API_ENDPOINTS, api } from "../lib/api";
import { COOKIE_KEYS } from "../lib/shared/cookies";

const SIGN_IN_PATH = "/auth/sign-in";

type RetriableRequest = InternalAxiosRequestConfig & { _retry?: boolean };

interface TokenPair {
  access_token: string;
  refresh_token: string;
}

interface ApiErrorData {
  message?: string;
  error?: {
    code?: string;
    message?: string;
    details?: string;
  };
}

interface QueuedRequest {
  resolve: (token: string) => void;
  reject: (reason?: unknown) => void;
}

// ---------------------------------------------------------------------------
// Refresh-token queue: requests that arrive mid-refresh wait here and replay
// once a fresh access token is available (or fail together if refresh fails).
// ---------------------------------------------------------------------------
let isRefreshing = false;
let failedQueue: QueuedRequest[] = [];

const processQueue = (error: unknown, token: string | null = null) => {
  failedQueue.forEach(({ resolve, reject }) => {
    if (error) {
      reject(error);
    } else if (token) {
      resolve(token);
    }
  });
  failedQueue = [];
};

const queueRequest = (
  originalRequest: RetriableRequest,
): Promise<AxiosResponse> =>
  new Promise<string>((resolve, reject) => {
    failedQueue.push({ resolve, reject });
  }).then((token) => {
    originalRequest.headers.Authorization = `Bearer ${token}`;
    return api(originalRequest);
  });

// ---------------------------------------------------------------------------
// Session / auth helpers
// ---------------------------------------------------------------------------
const clearLocalSession = () => {
  Cookies.remove(COOKIE_KEYS.access_token);
  Cookies.remove(COOKIE_KEYS.refresh_token);
  Cookies.remove(COOKIE_KEYS.user);

  if (typeof window !== "undefined") {
    localStorage.removeItem("auth-token");
    localStorage.removeItem("user-data");
  }
};

const redirectToSignIn = () => {
  if (typeof window !== "undefined") {
    window.location.href = SIGN_IN_PATH;
  }
};

/** Logs the user out via the registered callback, falling back to a manual reset. */
const forceLogout = () => {
  const logout = getLogoutCallback();
  if (logout) {
    logout();
    return;
  }

  clearLocalSession();
  redirectToSignIn();
};

// ---------------------------------------------------------------------------
// Error reporting
// ---------------------------------------------------------------------------
/**
 * Builds the message shown in the error toast, appending the API-provided
 * `details` (e.g. field-level validation hints) to the main error message
 * when available.
 */
const buildErrorMessage = (data: unknown, fallback: string): string => {
  const payload = data as ApiErrorData | undefined;
  const message = payload?.error?.message || payload?.message || fallback;
  const details = payload?.error?.details;

  return details ? `${message}: ${details}` : message;
};

const notifyError = (error: unknown) => {
  const data = (error as AxiosError).response?.data;
  toast.error(buildErrorMessage(data, (error as Error).message));
};

// ---------------------------------------------------------------------------
// Token refresh
// ---------------------------------------------------------------------------
const requestNewAccessToken = async (refreshToken: string): Promise<string> => {
  const response = await api.post(API_ENDPOINTS.USER.REFRESH_TOKEN, {
    refresh_token: refreshToken,
  });

  const { access_token, refresh_token } = response.data.data as TokenPair;

  if (!access_token || !refresh_token) {
    throw new Error("Invalid token received from refresh endpoint.");
  }

  Cookies.set(COOKIE_KEYS.access_token, access_token);
  Cookies.set(COOKIE_KEYS.refresh_token, refresh_token);

  return access_token;
};

/**
 * Handles a 401 by refreshing the access token and replaying the request.
 * Concurrent requests are queued until the in-flight refresh settles.
 */
const handleUnauthorized = async (originalRequest: RetriableRequest) => {
  if (isRefreshing) {
    return queueRequest(originalRequest);
  }

  originalRequest._retry = true;
  isRefreshing = true;

  try {
    const refreshToken = Cookies.get(COOKIE_KEYS.refresh_token);
    if (!refreshToken) {
      forceLogout();
      return Promise.reject(new Error("Refresh token not found."));
    }

    const accessToken = await requestNewAccessToken(refreshToken);
    processQueue(null, accessToken);
    originalRequest.headers.Authorization = `Bearer ${accessToken}`;
    return api(originalRequest);
  } catch (refreshError) {
    processQueue(refreshError, null);
    forceLogout();
    notifyError(refreshError);
    return Promise.reject(refreshError);
  } finally {
    isRefreshing = false;
  }
};

// ---------------------------------------------------------------------------
// Request predicates
// ---------------------------------------------------------------------------
const matchesEndpoint = (request: RetriableRequest | undefined, endpoint: string) =>
  request?.url?.includes(endpoint) ?? false;

// Login errors must surface to the caller instead of triggering a refresh.
const isLoginRequest = (request?: RetriableRequest) =>
  matchesEndpoint(request, API_ENDPOINTS.USER.LOGIN);

const isRefreshTokenRequest = (request?: RetriableRequest) =>
  matchesEndpoint(request, API_ENDPOINTS.USER.REFRESH_TOKEN);

// ---------------------------------------------------------------------------
// Interceptors
// ---------------------------------------------------------------------------
export const responseInterceptor = (response: AxiosResponse) => response;

export const responseErrorHandler = (error: AxiosError) => {
  const originalRequest = error.config as RetriableRequest | undefined;
  const isUnauthorized = error.response?.status === 401;

  // A 401 from the refresh endpoint itself means the session is unrecoverable.
  if (isUnauthorized && isRefreshTokenRequest(originalRequest)) {
    forceLogout();
    return Promise.reject(error);
  }

  if (
    isUnauthorized &&
    originalRequest &&
    !originalRequest._retry &&
    !isLoginRequest(originalRequest)
  ) {
    return handleUnauthorized(originalRequest);
  }

  notifyError(error);
  return Promise.reject(error);
};
