import * as dns from 'dns';
import { isIP } from 'node:net';
import { isPrivateOrForbidden } from '../../../net/ssrf-strict.js';

// These delegate to the hardened range check in src/net/ssrf-strict.ts so that
// WebFetch / DownloadFile / BrowseWeb get the same coverage as MCP and SSH:
// loopback, RFC1918, link-local + cloud metadata (169.254/16, fd00:ec2::),
// CGNAT (100.64/10), 0.0.0.0/8, IPv4-mapped IPv6, NAT64, multicast, reserved.
export function isPrivateIPv4(ip: string): boolean {
  if (isIP(ip) !== 4) return false;
  return isPrivateOrForbidden(ip, 4);
}

export function isPrivateIPv6(ip: string): boolean {
  const normalized = ip.toLowerCase().replace(/^\[|\]$/g, '');
  if (isIP(normalized) !== 6) return false;
  return isPrivateOrForbidden(normalized, 6);
}

export function isHostAllowed(hostname: string, allowedHosts: string[]): boolean {
  return allowedHosts.includes(hostname);
}

/**
 * Block requests to private/forbidden destinations.
 *
 * Resolves ALL addresses for the hostname (not just the first) and rejects if
 * any resolves to a private/forbidden range. An explicit allowlist entry
 * bypasses the check (used for trusted internal hosts).
 */
export async function checkSSRF(hostname: string, allowedHosts: string[]): Promise<void> {
  if (hostname === 'localhost' && !isHostAllowed(hostname, allowedHosts)) {
    throw new Error(`SSRF blocked: hostname "localhost" is not allowed`);
  }
  if (isHostAllowed(hostname, allowedHosts)) {
    return;
  }
  let addrs: Array<{ address: string; family: number }>;
  try {
    addrs = await dns.promises.lookup(hostname, { all: true });
  } catch (e) {
    throw new Error(`DNS resolution failed for "${hostname}": ${(e as Error).message}`);
  }
  if (addrs.length === 0) {
    throw new Error(`SSRF blocked: "${hostname}" resolved to no addresses`);
  }
  for (const a of addrs) {
    if (isPrivateOrForbidden(a.address, a.family as 4 | 6)) {
      throw new Error(`SSRF blocked: "${hostname}" resolves to forbidden IP "${a.address}"`);
    }
  }
}

/**
 * SSRF-safe fetch that re-validates every redirect hop.
 *
 * `fetch`'s default redirect following re-resolves DNS and would happily
 * follow a 30x to http://169.254.169.254/ (cloud metadata) or an internal
 * host. This follows redirects manually and runs `checkSSRF` against each
 * Location before requesting it, so a public URL cannot bounce the request
 * into a private destination.
 *
 * Residual: this does not pin the resolved IP, so a sub-second DNS-rebinding
 * attacker can still race the validation lookup against the connection lookup.
 * Full pinning (as in src/net/ssrf-strict.ts#pinnedFetch) is the follow-up;
 * this closes the redirect path, which is the practically exploitable one.
 */
export async function ssrfSafeFetch(
  url: string,
  allowedHosts: string[],
  init: RequestInit = {},
  maxRedirects = 5,
): Promise<Response> {
  let current = url;
  for (let hop = 0; hop <= maxRedirects; hop++) {
    const parsed = new URL(current);
    await checkSSRF(parsed.hostname, allowedHosts);
    const res = await fetch(current, { ...init, redirect: 'manual' });
    const location = res.status >= 300 && res.status < 400 ? res.headers.get('location') : null;
    if (!location) {
      return res;
    }
    // Resolve relative redirects against the current URL.
    current = new URL(location, current).toString();
  }
  throw new Error('SSRF blocked: too many redirects');
}
