<?php
namespace Misuzu;

use stdClass;
use JsonSerializable;
use RuntimeException;
use Stringable;
use DeviceDetector\ClientHints;
use DeviceDetector\DeviceDetector;

class ClientInfo implements Stringable, JsonSerializable {
    private const SERIALIZE_VERSION = 1;

    public function __construct(
        private array|bool|null $botInfo,
        private ?array $clientInfo,
        private ?array $osInfo,
        private string $brandName,
        private string $modelName
    ) {}

    public function __toString(): string {
        if($this->botInfo === true || is_array($this->botInfo)) {
            if($this->botInfo === true)
                return 'a bot';
            return $this->botInfo['name'] ?? 'an unknown bot';
        }

        if(empty($this->clientInfo['name']))
            return 'an unknown browser';

        $string = $this->clientInfo['name'];
        if(!empty($this->clientInfo['version']))
            $string .= ' ' . $this->clientInfo['version'];

        $hasOsInfo = !empty($this->osInfo['name']);
        $hasModelName = !empty($this->modelName);

        if($hasOsInfo || $hasModelName)
            $string .= ' on ';

        if($hasModelName) {
            $deviceName = trim($this->brandName . ' ' . $this->modelName);
            // most naive check in the world but it works well enough for this lol
            $firstCharIsVowel = in_array(strtolower($deviceName[0]), ['a', 'i', 'u', 'e', 'o']);
            $string .= ($firstCharIsVowel ? 'an' : 'a') . ' ' . $deviceName;
        }

        if($hasOsInfo) {
            if($hasModelName)
                $string .= ' running ';

            $string .= $this->osInfo['name'];
            if(!empty($this->osInfo['version']))
                $string .= ' ' . $this->osInfo['version'];
            if(!empty($this->osInfo['platform']))
                $string .= ' (' . $this->osInfo['platform'] . ')';
        }

        return $string;
    }

    public function encode(): string {
        return json_encode($this);
    }

    public function jsonSerialize(): mixed {
        $data = new stdClass;
        $data->version = self::SERIALIZE_VERSION;

        if($this->botInfo === true || is_array($this->botInfo))
            $data->bot = $this->botInfo;
        if($this->clientInfo !== null)
            $data->client = $this->clientInfo;
        if($this->osInfo !== null)
            $data->os = $this->osInfo;
        if($this->brandName !== '')
            $data->vendor = $this->brandName;
        if($this->modelName !== '')
            $data->model = $this->modelName;

        return $data;
    }

    public static function decode(string $encoded): self {
        $data = json_decode($encoded, true);
        $version = $data['version'] ?? 0;
        if($version < 0 || $version > self::SERIALIZE_VERSION)
            throw new RuntimeException('$data does not contain a valid version argument');

        return new ClientInfo(
            $data['bot'] ?? null,
            $data['client'] ?? null,
            $data['os'] ?? null,
            $data['vendor'] ?? '',
            $data['model'] ?? ''
        );
    }

    public static function parse(array|string $serverVarsOrUserAgent): self {
        static $dd = null;
        $dd ??= new DeviceDetector();

        if(is_string($serverVarsOrUserAgent)) {
            $dd->setUserAgent($serverVarsOrUserAgent);
        } else {
            $dd->setUserAgent(
                array_key_exists('HTTP_USER_AGENT', $serverVarsOrUserAgent)
                    ? $serverVarsOrUserAgent['HTTP_USER_AGENT'] : ''
            );
            $dd->setClientHints(ClientHints::factory($serverVarsOrUserAgent));
        }

        $dd->parse();

        return new ClientInfo(
            $dd->getBot(),
            $dd->getClient(),
            $dd->getOs(),
            $dd->getBrandName(),
            $dd->getModel()
        );
    }

    public static function fromRequest(): self {
        return self::parse($_SERVER);
    }
}
