import dns from 'dns/promises';
import net from 'net';

const MAX_REDIRECTS = 3;

function isPrivateIpv4(ip: string): boolean {
  const parts = ip.split('.').map(part => Number(part));
  if (parts.length !== 4 || parts.some(part => !Number.isInteger(part) || part < 0 || part > 255)) return true;
  const [a, b] = parts;
  return a === 10
    || a === 127
    || (a === 169 && b === 254)
    || (a === 172 && b >= 16 && b <= 31)
    || (a === 192 && b === 168)
    || a === 0;
}

function isPrivateIpv6(ip: string): boolean {
  const normalized = ip.toLowerCase();
  return normalized === '::1'
    || normalized === '::'
    || normalized.startsWith('fc')
    || normalized.startsWith('fd')
    || normalized.startsWith('fe80:');
}

function isPrivateAddress(ip: string): boolean {
  const family = net.isIP(ip);
  if (family === 4) return isPrivateIpv4(ip);
  if (family === 6) return isPrivateIpv6(ip);
  return true;
}

async function assertPublicHttpUrl(url: URL): Promise<void> {
  if (url.protocol !== 'http:' && url.protocol !== 'https:') {
    throw new Error('Only HTTP(S) URLs are supported');
  }
  if (url.username || url.password) {
    throw new Error('URL credentials are not allowed');
  }

  const addresses = await dns.lookup(url.hostname, { all: true, verbatim: true });
  if (addresses.length === 0 || addresses.some(address => isPrivateAddress(address.address))) {
    throw new Error('Private or local network URLs are not allowed');
  }
}

export async function fetchPublicHttpUrl(input: string, init: RequestInit = {}, redirectCount = 0): Promise<Response> {
  if (redirectCount > MAX_REDIRECTS) throw new Error('Too many redirects');

  let url: URL;
  try {
    url = new URL(input);
  } catch {
    throw new Error('Invalid URL');
  }

  await assertPublicHttpUrl(url);
  const response = await fetch(url, {
    ...init,
    redirect: 'manual',
  });

  if (response.status >= 300 && response.status < 400) {
    const location = response.headers.get('location');
    if (!location) return response;
    const redirected = new URL(location, url).toString();
    return fetchPublicHttpUrl(redirected, init, redirectCount + 1);
  }

  return response;
}
