<?php

namespace App\Helpers\Utility;

use Exception;
use Symfony\Component\Process\Process;
use Illuminate\Support\Str;

class UtilPrefix {
  
    static function ip_in_blocks( $ip, $blocks, $return_addr = false ) {
        // Sanitise the IP...
        if(!Str::contains($ip, '/')) {
            $ip = "$ip/" . (Str::contains($ip, ':') ? '128' : '32');
        }
        
        [$ip_subnet, $ip_mask] = explode('/', $ip);        
        
        if(Str::contains($ip, ':')) {
        // Ganked and modified from: https://stackoverflow.com/questions/49365018/how-to-check-if-an-ipv6-address-is-in-a-specific-network-for-acl-implementation
            $ip_subnet = inet_pton($ip_subnet);
            $ip_length = strlen($ip_subnet);
    
            if (strlen($ip_subnet) < 4) {
                return false;
            }
            foreach ($blocks as $block) {
                // Sanitise the block...
                if(!Str::contains($block, '/')) {
                    $block = "$block/128";
                }

                $ar = explode('/',$block);

                if($ip_mask <= $ar[1]) {
                    continue;
                }
                
                if($ip === $block) {
                    if(!$return_addr) {
                        return [$ip_subnet, $ip_mask];
                    } else {
                        return true;
                    }
                }
    
                $ip1 = $ar[0];
                $ip1b = inet_pton($ip1);
                $ip1len = strlen($ip1b);
    
                if ($ip1len != $ip_length) {
                    continue;
                }
                if (count($ar)>1) {
                    $bits=(int)($ar[1]);
                } else {
                    $bits = $ip_length * 8;
                }
                for ($c=0; $bits>0; ++$c) {
                    $bytemask = ($bits < 8) ? 0xff ^ ((1 << (8-$bits))-1) : 0xff;
                    if (((ord($ip_subnet[$c]) ^ ord($ip1b[$c])) & $bytemask) != 0)
                        continue 2;
                    $bits-=8;
                }

                if(!$return_addr) {
                    return [$ip_subnet, $ip_mask];
                } else {
                    return true;
                }
            }
        }
        else {
            foreach($blocks as $block) {
                // Sanitise the block...
                if(!Str::contains($block, '/')) {
                    $block = "$block/32";
                }

                [$block_subnet, $block_mask] = explode('/', $block);

                if($ip_mask <= $block_mask) {
                    continue;
                }

                if ($ip === $block || ((ip2long($ip_subnet) & ~((1 << (32 - $block_mask)) - 1) ) === ip2long($block_subnet)))
                {
                    if(!$return_addr) {
                        return [$ip_subnet, $ip_mask];
                    } else {
                        return true;
                    }
                }
            }
        }

        return false;
    }

    static function ip_in_block( $ip, $block, $return_addr = false ) {
        // Sanitise the IP...
        if(!Str::contains($ip, '/')) {
            $ip = "$ip/" . (Str::contains($ip, ':') ? '128' : '32');
        }

        [$ip_subnet, $ip_mask] = explode('/', $ip);

        if($ip === $block) {
            if(!$return_addr) {
                return [$ip_subnet, $ip_mask];
            } else {
                return true;
            }
        }

        if(Str::contains($ip, ':')) {
            // Ganked and modified from: https://stackoverflow.com/questions/49365018/how-to-check-if-an-ipv6-address-is-in-a-specific-network-for-acl-implementation
            // Sanitise the block...
            if(!Str::contains($block, '/')) {
                $block = "$block/128";
            }

            $ar = explode('/',$block);

            if($ip_mask <= $ar[1]) {
                return false;
            }
            
            $ip_subnet = inet_pton(explode('/', $ip)[0]);
            $ip_length = strlen($ip_subnet);
    
            if (strlen($ip_subnet) < 4) {
                return false;
            }
    
            $ip1 = $ar[0];
            $ip1b = inet_pton($ip1);
            $ip1len = strlen($ip1b);
    
            if ($ip1len != $ip_length) {
                return false;
            }
    
            if (count($ar)>1) {
                $bits=(int)($ar[1]);
            } else {
                $bits = $ip_length * 8;
            }
            for ($c=0; $bits>0; ++$c) {
                $bytemask = ($bits < 8) ? 0xff ^ ((1 << (8-$bits))-1) : 0xff;
                if (((ord($ip_subnet[$c]) ^ ord($ip1b[$c])) & $bytemask) != 0)
                    return false;
                $bits-=8;
            }
    
            if(!$return_addr) {
                return [$ip_subnet, $ip_mask];
            } else {
                return true;
            }
        }
        else {
            // Sanitise the block...
            if(!Str::contains($block, '/')) {
                $block = "$block/32";
            }
            
            [$block_subnet, $block_mask] = explode('/', $block);

            if($ip_mask <= $block_mask) {
                return false;
            }


            if((ip2long($ip_subnet) & ~((1 << (32 - $block_mask)) - 1) ) === ip2long($block_subnet)) {
                if(!$return_addr) {
                    return [$ip_subnet, $ip_mask];
                } else {
                    return true;
                }
            }
    
        }

        return false;
    }

