import { BadRequestException } from '@nestjs/common';
import { promises as dns } from 'dns';
import { isIP } from 'net';

const isProd = process.env.NODE_ENV === 'production';

const LOOPBACK_HOSTS = new Set(['localhost', '127.0.0.1', '::1']);

/**
 * True si `ip` (v4 o v6) cae en un rango privado/reservado/loopback/link-local.
 * El objetivo es bloquear SSRF hacia metadata de la nube (169.254.169.254),
 * la red interna (10/8, 172.16/12, 192.168/16) y loopback.
 */
export function isPrivateOrReservedIp(ip: string): boolean {
  const version = isIP(ip);
  if (version === 4) {
    const p = ip.split('.').map(Number);
    if (p[0] === 0) return true; // "this" network
    if (p[0] === 10) return true; // privada
    if (p[0] === 127) return true; // loopback
    if (p[0] === 169 && p[1] === 254) return true; // link-local (metadata)
    if (p[0] === 172 && p[1] >= 16 && p[1] <= 31) return true; // privada
    if (p[0] === 192 && p[1] === 168) return true; // privada
    if (p[0] === 100 && p[1] >= 64 && p[1] <= 127) return true; // CGNAT
    return false;
  }
  if (version === 6) {
    const lower = ip.toLowerCase();
    if (lower === '::1' || lower === '::') return true; // loopback / unspecified
    if (lower.startsWith('fe80')) return true; // link-local
    if (lower.startsWith('fc') || lower.startsWith('fd')) return true; // unique-local (ULA)
    const mapped = lower.match(/^::ffff:(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/);
    if (mapped) return isPrivateOrReservedIp(mapped[1]); // IPv4-mapped
    return false;
  }
  return false;
}

/**
 * Validación sincrónica (config-time) de una URL de webhook. Lanza
 * `BadRequestException` si no es segura. Reglas:
 *  - Solo http(s).
 *  - Loopback (localhost/127.0.0.1/::1) permitido SOLO fuera de producción (dev).
 *  - Fuera de loopback: se exige HTTPS.
 *  - Si el host es una IP literal, se bloquean rangos privados/reservados.
 * (El chequeo de DNS se hace en `assertResolvesToPublicHost` al momento del fetch.)
 */
export function assertSafeWebhookUrl(rawUrl: string): void {
  let url: URL;
  try {
    url = new URL(rawUrl);
  } catch {
    throw new BadRequestException('URL de webhook inválida');
  }

  if (url.protocol !== 'http:' && url.protocol !== 'https:') {
    throw new BadRequestException('Solo se permiten URLs http(s)');
  }

  const host = url.hostname.toLowerCase();

  if (LOOPBACK_HOSTS.has(host)) {
    if (isProd) {
      throw new BadRequestException('No se permiten URLs a localhost en producción');
    }
    return; // dev: loopback permitido
  }

  if (url.protocol !== 'https:') {
    throw new BadRequestException('Solo se permiten URLs HTTPS (o http://localhost en desarrollo)');
  }

  if (isIP(host) && isPrivateOrReservedIp(host)) {
    throw new BadRequestException('No se permiten URLs a direcciones IP privadas o reservadas');
  }
}

/**
 * Validación asincrónica (fetch-time): re-valida la URL y resuelve el hostname
 * por DNS, rechazando si CUALQUIER dirección resuelta es privada/reservada.
 * Protege contra DNS rebinding y contra URLs almacenadas antes de este fix.
 */
export async function assertResolvesToPublicHost(rawUrl: string): Promise<void> {
  assertSafeWebhookUrl(rawUrl);
  const url = new URL(rawUrl);
  const host = url.hostname.toLowerCase();

  if (LOOPBACK_HOSTS.has(host)) return; // ya validado (solo dev)
  if (isIP(host)) return; // IP literal ya validada en assertSafeWebhookUrl

  const addrs = await dns.lookup(host, { all: true });
  for (const a of addrs) {
    if (isPrivateOrReservedIp(a.address)) {
      throw new BadRequestException(`El host ${host} resuelve a una IP privada/reservada`);
    }
  }
}
