import { lookup } from 'dns/promises';

const MAX_REDIRECTS = 5;

function isPrivateIp(ip: string): boolean {
  if (
    ip === '127.0.0.1' ||
    ip === '::1' ||
    ip === '0.0.0.0' ||
    ip === '::'
  )
    return true;

  // 10.x.x.x
  if (ip.startsWith('10.'))
    return true;

  // 172.16.0.0 - 172.31.255.255
  if (ip.startsWith('172.')) {
    const second = parseInt(ip.split('.')[1], 10);

    if (second >= 16 && second <= 31)
      return true;
  }

  // 192.168.x.x
  if (ip.startsWith('192.168.'))
    return true;

  // 169.254.x.x (link-local, AWS metadata)
  if (ip.startsWith('169.254.'))
    return true;

  // IPv6 private ranges (fc00::/7, fe80::/10)
  const lower = ip.toLowerCase();

  if (lower.startsWith('fc') || lower.startsWith('fd') || lower.startsWith('fe80'))
    return true;

  return false;
}

export async function resolveAndValidateHost(url: string): Promise<boolean> {
  let hostname: string;

  try {
    hostname = new URL(url).hostname;
  } catch {
    return false;
  }

  // Block obvious private hostnames
  if (hostname === 'localhost' || hostname === 'metadata.google.internal')
    return false;

  // If hostname is already an IP literal, check it directly
  if (/^[\d.]+$/.test(hostname) || hostname.startsWith('['))
    return !isPrivateIp(hostname.replace(/[[\]]/g, ''));

  try {
    const { address } = await lookup(hostname);

    return !isPrivateIp(address);
  } catch {
    return false;
  }
}

export interface SafeFetchOptions {
  signal?: AbortSignal;
  headers?: Record<string, string>;
}

/**
 * Fetches a URL while following redirects safely, validating each
 * hop against SSRF (private/reserved IPs, blocked hostnames).
 *
 * The caller must validate the initial URL with `resolveAndValidateHost`
 * before calling this function.
 */
export async function safeFetch(url: string, options: SafeFetchOptions = {}): Promise<Response | undefined> {
  let currentUrl = url;
  let response: Response | undefined;

  for (let redirects = 0; redirects <= MAX_REDIRECTS; redirects++) {
    response = await fetch(currentUrl, {
      redirect: 'manual',
      signal: options.signal,
      headers: options.headers
    });

    const location = response.headers.get('location');

    if (response.status >= 300 && response.status < 400 && location) {
      let nextUrl: string;

      try {
        nextUrl = new URL(location, currentUrl).href;
      } catch {
        break;
      }

      if (!/^https?:\/\//i.test(nextUrl))
        break;

      const redirectAllowed = await resolveAndValidateHost(nextUrl);

      if (!redirectAllowed)
        break;

      currentUrl = nextUrl;
      continue;
    }

    break;
  }

  return response;
}
