<?php

namespace WtbFirewall\Helpers;

// Prevent direct access.
if (!defined('ABSPATH')) {
    exit;
}

/**
 * Denne klassen holder funksjoner relatert til IP addresser.
 */
class IpHelper {

    /**
     * @return string IP addresse for besøkende. Tom string dersom feil.
     */
    public static function getVisitorIP(): string {
        $remoteAddr = '';

        if (isset($_SERVER['REMOTE_ADDR']) && is_string($_SERVER['REMOTE_ADDR'])) {
            $remoteAddr = trim($_SERVER['REMOTE_ADDR']);
        }

        if (!\WtbFirewall\Config\FirewallConfig::$proxyServer['enabled']) {
            return $remoteAddr;
        }

        if ((self::isValidIpv4($remoteAddr) && self::ipv4HasProxyServer()) ||
                (self::isValidIpv6($remoteAddr) && self::ipv6HasProxyServer())) {
            //Vi har en gyldig proxy server for $_SERVER['REMOTE_ADDR']
            return self::proxyGetIp(\WtbFirewall\Config\FirewallConfig::$proxyServer['header']);
        } else {
            //Ingen proxy server for IP
            return $remoteAddr;
        }
    }

    /**
     * Sjekker om $ip er en gyldig IPv4.
     * 
     * @param type $ip String som skal sjekkes.
     * @return bool TRUE dersom $ip er en gyldig IPv4 addresse.
     */
    public static function isValidIpv4($ip): bool {
        return (bool) filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4);
    }

    /**
     * Sjekker om $ip er en gyldig IPv6.
     * 
     * @param type $ip String som skal sjekkes.
     * @return bool TRUE dersom $ip er en gyldig IPv6 addresse.
     */
    public static function isValidIpv6($ip): bool {
        return (bool) filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6);
    }

    /**
     * Tar imot en IPv6 addresse, som binary string, og gjør om til vanlig tekst som kan vises til bruker.
     * 
     * @param string $ipv6Binary
     * @param int $prefix
     * @return string
     */
    public static function ipv6BinaryToString(string $ipv6Binary, int $prefix): string {
        if (strlen($ipv6Binary) !== 16) {
            return 'Invalid IPv6 Address: Incorrect Length';
        }

        if ($prefix < 0 || $prefix > 128) {
            return 'Invalid IPv6 Address: Incorrect Prefix';
        }

        $ip = @inet_ntop($ipv6Binary);
        if ($ip === false || $ip === '') {
            return 'Invalid IPv6 Address: Incorrect Format';
        }

        $ip = strtolower($ip);

        if ($prefix !== 128) {
            return $ip . '/' . $prefix;
        }

        return $ip;
    }

    /**
     * Tar imot array med informasjon om IPv4 addresse/range. Bruker samme format som returnert av Firewall()::parseIpv4Input()
     * Gjør til tekst som kan vises til bruker.
     * 
     * @param array $ipv4 IPv4 addresse informasjon.
     * @return string Tekst som kan vises til bruker.
     */
    public static function ipv4BinaryToString(array $ipv4): string {
        if ($ipv4['type'] == 'single') {
            return long2ip($ipv4['start']);
        } else if ($ipv4['type'] == 'range') {
            return long2ip($ipv4['start']) . '-' . long2ip($ipv4['end']);
        } else if ($ipv4['type'] == 'cidr') {
            return long2ip($ipv4['start']) . '/' . $ipv4['prefix'];
        }
    }

    /**
     * Går gjennom listen for godkjente IPv4 proxy servere og sjekker om $_SERVER['REMOTE_ADDR'] er i listen.
     * 
     * @return bool TRUE dersom $_SERVER['REMOTE_ADDR'] er en godkjent proxy server, FALSE hvis ikke.
     */
    private static function ipv4HasProxyServer(): bool {

        $ip = 0;

        if (isset($_SERVER['REMOTE_ADDR']) && is_string($_SERVER['REMOTE_ADDR'])) {
            $remoteAddr = trim($_SERVER['REMOTE_ADDR']);

            if (filter_var($remoteAddr, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
                $ip = ip2long($remoteAddr);
            }
        }

        if ($ip === 0) {
            //$_SERVER['REMOTE_ADDR'] var ikke gyldig IPv4 addresse.
            return false;
        }

        foreach (\WtbFirewall\Config\FirewallConfig::$proxyServer['proxyListIpv4'] as $proxy) {
            switch ($proxy['type']) {
                case 'single':
                    if ($ip == $proxy['start'])
                        return true;
                    break;

                case 'cidr':
                    $networkStart = $proxy['start'];
                    $networkEnd = $networkStart | ((1 << (32 - $proxy['prefix'])) - 1);
                    if ($ip >= $networkStart && $ip <= $networkEnd)
                        return true;
                    break;

                case 'range':
                    if ($ip >= $proxy['start'] && $ip <= $proxy['end'])
                        return true;
                    break;
            }
        }

        return false;
    }

    /**
     * Går gjennom listen for godkjente IPv6 proxy servere og sjekker om $_SERVER['REMOTE_ADDR'] er i listen.
     * 
     * @return bool TRUE dersom $_SERVER['REMOTE_ADDR'] er en godkjent proxy server, FALSE hvis ikke.
     */
    private static function ipv6HasProxyServer(): bool {

        $ip = false;

        $remoteAddr = $_SERVER['REMOTE_ADDR'] ?? '';

        if (is_string($remoteAddr)) {
            $remoteAddr = substr(trim($remoteAddr), 0, 45);

            if (filter_var($remoteAddr, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
                $ip = inet_pton($remoteAddr);
            }
        }

        if (!$ip) {
            //$_SERVER['REMOTE_ADDR'] er ikke gyldig IPv6 addresse.
            return false;
        }

        foreach (\WtbFirewall\Config\FirewallConfig::$proxyServer['proxyListIpv6'] as $proxy) {
            if (self::ipv6InNetwork($ip, $proxy['ipv6'], $proxy['prefix'])) {
                return true;
            }
        }

        return false;
    }

    /**
     * Sjekker om en IPv6 addresse tilhører et gitt IPv6 nettverk.
     * 
     * @param string $remoteBin IPv6 addresse, binary string.
     * @param string $networkBin IPv6 nettverket, binary string.
     * @param int $prefix Prefix for IPv6 nettverket.
     * 
     * @return bool TRUE dersom $remoteIP tilhører $networkBin
     */
    private static function ipv6InNetwork(string $remoteBin, string $networkBin, int $prefix): bool {
        //Prefix 0 matcher alle IP addresser.
        if ($prefix === 0) {
            return true;
        }

        $fullBytes = intdiv($prefix, 8);
        $remBits = $prefix % 8;

        //Sammenlign hele byte i IP addressene.
        if ($fullBytes > 0) {
            if (substr($remoteBin, 0, $fullBytes) !== substr($networkBin, 0, $fullBytes)) {
                return false;
            }
        }

        //Sammenlign eventuelle gjenværende bits.
        if ($remBits > 0) {
            $mask = 0xFF << (8 - $remBits);
            $r = ord($remoteBin[$fullBytes]) & $mask;
            $n = ord($networkBin[$fullBytes]) & $mask;
            if ($r !== $n) {
                return false;
            }
        }

        return true;
    }

    /**
     * Henter IP addresse mottatt fra proxy server.
     * 
     * @param string $header Proxy header vi skal bruke, eller auto for å automatisk velge header.
     * @return string IP addresse fra proxy header, eller $_SERVER['REMOTE_ADDR'] dersom header ikke funnet.
     *                Eventuelt tom string hvis ingen gyldig verdi i $_SERVER['REMOTE_ADDR']
     */
    private static function proxyGetIp(string $header = 'auto'): string {

        $supportedHeaders = [
            'x_forwarded_for' => 'HTTP_X_FORWARDED_FOR',
            'x_real_ip' => 'HTTP_X_REAL_IP',
            'forwarded' => 'HTTP_FORWARDED',
            'cf_connecting_ip' => 'HTTP_CF_CONNECTING_IP',
            'true_client_ip' => 'HTTP_TRUE_CLIENT_IP',
            'x_client_ip' => 'HTTP_X_CLIENT_IP',
        ];

        $remoteAddr = $_SERVER['REMOTE_ADDR'] ?? '';

        if (!is_string($remoteAddr) || !filter_var($remoteAddr, FILTER_VALIDATE_IP)) {
            return '';
        }

        // Auto-detect header (first match wins)
        if ($header === 'auto') {
            foreach ($supportedHeaders as $key => $serverKey) {
                if (!isset($_SERVER[$serverKey])) {
                    continue;
                }

                $value = trim((string) $_SERVER[$serverKey]);
                if ($value !== '') {
                    $header = $key;
                    break;
                }
            }
        }

        //Ingen proxy header.
        if (!isset($supportedHeaders[$header])) {
            return $remoteAddr;
        }

        $value = $_SERVER[$supportedHeaders[$header]] ?? '';

        if (!is_string($value) || $value === '') {
            return $remoteAddr;
        }

        $ip = '';

        switch ($header) {

            case 'x_forwarded_for':
                $parts = explode(',', $value);

                foreach ($parts as $part) {
                    $candidate = trim($part);
                    if (filter_var($candidate, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 | FILTER_FLAG_IPV6)) {
                        $ip = $candidate;
                        break;
                    }
                }
                break;

            case 'forwarded':
                // Take the first Forwarded "element" if multiple are present (comma-separated).
                $first = trim(strtok($value, ','));

                // Find the `for=` parameter value (quoted or unquoted) up to next ; or end.
                if (preg_match('/(?:^|;\s*)for=(?:"([^"]+)"|([^;]+))/i', $first, $m)) {
                    $forVal = trim($m[1] !== '' ? $m[1] : $m[2]);

                    // Remove optional "unknown" and obfuscated identifiers; only accept real IPs.
                    // Normalize: strip surrounding quotes already handled, now remove brackets for IPv6.
                    $forVal = trim($forVal);
                    $forVal = trim($forVal, " \t\n\r\0\x0B"); // whitespace
                    $forVal = trim($forVal, '"');            // just in case
                    // If bracketed IPv6 like [2001:db8::1] or [2001:db8::1]:1234
                    if (strlen($forVal) > 0 && $forVal[0] === '[') {
                        $end = strpos($forVal, ']');
                        if ($end !== false) {
                            $candidate = substr($forVal, 1, $end - 1);
                        } else {
                            $candidate = ltrim($forVal, '['); // malformed but try
                        }
                    } else {
                        $candidate = $forVal;

                        // Strip port for IPv4 (1.2.3.4:1234)
                        if (preg_match('/^\d{1,3}(?:\.\d{1,3}){3}:\d+$/', $candidate)) {
                            $candidate = preg_replace('/:\d+$/', '', $candidate);
                        }
                        // NOTE: Unbracketed IPv6 with port is ambiguous; Forwarded should bracket it.
                        // If it's unbracketed IPv6, we keep it as-is (no port stripping).
                    }

                    // Final validation (IPv4 or IPv6)
                    if (filter_var($candidate, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 | FILTER_FLAG_IPV6)) {
                        $ip = $candidate;
                    }
                }
                break;

            default:
                $ip = trim($value);
                if (!filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 | FILTER_FLAG_IPV6)) {
                    return $remoteAddr;
                }
        }

        //I tilfelle vi ikke klarer å få IP fra header.
        if (!filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 | FILTER_FLAG_IPV6)) {
            return $remoteAddr;
        }
        return $ip;
    }
}
