import axios, {
  AxiosRequestConfig,
  AxiosResponse,
  InternalAxiosRequestConfig,
} from "axios";
import { config } from "./config";
import { logout, getRefreshTokenCookie, setRefreshTokenCookie } from "@/lib/cookies";
import { sharedQueryClient } from "@/app/providers";

const instance = axios.create({
  baseURL: config.apiBaseUrl,
  withCredentials: true, // send HttpOnly cookies automatically with cross-origin requests
});

// Flags to prevent multiple parallel requests from each triggering redirects
let isLoggingOut = false;
let isRedirectingSubscription = false;

// Token refresh state — ensures only one refresh request runs at a time
let isRefreshing = false;
let refreshSubscribers: ((success: boolean) => void)[] = [];

function onRefreshComplete(success: boolean) {
  refreshSubscribers.forEach((cb) => cb(success));
  refreshSubscribers = [];
}

function subscribeToRefresh(): Promise<boolean> {
  return new Promise((resolve) => {
    refreshSubscribers.push(resolve);
  });
}

async function attemptTokenRefresh(): Promise<boolean> {
  const refreshToken = getRefreshTokenCookie();
  if (!refreshToken) return false;

  try {
    // Use raw axios to avoid interceptor loops
    const res = await axios.put(
      `${config.apiBaseUrl}/v1/token/refresh`,
      new URLSearchParams({
        device_name: 'web',
        device_type: 'web',
        device_id: `web-${navigator.userAgent.slice(0, 50)}`,
      }),
      {
        headers: {
          'Content-Type': 'application/x-www-form-urlencoded',
          'Refresh-token': refreshToken,
        },
        withCredentials: true, // backend sets new HttpOnly access_token cookie
      },
    );

    const newRefreshToken = res.data?.data?.refresh_token;
    if (newRefreshToken) {
      setRefreshTokenCookie(newRefreshToken);
    }
    return true;
  } catch {
    return false;
  }
}

instance.interceptors.request.use(async (request: InternalAxiosRequestConfig) => {
  // Block new requests if we're already redirecting (prevents duplicate backend logs)
  if (isLoggingOut || isRedirectingSubscription) {
    const controller = new AbortController();
    controller.abort();
    request.signal = controller.signal;
    return request;
  }

  const isServer = typeof window === "undefined";
  const lang = !isServer
    ? localStorage.getItem(config.languageSupport) || "en"
    : "en";

  request.headers["Accept"] = "application/json";
  request.headers["Accept-Language"] = lang;

  // x-timezone removed — backend CORS only allows: Content-Type, Authorization, Refresh-token
  // Ask backend team to add x-timezone to Access-Control-Allow-Headers if needed

  return request;
});

