import axios from "axios";
import toast from "react-hot-toast";

import { API_BASE_URL } from "@/constants/apiConfig";
import { captureApiError } from "@/lib/posthog";
import {
  AUTH_TOKEN_COOKIE,
  REFRESH_TOKEN_COOKIE,
  clearSessionCookies,
  getCookie,
  setCookie,
} from "./cookies";
import type { AuthTokens } from "./types";
import { extractApiErrorPayload } from "@/components/errors/ApiErrorModal";

declare module "axios" {
  export interface AxiosRequestConfig {
    /**
     * When true, the response interceptor does NOT raise the global error
     * toast for this request's failure. For calls the caller handles itself —
     * e.g. a probe that is expected to 404 before falling back (GL clearance
     * detail tries /contracts/:id then /bookings/:id). The rejection still
     * propagates.
     */
    suppressErrorModal?: boolean;
  }
}

type RetriableRequest = {
  _retry?: boolean;
  headers?: Record<string, string>;
  url?: string;
  suppressErrorModal?: boolean;
};

const api = axios.create({
  baseURL: `${API_BASE_URL}/api`,
  withCredentials: true,
});

let refreshPromise: Promise<AuthTokens> | null = null;

const applyTokens = ({ token, refreshToken }: AuthTokens) => {
  setCookie(AUTH_TOKEN_COOKIE, token);
  setCookie(REFRESH_TOKEN_COOKIE, refreshToken);
};

/**
 * Single-flight token refresh: concurrent callers (the 401 interceptor and
 * the proactive scheduler) share one in-flight request so the refresh token
 * is only rotated once. Throws if no refresh token is stored or the server
 * rejects it — callers decide how to end the session.
 */
const refreshSessionTokens = async (): Promise<AuthTokens> => {
  const refreshToken = getCookie(REFRESH_TOKEN_COOKIE);
  if (!refreshToken) {
    throw new Error("missing refresh token");
  }

  refreshPromise ??= api
    .post<AuthTokens>("/auth/refresh-token", { refreshToken })
    .then((response) => response.data)
    .finally(() => {
      refreshPromise = null;
    });

  const tokens = await refreshPromise;
  applyTokens(tokens);
  return tokens;
};

api.interceptors.request.use((config) => {
  const token = getCookie(AUTH_TOKEN_COOKIE);

  if (token) {
    config.headers.Authorization = `Bearer ${token}`;
  }

  return config;
});

api.interceptors.response.use(
  (response) => {
    if (
      response.data &&
      typeof response.data === "object" &&
      "success" in response.data &&
      "data" in response.data
    ) {
      response.data = response.data.data;
    }

    return response;
  },
  async (error) => {
    const originalRequest = error.config as RetriableRequest | undefined;

    // Report the failure to PostHog, including on suppressErrorModal paths —
    // those opt out of the user-facing toast, not of reporting.
    // 401s are skipped: an expired session is refreshed below, not a defect.
    if (!error.response || error.response.status !== 401) {
      captureApiError(error);
    }

    if (
      error.response?.status !== 401 ||
      !originalRequest ||
      originalRequest._retry ||
      originalRequest.url?.includes("/auth/login") ||
      originalRequest.url?.includes("/auth/mfa-verify") ||
      originalRequest.url?.includes("/auth/refresh-token")
    ) {
      // Surface the server's actual error message in a global toast — never
      // the error modal (401s are handled by the session-refresh flow, so skip
      // them). A request may opt out via `suppressErrorModal` when it handles
      // the failure itself.
      if (error.response && error.response.status !== 401) {
        const payload = extractApiErrorPayload(error);
        // Normalize the error's own `message` to the SERVER's actual message so
        // every downstream `toast.error(err.message)` handler shows the real
        // cause instead of "Request failed with status code NNN". Applies even
        // on suppressErrorModal paths — only the toast is opted out.
        if (payload?.messages.length) {
          const message = payload.messages.join("\n");
          (error as { message?: string }).message = message;
          // Keyed by message so a retried request replaces its toast instead
          // of stacking duplicates.
          if (!originalRequest?.suppressErrorModal) {
            toast.error(message, { id: message });
          }
        }
      }
      return Promise.reject(error);
    }

    if (!getCookie(REFRESH_TOKEN_COOKIE)) {
      clearSessionCookies();
      return Promise.reject(error);
    }

    originalRequest._retry = true;

    try {
      const tokens = await refreshSessionTokens();
      originalRequest.headers = {
        ...originalRequest.headers,
        Authorization: `Bearer ${tokens.token}`,
      };

      return api(originalRequest);
    } catch (refreshError) {
      clearSessionCookies();
      window.location.replace("/auth");
      return Promise.reject(refreshError);
    }
  },
);

export { api, applyTokens, refreshSessionTokens };
