/**
 * API Router
 *
 * Orchestre rate limiting, CSRF, enforcement d'auth et dispatch vers les handlers.
 * N'a aucune connaissance des features spécifiques — les routes s'auto-enregistrent.
 *
 * Initialisation requise :
 *   Appeler configureRouter({ resolveSession }) une fois au démarrage (dans initializeZen)
 *   avant toute requête. Le resolver est fourni par la feature auth, ce qui garde
 *   core/api/ libre de tout import feature.
 *
 * Cycle de vie d'une requête :
 *   route-handler.js → routeRequest()
 *     → rate limit (routes skipRateLimit exemptées)
 *     → validation CSRF (méthodes state-mutating uniquement)
 *     → matching sur toutes les routes (core en premier, puis features, puis modules)
 *       → enforcement auth depuis la définition de route
 *       → handler(request, params, context)
 */

import { getSessionCookieName } from '@zen/core/shared/config';
import { checkRateLimit, getIpFromRequest, formatRetryAfter } from '@zen/core/shared/rate-limit';
import { fail } from '@zen/core/shared/logger';
import { hasPermission, PERMISSIONS } from '@zen/core/users';
import { getCoreRoutes } from './core-routes.js';
import { getFeatureRoutes, getSessionResolver } from './runtime.js';
import { apiError } from './respond.js';

export { configureRouter, getSessionResolver, clearRouterConfig } from './runtime.js';

const COOKIE_NAME = getSessionCookieName();

// ---------------------------------------------------------------------------
// Auth helpers
// ---------------------------------------------------------------------------

/**
 * Exige une session valide. Lève une erreur si aucun cookie valide n'est présent.
 * @returns {Promise<Object>} session
 */
export async function requireAuth() {
  const { cookies } = await import('next/headers');
  const cookieStore = await cookies();
  const sessionToken = cookieStore.get(COOKIE_NAME)?.value;

  if (!sessionToken) {
    throw new Error('Unauthorized');
  }

  const session = await getSessionResolver()(sessionToken);

  if (!session || !session.user) {
    throw new Error('Unauthorized');
  }

  return session;
}

/**
 * Exige une session avec la permission admin.access.
 * @returns {Promise<Object>} session
 */
export async function requireAdmin() {
  const session = await requireAuth();

  const allowed = await hasPermission(session.user.id, PERMISSIONS.ADMIN_ACCESS);
  if (!allowed) {
    throw new Error('Admin access required');
  }

  return session;
}

// ---------------------------------------------------------------------------
// CSRF
// ---------------------------------------------------------------------------

/**
 * Résout l'URL canonique de l'application depuis les variables d'environnement.
 * Priorité : NEXT_PUBLIC_URL_DEV (développement) → NEXT_PUBLIC_URL (production).
 */
function resolveAppUrl() {
  if (process.env.NODE_ENV === 'development' && process.env.NEXT_PUBLIC_URL_DEV) {
    return process.env.NEXT_PUBLIC_URL_DEV;
  }
  return process.env.NEXT_PUBLIC_URL || null;
}

/**
 * Vérifie que les requêtes state-mutating proviennent de l'origine attendue.
 * GET, HEAD et OPTIONS sont exemptés (RFC 7231).
 * @param {Request} request
 * @returns {boolean}
 */
function passesCsrfCheck(request) {
  const safeMethods = new Set(['GET', 'HEAD', 'OPTIONS']);
  if (safeMethods.has(request.method)) return true;

  const appUrl = resolveAppUrl();
  if (!appUrl) {
    fail('CSRF: no app URL configured (NEXT_PUBLIC_URL_DEV / NEXT_PUBLIC_URL) — request denied');
    return false;
  }

  let expectedOrigin;
  try {
    expectedOrigin = new URL(appUrl).origin;
  } catch {
    fail(`CSRF: configured app URL is not valid: ${appUrl}`);
    return false;
  }

  const origin = request.headers.get('origin');
  if (origin) {
    return origin === expectedOrigin;
  }

  // Pas d'en-tête Origin : repli sur Referer (anciens navigateurs).
  const referer = request.headers.get('referer');
  if (referer) {
    try {
      return new URL(referer).origin === expectedOrigin;
    } catch {
      return false;
    }
  }

  // Ni Origin ni Referer — refus par sécurité.
  return false;
}

// ---------------------------------------------------------------------------
// Route matching
// ---------------------------------------------------------------------------

/**
 * Teste un pattern de route contre un chemin de requête.
 *
 * Supporte :
 *   - Segments exacts : '/health'
 *   - Paramètres nommés : '/users/:id'
 *   - Wildcard greedy (fin uniquement) : '/storage/**'
 *
 * @param {string} pattern
 * @param {string} path
 * @returns {boolean}
 */
function matchRoute(pattern, path) {
  const patternParts = pattern.split('/').filter(Boolean);
  const pathParts = path.split('/').filter(Boolean);

  const hasWildcard = patternParts[patternParts.length - 1] === '**';

  if (hasWildcard) {
    const fixedParts = patternParts.slice(0, -1);
    if (pathParts.length < fixedParts.length) return false;
    for (let i = 0; i < fixedParts.length; i++) {
      if (fixedParts[i].startsWith(':')) continue;
      if (fixedParts[i] !== pathParts[i]) return false;
    }
    return true;
  }

  if (patternParts.length !== pathParts.length) return false;

  for (let i = 0; i < patternParts.length; i++) {
    if (patternParts[i].startsWith(':')) continue;
    if (patternParts[i] !== pathParts[i]) return false;
  }

  return true;
}

