// requireAuthHook.ts
import { API_URL } from "@/config";
import type { AstroGlobal } from 'astro';

export interface AuthUser {
  id?: string;
  username?: string;
  roles?: string[];
  [key: string]: unknown;
}

// Short-term failure cache for auth timeouts/errors
let lastAuthFailureTime: number = 0;
const AUTH_FAILURE_CACHE_MS: number = 30000; // 30 seconds

// WeakMap to cache auth promises per Astro context (request)
const authCache = new WeakMap<AstroGlobal, Promise<AuthUser | null>>();

export const requireAuthHook = async (Astro: AstroGlobal): Promise<AuthUser | null> => {
  // If we recently failed due to API timeout/unreachability, fail closed quickly
  if (Date.now() - lastAuthFailureTime < AUTH_FAILURE_CACHE_MS) {
    return null;
  }
  // Check if we already have a cached promise for this request
  if (authCache.has(Astro)) {
    return authCache.get(Astro) ?? null;
  }

  // Create a promise and cache it immediately to prevent race conditions
  const authPromise = performAuth(Astro);
  authCache.set(Astro, authPromise);

  // Return the promise - all callers will await the same promise
  return authPromise;
};

async function performAuth(Astro: AstroGlobal): Promise<AuthUser | null> {
  try {
    const cookieHeader = Astro.request.headers.get("cookie") ?? "";
    // Add timeout to avoid hanging SSR render
    let controller = new AbortController();
    let timeout = setTimeout(() => controller.abort(), 3000);
    let res;
    try {
      res = await fetch(`${API_URL}/auth/id`, {
        headers: { Cookie: cookieHeader },
        credentials: "include",
        signal: controller.signal,
      });
    } catch (err) {
      clearTimeout(timeout);
      lastAuthFailureTime = Date.now();
      console.error("[SSR] auth/id failed or timed out", err);
      return null;
    }
    clearTimeout(timeout);

    if (res.status === 401) {
      // Check if we even have a refresh token before attempting refresh
      if (!cookieHeader.includes('refresh_token=')) {
        return null;
      }

      // Try refresh with timeout
      controller = new AbortController();
      timeout = setTimeout(() => controller.abort(), 3000);
      let refreshRes;
      try {
        refreshRes = await fetch(`${API_URL}/auth/refresh`, {
          method: "POST",
          headers: { Cookie: cookieHeader },
          credentials: "include",
          signal: controller.signal,
        });
      } catch (err) {
        clearTimeout(timeout);
        lastAuthFailureTime = Date.now();
        console.error("[SSR] auth/refresh failed or timed out", err);
        return null;
      }
      clearTimeout(timeout);

      if (!refreshRes.ok) {
        let errorDetail = '';
        try {
          const errorBody = await refreshRes.text();
          errorDetail = ` - ${errorBody}`;
        } catch {}
        console.error(`[SSR] Token refresh failed ${refreshRes.status}${errorDetail}`);
        return null;
      }

      // Get all Set-Cookie headers (getSetCookie returns an array)
      let setCookies: string[] = [];
      if (typeof refreshRes.headers.getSetCookie === 'function') {
        setCookies = refreshRes.headers.getSetCookie();
      } else {
        // Fallback for older Node versions
        const setCookieHeader = refreshRes.headers.get("set-cookie");
        if (setCookieHeader) {
          // Split on comma followed by a cookie name (word=), avoiding splitting on Expires dates
          setCookies = setCookieHeader.split(/,(?=\s*[a-zA-Z_][a-zA-Z0-9_]*=)/);
        }
      }

      if (setCookies.length === 0) {
        console.error("No set-cookie header found in refresh response");
        return null;
      }

      // Forward cookies to client
      setCookies.forEach((c) => Astro.response.headers.append("set-cookie", c.trim()));

      // Build new cookie header for the retry request
      const newCookieHeader = setCookies.map(c => c.split(";")[0].trim()).join("; ");

      controller = new AbortController();
      timeout = setTimeout(() => controller.abort(), 3000);
      try {
        res = await fetch(`${API_URL}/auth/id`, {
          headers: { Cookie: newCookieHeader },
          credentials: "include",
          signal: controller.signal,
        });
      } catch (err) {
        clearTimeout(timeout);
        lastAuthFailureTime = Date.now();
        console.error("[SSR] auth/id retry failed or timed out", err);
        return null;
      }
      clearTimeout(timeout);
    }

    if (!res.ok) {
      console.error("Failed to fetch user ID after token refresh", res.status);
      return null;
    }

    const user = await res.json();
    return user;

  } catch (err) {
    console.error("[SSR] requireAuthHook error:", err);
    return null;
  }
}
