<?php

declare(strict_types=1);

namespace BotGuard\Helpers;

/**
 * Self-contained User-Agent parser.
 *
 * Deliberately dependency-free: it extracts browser, version, engine, OS,
 * device type and — importantly for bot detection — headless/automation and
 * known-bot indicators. It is heuristic, not exhaustive, but covers the vast
 * majority of real-world traffic and the common automation signatures.
 */
final class UserAgentParser
{
    /** Substrings that betray automation frameworks / headless runtimes. */
    private const HEADLESS = [
        'headlesschrome', 'phantomjs', 'slimerjs', 'electron', 'puppeteer',
        'playwright', 'selenium', 'webdriver', 'cypress', 'nightmare',
    ];

    /** Substrings common to crawlers, libraries and CLI HTTP clients. */
    private const BOTS = [
        'bot', 'spider', 'crawler', 'crawl', 'slurp', 'curl', 'wget', 'python',
        'python-requests', 'httpclient', 'okhttp', 'go-http-client', 'java/',
        'libwww', 'scrapy', 'aiohttp', 'axios', 'node-fetch', 'guzzle',
        'headless', 'phantom', 'apache-httpclient', 'postman', 'insomnia',
    ];

    /**
     * Parse a raw UA string into a structured result.
     *
     * @return array{
     *   browser:string, version:string, engine:string, os:string,
     *   device_type:string, is_mobile:bool, is_bot:bool, is_headless:bool, empty:bool
     * }
     */
    public static function parse(string $ua): array
    {
        $ua = trim($ua);
        $lower = strtolower($ua);

        $result = [
            'browser'     => 'Unknown',
            'version'     => '',
            'engine'      => 'Unknown',
            'os'          => 'Unknown',
            'device_type' => 'desktop',
            'is_mobile'   => false,
            'is_bot'      => false,
            'is_headless' => false,
            'empty'       => $ua === '',
        ];

        if ($ua === '') {
            $result['device_type'] = 'bot';

            return $result;
        }

        $result['is_headless'] = self::matchesAny($lower, self::HEADLESS);
        $result['is_bot'] = $result['is_headless'] || self::matchesAny($lower, self::BOTS);

        // Engine.
        if (str_contains($lower, 'gecko/') && str_contains($lower, 'firefox')) {
            $result['engine'] = 'Gecko';
        } elseif (str_contains($lower, 'applewebkit')) {
            $result['engine'] = str_contains($lower, 'edg/') || str_contains($lower, 'chrome') ? 'Blink' : 'WebKit';
        } elseif (str_contains($lower, 'trident')) {
            $result['engine'] = 'Trident';
        }

        // Browser + version (order matters: Edge/Opera masquerade as Chrome).
        $browserMap = [
            'edg/'     => 'Edge',
            'opr/'     => 'Opera',
            'firefox/' => 'Firefox',
            'chrome/'  => 'Chrome',
            'crios/'   => 'Chrome',
            'version/' => 'Safari', // Safari reports version/ before safari/
            'msie '    => 'Internet Explorer',
            'trident/' => 'Internet Explorer',
        ];

        foreach ($browserMap as $needle => $name) {
            if (str_contains($lower, $needle)) {
                $result['browser'] = $name;
                $result['version'] = self::extractVersion($lower, $needle);
                break;
            }
        }

        // OS.
        $result['os'] = match (true) {
            str_contains($lower, 'windows nt 10') => 'Windows 10/11',
            str_contains($lower, 'windows')       => 'Windows',
            str_contains($lower, 'android')        => 'Android',
            str_contains($lower, 'iphone'), str_contains($lower, 'ipad') => 'iOS',
            str_contains($lower, 'mac os x'), str_contains($lower, 'macintosh') => 'macOS',
            str_contains($lower, 'cros')           => 'ChromeOS',
            str_contains($lower, 'linux')          => 'Linux',
            default                                 => 'Unknown',
        };

        // Device type.
        $result['is_mobile'] = str_contains($lower, 'mobi') || str_contains($lower, 'android') || str_contains($lower, 'iphone');
        if ($result['is_bot']) {
            $result['device_type'] = 'bot';
        } elseif (str_contains($lower, 'ipad') || (str_contains($lower, 'tablet'))) {
            $result['device_type'] = 'tablet';
        } elseif ($result['is_mobile']) {
            $result['device_type'] = 'mobile';
        }

        return $result;
    }

    private static function extractVersion(string $ua, string $needle): string
    {
        $pos = strpos($ua, $needle);
        if ($pos === false) {
            return '';
        }

        $tail = substr($ua, $pos + strlen($needle));

        return preg_match('/^([0-9]+(?:\.[0-9]+)*)/', $tail, $m) ? $m[1] : '';
    }

    /**
     * @param string[] $needles
     */
    private static function matchesAny(string $haystack, array $needles): bool
    {
        foreach ($needles as $needle) {
            if (str_contains($haystack, $needle)) {
                return true;
            }
        }

        return false;
    }
}