    static function get_excluded_blocks() {
        return [
            // Predefined
            //"0.0.0.0/8",
            "10.0.0.0/8",
            "100.64.0.0/10",
            "127.0.0.0/8",
            "169.254.0.0/16",
            "172.16.0.0/12",
            "192.0.0.0/24",
            "192.0.2.0/24",
            "192.88.99.0/24",
            "192.168.0.0/16",
            "198.18.0.0/15",
            "198.51.100.0/24",
            "203.0.113.0/24",
            "224.0.0.0/4",
            "233.252.0.0/24",
            "240.0.0.0/4",
            "255.255.255.255/32",
            // GSL
            '31.217.251.0/24',
            '103.137.13.0/24',
            '223.165.7.0/24',
        ];
    }

    static function valid_subnet($supernet_cidr, $subnet_cidr) {
        list($subnet_ip, $subnet_mask) = explode('/', $subnet_cidr);
        list($supernet_ip, $supernet_mask) = explode('/', $supernet_cidr);

        if($subnet_mask <= $supernet_mask) {
            return false;
        }
    
        $num_subnets = pow(2, $subnet_mask - $supernet_mask);
        
        // Calculate the number of IP addresses in each subnet
        $subnet_step = pow(2, 32 - $subnet_mask);
        
        // Initialize the first subnet to the first IP address in the supernet
        $subnet = ip2long($supernet_ip);
        
        // Generate the subnets
        $subnets = array();
        for ($i = 0; $i < $num_subnets; $i++) {
            // Generate the subnet IP address from the subnet base
            $subnet_ip = long2ip($subnet);

            // Add the subnet IP address to the subnets array
            $subnets[] = $subnet_ip.'/'.$subnet_mask;

            if("$subnet_ip/$subnet_mask" == $subnet_cidr) {
                return true;
            }
            
            // Increment the subnet base by the subnet size
            $subnet += $subnet_step;
        }
        
        return false;
    }

    static function valid_super_containing_sub($_supernet, $_subnets) {
        foreach($_subnets as $subnet) {
            if(static::ip_in_block($subnet, $_supernet)) {
                return true;
            }
        }

        return false;
    }

    static function valid_sub_in_supers($_subnet, $_supernets) {
        return !static::ip_in_blocks($_subnet, static::get_excluded_blocks()) && static::ip_in_blocks($_subnet, $_supernets);
    }

    static function valid_sub_in_super($_subnet, $_supernet) {
        return !static::ip_in_blocks($_subnet, static::get_excluded_blocks()) && static::ip_in_block($_subnet, $_supernet);
    }

    static function get_detailed() {
        return static::get_detailed_cached('prefix.detailed.json');
    }

    static function get_detailed_v6() {
        return static::get_detailed_cached('prefix.detailed6.json');
    }

    private static function get_detailed_cached($_file) {

        // Decoding and normalising the full route table is expensive and it is read
        // many times within a single run - cache per process. Validation is against
        // a hash of the raw file rather than filemtime: mtime only has one-second
        // resolution, so a rewrite within the same second would serve stale routes.
        static $cache = [];

        $path = storage_path($_file);

        $contents = file_exists($path) ? file_get_contents($path) : false;

        $to_return = [];
        $hash = null;

        if($contents !== false) {
            $hash = hash('xxh128', $contents);

            if(isset($cache[$_file]) && $cache[$_file]['hash'] === $hash) {
                return $cache[$_file]['data'];
            }

            $to_return = json_decode($contents);
        }

        if($to_return === null) {
            \Log::warning(json_last_error_msg());
            throw new Exception('Detailed configuration yields null when decoded.');
        }

        if(sizeof($to_return) === 0) {
            \Log::error("Size of imported prefixes was zero.");
            throw new Exception('Detailed configuration yields zero prefixes when decoded.');
        }

        $filtered = [];

        foreach($to_return as $x) {
            [$address, $mask] = explode('/', $x->prefix);

            $prefix = inet_ntop(inet_pton($address)) . "/$mask";

            if($prefix !== false) {
                $x->prefix = $prefix;

                $filtered[] = $x;
            }
        }

        $cache[$_file] = [ 'hash' => $hash, 'data' => collect($filtered) ];

        return $cache[$_file]['data'];
    }
}

?>