/**
 * Private / reserved IP range detection.
 *
 * Pure CIDR membership check — no I/O, no DNS. Used by the SSRF guard to
 * reject monitor checks aimed at internal infrastructure (cloud metadata,
 * loopback, RFC1918, link-local, etc.).
 *
 * Coverage mirrors what UptimeRobot / BetterStack block. Conservative on
 * purpose: better to refuse a legitimate intranet target (operator can flip
 * ALLOW_PRIVATE_NETWORK) than to let one slip through.
 *
 * BigInt arithmetic uses BigInt() calls rather than `n` literals so the
 * module compiles under the project's ES2017 TypeScript target while still
 * running on the Node 20 runtime that supports it.
 */

const ZERO = BigInt(0);
const ONE = BigInt(1);
const TWO_POW_32_MINUS_1 = (ONE << BigInt(32)) - ONE;
const TWO_POW_128_MINUS_1 = (ONE << BigInt(128)) - ONE;
const NOT_ZERO = ~ZERO;
const MASK_16 = BigInt(0xffff);
const MASK_32 = BigInt(0xffffffff);
const IPV4_OCTET_SHIFT = BigInt(8);
const IPV6_GROUP_SHIFT = BigInt(16);

interface Cidr {
    base: bigint;
    mask: bigint;
}

function ipv4ToBigInt(ip: string): bigint | null {
    const m = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(ip);
    if (!m) return null;
    let result = ZERO;
    for (let i = 1; i <= 4; i++) {
        const octet = parseInt(m[i], 10);
        if (octet < 0 || octet > 255) return null;
        result = (result << IPV4_OCTET_SHIFT) | BigInt(octet);
    }
    return result;
}

