<?php
namespace Misuzu;

use Misuzu\Users\User;
use Misuzu\Users\UserSession;
use Index\IO\MemoryStream;
use Index\Serialisation\UriBase64;

class AuthToken {
    private const EPOCH = 1682985600;

    private int $timestamp = 0;
    private int $cookieExpires = 0;
    private array $props = [];

    private static string $secretKey = '';

    public static function setSecretKey(string $secretKey): void {
        self::$secretKey = $secretKey;
    }

    public function getTimestamp(): int {
        return $this->timestamp;
    }
    public function updateTimestamp(): void {
        $this->timestamp = self::timestamp();
    }

    public function hasProperty(string $name): bool {
        return isset($this->props[$name]);
    }
    public function getProperty(string $name): string {
        return $this->props[$name] ?? '';
    }
    public function setProperty(string $name, string $value): void {
        $this->props[$name] = $value;
        $this->updateTimestamp();
    }
    public function removeProperty(string $name): void {
        unset($this->props[$name]);
        $this->updateTimestamp();
    }

    public function isValid(): bool {
        if($this->getUserId() < 1 || empty($this->getSessionToken()))
            return false;
        return true;
    }

    public function getUserId(): int {
        $value = (int)$this->getProperty('u');
        return $value < 1 ? -1 : $value;
    }
    public function setUserId(int $userId): self {
        $this->setProperty('u', (string)$userId);
        return $this;
    }

    public function getSessionToken(): string {
        if(!$this->hasProperty('t'))
            return '';
        return bin2hex($this->getProperty('t'));
    }
    public function setSessionToken(string $token): self {
        $this->setProperty('t', hex2bin($token));
        return $this;
    }

    public function hasImpersonatedUserId(): bool {
        return $this->hasProperty('i');
    }
    public function getImpersonatedUserId(): int {
        $value = (int)$this->getProperty('i');
        return $value < 1 ? -1 : $value;
    }
    public function setImpersonatedUserId(int $userId): void {
        $this->setProperty('i', (string)$userId);
    }
    public function removeImpersonatedUserId(): void {
        $this->removeProperty('i');
    }

    public function pack(bool $base64 = true): string {
        $data = '';

        foreach($this->props as $name => $value) {
            // very smart solution for this issue, you definitely won't be confused by this later
            // down the line when a variable suddenly despawns from the token
            $nameLength = strlen($name);
            $valueLength = strlen($value);
            if($nameLength > 255 || $valueLength > 255)
                continue;

            $data .= chr($nameLength) . $name . chr($valueLength) . $value;
        }

        $prefix = pack('CN', 2, $this->getTimestamp());
        $data = $prefix . hash_hmac('sha3-256', $prefix . $data, self::$secretKey, true) . $data;
        if($base64)
            $data = UriBase64::encode($data);

        return $data;
    }

    public static function unpack(string $data, bool $base64 = true): self {
        $obj = new AuthToken;

        if(empty($data))
            return $obj;
        if($base64)
            $data = UriBase64::decode($data);
        if(empty($data))
            return $obj;

        $version = ord($data[0]);
        $data = substr($data, 1);

        if($version === 1) {
            $data = str_pad($data, 36, "\x00");
            $data = unpack('Nuser/H*token', $data);

            $obj->props['u'] = (string)$data['user'];
            $obj->props['t'] = hex2bin($data['token']);
            $obj->updateTimestamp();
        } elseif($version === 2) {
            $timestamp = substr($data, 0, 4);
            $userHash = substr($data, 4, 32);
            $data = substr($data, 36);
            $realHash = hash_hmac('sha3-256', chr($version) . $timestamp . $data, self::$secretKey, true);

            if(!hash_equals($realHash, $userHash))
                return $obj;

            $unpacked = unpack('Nts', $timestamp);
            $obj->timestamp = (int)$unpacked['ts'];

            $stream = MemoryStream::fromString($data);
            $stream->seek(0);

            for(;;) {
                $length = $stream->readChar();
                if($length === null)
                    break;

                $length = ord($length);
                if($length < 1)
                    break;

                $name = $stream->read($length);
                $value = null;
                $length = $stream->readChar();
                if($length !== null) {
                    $length = ord($length);
                    if($length > 0)
                        $value = $stream->read($length);
                }

                $obj->props[$name] = $value;
            }
        }

        return $obj;
    }

    public static function timestamp(): int {
        return time() - self::EPOCH;
    }

    public static function create(User $user, UserSession $session): self {
        $token = new AuthToken;
        $token->setUserId($user->getId());
        $token->setSessionToken($session->getToken());
        return $token;
    }

    public static function cookieDomain(bool $compatible = true): string {
        $url = parse_url($_SERVER['HTTP_HOST'], PHP_URL_HOST);
        if(empty($url))
            $url = $_SERVER['HTTP_HOST'];

        if(!filter_var($url, FILTER_VALIDATE_IP) && $compatible)
            $url = '.' . $url;

        return $url;
    }

    public function applyCookie(int $expires = 0): void {
        if($expires > 0)
            $this->cookieExpires = $expires;
        else
            $expires = $this->cookieExpires;

        setcookie('msz_auth', $this->pack(), $expires, '/', self::cookieDomain(), !empty($_SERVER['HTTPS']), true);
    }

    public static function nukeCookie(): void {
        setcookie('msz_auth', '', -9001, '/', self::cookieDomain(), !empty($_SERVER['HTTPS']), true);
        setcookie('msz_auth', '', -9001, '/', '', !empty($_SERVER['HTTPS']), true);
    }

    public static function nukeCookieLegacy(): void {
        setcookie('msz_uid', '', -3600, '/', '', !empty($_SERVER['HTTPS']), true);
        setcookie('msz_sid', '', -3600, '/', '', !empty($_SERVER['HTTPS']), true);
    }

    // please never use the below functions beyond the scope of the sharpchat auth stuff
    // a better mechanism for keeping a global instance of this available
    //  that isn't a $GLOBAL variable or static instance needs to be established, for User and UserSession as well

    private static $localToken = null;

    public function setCurrent(): void {
        self::$localToken = $this;
    }
    public static function unsetCurrent(): void {
        self::$localToken = null;
    }
    public static function getCurrent(): ?self {
        return self::$localToken;
    }
    public static function hasCurrent(): bool {
        return self::$localToken !== null;
    }
}
