import { defineMiddleware } from 'astro:middleware';
import { SUBSITES, PROTECTED_ROUTES, PUBLIC_ROUTES } from './config.js';
import { getSubsiteByHost, getSubsiteFromSignal } from './utils/subsites.js';

// Polyfill Headers.getSetCookie for environments where it's not present.
// Astro's Node adapter expects headers.getSetCookie() to exist when
// 'set-cookie' headers are present; in some Node runtimes Headers lacks it
// which leads to TypeError: headers.getSetCookie is not a function.
if (typeof globalThis.Headers !== 'undefined' && typeof globalThis.Headers.prototype.getSetCookie !== 'function') {
  try {
    Object.defineProperty(globalThis.Headers.prototype, 'getSetCookie', {
      value: function () {
        const cookies = [];
        for (const [name, val] of this.entries()) {
          if (name && name.toLowerCase() === 'set-cookie') cookies.push(val);
        }
        return cookies;
      },
      configurable: true,
      writable: true,
    });
  } catch (err) {
    // If we can't patch Headers, swallow silently — code will still try to
    // access getSetCookie, but our other guards handle missing function calls.
    console.warn('[middleware] Failed to polyfill Headers.getSetCookie', err);
  }
}

const API_URL = "https://api.codey.horse";
const AUTH_TIMEOUT_MS = 3000; // 3 second timeout for auth requests

// Deduplication for concurrent refresh requests (prevents race condition where
// multiple SSR requests try to refresh simultaneously, causing 401s after the
// first one rotates the refresh token)
let refreshPromise = null;
let lastRefreshResult = null;
let lastRefreshTime = 0;
const REFRESH_RESULT_TTL = 5000; // Cache successful refresh result for 5 seconds

// Auth check function (mirrors requireAuthHook logic but for middleware)
async function checkAuth(request) {
  try {
    const cookieHeader = request.headers.get("cookie") ?? "";
    
    // Add timeout to prevent hanging
    let controller = new AbortController;
    let timeout = setTimeout(() => controller.abort(), AUTH_TIMEOUT_MS);
    let res;
    try {
      res = await fetch(`${API_URL}/auth/id`, {
        headers: { Cookie: cookieHeader },
        credentials: "include",
        signal: controller.signal,
      });
    } catch (err) {
      clearTimeout(timeout);
      console.error("[middleware] auth/id failed or timed out", err.name === 'AbortError' ? 'timeout' : err);
      return { authenticated: false, user: null, cookies: null };
    }
    clearTimeout(timeout);

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

      // Check if we have a recent successful refresh result we can reuse
      const now = Date.now();
      if (lastRefreshResult && (now - lastRefreshTime) < REFRESH_RESULT_TTL) {
        console.log(`[middleware] Reusing cached refresh result from ${now - lastRefreshTime}ms ago`);
        return lastRefreshResult;
      }

      // Deduplicate concurrent refresh requests
      if (refreshPromise) {
        console.log(`[middleware] Waiting for in-flight refresh request`);
        return refreshPromise;
      }

      // Start a new refresh request
      refreshPromise = (async () => {
        console.log(`[middleware] Starting token refresh...`);
        let controller = new AbortController();
        let timeout = setTimeout(() => controller.abort(), AUTH_TIMEOUT_MS);
        let refreshRes;
        try {
          refreshRes = await fetch(`${API_URL}/auth/refresh`, {
            method: "POST",
            headers: { Cookie: cookieHeader },
            credentials: "include",
            signal: controller.signal,
          });
        } catch (err) {
          clearTimeout(timeout);
          console.error("[middleware] auth/refresh failed or timed out", err.name === 'AbortError' ? 'timeout' : err);
          return { authenticated: false, user: null, cookies: null };
        }
        clearTimeout(timeout);

        if (!refreshRes.ok) {
          // Log the response body for debugging
          let errorDetail = '';
          try {
            const errorBody = await refreshRes.text();
            errorDetail = ` - ${errorBody}`;
          } catch {}
          console.error(`[middleware] Token refresh failed ${refreshRes.status}${errorDetail}`);
          return { authenticated: false, user: null, cookies: null };
        }

        console.log(`[middleware] Token refresh succeeded`);

        // Get refreshed cookies
        let setCookies = [];
        if (typeof refreshRes.headers.getSetCookie === 'function') {
          setCookies = refreshRes.headers.getSetCookie();
        } else {
          const setCookieHeader = refreshRes.headers.get("set-cookie");
          if (setCookieHeader) {
            setCookies = setCookieHeader.split(/,(?=\s*[a-zA-Z_][a-zA-Z0-9_]*=)/);
          }
        }

        if (setCookies.length === 0) {
          console.error("[middleware] No set-cookie headers in refresh response");
          return { authenticated: false, user: null, cookies: null };
        }

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

        // Retry auth/id with new cookies and timeout
        controller = new AbortController();
        timeout = setTimeout(() => controller.abort(), AUTH_TIMEOUT_MS);
        let retryRes;
        try {
          retryRes = await fetch(`${API_URL}/auth/id`, {
            headers: { Cookie: newCookieHeader },
            credentials: "include",
            signal: controller.signal,
          });
        } catch (err) {
          clearTimeout(timeout);
          console.error("[middleware] auth/id retry failed or timed out", err.name === 'AbortError' ? 'timeout' : err);
          return { authenticated: false, user: null, cookies: null };
        }
        clearTimeout(timeout);

        if (!retryRes.ok) {
          console.error(`[middleware] auth/id retry failed with status ${retryRes.status}`);
          return { authenticated: false, user: null, cookies: null };
        }

        const user = await retryRes.json();
        return { authenticated: true, user, cookies: setCookies };
      })();

      // Clear the promise when done and cache the result
      refreshPromise.then(result => {
        if (result.authenticated) {
          lastRefreshResult = result;
          lastRefreshTime = Date.now();
        }
        refreshPromise = null;
      }).catch(() => {
        refreshPromise = null;
      });

      return refreshPromise;
    }

    if (!res.ok) {
      return { authenticated: false, user: null, cookies: null };
    }

    const user = await res.json();
    return { authenticated: true, user, cookies: null };
  } catch (err) {
    console.error("[middleware] Auth check error:", err);
    return { authenticated: false, user: null, cookies: null };
  }
}

