<?php

/**
 * IP Blacklist utility
 *
 * Checks whether a visitor's IP is blacklisted in Redis.
 * Supports both IPv4 and IPv6. Whitelisted IPs are never blocked.
 *
 * Redis keys used:
 *   blacklist_ips      – set of integer-encoded blocked IPs
 *   ivt_ip_whitelist   – set of integer-encoded trusted IPs (overrides blacklist)
 *
 * Constants (define in your app to override; defaults to localhost if not set):
 *   BLACKLIST_REDIS_SERVER  – Redis host  (default: 127.0.0.1)
 *   BLACKLIST_REDIS_PORT    – Redis port  (default: 22122)
 *
 * Usage:
 *   require_once '/path/to/IpBlacklist.php';
 *   if (isBlacklistedIp()) { // redirect or block }
 */

if (!defined('BLACKLIST_REDIS_SERVER')) {
    define('BLACKLIST_REDIS_SERVER', '127.0.0.1');
}
if (!defined('BLACKLIST_REDIS_PORT')) {
    define('BLACKLIST_REDIS_PORT', 22122);
}

if (!function_exists('ip2long_v6')) {
    /**
     * Convert an IPv6 address to its integer string representation.
     * Requires either the GMP or BCMath PHP extension.
     */
    function ip2long_v6($ip)
    {
        $ip_n = inet_pton($ip);
        $bin  = '';
        for ($bit = strlen($ip_n) - 1; $bit >= 0; $bit--) {
            $bin = sprintf('%08b', ord($ip_n[$bit])) . $bin;
        }

        if (function_exists('gmp_init')) {
            return gmp_strval(gmp_init($bin, 2), 10);
        } elseif (function_exists('bcadd')) {
            $dec = '0';
            for ($i = 0; $i < strlen($bin); $i++) {
                $dec = bcmul($dec, '2', 0);
                $dec = bcadd($dec, $bin[$i], 0);
            }
            return $dec;
        } else {
            trigger_error('GMP or BCMATH extension not installed!', E_USER_ERROR);
        }
    }
}

if (!function_exists('long2ip_v6')) {
    /**
     * Inverse of ip2long_v6(): convert a 128-bit decimal string back to an
     * IPv6 address. Requires either the GMP or BCMath PHP extension.
     */
    function long2ip_v6($dec)
    {
        $dec = (string) $dec;

        if (function_exists('gmp_init')) {
            $bin = gmp_strval(gmp_init($dec, 10), 2);
        } elseif (function_exists('bcadd')) {
            $bin = '';
            do {
                $bin = bcmod($dec, '2') . $bin;
                $dec = bcdiv($dec, '2', 0);
            } while (bccomp($dec, '0'));
        } else {
            trigger_error('GMP or BCMATH extension not installed!', E_USER_ERROR);
            return '';
        }

        $bin   = str_pad($bin, 128, '0', STR_PAD_LEFT);
        $parts = [];
        for ($bit = 0; $bit <= 7; $bit++) {
            $parts[] = dechex(bindec(substr($bin, $bit * 16, 16)));
        }
        return inet_ntop(inet_pton(implode(':', $parts)));
    }
}

if (!function_exists('intToIP')) {
    /**
     * Inverse of ipToInt(): convert a stored numeric IP value back to its
     * printable form. Handles the three formats found in legacy click logs:
     *   - empty / null / "0"                    → ''
     *   - already-formatted "1.2.3.4" or "::1"  → returned as-is
     *   - numeric, ≤ 4294967295                 → IPv4 via long2ip()
     *   - numeric, > 4294967295                 → IPv6 via long2ip_v6()
     *
     * MySQL's INET_NTOA() only handles 32-bit IPv4, so callers that select
     * raw click_ip values should use this helper to render IPv6 correctly.
     */
    function intToIP($value)
    {
        if ($value === null || $value === '' || $value === 0 || $value === '0') {
            return '';
        }

        $s = (string) $value;
        if (strpos($s, '.') !== false || strpos($s, ':') !== false) {
            return $s;
        }
        if (!ctype_digit($s)) {
            return $s;
        }

        if (bccomp($s, '4294967295', 0) <= 0) {
            return long2ip((int) $s);
        }

        $out = long2ip_v6($s);
        return $out !== false && $out !== null ? $out : '';
    }
}

/**
 * Resolve the real visitor IP from request headers.
 * Honors HTTP_X_FORWARDED_FOR (takes the first/leftmost IP).
 * Allows ?ip= override for testing purposes.
 *
 * @return string|false  IP string, or false if it cannot be determined.
 */
function getVisitorIp()
{
    $raw = isset($_SERVER['HTTP_X_FORWARDED_FOR'])
        ? $_SERVER['HTTP_X_FORWARDED_FOR']
        : (isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : '');
    $parts = explode(',', $raw);
    $ip = trim($parts[0]);

    // Allow IP override via query param for testing
    if (!empty($_GET['ip'])) {
        $ip = $_GET['ip'];
    }

    return $ip !== '' ? $ip : false;
}

/**
 * Convert a validated IP address (v4 or v6) to its integer representation.
 *
 * @return int|string|false  Integer for IPv4, numeric string for IPv6, false on failure.
 */
function ipToInt($ip)
{
    if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
        return ip2long_v6($ip);
    }

    $int = ip2long($ip);
    return $int !== false ? $int : false;
}

/**
 * Check whether the current visitor's IP is blacklisted in Redis.
 *
 * Returns true  → IP is blacklisted (caller should block/redirect).
 * Returns false → IP is clean, whitelisted, or the check could not be performed.
 *
 * @param bool $debug  When true, prints the resolved IP and integer, then returns false
 *                     (mirrors the ?debug= behaviour from HomeController).
 *                     You can also trigger this via $_REQUEST['debug'].
 */
function isBlacklistedIp($debug = false)
{
    $ip = getVisitorIp();
    if (!$ip) {
        return false;
    }

    $ipInt = ipToInt($ip);
    if ($ipInt === false) {
        return false;
    }

    // Debug mode: show resolved values and skip the actual block
    if ($debug || !empty($_REQUEST['debug'])) {
        echo "Blacklist IP check — IP: {$ip}, ipInt: {$ipInt}";
        return false;
    }

    try {
        $redis = new Redis();
        $redis->pconnect(BLACKLIST_REDIS_SERVER, BLACKLIST_REDIS_PORT);

        if ($redis->sIsMember('ivt_ip_whitelist', $ipInt)) {
            return false;
        }

        return (bool) $redis->sIsMember('blacklist_ips', $ipInt);
    } catch (Exception $e) {
        error_log('IP blacklist Redis error: ' . $e->getMessage());
        return false;
    }
}
