import { lookup } from "node:dns/promises";
import { isIP } from "node:net";

import { z } from "zod";

const REACHABILITY_TIMEOUT_MS = 5000;
const MAX_REDIRECT_HOPS = 5;

export type NormalizedUrl = {
  url: string;
  hostname: string;
  isApex: boolean;
};

const urlInputSchema = z.string().trim().min(1, "URL requise").max(2048, "URL trop longue");

export function normalizeUrl(input: string): NormalizedUrl {
  const trimmed = urlInputSchema.parse(input);

  const withProtocol = /^https?:\/\//i.test(trimmed) ? trimmed : `https://${trimmed}`;

  let parsed: URL;
  try {
    parsed = new URL(withProtocol);
  } catch {
    throw new Error("URL invalide");
  }

  if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
    throw new Error("URL invalide (protocole non supporté)");
  }
  if (!parsed.hostname || !parsed.hostname.includes(".")) {
    throw new Error("URL invalide (hostname manquant)");
  }

  parsed.hostname = parsed.hostname.toLowerCase();
  parsed.hash = "";

  // "apex" here = the hostname has no `www.` prefix, regardless of label count.
  // Used to decide whether to retry with `www.` if the URL is unreachable.
  const isApex = !parsed.hostname.startsWith("www.");

  return {
    url: parsed.toString(),
    hostname: parsed.hostname,
    isApex,
  };
}

/* ---------- SSRF guard ------------------------------------------------- */

// IPv4 ranges we never want the server to dial. Covers RFC1918 (private),
// loopback, link-local (incl. AWS/GCP/Azure IMDS at 169.254.169.254),
// 0.0.0.0/8, and the carrier-grade-NAT 100.64.0.0/10 range.
const PRIVATE_V4_PATTERNS: RegExp[] = [
  /^10\./,
  /^192\.168\./,
  /^172\.(1[6-9]|2\d|3[0-1])\./,
  /^127\./,
  /^169\.254\./,
  /^0\./,
  /^100\.(6[4-9]|[7-9]\d|1[01]\d|12[0-7])\./,
];

function isPrivateV4(ip: string): boolean {
  return PRIVATE_V4_PATTERNS.some((re) => re.test(ip));
}

function isPrivateV6(ip: string): boolean {
  const lower = ip.toLowerCase();
  if (lower === "::1" || lower === "::") return true;
  if (lower.startsWith("fe80:") || lower.startsWith("fe80::")) return true;
  // fc00::/7 = ULA (fc.., fd..)
  if (lower.startsWith("fc") || lower.startsWith("fd")) return true;
  // ::ffff:<ipv4> — IPv4-mapped IPv6; rewrap to test the inner v4.
  const mapped = lower.match(/^::ffff:(\d+\.\d+\.\d+\.\d+)$/);
  if (mapped) return isPrivateV4(mapped[1]!);
  return false;
}

/* Reject hostnames that resolve to private / loopback / link-local /
   metadata-endpoint IPs. Called before every server-side HTTP fetch
   originating from a user-supplied URL — the prod-test stack puts the
   app on an `internal: true` Docker network, so an unfiltered
   `fetch(<internal-host>)` would still reach every other container. */
async function assertPublicHost(hostname: string): Promise<void> {
  if (isIP(hostname)) {
    const family = isIP(hostname);
    if ((family === 4 && isPrivateV4(hostname)) || (family === 6 && isPrivateV6(hostname))) {
      throw new Error(`Host ${hostname} resolves to a non-public address`);
    }
    return;
  }
  const records = await lookup(hostname, { all: true, verbatim: true });
  for (const r of records) {
    if (r.family === 4 && isPrivateV4(r.address)) {
      throw new Error(`Host ${hostname} resolves to private IPv4 ${r.address}`);
    }
    if (r.family === 6 && isPrivateV6(r.address)) {
      throw new Error(`Host ${hostname} resolves to private IPv6 ${r.address}`);
    }
  }
}

/**
 * GET the URL with a short timeout to verify it actually responds.
 * Sitespeed's headless Chrome fails on unreachable hosts and produces
 * useless "couldn't load" screenshots — better to reject upfront.
 *
 * Follows redirects MANUALLY (max 5 hops), re-validating each hop's
 * hostname against the private-IP blocklist. This is the SSRF mitigation:
 * `redirect: "follow"` would let an attacker bounce a public URL into
 * `http://169.254.169.254/` (cloud metadata) in a single 302.
 */
async function isReachable(url: string): Promise<boolean> {
  let current = url;
  for (let hop = 0; hop < MAX_REDIRECT_HOPS; hop++) {
    let parsed: URL;
    try {
      parsed = new URL(current);
    } catch {
      return false;
    }
    if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return false;
    try {
      await assertPublicHost(parsed.hostname);
    } catch {
      return false;
    }

    const ctl = new AbortController();
    const timer = setTimeout(() => ctl.abort(), REACHABILITY_TIMEOUT_MS);
    let res: Response;
    try {
      res = await fetch(current, {
        method: "GET",
        redirect: "manual",
        signal: ctl.signal,
        headers: {
          "User-Agent": "Mozilla/5.0 (compatible; ElevaSpeedBot/1.0; +https://speed.elevaseo.app)",
        },
      });
    } catch {
      return false;
    } finally {
      clearTimeout(timer);
    }

    if (res.status >= 300 && res.status < 400) {
      const loc = res.headers.get("location");
      if (!loc) return res.status < 500;
      try {
        current = new URL(loc, current).toString();
      } catch {
        return false;
      }
      continue;
    }
    return res.status < 500;
  }
  return false;
}

/**
 * Resolve the URL the user intended:
 *   - validate the hostname resolves to a public IP (SSRF guard)
 *   - try the normalized URL first
 *   - if the apex fails, try `https://www.<host>` automatically
 *   - return the variant that actually responds
 *
 * Throws if the hostname is non-public or if neither variant is reachable.
 */
export async function resolveReachableUrl(input: string): Promise<NormalizedUrl> {
  const primary = normalizeUrl(input);
  await assertPublicHost(primary.hostname);

  if (await isReachable(primary.url)) return primary;

  if (primary.isApex) {
    const withWww = normalizeUrl(`https://www.${primary.hostname}`);
    if (await isReachable(withWww.url)) return withWww;
  }

  throw new Error(`Impossible d'accéder à ${primary.hostname}`);
}
