<?php declare(strict_types=1);
/**
 * @package messenger
 * @author kputyr
 * @date 23.03.2026
 */

namespace IMATHUZH\Messenger;

/**
 * An implementation of the token manager interface that wraps a PHP array.
 * 
 * The main motivation is to use $_SESSION array for token storage, but it
 * can be used with any array reference.
 */
class SessionTokenManager implements TokenManagerInterface
{
    /** A separator between group name and channel type */
    // We cannot use a dot, because dots are replaced with underscores by parse_str()
    protected const TSEP = '/';

    /** A separator between channel names */
    protected const CSEP = ',';

    /** @var array A reference to the writable storage */
    protected array $storage;

    /** @var array[] A list of additional readonly storage places */
    protected array $readOnlyStorage;

    /**
     * Creates a new SessionTokenManager instance using the QFQ session for storage.
     * 
     * The following sources for tokens are used:
     *  + SIP store (read and write)
     *  + U store (readonly)
     *  + W store (readonly)
     * 
     * It is assumed that a session is already started.
     * 
     * @throws \Exception if no QFQ session has been detected.
     */
    public static function fromQfqSession(): self
    {
        if (!isset($_SESSION['qfq'])) {
            throw new \Exception('Unauthorized: QFQ session not detected');
        }

        $uStore = unserialize($_SESSION['qfq']['store-U'] ?? '') ?: [];                                                                                                                                                      
        $wStore = unserialize($_SESSION['qfq']['store-W'] ?? '') ?: [];    
                                                                                                                                                          
        return new self($_SESSION['qfq'], $uStore, $wStore); 
    }

    public function __construct(array &$storage, array ...$readOnlyStorage)
    {
        $this->storage =& $storage;
        $this->readOnlyStorage = $readOnlyStorage;
    }

    /**
     * Returns the first storage with the token or null if no storage contains the token.
     * 
     * @param string $token the token to search
     * @return array|null the storage with the token or null if none exists
     */
    protected function findTokenStorage(string $tokenId): ?array
    {
        if (isset($this->storage[$tokenId])) return $this->storage;
        foreach($this->readOnlyStorage as $storage)
            if (isset($storage[$tokenId]))
                return $storage;
        return null;
    }

    /**
     * Generates a unique token that is not already in use.
     * 
     * @return string A unique token string
     */
    protected function getAvailableToken(): string
    {
        do {
            $token = bin2hex(random_bytes(Constants::TOKEN_SIZE));
        } while ($this->tokenExists($token));
        return $token;
    }
    
    /**
     * @inheritDoc
     */
    public function tokenExists(string $tokenId): bool
    {
        return $this->findTokenStorage($tokenId) !== null;
    }

    /**
     * @inheritDoc
     */
    public function deleteToken(string $tokenId): bool
    {
        if (isset($this->storage[$tokenId])) {
            unset($this->storage[$tokenId]);
            return true;
        } else {
            return false;
        }
    }

    /**
     * @inheritDoc
     */
    public function saveToken(TokenInterface $token, string $tokenId = ''): string
    {
        $data = [];
        $tokenId = $tokenId ?: $this->getAvailableToken();
        foreach ($token as $group => $types) {
            foreach ($types as $type => $channels) {
                $data[$group . self::TSEP . $type] = implode(self::CSEP, $channels);
            }
        }
        $this->storage[$tokenId] = http_build_query($data);
        return $tokenId;
    }

    /**
     * @inheritDoc
     */
    public function loadToken(string $token): TokenInterface
    {
        $storage = $this->findTokenStorage($token);
        if (!$storage) {
            throw new \Exception('Token not found');
        }
        parse_str($storage[$token], $payload);
        $data = [];
        foreach ($payload as $key => $channels) {
            $keyParts = explode(self::TSEP, $key, 2);
            if (count($keyParts) === 2) {
                list($group, $type) = $keyParts;
                $data[$group][$type] = explode(self::CSEP, $channels);
            }
        }
        return new AuthorizationToken($data);
    }

}
