<?php
namespace Misuzu\Auth;

use RuntimeException;
use Index\IO\MemoryStream;
use Index\Serialisation\UriBase64;

class AuthTokenPacker {
    private const EPOCH_V2 = 1682985600;

    public function __construct(private string $secretKey) {}

    public function pack(AuthTokenBuilder|AuthTokenInfo $tokenInfo): string {
        $props = $tokenInfo->getProperties();
        $timestamp = $tokenInfo instanceof AuthTokenInfo ? $tokenInfo->getTimestamp() : time();

        $data = '';

        foreach($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, $timestamp - self::EPOCH_V2);
        $data = $prefix . hash_hmac('sha3-256', $prefix . $data, $this->secretKey, true) . $data;

        return UriBase64::encode($data);
    }

    public function unpack(?string $token): AuthTokenInfo {
        if($token === null || $token === '')
            return AuthTokenInfo::empty();

        $data = UriBase64::decode($token);
        if($data === false || $data === '')
            return AuthTokenInfo::empty();

        $builder = new AuthTokenBuilder;
        $version = ord($data[0]);
        $data = str_pad(substr($data, 1), 36, "\x00");
        $timestamp = null;

        if($version === 1) {
            $data = unpack('Nuser/H*token', $data);
            if($data === false)
                return AuthTokenInfo::empty();

            $builder->setUserId((string)$data['user']);
            $builder->setSessionToken($data['token']);
        } 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, $this->secretKey, true);

            if(!hash_equals($realHash, $userHash))
                return AuthTokenInfo::empty();

            $unpackTime = unpack('Nts', $timestamp);
            if($unpackTime === false)
                throw new RuntimeException('$token does not contain a valid timestamp.');

            $timestamp = $unpackTime['ts'] + self::EPOCH_V2;

            $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);
                }

                $builder->setProperty($name, $value);
            }
        } else
            return AuthTokenInfo::empty();

        return $builder->toInfo($timestamp);
    }
}
