import { Response, NextFunction } from 'express';
import { apiKeysService } from '../../modules/auth/apiKeys.service.js';
import { AuthenticatedRequest, UnauthorizedError, ForbiddenError } from '../types/index.js';
import { logger } from '../utils/logger.js';

// ============================================================================
// API KEY AUTHENTICATION MIDDLEWARE
// ============================================================================

/**
 * Header name for API Key authentication
 * Supports both X-API-Key and Authorization: ApiKey xxx
 */
const API_KEY_HEADER = 'x-api-key';
const API_KEY_AUTH_PREFIX = 'ApiKey ';

/**
 * Extract API key from request headers
 */
function extractApiKey(req: AuthenticatedRequest): string | null {
  // Check X-API-Key header first
  const xApiKey = req.headers[API_KEY_HEADER] as string;
  if (xApiKey) {
    return xApiKey;
  }

  // Check Authorization header with ApiKey prefix
  const authHeader = req.headers.authorization;
  if (authHeader && authHeader.startsWith(API_KEY_AUTH_PREFIX)) {
    return authHeader.substring(API_KEY_AUTH_PREFIX.length);
  }

  return null;
}

/**
 * Get client IP address from request
 */
function getClientIp(req: AuthenticatedRequest): string | undefined {
  // Check X-Forwarded-For header (for proxies/load balancers)
  const forwardedFor = req.headers['x-forwarded-for'];
  if (forwardedFor) {
    const ips = (forwardedFor as string).split(',');
    return ips[0].trim();
  }

  // Check X-Real-IP header
  const realIp = req.headers['x-real-ip'] as string;
  if (realIp) {
    return realIp;
  }

  // Fallback to socket remote address
  return req.socket.remoteAddress;
}

/**
 * Authenticate request using API Key
 * Use this middleware for API endpoints that should accept API Key authentication
 */
export function authenticateApiKey(
  req: AuthenticatedRequest,
  _res: Response,
  next: NextFunction
): void {
  (async () => {
    try {
      const apiKey = extractApiKey(req);

      if (!apiKey) {
        throw new UnauthorizedError('API key requerida');
      }

      const clientIp = getClientIp(req);
      const result = await apiKeysService.validate(apiKey, clientIp);

      if (!result.valid || !result.user) {
        logger.warn('API key validation failed', {
          error: result.error,
          clientIp,
        });
        throw new UnauthorizedError(result.error || 'API key inválida');
      }

      // Set user info on request (same format as JWT auth)
      req.user = {
        userId: result.user.id,
        tenantId: result.user.tenant_id,
        email: result.user.email,
        roles: result.user.roles,
      };
      req.tenantId = result.user.tenant_id;

      // Mark request as authenticated via API Key (for logging/audit)
      (req as any).authMethod = 'api_key';
      (req as any).apiKeyId = result.apiKey?.id;

      next();
    } catch (error) {
      next(error);
    }
  })();
}

/**
 * Authenticate request using either JWT or API Key
 * Use this for endpoints that should accept both authentication methods
 */
export function authenticateJwtOrApiKey(
  req: AuthenticatedRequest,
  res: Response,
  next: NextFunction
): void {
  const apiKey = extractApiKey(req);
  const jwtToken = req.headers.authorization?.startsWith('Bearer ');

  if (apiKey) {
    // Use API Key authentication
    authenticateApiKey(req, res, next);
  } else if (jwtToken) {
    // Use JWT authentication - import dynamically to avoid circular deps
    import('./auth.middleware.js').then(({ authenticate }) => {
      authenticate(req, res, next);
    });
  } else {
    next(new UnauthorizedError('Autenticación requerida (JWT o API Key)'));
  }
}

/**
 * Require specific API key scope
 * Use after authenticateApiKey to enforce scope restrictions
 */
export function requireApiKeyScope(requiredScope: string) {
  return (req: AuthenticatedRequest, _res: Response, next: NextFunction): void => {
    try {
      const apiKeyId = (req as any).apiKeyId;
      const authMethod = (req as any).authMethod;

      // Only check scope for API Key auth
      if (authMethod !== 'api_key') {
        return next();
      }

      // Get API key scope from database (cached in validation result)
      // For now, we'll re-validate - in production, cache this
      (async () => {
        const apiKey = extractApiKey(req);
        if (!apiKey) {
          throw new ForbiddenError('API key no encontrada');
        }

        const result = await apiKeysService.validate(apiKey);
        if (!result.valid || !result.apiKey) {
          throw new ForbiddenError('API key inválida');
        }

        // Null scope means full access
        if (result.apiKey.scope === null) {
          return next();
        }

        // Check if scope matches
        if (result.apiKey.scope !== requiredScope) {
          logger.warn('API key scope mismatch', {
            apiKeyId,
            requiredScope,
            actualScope: result.apiKey.scope,
          });
          throw new ForbiddenError(`API key no tiene el scope requerido: ${requiredScope}`);
        }

        next();
      })();
    } catch (error) {
      next(error);
    }
  };
}

/**
 * Rate limiting for API Key requests
 * Simple in-memory rate limiter - use Redis in production
 */
const rateLimitStore = new Map<string, { count: number; resetTime: number }>();

export function apiKeyRateLimit(maxRequests: number = 1000, windowMs: number = 60000) {
  return (req: AuthenticatedRequest, _res: Response, next: NextFunction): void => {
    try {
      const apiKeyId = (req as any).apiKeyId;
      if (!apiKeyId) {
        return next();
      }

      const now = Date.now();
      const record = rateLimitStore.get(apiKeyId);

      if (!record || now > record.resetTime) {
        rateLimitStore.set(apiKeyId, {
          count: 1,
          resetTime: now + windowMs,
        });
        return next();
      }

      if (record.count >= maxRequests) {
        logger.warn('API key rate limit exceeded', { apiKeyId, count: record.count });
        throw new ForbiddenError('Rate limit excedido. Intente más tarde.');
      }

      record.count++;
      next();
    } catch (error) {
      next(error);
    }
  };
}