/**
 * Extrait les paramètres de chemin nommés (et le wildcard) d'une route matchée.
 *
 * @param {string} pattern - Ex. '/users/:id'
 * @param {string} path    - Ex. '/users/42'
 * @returns {Object} params — paramètres nommés + `wildcard` optionnel
 */
function extractPathParams(pattern, path) {
  const params = {};
  const patternParts = pattern.split('/').filter(Boolean);
  const pathParts = path.split('/').filter(Boolean);

  for (let i = 0; i < patternParts.length; i++) {
    const part = patternParts[i];
    if (part === '**') {
      params.wildcard = pathParts.slice(i).join('/');
      break;
    }
    if (part.startsWith(':')) {
      params[part.slice(1)] = pathParts[i];
    }
  }

  return params;
}

// ---------------------------------------------------------------------------
// Main router
// ---------------------------------------------------------------------------

// Messages sûrs à exposer verbatim au client.
const SAFE_AUTH_MESSAGES = new Set(['Unauthorized', 'Admin access required']);

// Émis au plus une fois par lifetime de process pour éviter le log flooding.
let _rateLimitUnavailableWarned = false;

/**
 * Route une requête API vers le handler approprié.
 *
 * @param {Request} request  - Requête Next.js entrante
 * @param {string[]} path    - Segments de chemin après /zen/api/
 * @returns {Promise<Object>} Payload de réponse (sérialisé en JSON par route-handler.js)
 */
export async function routeRequest(request, path) {
  const method = request.method;
  const pathString = '/' + path.join('/');

  // Fusion de toutes les routes — core en premier pour que les built-ins aient priorité.
  // Le rate limit est différé après le matching pour pouvoir honorer skipRateLimit
  // sans hardcoder de chemins dans le router.
  const allRoutes = [...getCoreRoutes(), ...getFeatureRoutes()];

  const matchedRoute = allRoutes.find(
    route => matchRoute(route.path, pathString) && route.method === method
  );

  // Rate limit par IP. Les routes avec skipRateLimit: true sont exemptées
  // (ex. GET /health pour que les sondes de monitoring ne consomment pas de quota).
  if (!matchedRoute?.skipRateLimit) {
    const ip = getIpFromRequest(request);
    if (ip === 'unknown') {
      // L'IP client ne peut pas être résolue — appliquer le rate limit sur la clé
      // 'unknown' partagée effondrerait tout le trafic dans un seul bucket, permettant
      // à un seul attaquant d'épuiser le quota et de dénier le service à tous les autres.
      // Le rate limiting est donc suspendu jusqu'à ce qu'un reverse proxy de confiance
      // soit configuré avec ZEN_TRUST_PROXY=true.
      if (!_rateLimitUnavailableWarned) {
        _rateLimitUnavailableWarned = true;
        fail(
          'Rate limiting inactive: client IP cannot be determined. ' +
          'Set ZEN_TRUST_PROXY=true behind a verified reverse proxy to enable per-IP rate limiting.'
        );
      }
    } else {
      const rl = checkRateLimit(ip, 'api');
      if (!rl.allowed) {
        return apiError(
          'Too Many Requests',
          `Trop de requêtes. Réessayez dans ${formatRetryAfter(rl.retryAfterMs)}.`
        );
      }
    }
  }

  // Validation CSRF pour les requêtes state-mutating.
  if (!passesCsrfCheck(request)) {
    return apiError('Forbidden', 'CSRF validation failed');
  }

  if (!matchedRoute) {
    // Aucune route matchée — message générique sans refléter la méthode ou le chemin
    // pour éviter l'énumération de routes.
    return apiError('Not Found', 'The requested resource does not exist');
  }

  // Enforcement auth depuis la définition de route, avant d'appeler le handler.
  const context = {};
  try {
    if (matchedRoute.auth === 'admin') {
      context.session = await requireAdmin();
      if (matchedRoute.permission) {
        const allowed = await hasPermission(context.session.user.id, matchedRoute.permission);
        if (!allowed) {
          return apiError('Forbidden', 'Permission insuffisante');
        }
      }
    } else if (matchedRoute.auth === 'user') {
      context.session = await requireAuth();
    }
    // 'public' — context.session reste undefined
  } catch (err) {
    const code = SAFE_AUTH_MESSAGES.has(err.message) ? err.message : 'Internal Server Error';
    if (!SAFE_AUTH_MESSAGES.has(err.message)) {
      fail(`Auth error: ${err.message}`);
    }
    return apiError(code, code);
  }

  const params = extractPathParams(matchedRoute.path, pathString);

  try {
    return await matchedRoute.handler(request, params, context);
  } catch (err) {
    fail(`Route handler error [${method} ${matchedRoute.path}]: ${err.message}`);
    return apiError('Internal Server Error', 'An unexpected error occurred. Please try again later.');
  }
}
