import axios, { AxiosInstance } from 'axios';
import { TokenStorage } from './tokenStorage';
import { logger } from '@/utils/logger';

// T0177: Créer un client axios séparé pour le refresh pour éviter les interceptors
// Lazy initialization pour faciliter les tests
let refreshClient: AxiosInstance | null = null;

// INT-016: Refresh proactif - rafraîchir 1 minute avant expiration (tokens expirent en 5 min)
// Action 5.1.1.5: Refresh every 4 minutes to ensure tokens don't expire
const PROACTIVE_REFRESH_BUFFER_MS = 1 * 60 * 1000; // 1 minute buffer
const PROACTIVE_REFRESH_INTERVAL_MS = 4 * 60 * 1000; // 4 minutes interval

// INT-016: Timer pour le refresh proactif (one-time)
let proactiveRefreshTimer: ReturnType<typeof setTimeout> | null = null;
// Action 5.1.1.5: Interval pour le refresh proactif périodique (every 4 minutes)
let proactiveRefreshInterval: ReturnType<typeof setInterval> | null = null;

function getRefreshClient(): AxiosInstance {
  if (!refreshClient) {
    const baseURL = (() => {
      const url = import.meta.env.VITE_API_URL;
      if (!url) {
        if (import.meta.env.PROD) {
          throw new Error('VITE_API_URL must be defined in production');
        }
        // Fallback uniquement en développement
        return 'http://127.0.0.1:8080/api/v1';
      }
      return url;
    })();

    refreshClient = axios.create({
      baseURL,
      timeout: 10000,
      headers: {
        'Content-Type': 'application/json',
      },
      // SECURITY: Activer withCredentials pour envoyer les cookies httpOnly lors du refresh
      withCredentials: true,
    });
  }
  return refreshClient;
}

/**
 * TokenRefresh - Service de rafraîchissement des tokens d'authentification
 * T0176: Service pour rafraîchir les tokens via l'endpoint /auth/refresh
 *
 * Format de réponse attendu du backend :
 * {
 *   "success": true,
 *   "data": {
 *     "access_token": "...",
 *     "refresh_token": "...",
 *     "expires_in": 3600
 *   }
 * }
 */

export interface RefreshTokenResponse {
  access_token: string;
  refresh_token: string;
  expires_in: number;
}

/**
 * INT-016: Décode un JWT pour extraire les claims (sans vérifier la signature)
 * Utilisé uniquement pour lire l'expiration, pas pour la validation de sécurité
 */
interface JWTPayload {
  exp?: number; // Expiration timestamp (seconds)
  iat?: number; // Issued at timestamp (seconds)
}

function decodeJWT(token: string): JWTPayload | null {
  try {
    const parts = token.split('.');
    if (parts.length !== 3) {
      return null;
    }
    // Décoder le payload (base64url)
    const payload = parts[1];
    const decoded = atob(payload.replace(/-/g, '+').replace(/_/g, '/'));
    return JSON.parse(decoded) as JWTPayload;
  } catch (error) {
    logger.warn('Failed to decode JWT', {
      error: error instanceof Error ? error.message : String(error),
      stack: error instanceof Error ? error.stack : undefined,
    });
    return null;
  }
}

/**
 * INT-016: Vérifie si un token est expiré ou proche de l'expiration
 * INT-AUTH-004: Exporté pour utilisation dans l'interceptor de requête
 * @param token - Token JWT à vérifier
 * @param bufferMs - Buffer en millisecondes avant expiration (défaut: 5 minutes)
 * @returns true si le token est expiré ou proche de l'expiration
 */
export function isTokenExpiringSoon(
  token: string | null,
  bufferMs: number = PROACTIVE_REFRESH_BUFFER_MS,
): boolean {
  if (!token) {
    return true;
  }

  const payload = decodeJWT(token);
  if (!payload || !payload.exp) {
    // Si on ne peut pas décoder, on considère qu'il faut rafraîchir
    return true;
  }

  const expirationTime = payload.exp * 1000; // Convertir en millisecondes
  const now = Date.now();
  const timeUntilExpiration = expirationTime - now;

  // Rafraîchir si expiré ou si expiration dans moins de bufferMs
  return timeUntilExpiration <= bufferMs;
}

/**
 * Rafraîchit le token d'accès en utilisant le refresh token
 * T0176: Appelle l'endpoint POST /api/v1/auth/refresh et met à jour les tokens
 * @returns Promise qui se résout quand le token est rafraîchi
 * @throws Error si le refresh token n'est pas disponible ou si le refresh échoue
 */
