/**
 * DDoS Protection Middleware
 * Enhanced rate limiting with IP blocking, fingerprinting, and distributed limits.
 * Operator guide: docs/GATEWAY-DDOS-RATE-M4.md (internet-facing deploys, probe exemptions, strict vs global layers).
 */

import { FastifyRequest, FastifyReply } from 'fastify';
import { createLogger } from '@4runr/shared';
import crypto from 'crypto';

const logger = createLogger('Gateway:DDOS:Protection');

interface RequestFingerprint {
  ip: string;
  userAgent: string;
  fingerprint: string;
  count: number;
  firstSeen: number;
  lastSeen: number;
  blocked: boolean;
  blockedUntil?: number;
}

interface IPBlock {
  ip: string;
  blockedAt: number;
  blockedUntil: number;
  reason: string;
  requestCount: number;
}

// In-memory stores (in production, use Redis)
const requestFingerprints = new Map<string, RequestFingerprint>();
const ipBlocks = new Map<string, IPBlock>();
const rateLimitBuckets = new Map<string, { count: number; resetAt: number }>();

// Configuration
const CONFIG = {
  // Rate limits
  MAX_REQUESTS_PER_MINUTE: 100,
  MAX_REQUESTS_PER_SECOND: 20,
  MAX_CONCURRENT_REQUESTS: 50,

  // Blocking thresholds
  BLOCK_THRESHOLD_REQUESTS: 200, // Block after 200 requests in short time
  BLOCK_DURATION_MS: 15 * 60 * 1000, // 15 minutes
  BLOCK_THRESHOLD_FAILURES: 10, // Block after 10 failed auth attempts

  // Cleanup
  CLEANUP_INTERVAL_MS: 60 * 1000, // 1 minute
  FINGERPRINT_TTL_MS: 60 * 60 * 1000, // 1 hour
};

/**
 * Generate request fingerprint
 */
function generateFingerprint(request: FastifyRequest): string {
  const ip = getClientIP(request);
  const userAgent = request.headers['user-agent'] || 'unknown';
  const accept = request.headers['accept'] || '';
  const acceptLanguage = request.headers['accept-language'] || '';
  
  const data = `${ip}:${userAgent}:${accept}:${acceptLanguage}`;
  return crypto.createHash('sha256').update(data).digest('hex').substring(0, 16);
}

function isLoopbackIP(ip: string): boolean {
  const n = ip.toLowerCase();
  return (
    n === '127.0.0.1' ||
    n === '::1' ||
    n === '::ffff:127.0.0.1' ||
    n === 'localhost'
  );
}

/** Local dev + smoke tests: loopback is not DDoS-blocked unless explicitly disabled. */
function loopbackExemptFromDdos(): boolean {
  if (process.env['DDOS_LOOPBACK_EXEMPT'] === 'false') return false;
  return process.env['NODE_ENV'] !== 'production';
}

/**
 * Get client IP address
 */
function getClientIP(request: FastifyRequest): string {
  const forwarded = request.headers['x-forwarded-for'];
  if (forwarded) {
    const forwardedStr = Array.isArray(forwarded) ? forwarded[0] : forwarded;
    if (forwardedStr) {
      const ips = forwardedStr.split(',');
      return ips[0]?.trim() || 'unknown';
    }
  }
  const realIp = request.headers['x-real-ip'];
  const realIpStr = Array.isArray(realIp) ? realIp[0] : realIp;
  return request.ip || realIpStr || 'unknown';
}

/**
 * Check if IP is blocked
 */
function isIPBlocked(ip: string): boolean {
  const block = ipBlocks.get(ip);
  if (!block) return false;

  if (Date.now() > block.blockedUntil) {
    // Block expired
    ipBlocks.delete(ip);
    return false;
  }

  return true;
}

/**
 * Block an IP address
 */
function blockIP(ip: string, reason: string, requestCount: number = 0): void {
  const blockedUntil = Date.now() + CONFIG.BLOCK_DURATION_MS;
  ipBlocks.set(ip, {
    ip,
    blockedAt: Date.now(),
    blockedUntil,
    reason,
    requestCount
  });

  logger.warn('IP blocked', { ip, reason, blockedUntil: new Date(blockedUntil) });
}

/**
 * Check rate limits
 */
function checkRateLimit(fingerprint: string, ip: string): { allowed: boolean; reason?: string } {
  const now = Date.now();
  const minuteKey = `minute:${fingerprint}:${Math.floor(now / 60000)}`;
  const secondKey = `second:${fingerprint}:${Math.floor(now / 1000)}`;

  // Check per-minute limit
  let minuteBucket = rateLimitBuckets.get(minuteKey);
  if (!minuteBucket || now >= minuteBucket.resetAt) {
    minuteBucket = { count: 0, resetAt: now + 60000 };
    rateLimitBuckets.set(minuteKey, minuteBucket);
  }
  minuteBucket.count++;

  if (minuteBucket.count > CONFIG.MAX_REQUESTS_PER_MINUTE) {
    blockIP(ip, 'rate_limit_exceeded', minuteBucket.count);
    return { allowed: false, reason: 'Rate limit exceeded (per minute)' };
  }

  // Check per-second limit
  let secondBucket = rateLimitBuckets.get(secondKey);
  if (!secondBucket || now >= secondBucket.resetAt) {
    secondBucket = { count: 0, resetAt: now + 1000 };
    rateLimitBuckets.set(secondKey, secondBucket);
  }
  secondBucket.count++;

  if (secondBucket.count > CONFIG.MAX_REQUESTS_PER_SECOND) {
    blockIP(ip, 'rate_limit_exceeded', secondBucket.count);
    return { allowed: false, reason: 'Rate limit exceeded (per second)' };
  }

  return { allowed: true };
}