function ipv6ToBigInt(ip: string): bigint | null {
    const stripped = ip.replace(/^\[|\]$/g, '').split('%')[0];

    // IPv4-mapped form like ::ffff:127.0.0.1 — extract the IPv4 tail
    const v4MappedMatch = /^(.+:)?(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/.exec(stripped);
    let head = stripped;
    let v4Tail: bigint | null = null;
    if (v4MappedMatch && v4MappedMatch[2]) {
        v4Tail = ipv4ToBigInt(v4MappedMatch[2]);
        if (v4Tail === null) return null;
        // Strip only a single trailing colon — preserve `::` shorthand
        head = (v4MappedMatch[1] ?? '').replace(/(?<!:):$/, '');
        if (head === '') head = '::';
    }

    if (!/^[0-9a-fA-F:]*$/.test(head)) return null;

    // Reject three-or-more consecutive colons and multiple `::` shorthands
    if (/:{3,}/.test(head)) return null;
    if ((head.split('::').length - 1) > 1) return null;

    const doubleColon = head.indexOf('::');
    let groups: string[];
    if (doubleColon === -1) {
        groups = head === '' ? [] : head.split(':');
    } else {
        const left = head.slice(0, doubleColon);
        const right = head.slice(doubleColon + 2);
        const leftGroups = left === '' ? [] : left.split(':');
        const rightGroups = right === '' ? [] : right.split(':');
        const totalExplicit = leftGroups.length + rightGroups.length + (v4Tail !== null ? 2 : 0);
        const fill = 8 - totalExplicit;
        if (fill < 0) return null;
        groups = [...leftGroups, ...Array(fill).fill('0'), ...rightGroups];
    }

    if (v4Tail !== null) {
        const hi = Number((v4Tail >> IPV6_GROUP_SHIFT) & MASK_16).toString(16);
        const lo = Number(v4Tail & MASK_16).toString(16);
        groups.push(hi, lo);
    }

    if (groups.length !== 8) return null;

    let result = ZERO;
    for (const g of groups) {
        if (g.length > 4) return null;
        const n = parseInt(g || '0', 16);
        if (Number.isNaN(n) || n < 0 || n > 0xffff) return null;
        result = (result << IPV6_GROUP_SHIFT) | BigInt(n);
    }
    return result;
}

function cidrV4(prefix: string): Cidr {
    const [ip, bits] = prefix.split('/');
    const base = ipv4ToBigInt(ip);
    if (base === null) throw new Error(`Invalid IPv4 CIDR: ${prefix}`);
    const b = parseInt(bits, 10);
    const mask = b === 0 ? ZERO : (NOT_ZERO << BigInt(32 - b)) & TWO_POW_32_MINUS_1;
    return { base: base & mask, mask };
}

function cidrV6(prefix: string): Cidr {
    const [ip, bits] = prefix.split('/');
    const base = ipv6ToBigInt(ip);
    if (base === null) throw new Error(`Invalid IPv6 CIDR: ${prefix}`);
    const b = parseInt(bits, 10);
    const mask = b === 0 ? ZERO : (NOT_ZERO << BigInt(128 - b)) & TWO_POW_128_MINUS_1;
    return { base: base & mask, mask };
}

const PRIVATE_V4: Cidr[] = [
    cidrV4('0.0.0.0/8'),           // "this network"
    cidrV4('10.0.0.0/8'),          // RFC1918
    cidrV4('100.64.0.0/10'),       // CGNAT
    cidrV4('127.0.0.0/8'),         // loopback
    cidrV4('169.254.0.0/16'),      // link-local (incl. cloud metadata 169.254.169.254)
    cidrV4('172.16.0.0/12'),       // RFC1918
    cidrV4('192.0.0.0/24'),        // IETF protocol
    cidrV4('192.0.2.0/24'),        // documentation
    cidrV4('192.168.0.0/16'),      // RFC1918
    cidrV4('198.18.0.0/15'),       // benchmarking
    cidrV4('198.51.100.0/24'),     // documentation
    cidrV4('203.0.113.0/24'),      // documentation
    cidrV4('224.0.0.0/4'),         // multicast
    cidrV4('240.0.0.0/4'),         // reserved
    cidrV4('255.255.255.255/32'),  // broadcast
];

const PRIVATE_V6: Cidr[] = [
    cidrV6('::/128'),              // unspecified
    cidrV6('::1/128'),             // loopback
    cidrV6('64:ff9b::/96'),        // NAT64
    cidrV6('100::/64'),            // discard
    cidrV6('2001::/32'),           // Teredo
    cidrV6('2001:db8::/32'),       // documentation
    cidrV6('fc00::/7'),            // unique local
    cidrV6('fe80::/10'),           // link-local
    cidrV6('ff00::/8'),            // multicast
];

// IPv4-mapped IPv6 prefix ::ffff:0:0/96
const V4_MAPPED_PREFIX = MASK_16 << BigInt(32);
const V4_MAPPED_MASK = (NOT_ZERO << BigInt(32)) & TWO_POW_128_MINUS_1;

/**
 * Returns true if the given IP literal sits inside any blocked range.
 * Handles IPv4-mapped IPv6 (::ffff:127.0.0.1) by re-checking against
 * IPv4 ranges. Returns false for unparseable input (caller should
 * reject upstream — we err on safety only for *recognised* private space).
 */
export function isPrivateIp(ip: string): boolean {
    const v4 = ipv4ToBigInt(ip);
    if (v4 !== null) {
        return PRIVATE_V4.some(r => (v4 & r.mask) === r.base);
    }

    const v6 = ipv6ToBigInt(ip);
    if (v6 === null) return false;

    if ((v6 & V4_MAPPED_MASK) === V4_MAPPED_PREFIX) {
        const embedded = v6 & MASK_32;
        return PRIVATE_V4.some(r => (embedded & r.mask) === r.base);
    }

    return PRIVATE_V6.some(r => (v6 & r.mask) === r.base);
}

/**
 * Convenience: true if hostname looks like an IPv4 or IPv6 literal.
 * Used by the SSRF guard to skip DNS for direct-IP monitor URLs.
 */
export function isIpLiteral(host: string): boolean {
    return ipv4ToBigInt(host) !== null || ipv6ToBigInt(host.replace(/^\[|\]$/g, '')) !== null;
}

// Ranges that ALLOW_PRIVATE_NETWORK cannot unlock. Today: link-local IPv4
// (covers the cloud metadata endpoints — 169.254.169.254 on AWS / GCP / Azure)
// and IPv6 link-local. Intranet monitoring should never need to probe these
// from inside the worker, and one mis-typed monitor URL on a cloud host could
// otherwise exfiltrate IAM credentials.
const ALWAYS_BLOCKED_V4: Cidr[] = [
    cidrV4('169.254.0.0/16'),
];
const ALWAYS_BLOCKED_V6: Cidr[] = [
    cidrV6('fe80::/10'),
];

/**
 * True if the IP sits in a range that ALLOW_PRIVATE_NETWORK must never
 * unblock. Cloud metadata is the primary motivator.
 */
export function isAlwaysBlocked(ip: string): boolean {
    const v4 = ipv4ToBigInt(ip);
    if (v4 !== null) {
        return ALWAYS_BLOCKED_V4.some(r => (v4 & r.mask) === r.base);
    }

    const v6 = ipv6ToBigInt(ip);
    if (v6 === null) return false;

    if ((v6 & V4_MAPPED_MASK) === V4_MAPPED_PREFIX) {
        const embedded = v6 & MASK_32;
        return ALWAYS_BLOCKED_V4.some(r => (embedded & r.mask) === r.base);
    }

    return ALWAYS_BLOCKED_V6.some(r => (v6 & r.mask) === r.base);
}

// AUDIT-2 #2 (2026-05-24): on Docker / multi-container hosts the worker
// shares loopback + ULA with sibling containers (a DB, a vault, a Tailscale
// sidecar). ALLOW_PRIVATE_NETWORK was unlocking those — the operator
// probably enabled it for RFC1918 intranet monitoring, not "let the
// monitor probe its host's own services." Promote them to a separate tier
// that needs a *second* explicit opt-in (ALLOW_LOOPBACK_PROBES=true).
const LOOPBACK_OR_ULA_V4: Cidr[] = [
    cidrV4('127.0.0.0/8'),         // IPv4 loopback
];
const LOOPBACK_OR_ULA_V6: Cidr[] = [
    cidrV6('::1/128'),             // IPv6 loopback
    cidrV6('fc00::/7'),            // ULA (IPv6 same-host private)
];

/**
 * True if the IP sits in the loopback/ULA tier. `ALLOW_PRIVATE_NETWORK`
 * does NOT unlock these; the caller must also pass `allowLoopback: true`
 * (driven by `ALLOW_LOOPBACK_PROBES=true`).
 */
export function isLoopbackOrUla(ip: string): boolean {
    const v4 = ipv4ToBigInt(ip);
    if (v4 !== null) {
        return LOOPBACK_OR_ULA_V4.some(r => (v4 & r.mask) === r.base);
    }

    const v6 = ipv6ToBigInt(ip);
    if (v6 === null) return false;

    if ((v6 & V4_MAPPED_MASK) === V4_MAPPED_PREFIX) {
        const embedded = v6 & MASK_32;
        return LOOPBACK_OR_ULA_V4.some(r => (embedded & r.mask) === r.base);
    }

    return LOOPBACK_OR_ULA_V6.some(r => (v6 & r.mask) === r.base);
}
