/**
 * Client-side CSRF utilities
 *
 * This module provides CSRF token handling for client-side code.
 * Uses the double-submit cookie pattern with singleton deduplication.
 *
 * IMPORTANT: The /api/csrf endpoint is now idempotent - it returns the
 * existing valid token from cookie if present. This eliminates race
 * conditions at the server level, but we still use client-side deduplication
 * to avoid unnecessary network requests.
 */

// Client-side singleton state (only used in browser)
// These are intentionally module-level for singleton behavior
let cachedToken: string | null = null;
let pendingRequest: Promise<string | null> | null = null;

/**
 * Get CSRF token from cookie
 * Must be called client-side only (document must exist)
 */
export function getCSRFToken(): string | null {
  if (typeof document === "undefined") return null;
  const match = document.cookie.match(/csrf-token=([^;]+)/);
  return match ? decodeURIComponent(match[1]) : null;
}

/**
 * Create headers object with CSRF token included
 * Use this for all mutation requests (POST, PUT, PATCH, DELETE)
 */
export function getHeadersWithCSRF(
  additionalHeaders?: Record<string, string>,
): HeadersInit {
  const csrfToken = getCSRFToken();
  const headers: Record<string, string> = {
    "Content-Type": "application/json",
    ...additionalHeaders,
  };

  if (csrfToken) {
    headers["x-csrf-token"] = csrfToken;
  }

  return headers;
}

/**
 * Initialize CSRF token by calling the CSRF endpoint
 * Call this on component mount with useEffect
 *
 * @deprecated Use getCSRFTokenAsync() instead for better deduplication
 */
export async function initializeCSRFToken(): Promise<void> {
  await getCSRFTokenAsync();
}

/**
 * Fetch a fresh CSRF token from the server and return it
 * This avoids race conditions where the cookie isn't updated yet after fetch
 * Use this when you need to ensure you have a valid token for a mutation
 *
 * @deprecated Use getCSRFTokenAsync() instead for better deduplication
 */
export async function fetchCSRFToken(): Promise<string | null> {
  return getCSRFTokenAsync();
}

/**
 * Get CSRF token with client-side request deduplication
 *
 * This function provides a singleton pattern for CSRF token retrieval:
 * 1. Returns cached token if available (avoids network request)
 * 2. Returns pending request promise if one exists (deduplication)
 * 3. Only fetches from server if no cache/pending request exists
 *
 * Since /api/csrf is now idempotent (returns existing cookie token),
 * multiple components can safely call this without race conditions.
 */
export async function getCSRFTokenAsync(): Promise<string | null> {
  // SSR guard - never fetch CSRF on server
  if (typeof window === "undefined") {
    return null;
  }

  // First, check if we already have a valid token in cookie
  const cookieToken = getCSRFToken();
  if (cookieToken && cookieToken.length === 64) {
    cachedToken = cookieToken;
    return cookieToken;
  }

  // IMPORTANT: If no valid cookie exists, clear the cached token to prevent
  // using a stale token that won't match the server-side cookie validation.
  // This can happen if the cookie expired or was cleared.
  if (!cookieToken && cachedToken) {
    if (process.env.NODE_ENV !== "production") {
      console.log("[CSRF] Clearing stale cached token - cookie missing");
    }
    cachedToken = null;
  }

  // Deduplicate concurrent requests - return existing pending promise
  if (pendingRequest) {
    return pendingRequest;
  }

  // No cache or pending request - fetch from server
  pendingRequest = (async () => {
    try {
      const response = await fetch("/api/csrf", {
        credentials: "include",
        cache: "no-store",
      });

      if (!response.ok) {
        return null;
      }

      const data = await response.json();
      const token = data.csrfToken || null;

      // Cache the token for future calls
      if (token) {
        cachedToken = token;
      }

      return token;
    } catch {
      return null;
    } finally {
      // Clear pending request so future calls can try again
      pendingRequest = null;
    }
  })();

  return pendingRequest;
}

/**
 * Force refresh the CSRF token
 *
 * Use this for error recovery after a 403 CSRF error.
 * Clears the client-side cache and cookie, then fetches a new token.
 */
export async function refreshCSRFToken(): Promise<string | null> {
  // SSR guard
  if (typeof window === "undefined") {
    return null;
  }

  // Clear client-side cache
  cachedToken = null;
  pendingRequest = null;

  // Clear the cookie with multiple attribute combinations to ensure removal
  const cookieClearOptions = [
    "csrf-token=; path=/; max-age=0",
    "csrf-token=; path=/; max-age=0; secure",
    "csrf-token=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT",
    "csrf-token=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT; secure",
  ];

  cookieClearOptions.forEach((opt) => {
    document.cookie = opt;
  });

  try {
    const response = await fetch("/api/csrf?force=true", {
      credentials: "include",
      cache: "no-store",
    });

    if (!response.ok) {
      if (process.env.NODE_ENV !== "production") {
        console.warn("[CSRF] Failed to refresh token:", response.status);
      }
      return null;
    }

    const data = await response.json();
    const token = data.csrfToken || null;

    if (token) {
      cachedToken = token;
      if (process.env.NODE_ENV !== "production") {
        console.log("[CSRF] Token refreshed successfully");
      }
    }

    return token;
  } catch (err) {
    if (process.env.NODE_ENV !== "production") {
      console.error("[CSRF] Refresh error:", err);
    }
    return null;
  }
}

/**
 * Clear cached CSRF token (useful for testing or logout)
 */
export function clearCSRFCache(): void {
  cachedToken = null;
  pendingRequest = null;
}

/**
 * Helper to make a fetch request with CSRF token and automatic 403 retry
 *
 * This function:
 * 1. Gets a valid CSRF token (from cookie or fetches new one)
 * 2. Makes the request with the token in headers
 * 3. On 403 error, refreshes the token and retries once
 *
 * Use this in mutation handlers for consistent CSRF handling with recovery.
 *
 * @param url - The URL to fetch
 * @param options - Fetch options (method, body, etc.)
 * @returns The fetch Response object
 */
export async function fetchWithCSRF(
  url: string,
  options: RequestInit = {},
): Promise<Response> {
  // Get CSRF token - try cookie first, then fetch if needed
  let csrfToken = getCSRFToken();
  if (!csrfToken) {
    csrfToken = await getCSRFTokenAsync();
  }

  // Build headers with CSRF token
  const headers = new Headers(options.headers);
  if (!headers.has("Content-Type")) {
    headers.set("Content-Type", "application/json");
  }
  if (csrfToken) {
    headers.set("x-csrf-token", csrfToken);
  }

  // Make the request
  const response = await fetch(url, {
    ...options,
    headers,
    credentials: "include",
  });

  // On 403, try refreshing token and retry once
  if (response.status === 403) {
    const errorText = await response.text();
    if (errorText.toLowerCase().includes("csrf")) {
      const newToken = await refreshCSRFToken();
      if (newToken) {
        headers.set("x-csrf-token", newToken);
        return fetch(url, {
          ...options,
          headers,
          credentials: "include",
        });
      }
    }
    // If not a CSRF error or refresh failed, return original 403 response
    return new Response(errorText, {
      status: 403,
      statusText: response.statusText,
      headers: response.headers,
    });
  }

  return response;
}
