<?php

declare(strict_types=1);

namespace XBotControl;

use Psr\Http\Message\ServerRequestInterface;
use Clue\React\SQLite\DatabaseInterface;
use Clue\React\SQLite\Result;
use React\Promise\PromiseInterface;
use React\Promise\Promise;

class IPMatch

{
    /**
     * Main function to check if a given IP (IPv4 or IPv6) matches a whitelist of ranges from the database.
     *
     * @param string $ip The IP address to check.
     * @param object $db The database connection object (ReactPHP-based).
     * @return \React\Promise\Promise Promise resolving to true if the IP matches any of the whitelist ranges, false otherwise.
     */
    public static function checkIPWhitelistAsync(string $ip, $db): Promise
    {
        return self::getIPVersion($ip)->then(
            function ($ipVersion) use ($ip, $db) {
                if ($ipVersion === null) {
                    return false; // Invalid IP
                }

                return self::fetchWhitelistRanges($db, $ipVersion)->then(
                    function ($ranges) use ($ip, $ipVersion) {
                        if ($ipVersion === 'ipv4') {
                            return self::checkIPv4($ip, $ranges);
                        } elseif ($ipVersion === 'ipv6') {
                            return self::checkIPv6($ip, $ranges);
                        }
                        return false;
                    }
                );
            }
        );
    }

    /**
     * Determine the IP version (IPv4, IPv6, or null for invalid).
     *
     * @param string $ip The IP address to validate.
     * @return \React\Promise\Promise Promise resolving to 'ipv4', 'ipv6', or null.
     */
    private static function getIPVersion(string $ip): Promise
    {
        return new Promise(function (callable $resolve) use ($ip) {
            if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
                $resolve('ipv4');
            } elseif (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
                $resolve('ipv6');
            } else {
                $resolve(null);
            }
        });
    }

    /**
     * Fetch whitelist ranges from the database for a specific IP version.
     *
     * @param object $db The database connection object.
     * @param string $ipVersion The IP version ('ipv4' or 'ipv6').
     * @return \React\Promise\Promise Promise resolving to an array of CIDR ranges.
     */
    private static function fetchWhitelistRanges($db, string $ipVersion): Promise
    {
        return new Promise(function (callable $resolve, callable $reject) use ($db, $ipVersion) {
            $query = 'SELECT range FROM whitelist WHERE ip_version = ?';

            $db->query($query, [$ipVersion])->then(
                function ($result) use ($resolve) {
                    $ranges = array_column($result->resultRows, 'range');
                    $resolve($ranges);
                },
                function ($error) use ($reject) {
                    $reject($error); // Reject if the query fails
                }
            );
        });
    }




    /**
     * Check if a given IPv6 address is in a network from an array of ranges (asynchronous with ReactPHP Promise)
     *
     * @param string $ip IPv6 address to check
     * @param array $ranges Array of IPv6/CIDR ranges, e.g., ['2001:db8::/32', '2001:0db8:85a3::8a2e:0370:7334/128']
     * @return \React\Promise\Promise Promise resolving to true if the IPv6 is in any of the ranges, false otherwise
     */
    public static function checkIPv6(string $ip, array $ranges): PromiseInterface


    {
        return new Promise(function (callable $resolve) use ($ip, $ranges) {
            if (!filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
                $resolve(false);
                return;
            }

            foreach ($ranges as $range) {
                if (!is_string($range)) {
                    continue;
                }

                if (strpos($range, '/') === false) {
                    $range .= '/128';
                }

                [$range_ip, $netmask] = explode('/', $range, 2);
                if (!filter_var($range_ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) || !is_numeric($netmask) || $netmask < 0 || $netmask > 128) {
                    continue; // Skip invalid ranges
                }

                $ip_bin = inet_pton($ip);
                $range_bin = inet_pton($range_ip);
                $netmask_bin = str_repeat("\xff", (int)($netmask / 8));

                if ($netmask % 8 !== 0) {
                    $netmask_bin .= chr(0xff << (8 - ($netmask % 8)));
                }

                $netmask_bin = str_pad($netmask_bin, strlen($ip_bin), "\x00");

                if (($ip_bin & $netmask_bin) === ($range_bin & $netmask_bin)) {
                    $resolve(true); // Resolve with true if a match is found
                    return;
                }
            }

            $resolve(false); // Resolve with false if no matches are found
        });
    }

    /**
     * Check if a given IPv4 address is in a network from an array of ranges (asynchronous with ReactPHP Promise)
     *
     * @param string $ip IPv4 address to check
     * @param array $ranges Array of IPv4/CIDR ranges, e.g., ['192.168.1.0/24', '10.0.0.1/32']
     * @return \React\Promise\Promise Promise resolving to true if the IPv4 is in any of the ranges, false otherwise
     */
    public static function checkIPv4(string $ip, array $ranges): Promise
    {



        return new Promise(function (callable $resolve) use ($ip, $ranges) {
            if (!filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
                $resolve(false);
                return;
            }

            foreach ($ranges as $range) {
                if (!is_string($range)) {
                    continue;
                }

                if (strpos($range, '/') === false) {
                    $range .= '/32';
                }

                [$range_ip, $netmask] = explode('/', $range, 2);
                if (!filter_var($range_ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) || !is_numeric($netmask) || $netmask < 0 || $netmask > 32) {
                    continue; // Skip invalid ranges
                }

                $range_decimal = ip2long($range_ip);
                $ip_decimal = ip2long($ip);
                $netmask_decimal = -1 << (32 - (int)$netmask);

                if (($ip_decimal & $netmask_decimal) === ($range_decimal & $netmask_decimal)) {
                    $resolve(true); // Resolve with true if a match is found
                    return;
                }
            }

            $resolve(false); // Resolve with false if no matches are found
        });
    }
}