// Check if a path matches any protected route and return the config
function getProtectedRouteConfig(pathname) {
  // Normalize pathname for comparison (lowercase)
  const normalizedPath = pathname.toLowerCase();
  
  for (const route of PROTECTED_ROUTES) {
    const routePath = typeof route === 'string' ? route : route.path;
    const normalizedRoute = routePath.toLowerCase();
    const matches = normalizedPath === normalizedRoute || normalizedPath.startsWith(normalizedRoute + '/');
    if (matches) {
      // Check if this path is excluded from protection
      const excludes = (typeof route === 'object' && route.exclude) || [];
      for (const excludePath of excludes) {
        const normalizedExclude = excludePath.toLowerCase();
        if (normalizedPath === normalizedExclude || normalizedPath.startsWith(normalizedExclude + '/')) {
          if (import.meta.env.DEV) console.log(`[middleware] Path ${pathname} excluded from protection by ${excludePath}`);
          return null; // Excluded, not protected
        }
      }
      return typeof route === 'string' ? { path: route, roles: null } : route;
    }
  }
  return null;
}

// Check if a path is explicitly public (but NOT if it matches a protected route)
function isPublicRoute(pathname) {
  // If the path matches a protected route, it's NOT public
  if (getProtectedRouteConfig(pathname)) {
    return false;
  }
  
  for (const route of PUBLIC_ROUTES) {
    // For routes ending with /, match any path starting with that prefix
    if (route.endsWith('/') && route !== '/') {
      if (pathname.startsWith(route)) {
        if (import.meta.env.DEV) console.log(`[middleware] isPublicRoute: ${pathname} matched ${route} (prefix)`);
        return true;
      }
    } else {
      // For other routes, match exactly or as a path prefix (route + /)
      if (pathname === route) {
        if (import.meta.env.DEV) console.log(`[middleware] isPublicRoute: ${pathname} matched ${route} (exact)`);
        return true;
      }
      // Special case: don't treat / as a prefix for all paths
      if (route !== '/' && pathname.startsWith(route + '/')) {
        if (import.meta.env.DEV) console.log(`[middleware] isPublicRoute: ${pathname} matched ${route}/ (subpath)`);
        return true;
      }
    }
  }
  if (import.meta.env.DEV) console.log(`[middleware] isPublicRoute(${pathname}) = false`);
  return false;
}