instance.interceptors.response.use(
  (response: AxiosResponse) => {
    return response;
  },
  async (error) => {
    const { response } = error;

    // Handle 401 Unauthorized — attempt token refresh before logging out
    if (response?.status === 401 && typeof window !== 'undefined') {
      if (isLoggingOut) {
        return new Promise(() => {}); // Already logging out, silently drop this request
      }
      const path = window.location.pathname;
      const isAuthPage = path.includes('/login') || path.includes('/signup') || path.includes('/forgot-password') || path.includes('/reset-password') || path.includes('/accept-invitation') || path.includes('/verify-email') || path.includes('/bibliography');

      // Skip refresh for auth pages, already-retried requests, and callers
      // that opt out (e.g., landing page session probe — see Header.tsx)
      if (!isAuthPage && !error.config?._isRetryAfterRefresh && !error.config?._skipAuthRedirect) {
        const errorMessage = response?.data?.message || '';
        const isSessionInactivity = errorMessage === 'auth.session_expired_inactivity';

        // Session inactivity timeout — skip refresh, go straight to logout
        // Login page reads 'auth_logout_reason' from sessionStorage and shows localized banner
        if (isSessionInactivity) {
          isLoggingOut = true;
          sharedQueryClient?.cancelQueries();
          sessionStorage.setItem('auth_logout_reason', 'session_expired');
          logout();
          return new Promise(() => {});
        }

        // If a refresh is already in progress, wait for it
        if (isRefreshing) {
          const success = await subscribeToRefresh();
          if (success) {
            error.config._isRetryAfterRefresh = true;
            return instance(error.config);
          }
        } else {
          // Attempt token refresh
          isRefreshing = true;
          const success = await attemptTokenRefresh();
          isRefreshing = false;
          onRefreshComplete(success);

          if (success) {
            error.config._isRetryAfterRefresh = true;
            return instance(error.config);
          }
        }

        // Refresh failed — log out
        isLoggingOut = true;
        sharedQueryClient?.cancelQueries();
        sessionStorage.setItem('auth_logout_reason', 'session_expired');
        logout();
        return new Promise(() => {}); // Prevent further error handling while redirecting
      }
    }

    // Handle subscription 403 errors
    // Use isRedirectingSubscription flag to prevent multiple parallel requests from each triggering redirect
    if (response?.status === 403 && typeof window !== 'undefined') {
      if (isRedirectingSubscription) {
        return new Promise(() => {}); // Already redirecting, silently drop this request
      }
      const errorCode = response?.data?.error;
      const errorMessage = response?.data?.message || '';
      const path = window.location.pathname;
      const locale = path.split('/')[1] || 'en';

      // Subscription-related force-logouts. Backend distinguishes between:
      //   • complimentary_revoked  — admin manually revoked the comp grant
      //   • complimentary_expired  — cron auto-expired a comp grant
      //   • cancelled_access_ended — paid sub past Stripe billing period
      // Each gets a distinct logout reason so the login page can show the right banner.
      // JWT is still valid server-side until backend's PAT-kill ships; FE force-logout
      // ensures the user lands on /login with a clean state in the meantime.
      const subscriptionLogoutReasons: Record<string, string> = {
        'subscriptions.complimentary_revoked': 'complimentary_revoked',
        'subscriptions.complimentary_expired': 'complimentary_expired',
        'subscriptions.cancelled_access_ended': 'subscription_ended',
      };
      const logoutReason = subscriptionLogoutReasons[errorMessage];
      if (logoutReason && !isLoggingOut) {
        isLoggingOut = true;
        sharedQueryClient?.cancelQueries();
        sessionStorage.setItem('auth_logout_reason', logoutReason);
        logout();
        return new Promise(() => {});
      }

      // Helper to redirect once and block further 403 handling
      const redirectToSubscription = (url: string) => {
        isRedirectingSubscription = true;
        sharedQueryClient?.cancelQueries();
        window.location.href = url;
        return new Promise(() => {});
      };

      const isTrialRequired =
        errorCode === 'TRIAL_REQUIRED' ||
        errorCode === 'SUBSCRIPTION_EXPIRED' ||
        errorCode === 'TRIAL_LIMIT_REACHED' ||
        errorMessage === 'subscriptions.trial_required';

      const isSubscriptionRequired =
        errorMessage.toLowerCase().includes('subscription required') ||
        errorMessage.toLowerCase().includes('group subscription');

      if (isTrialRequired || isSubscriptionRequired) {
        if (path.includes('/practice-manager') && !path.includes('/practice-manager/subscription')) {
          return redirectToSubscription(`/${locale}/practice-manager/subscription`);
        }
        if (path.includes('/practitioner') && !path.includes('/practitioner/subscription')) {
          return redirectToSubscription(`/${locale}/practitioner/subscription`);
        }
        if (path.includes('/individual') && !path.includes('/individual/subscription')) {
          return redirectToSubscription(`/${locale}/individual/subscription`);
        }
      }

      // Handle expired / cancelled / inactive subscription errors
      const reasonMap: Record<string, string> = {
        'subscriptions.expired': 'expired',
        'subscriptions.cancelled_access_ended': 'cancelled',
        'subscriptions.inactive': 'inactive',
      };
      const reason = reasonMap[errorMessage];
      if (reason) {
        if (path.includes('/practice-manager') && !path.includes('/practice-manager/subscription')) {
          return redirectToSubscription(`/${locale}/practice-manager/subscription?reason=${reason}`);
        }
        if (path.includes('/practitioner') && !path.includes('/practitioner/subscription')) {
          return redirectToSubscription(`/${locale}/practitioner/subscription?reason=${reason}`);
        }
        if (path.includes('/individual') && !path.includes('/individual/subscription')) {
          return redirectToSubscription(`/${locale}/individual/subscription?reason=${reason}`);
        }
      }
    }

    // Handle network errors (no response from server)
    if (!response) {
      const networkError = new Error("Unable to connect to server. Please try again later.");
      (networkError as any).code = error.code;
      (networkError as any).isNetworkError = true;
      throw networkError;
    }

    // Preserve the original error object for React Query
    throw error;
  }
);

export interface CancellablePromise<T> extends Promise<T> {
  cancel?: () => void;
}

// Deduplicate identical concurrent GET requests — same URL + params share one network call
const pendingGetRequests = new Map<string, Promise<any>>();

const getRequestKey = (requestConfig: AxiosRequestConfig, options?: AxiosRequestConfig): string | null => {
  const method = (requestConfig.method || options?.method || 'GET').toUpperCase();
  if (method !== 'GET') return null; // Only deduplicate GET requests
  const url = requestConfig.url || '';
  const params = JSON.stringify(requestConfig.params || options?.params || {});
  return `${method}:${url}:${params}`;
};

export const customInstance = <T>(
  requestConfig: AxiosRequestConfig,
  options?: AxiosRequestConfig
): CancellablePromise<T> => {
  const source = axios.CancelToken.source();
  const dedupeKey = getRequestKey(requestConfig, options);

  // If an identical GET request is already in-flight, reuse it
  if (dedupeKey && pendingGetRequests.has(dedupeKey)) {
    const existing = pendingGetRequests.get(dedupeKey)! as CancellablePromise<T>;
    return existing;
  }

  const promise = instance({
    ...requestConfig,
    ...options,
    cancelToken: source.token,
  }).then(({ data, headers }) => {
    const roleAccess = headers["x-role-access"];
    if (roleAccess) {
      return { ...data, role: roleAccess };
    } else {
      return data;
    }
  }).finally(() => {
    if (dedupeKey) pendingGetRequests.delete(dedupeKey);
  });

  if (dedupeKey) pendingGetRequests.set(dedupeKey, promise);

  (promise as CancellablePromise<T>).cancel = () => {
    source.cancel("Query was cancelled");
  };

  return promise as CancellablePromise<T>;
};

export { instance };