export async function refreshToken(): Promise<void> {
  // SECURITY: Détecter si le backend utilise des cookies httpOnly
  // Si le refresh token n'est pas dans localStorage mais que l'utilisateur est authentifié,
  // c'est qu'on utilise des cookies httpOnly
  const refreshTokenFromStorage = TokenStorage.getRefreshToken();
  const hasAccessToken = !!TokenStorage.getAccessToken();

  // Si pas de refresh token dans localStorage mais access token présent,
  // on suppose que le backend utilise des cookies httpOnly
  const useHttpOnlyCookies =
    (!refreshTokenFromStorage || refreshTokenFromStorage === 'cookie-based') &&
    hasAccessToken;

  // Vérifier qu'on a un moyen de rafraîchir (soit localStorage, soit cookies httpOnly)
  if (
    !useHttpOnlyCookies &&
    (!refreshTokenFromStorage || refreshTokenFromStorage.trim() === '')
  ) {
    throw new Error('No refresh token available');
  }

  try {
    // T0176: Appeler l'endpoint POST /auth/refresh
    // T0177: Utiliser refreshClient pour éviter les interceptors (qui causeraient une boucle)
    const client = getRefreshClient();

    // SECURITY: Si on utilise des cookies httpOnly, ne pas envoyer le refresh token dans le body
    // Le cookie sera envoyé automatiquement via withCredentials
    const requestBody = useHttpOnlyCookies
      ? {} // Le refresh token est dans le cookie httpOnly, envoyé automatiquement
      : { refresh_token: refreshTokenFromStorage }; // Mode legacy: envoyer dans le body

    const response = await client.post<{
      success?: boolean;
      data?: RefreshTokenResponse;
      // Support pour format direct aussi (sans wrapper success/data)
      access_token?: string;
      refresh_token?: string;
      expires_in?: number;
    }>('/auth/refresh', requestBody);

    // Le backend peut retourner deux formats :
    // 1. { success: true, data: { access_token, refresh_token, expires_in } }
    // 2. { access_token, refresh_token, expires_in } (format direct)
    let access_token: string;
    let refresh_token: string;
    let expires_in: number;

    if (response.data?.success && response.data?.data) {
      // Format wrapper
      access_token = response.data.data.access_token;
      refresh_token = response.data.data.refresh_token;
      expires_in = response.data.data.expires_in;
    } else if (response.data?.access_token) {
      // Format direct
      access_token = response.data.access_token;
      refresh_token = response.data.refresh_token || 'cookie-based';
      expires_in = response.data.expires_in || 3600;
    } else {
      throw new Error(
        `Invalid refresh response format. Expected { success: true, data: { access_token, refresh_token, expires_in } } or { access_token, refresh_token, expires_in }, got: ${JSON.stringify(response.data)}`,
      );
    }

    if (!access_token) {
      throw new Error('Invalid refresh response: missing access_token');
    }

    // SECURITY: Si on utilise des cookies httpOnly, le refresh_token sera dans le cookie
    // Sinon, utiliser le refresh_token de la réponse (mode legacy)
    const finalRefreshToken = useHttpOnlyCookies
      ? refresh_token || 'cookie-based' // Le backend sette le cookie, on utilise une valeur placeholder
      : refresh_token;

    if (!finalRefreshToken) {
      throw new Error('Invalid refresh response: missing refresh_token');
    }

    // T0176: Mettre à jour les tokens stockés
    // SECURITY: Access token en mémoire uniquement, refresh token dans cookie httpOnly (ou localStorage en mode legacy)
    TokenStorage.setTokens(access_token, finalRefreshToken);

    // INT-016: Programmer le refresh proactif pour le nouveau token
    scheduleProactiveRefresh(access_token, expires_in);
  } catch (error) {
    // T0176: Gérer les erreurs - supprimer les tokens en cas d'échec
    TokenStorage.clearTokens();
    // INT-016: Annuler le refresh proactif en cas d'erreur
    cancelProactiveRefresh();
    throw error;
  }
}

/**
 * INT-016: Programme un refresh proactif avant l'expiration du token
 * Action 5.1.1.5: Also sets up periodic refresh every 4 minutes
 * @param _accessToken - Token d'accès actuel (non utilisé directement, récupéré depuis storage)
 * @param expiresIn - Durée de validité en secondes
 */
