<?php

declare(strict_types=1);

namespace BotGuard\Services\Detection\Modules;

use BotGuard\Services\Detection\ModuleResult;
use BotGuard\Services\Detection\SignalBag;

/**
 * Browser fingerprinting module.
 *
 * Evaluates the plausibility of the client fingerprint collected in JS:
 * screen geometry, timezone, languages, canvas/WebGL/audio hashes, hardware
 * concurrency, device memory, colour depth, touch support, plugins and fonts.
 * Headless/automation environments typically expose tell-tale gaps (no plugins,
 * SwiftShader/llvmpipe WebGL renderer, missing canvas/audio, zero cores).
 */
final class BrowserFingerprintModule extends AbstractModule
{
    /** WebGL renderers associated with software/headless rendering. */
    private const SOFTWARE_GL = ['swiftshader', 'llvmpipe', 'software', 'mesa offscreen', 'google inc. (google)'];

    public function key(): string
    {
        return 'fingerprint';
    }

    public function analyze(SignalBag $signals): ModuleResult
    {
        $fp = $signals->clientArray('fingerprint');

        // No fingerprint at all (JS blocked or non-browser client) is bot-like.
        if ($fp === []) {
            return $this->scored(0.1, [], ['No browser fingerprint collected'], ['present' => false]);
        }

        $width = (int) ($fp['screen_width'] ?? 0);
        $height = (int) ($fp['screen_height'] ?? 0);
        $timezone = (string) ($fp['timezone'] ?? '');
        $languages = $fp['languages'] ?? [];
        $canvas = (string) ($fp['canvas_hash'] ?? '');
        $webglVendor = strtolower((string) ($fp['webgl_vendor'] ?? ''));
        $webglRenderer = strtolower((string) ($fp['webgl_renderer'] ?? ''));
        $audio = (string) ($fp['audio_hash'] ?? '');
        $cores = (int) ($fp['hardware_concurrency'] ?? 0);
        $memory = (float) ($fp['device_memory'] ?? 0);
        $colorDepth = (int) ($fp['color_depth'] ?? 0);
        $touch = (bool) ($fp['touch_support'] ?? false);
        $plugins = $fp['plugins'] ?? [];
        $fonts = $fp['fonts'] ?? [];
        $hash = (string) ($fp['hash'] ?? '');

        $this->detail('screen', "{$width}x{$height}");
        $this->detail('timezone', $timezone);
        $this->detail('webgl_renderer', $webglRenderer);
        $this->detail('hardware_concurrency', $cores);
        $this->detail('hash', $hash !== '' ? $hash : $this->computeHash($fp));

        // Plausible screen geometry.
        $this->check('Plausible screen resolution', $width >= 320 && $height >= 240, 1.5);
        $this->check('Sensible colour depth', in_array($colorDepth, [24, 30, 32, 48], true), 1.0);

        // Timezone + language present.
        $this->check('Timezone reported', $timezone !== '', 1.0);
        $this->check('Language(s) reported', is_array($languages) && $languages !== [], 1.0);

        // Canvas + audio fingerprints computed (headless often returns blanks).
        $this->check('Canvas fingerprint present', $canvas !== '', 1.5);
        $this->check('Audio fingerprint present', $audio !== '', 1.0);

        // WebGL present and NOT a software renderer.
        $hasWebgl = $webglRenderer !== '';
        $isSoftware = $this->isSoftwareRenderer($webglRenderer) || $this->isSoftwareRenderer($webglVendor);
        $this->check('Hardware WebGL renderer', $hasWebgl && ! $isSoftware, 2.0);

        // Hardware concurrency / device memory in human ranges.
        $this->check('Reasonable CPU cores', $cores >= 1 && $cores <= 64, 1.5);
        $this->check('Device memory reported', $memory > 0 || true, 0.5); // deviceMemory absent on Firefox/Safari — soft

        // Plugins/fonts: modern Chrome reports ~5 plugins, real machines many fonts.
        $this->check('Fonts enumerated', is_array($fonts) && count((array) $fonts) >= 3, 1.0);

        // Touch consistency: a "mobile" UA should report touch; a desktop may not.
        $uaMobile = str_contains(strtolower($signals->userAgent()), 'mobi');
        $this->check('Touch/UA consistency', ! $uaMobile || $touch, 1.0);

        $this->detail('plugin_count', is_array($plugins) ? count($plugins) : 0);

        return $this->result();
    }

    private function isSoftwareRenderer(string $value): bool
    {
        foreach (self::SOFTWARE_GL as $needle) {
            if ($value !== '' && str_contains($value, $needle)) {
                return true;
            }
        }

        return false;
    }

    /**
     * Deterministic fingerprint hash from the component bag (fallback when the
     * client did not precompute one).
     *
     * @param array<string, mixed> $fp
     */
    private function computeHash(array $fp): string
    {
        $canonical = [
            $fp['screen_width'] ?? '', $fp['screen_height'] ?? '', $fp['timezone'] ?? '',
            $fp['canvas_hash'] ?? '', $fp['webgl_renderer'] ?? '', $fp['audio_hash'] ?? '',
            $fp['hardware_concurrency'] ?? '', $fp['color_depth'] ?? '',
            implode(',', (array) ($fp['languages'] ?? [])),
            implode(',', (array) ($fp['fonts'] ?? [])),
        ];

        return hash('sha256', implode('|', array_map('strval', $canonical)));
    }
}
