import { isIP } from 'net';
import { promises as dns } from 'dns';
import { loadEnv } from '@/config/env';
import { ValidationError } from '@/common/errors/domain-error';

/**
 * Outbound HTTP targets (webhooks, ERP push) are tenant-controlled, so the API
 * must not be turned into a proxy to its own network (SSRF). Private, loopback
 * and link-local addresses are rejected unless OUTBOUND_ALLOW_PRIVATE_HOSTS=true,
 * which on-prem installations set when the ERP lives on the LAN.
 */
export async function assertPublicUrl(raw: string, what = 'URL'): Promise<void> {
  let url: URL;
  try {
    url = new URL(raw);
  } catch {
    throw new ValidationError(`${what} is not a valid URL`);
  }
  if (!['http:', 'https:'].includes(url.protocol)) throw new ValidationError(`${what} must use http or https`);
  if (loadEnv().OUTBOUND_ALLOW_PRIVATE_HOSTS) return;
  const host = url.hostname.replace(/^\[|\]$/g, '').toLowerCase();
  if (host === 'localhost' || host.endsWith('.localhost') || host.endsWith('.local') || host.endsWith('.internal') || host === 'metadata.google.internal') {
    throw new ValidationError(`${what} must not point at a private or internal host`);
  }
  const addresses: string[] = [];
  if (isIP(host)) addresses.push(host);
  else {
    try {
      const res = await dns.lookup(host, { all: true });
      addresses.push(...res.map((r) => r.address));
    } catch {
      throw new ValidationError(`${what}: host name cannot be resolved`);
    }
  }
  for (const a of addresses) {
    if (isPrivateAddress(a)) throw new ValidationError(`${what} must not point at a private or internal network address`);
  }
}

export function isPrivateAddress(ip: string): boolean {
  if (isIP(ip) === 4) {
    const [a, b] = ip.split('.').map(Number);
    return a === 10 || a === 127 || a === 0 || (a === 169 && b === 254) || (a === 172 && b >= 16 && b <= 31) || (a === 192 && b === 168) || (a === 100 && b >= 64 && b <= 127);
  }
  const v6 = ip.toLowerCase();
  if (v6 === '::1' || v6 === '::') return true;
  if (v6.startsWith('fc') || v6.startsWith('fd') || v6.startsWith('fe80')) return true;
  if (v6.startsWith('::ffff:')) return isPrivateAddress(v6.slice(7));
  return false;
}

/** fetch with a hard timeout (outbound calls must never hold a worker or a transaction). */
export async function fetchWithTimeout(url: string, init: RequestInit, timeoutMs: number): Promise<Response> {
  const controller = new AbortController();
  const t = setTimeout(() => controller.abort(), timeoutMs);
  try {
    return await fetch(url, { ...init, signal: controller.signal });
  } finally {
    clearTimeout(t);
  }
}
