<?php

class RequestLogger
{
    private string $logFile;

    public function __construct(string $logFile)
    {
        $this->logFile = $logFile;
    }

    public function log(string $page, string $ip, string $userAgent, string $referrer, string $host, string $acceptLanguage, string $status = 'ok', ?string $country = null): void
    {
        $dir = dirname($this->logFile);
        if (!is_dir($dir)) {
            @mkdir($dir, 0755, true);
        }

        $timestamp = date('Y-m-d H:i:s');
        $ua = substr(preg_replace('/[[:cntrl:]]/', '', $userAgent), 0, 500);
        $ref = substr(preg_replace('/[[:cntrl:]]/', '', $referrer), 0, 500);
        $cc = ($country && strlen($country) === 2) ? strtoupper($country) : '';
        $line = "[{$timestamp}] [{$ip}] [{$host}] [{$acceptLanguage}] [{$page}] [{$ua}] [{$ref}] [{$status}] [{$cc}]\n";
        @file_put_contents($this->logFile, $line, FILE_APPEND | LOCK_EX);
    }

    /**
     * Mask the last octet (IPv4) or last block (IPv6) of an IP address
     */
    public static function anonymizeIp(string $ip): string
    {
        if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
            $parts = explode('.', $ip);
            if (count($parts) === 4) {
                $parts[3] = 'x';
                return implode('.', $parts);
            }
        }
        if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
            $parts = explode(':', $ip);
            $keep = array_slice($parts, 0, 4);
            return implode(':', $keep) . '::x';
        }
        return $ip;
    }

    /**
     * Check whether an IP matches any entry in a list of IPs/CIDR ranges.
     *
     * Supports exact IPv4/IPv6 addresses and CIDR notation (e.g. 192.168.0.0/16).
     *
     * @param string $ip The client IP to test
     * @param array $list List of IPs and/or CIDR ranges
     * @return bool True if the IP matches any entry
     */
    public static function ipMatchesList(string $ip, array $list): bool
    {
        $ip = trim($ip);
        if ($ip === '') {
            return false;
        }
        $isV6 = filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) !== false;
        $packed = $isV6 ? inet_pton($ip) : inet_pton($ip);

        foreach ($list as $entry) {
            $entry = trim((string)$entry);
            if ($entry === '') {
                continue;
            }

            // Exact match
            if ($entry === $ip) {
                return true;
            }

            // CIDR notation
            if (strpos($entry, '/') !== false) {
                [$subnet, $bits] = array_pad(explode('/', $entry, 2), 2, null);
                $subnet = trim($subnet);
                $subnetPacked = inet_pton($subnet);
                if ($subnetPacked === false || $packed === false) {
                    continue;
                }
                // Ensure both are the same address family
                if (strlen($subnetPacked) !== strlen($packed)) {
                    continue;
                }
                $maxBits = strlen($packed) * 8;
                $bits = (int)$bits;
                if ($bits < 0 || $bits > $maxBits) {
                    continue;
                }
                if ($bits === 0) {
                    return true;
                }
                $fullBytes = intdiv($bits, 8);
                $remainingBits = $bits % 8;
                $match = true;
                for ($i = 0; $i < $fullBytes; $i++) {
                    if ($subnetPacked[$i] !== $packed[$i]) {
                        $match = false;
                        break;
                    }
                }
                if ($match && $remainingBits > 0) {
                    $mask = 0xFF << (8 - $remainingBits);
                    if ((ord($subnetPacked[$fullBytes]) & $mask) !== (ord($packed[$fullBytes]) & $mask)) {
                        $match = false;
                    }
                }
                if ($match) {
                    return true;
                }
            }
        }

        return false;
    }

    public static function getClientIp(): string
    {
        $headerKeys = [
            'HTTP_CF_CONNECTING_IP',
            'HTTP_X_REAL_IP',
            'HTTP_CLIENT_IP',
            'HTTP_X_CLIENT_IP',
            'HTTP_X_CLUSTER_CLIENT_IP',
            'HTTP_X_FORWARDED_FOR',
            'HTTP_X_FORWARDED',
            'HTTP_FORWARDED_FOR',
            'HTTP_FORWARDED',
            'REMOTE_ADDR',
        ];

        // Pass 1: Prioritize valid PUBLIC IP addresses (skips 127.0.0.1, 10.x, 172.x, 192.168.x proxy/internal IPs)
        foreach ($headerKeys as $key) {
            if (empty($_SERVER[$key])) continue;

            $value = $_SERVER[$key];
            $ips = str_contains($value, ',') ? explode(',', $value) : [$value];

            foreach ($ips as $rawIp) {
                $ip = trim($rawIp);
                if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) !== false) {
                    return $ip;
                }
            }
        }

        // Pass 2: Fallback for local development environments
        foreach ($headerKeys as $key) {
            if (empty($_SERVER[$key])) continue;

            $value = $_SERVER[$key];
            $ips = str_contains($value, ',') ? explode(',', $value) : [$value];

            foreach ($ips as $rawIp) {
                $ip = trim($rawIp);
                if (filter_var($ip, FILTER_VALIDATE_IP) !== false) {
                    return $ip;
                }
            }
        }

        return $_SERVER['REMOTE_ADDR'] ?? '127.0.0.1';
    }

    public static function detectVisitorInfo(string $ua, string $user = ''): array
    {
        if (class_exists('BotGuard')) {
            $id = BotGuard::identify($ua);
            $cat = $id['category'];
            $label = $id['label'];

            if ($cat === 'ai') {
                return ['type' => 'ai', 'label' => $label, 'badge' => 'danger', 'icon' => 'bi-robot'];
            }
            if ($cat === 'search') {
                return ['type' => 'search', 'label' => $label, 'badge' => 'primary', 'icon' => 'bi-search'];
            }
            if ($cat === 'scraper') {
                return ['type' => 'scraper', 'label' => $label, 'badge' => 'warning text-dark', 'icon' => 'bi-bug'];
            }
            if ($cat === 'generic') {
                return ['type' => 'bot', 'label' => 'Bot', 'badge' => 'secondary', 'icon' => 'bi-robot'];
            }
            if ($cat === 'empty') {
                return ['type' => 'empty', 'label' => 'Lege UA', 'badge' => 'secondary', 'icon' => 'bi-slash-circle'];
            }

            $userPrefix = ($user && $user !== 'Gast') ? htmlspecialchars($user) . ' (' : '';
            $userSuffix = ($user && $user !== 'Gast') ? ')' : '';

            return [
                'type' => 'human',
                'label' => $userPrefix . 'Mens' . $userSuffix,
                'badge' => 'success',
                'icon' => 'bi-person-check',
            ];
        }

        return ['type' => 'unknown', 'label' => 'Bezoeker', 'badge' => 'secondary', 'icon' => 'bi-person'];
    }

    public static function detectBot(): ?string
    {
        $ua = $_SERVER['HTTP_USER_AGENT'] ?? '';
        if (class_exists('BotGuard')) {
            $id = BotGuard::identify($ua);
            if (in_array($id['category'], ['ai', 'search', 'scraper'], true)) {
                return strtoupper($id['category']);
            }
        }
        return null;
    }

    public function getLogs(int $lines = 100): array
    {
        if (!file_exists($this->logFile)) {
            return [];
        }

        $content = file($this->logFile);
        $content = array_slice($content, -$lines);
        $logs = [];

        foreach ($content as $line) {
            $trimmed = trim($line);
            if (preg_match('/^\[([^\]]+)\] \[([^\]]+)\] \[([^\]]+)\] \[([^\]]*)\] \[([^\]]+)\] \[([^\]]*)\] \[([^\]]*)\](?: \[([^\]]*)\])?(?: \[([^\]]*)\])?$/', $trimmed, $m)) {
                $user = $m[3];
                if (str_contains($user, '.') || str_contains($user, ':') || $user === 'cli') {
                    $user = 'Gast';
                }
                $status = $m[8] ?? 'ok';
                if ($status === '') $status = 'ok';
                $country = $m[9] ?? '';

                $visitorInfo = self::detectVisitorInfo($m[6], $user);
                $logs[] = [
                    'time' => $m[1],
                    'ip' => $m[2],
                    'user' => $user,
                    'visitor_info' => $visitorInfo,
                    'lang' => $m[4],
                    'page' => $m[5],
                    'ua' => $m[6],
                    'referrer' => $m[7],
                    'status' => $status,
                    'country' => $country,
                ];
            }
        }

        return array_reverse($logs);
    }
}