function scheduleProactiveRefresh(
  _accessToken: string,
  expiresIn: number,
): void {
  // Annuler les timers précédents s'ils existent
  cancelProactiveRefresh();

  // Action 5.1.1.5: Set up periodic refresh every 4 minutes
  startPeriodicRefresh();

  // Calculer le temps jusqu'au refresh proactif initial (basé sur expiration)
  const expiresInMs = expiresIn * 1000;
  const refreshTime = Math.max(0, expiresInMs - PROACTIVE_REFRESH_BUFFER_MS);

  if (refreshTime <= 0) {
    // Le token expire bientôt, rafraîchir immédiatement
    refreshToken().catch((error) => {
      logger.warn('Proactive token refresh failed', {
        error: error instanceof Error ? error.message : String(error),
        stack: error instanceof Error ? error.stack : undefined,
      });
    });
    return;
  }

  // Programmer le refresh proactif initial (basé sur expiration)
  proactiveRefreshTimer = setTimeout(() => {
    const currentToken = TokenStorage.getAccessToken();
    if (
      currentToken &&
      isTokenExpiringSoon(currentToken, PROACTIVE_REFRESH_BUFFER_MS)
    ) {
      refreshToken().catch((error) => {
        logger.warn('Proactive token refresh failed', {
          error: error instanceof Error ? error.message : String(error),
          stack: error instanceof Error ? error.stack : undefined,
        });
      });
    }
    proactiveRefreshTimer = null;
  }, refreshTime);
}

/**
 * Action 5.1.1.5: Start periodic refresh every 4 minutes
 * This ensures tokens are refreshed before they expire (5 min expiry - 4 min refresh = 1 min buffer)
 */
function startPeriodicRefresh(): void {
  // Clear any existing interval
  if (proactiveRefreshInterval) {
    clearInterval(proactiveRefreshInterval);
  }

  // Set up interval to refresh every 4 minutes
  proactiveRefreshInterval = setInterval(() => {
    const currentToken = TokenStorage.getAccessToken();
    if (currentToken) {
      // Check if token is still valid before refreshing
      const payload = decodeJWT(currentToken);
      if (payload?.exp) {
        const expirationTime = payload.exp * 1000;
        const now = Date.now();
        const timeUntilExpiration = expirationTime - now;

        // Only refresh if token is still valid (not expired)
        if (timeUntilExpiration > 0) {
          refreshToken().catch((error) => {
            logger.warn('Periodic token refresh failed', {
              error: error instanceof Error ? error.message : String(error),
              stack: error instanceof Error ? error.stack : undefined,
            });
          });
        } else {
          // Token expired, stop periodic refresh
          cancelProactiveRefresh();
        }
      }
    } else {
      // No token, stop periodic refresh
      cancelProactiveRefresh();
    }
  }, PROACTIVE_REFRESH_INTERVAL_MS);
}

/**
 * INT-016: Annule le refresh proactif programmé
 * Action 5.1.1.5: Also cancels periodic refresh interval
 */
function cancelProactiveRefresh(): void {
  if (proactiveRefreshTimer) {
    clearTimeout(proactiveRefreshTimer);
    proactiveRefreshTimer = null;
  }
  if (proactiveRefreshInterval) {
    clearInterval(proactiveRefreshInterval);
    proactiveRefreshInterval = null;
  }
}

/**
 * INT-016: Vérifie si le token actuel doit être rafraîchi et le rafraîchit si nécessaire
 * @returns Promise qui se résout si le token est valide ou a été rafraîchi
 */
export async function ensureValidToken(): Promise<void> {
  const accessToken = TokenStorage.getAccessToken();
  const refreshTokenValue = TokenStorage.getRefreshToken();

  if (!accessToken || !refreshTokenValue) {
    throw new Error('No tokens available');
  }

  // Si le token est expiré ou proche de l'expiration, le rafraîchir
  if (isTokenExpiringSoon(accessToken, PROACTIVE_REFRESH_BUFFER_MS)) {
    await refreshToken();
  } else {
    // Programmer le refresh proactif si pas déjà programmé
    const payload = decodeJWT(accessToken);
    if (payload?.exp) {
      const expiresIn = Math.max(
        0,
        payload.exp - Math.floor(Date.now() / 1000),
      );
      scheduleProactiveRefresh(accessToken, expiresIn);
    }
  }
}

/**
 * INT-016: Initialise le système de refresh proactif
 * À appeler après un login ou un refresh réussi
 */
export function initializeProactiveRefresh(): void {
  const accessToken = TokenStorage.getAccessToken();
  if (!accessToken) {
    return;
  }

  const payload = decodeJWT(accessToken);
  if (payload?.exp) {
    const expiresIn = Math.max(0, payload.exp - Math.floor(Date.now() / 1000));
    scheduleProactiveRefresh(accessToken, expiresIn);
  }
}

/**
 * INT-016: Nettoie le système de refresh proactif
 * À appeler lors du logout
 */
export function cleanupProactiveRefresh(): void {
  cancelProactiveRefresh();
}