export const onRequest = defineMiddleware(async (context, next) => {
  try {
    const pathname = context.url.pathname;
    
    // Skip auth check for static assets
    const skipAuthPrefixes = ['/_astro', '/_', '/assets', '/scripts', '/favicon', '/images', '/_static'];
    const shouldSkipAuth = skipAuthPrefixes.some(p => pathname.startsWith(p));
    
    // Check if route is protected (requires auth)
    const protectedConfig = getProtectedRouteConfig(pathname);
    const isApiRoute = pathname.startsWith('/api/');
    if (import.meta.env.DEV) console.log(`[middleware] Path: ${pathname}, Protected: ${!!protectedConfig}, SkipAuth: ${shouldSkipAuth}`);
    
    // Always attempt auth for non-static routes to populate user info
    if (!shouldSkipAuth) {
      const { authenticated, user, cookies } = await checkAuth(context.request);
      if (import.meta.env.DEV) console.log(`[middleware] Auth result: authenticated=${authenticated}`);
      
      // Expose authenticated user and refreshed cookies to downstream handlers/layouts
      if (authenticated && user) {
        context.locals.user = user;
        if (cookies && cookies.length > 0) {
          context.locals.refreshedCookies = cookies;
        }
      }
      
      // For protected routes, enforce authentication
      if (protectedConfig && !isPublicRoute(pathname)) {
        if (!authenticated) {
          if (isApiRoute) {
            // Return JSON 401 for API routes
            return new Response(JSON.stringify({ error: 'Unauthorized', message: 'Authentication required' }), {
              status: 401,
              headers: { 'Content-Type': 'application/json' },
            });
          }
          // Redirect to login with return URL
          const returnUrl = encodeURIComponent(pathname + context.url.search);
          if (import.meta.env.DEV) console.log(`[middleware] Auth required for ${pathname}, redirecting to login`);
          return context.redirect(`/login?returnUrl=${returnUrl}`, 302);
        }
        
        // Check role-based access if roles are specified
        if (protectedConfig.roles && protectedConfig.roles.length > 0) {
          const userRoles = user?.roles || [];
          const isAdmin = userRoles.includes('admin');
          const hasRequiredRole = isAdmin || protectedConfig.roles.some(role => userRoles.includes(role));
          if (!hasRequiredRole) {
            if (import.meta.env.DEV) console.log(`[middleware] User lacks required role for ${pathname}`);
            if (isApiRoute) {
              // Return JSON 403 for API routes
              return new Response(JSON.stringify({ 
                error: 'Forbidden', 
                message: 'Insufficient permissions',
                requiredRoles: protectedConfig.roles 
              }), {
                status: 403,
                headers: { 'Content-Type': 'application/json' },
              });
            }
            // Store required roles in locals for the login page to access
            context.locals.accessDenied = true;
            context.locals.requiredRoles = protectedConfig.roles;
            context.locals.returnUrl = pathname + context.url.search;
            // Rewrite to login page - this renders /login but keeps the URL and locals
            return context.rewrite('/login');
          }
        }
        
        if (import.meta.env.DEV) console.log(`[middleware] Auth OK for ${pathname}, user: ${user?.username || user?.id || 'unknown'}`);
      }
    }

    // Check the Host header to differentiate subdomains
    // Build a headers map safely because Headers.get(':authority') throws
    const headersMap = {};
    for (const [k, v] of context.request.headers) {
      headersMap[k.toLowerCase()] = v;
    }
    const hostHeader = headersMap['host'] || '';
    // Node/http2 might store host as :authority (pseudo header); it appears under iteration as ':authority'
    const authorityHeader = headersMap[':authority'] || '';
    // Fallback to context.url.hostname if available (some environments populate it)
    const urlHost = context.url?.hostname || '';
    const host = (hostHeader || authorityHeader || urlHost).split(':')[0]; // normalize remove port

    const requestIp = (headersMap['x-forwarded-for']?.split(',')[0]?.trim())
      || headersMap['x-real-ip']
      || headersMap['cf-connecting-ip']
      || headersMap['forwarded']?.split(';').find(kv => kv.trim().startsWith('for='))?.split('=')[1]?.replace(/"/g, '')
      || context.request.headers.get('x-client-ip')
      || 'unknown';

    // Cloudflare geo data (available in production)
    const cfCountry = headersMap['cf-ipcountry'] || null;
    const userAgent = headersMap['user-agent'] || null;

    // Debug info for incoming requests
    if (import.meta.env.DEV) console.log(`[middleware] incoming: host=${hostHeader} ip=${requestIp} path=${context.url.pathname}`);


    // When the host header is missing, log all headers for debugging and
    // attempt to determine host from forwarded headers or a dev query header.
    if (!host) {
      if (import.meta.env.DEV) console.log('[middleware] WARNING: Host header missing. Dumping headers:');
      for (const [k, v] of context.request.headers) {
        if (import.meta.env.DEV) console.log(`[middleware]   header: ${k}=${v}`);
      }
    }

    // Determine if how the request says it wants the whitelabel
    const forwardedHost = headersMap['x-forwarded-host'] || '';
    const xWhitelabel = headersMap['x-whitelabel'] || '';
    const forceWhitelabelQuery = context.url.searchParams.get('whitelabel');

    // Make whitelabel detection dynamic based on `SUBSITES` mapping and incoming signals
    // We'll detect by full host (host/forwarded/authority) or by short-name match
    const subsiteHosts = Object.keys(SUBSITES || {});

    const hostSubsite = getSubsiteByHost(host);
    const forwardedSubsite = getSubsiteByHost(forwardedHost);
    const authoritySubsite = getSubsiteByHost(authorityHeader);
    const headerSignalSubsite = getSubsiteFromSignal(xWhitelabel);
    const querySignalSubsite = getSubsiteFromSignal(forceWhitelabelQuery);

    const wantsSubsite = Boolean(hostSubsite || forwardedSubsite || authoritySubsite || headerSignalSubsite || querySignalSubsite);

    // Use central SUBSITES mapping
    // import from config.js near top to ensure single source of truth
    // (we import lazily here for compatibility with middleware runtime)
    const subsites = SUBSITES || {};

    // Check if the request matches a subsite
    const subsitePath = subsites[host];
    if (subsitePath) {
      const skipPrefixes = ['/_astro', '/_', '/assets', '/scripts', '/favicon', '/api', '/robots.txt', '/_static'];
      const shouldSkip = skipPrefixes.some((p) => context.url.pathname.startsWith(p));

      if (!shouldSkip && !context.url.pathname.startsWith(subsitePath)) {
        const newPath = `${subsitePath}${context.url.pathname}`;
        if (import.meta.env.DEV) console.log(`[middleware] Rewriting ${host} ${context.url.pathname} -> ${newPath}`);
        return context.rewrite(newPath);
      }
    } else {
      // If the path appears to be a subsite path (like /subsites/req) but the host isn't a subsite,
      // block so the main site doesn't accidentally serve that content.
      const allPaths = Object.values(subsites || {});
      const pathLooksLikeSubsite = allPaths.some((p) => context.url.pathname.startsWith(p));
      if (pathLooksLikeSubsite) {
        if (import.meta.env.DEV) console.log(`[middleware] Blocking subsite path on main domain: ${host}${context.url.pathname}`);
        return new Response('Not found', { status: 404 });
      }
    }

    // Block /subsites/req/* on main domain (codey.horse, local.codey.horse, etc)
    const isMainDomain = !wantsSubsite;
    if (isMainDomain && Object.values(subsites || {}).some(p => context.url.pathname.startsWith(p))) {
      // Immediately return a 404 for /req on the main domain
      if (Object.values(subsites || {}).includes(context.url.pathname)) {
        if (import.meta.env.DEV) console.log(`[middleware] Blocking subsite root on main domain: ${hostHeader}${context.url.pathname}`);
        return new Response('Not found', { status: 404 });
      }
      if (import.meta.env.DEV) console.log(`[middleware] Blocking subsite wildcard path on main domain: ${hostHeader}${context.url.pathname}`);
      return new Response('Not found', { status: 404 });
    }

    // Explicitly handle the forceWhitelabelQuery to rewrite the path
    if (forceWhitelabelQuery) {
      const forced = getSubsiteFromSignal(forceWhitelabelQuery);
      const subsitePath = forced?.path || (`/${forceWhitelabelQuery}`.startsWith('/') ? `/${forceWhitelabelQuery}` : `/${forceWhitelabelQuery}`);
      if (import.meta.env.DEV) console.log(`[middleware] Forcing whitelabel via query: ${forceWhitelabelQuery} -> rewrite to ${subsitePath}${context.url.pathname}`);
      context.url.pathname = `${subsitePath}${context.url.pathname}`;
    }

    // Pass the whitelabel value explicitly to context.locals
    // set a normalized whitelabel short name if available
    const chosen = querySignalSubsite?.short || headerSignalSubsite?.short || hostSubsite?.short || forwardedSubsite?.short || authoritySubsite?.short || null;
    context.locals.whitelabel = chosen;
    context.locals.isSubsite = Boolean(wantsSubsite);
    if (import.meta.env.DEV) console.log(`[middleware] Setting context.locals.whitelabel: ${context.locals.whitelabel}`);

    // Final debug: show the final pathname we hand to Astro
    if (import.meta.env.DEV) console.log(`[middleware] Final pathname: ${context.url.pathname}`);

    // Let Astro handle the request first
    const response = await next();

    // Add security headers to response
    const securityHeaders = new Headers(response.headers);
    
    // Prevent clickjacking
    securityHeaders.set('X-Frame-Options', 'SAMEORIGIN');
    
    // Prevent MIME type sniffing
    securityHeaders.set('X-Content-Type-Options', 'nosniff');
    
    // XSS protection (legacy, but still useful)
    securityHeaders.set('X-XSS-Protection', '1; mode=block');
    
    // Referrer policy - send origin only on cross-origin requests
    securityHeaders.set('Referrer-Policy', 'strict-origin-when-cross-origin');
    
    // HSTS - enforce HTTPS (only in production)
    if (!import.meta.env.DEV) {
      securityHeaders.set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
    }
    
    // Permissions policy - restrict sensitive APIs
    securityHeaders.set('Permissions-Policy', 'geolocation=(), microphone=(), camera=()');
    
    // Content Security Policy - restrict resource loading
    // Note: 'unsafe-inline' and 'unsafe-eval' needed for React/Astro hydration
    // In production, consider using nonces or hashes for stricter CSP
    const cspDirectives = [
      "default-src 'self'",
      "script-src 'self' 'unsafe-inline' 'unsafe-eval' https://www.youtube.com https://www.youtube-nocookie.com https://s.ytimg.com https://challenges.cloudflare.com https://static.cloudflareinsights.com",
      // Allow Cloudflare's inline event handlers (for Turnstile/challenges)
      "script-src-attr 'unsafe-inline'",
      "style-src 'self' 'unsafe-inline' https://fonts.googleapis.com",
      "font-src 'self' https://fonts.gstatic.com data:",
      "img-src 'self' data: blob: https: http:",
      "media-src 'self' blob: https:",
      "connect-src 'self' https://api.codey.horse https://*.codey.horse https://*.audio.tidal.com wss:",
      // Allow YouTube for video embeds and Cloudflare for challenges/Turnstile
      "frame-src 'self' https://www.youtube.com https://www.youtube-nocookie.com https://challenges.cloudflare.com",
      "object-src 'none'",
      "base-uri 'self'",
      "form-action 'self'",
      "frame-ancestors 'self'",
      "upgrade-insecure-requests",
    ].join('; ');
    securityHeaders.set('Content-Security-Policy', cspDirectives);

    // Forward any refreshed auth cookies to the client
    if (context.locals.refreshedCookies && context.locals.refreshedCookies.length > 0) {
      const newResponse = new Response(response.body, {
        status: response.status,
        statusText: response.statusText,
        headers: securityHeaders,
      });
      context.locals.refreshedCookies.forEach(cookie => {
        newResponse.headers.append('set-cookie', cookie.trim());
      });
      
      // If it's a 404, redirect to home
      if (newResponse.status === 404 && !context.url.pathname.startsWith('/api/')) {
        if (import.meta.env.DEV) console.log(`404 redirect: ${context.url.pathname} -> /`);
        return context.redirect('/', 302);
      }
      
      return newResponse;
    }

    // If it's a 404, redirect to home
    if (response.status === 404 && !context.url.pathname.startsWith('/api/')) {
      if (import.meta.env.DEV) console.log(`404 redirect: ${context.url.pathname} -> /`);
      return context.redirect('/', 302);
    }

    // Return response with security headers
    return new Response(response.body, {
      status: response.status,
      statusText: response.statusText,
      headers: securityHeaders,
    });
  } catch (error) {
    // Handle any middleware errors by redirecting to home
    console.error('Middleware error:', error);
    return context.redirect('/', 302);
  }
});