/**
 * Track request fingerprint
 */
function trackFingerprint(fingerprint: string, ip: string, userAgent: string): void {
  const now = Date.now();
  const existing = requestFingerprints.get(fingerprint);

  if (existing) {
    existing.count++;
    existing.lastSeen = now;

    // Check if suspicious (too many requests)
    if (existing.count > CONFIG.BLOCK_THRESHOLD_REQUESTS) {
      if (!existing.blocked) {
        existing.blocked = true;
        existing.blockedUntil = now + CONFIG.BLOCK_DURATION_MS;
        blockIP(ip, 'suspicious_activity', existing.count);
      }
    }
  } else {
    requestFingerprints.set(fingerprint, {
      ip,
      userAgent,
      fingerprint,
      count: 1,
      firstSeen: now,
      lastSeen: now,
      blocked: false
    });
  }
}

/**
 * Cleanup old entries
 */
function cleanup(): void {
  const now = Date.now();

  // Cleanup fingerprints
  for (const [key, fp] of requestFingerprints.entries()) {
    if (now - fp.lastSeen > CONFIG.FINGERPRINT_TTL_MS) {
      requestFingerprints.delete(key);
    }
  }

  // Cleanup expired IP blocks
  for (const [ip, block] of ipBlocks.entries()) {
    if (now > block.blockedUntil) {
      ipBlocks.delete(ip);
    }
  }

  // Cleanup old rate limit buckets
  for (const [key, bucket] of rateLimitBuckets.entries()) {
    if (now >= bucket.resetAt) {
      rateLimitBuckets.delete(key);
    }
  }
}

// Start cleanup interval
setInterval(cleanup, CONFIG.CLEANUP_INTERVAL_MS);

/** Lightweight probes used by CLI/TUI, load balancers, and Prometheus — must not trip per-second limits. */
function isProbeExemptPath(method: string, rawUrl: string): boolean {
  if (method !== 'GET') return false;
  const path = (rawUrl.split('?')[0] || '').toLowerCase();
  return path === '/health' || path === '/ready' || path === '/metrics';
}

/**
 * DDoS Protection Middleware
 */
export async function ddosProtection(
  request: FastifyRequest,
  reply: FastifyReply
): Promise<void> {
  if (isProbeExemptPath(request.method, request.url)) {
    return;
  }

  const ip = getClientIP(request);
  if (loopbackExemptFromDdos() && isLoopbackIP(ip)) {
    return;
  }

  const userAgent = request.headers['user-agent'] || 'unknown';

  // Check if IP is blocked
  if (isIPBlocked(ip)) {
    const block = ipBlocks.get(ip)!;
    const retryAfter = Math.ceil((block.blockedUntil - Date.now()) / 1000);
    
    reply.status(429).send({
      error: 'ip_blocked',
      message: 'IP address is temporarily blocked',
      reason: block.reason,
      retry_after: retryAfter,
      blocked_until: new Date(block.blockedUntil).toISOString()
    });
    return;
  }

  // Generate fingerprint
  const fingerprint = generateFingerprint(request);

  // Check rate limits
  const rateLimitCheck = checkRateLimit(fingerprint, ip);
  if (!rateLimitCheck.allowed) {
    reply.status(429).send({
      error: 'rate_limited',
      message: rateLimitCheck.reason || 'Too many requests'
    });
    return;
  }

  // Track fingerprint
  trackFingerprint(fingerprint, ip, userAgent);

  // Check if fingerprint is blocked
  const fp = requestFingerprints.get(fingerprint);
  if (fp?.blocked && fp.blockedUntil && Date.now() < fp.blockedUntil) {
    reply.status(429).send({
      error: 'suspicious_activity',
      message: 'Request pattern flagged as suspicious'
    });
    return;
  }

  // Attach fingerprint to request for logging
  (request as any).ddosFingerprint = fingerprint;
  (request as any).clientIP = ip;
}

/**
 * Get DDoS protection stats (for monitoring)
 */
export function getDDoSStats() {
  return {
    blockedIPs: ipBlocks.size,
    trackedFingerprints: requestFingerprints.size,
    activeRateLimitBuckets: rateLimitBuckets.size,
    blockedIPsList: Array.from(ipBlocks.values()).map(b => ({
      ip: b.ip,
      reason: b.reason,
      blockedUntil: new Date(b.blockedUntil).toISOString()
    }))
  };
}

