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

namespace IMATHUZH\Messenger;

use ArrayIterator;
use Traversable;

/**
 * An authorization token granting access to messenger channels.
 */
class AuthorizationToken implements TokenInterface
{
    /**
     * @inheritDoc
     */
    public static function sanitizeChannels(array|string $channels): array
    {
        if (is_string($channels)) $channels = explode(",", $channels);
        return array_values(array_unique(preg_grep(
            '/^(?:[\w\-\*]+\.)?[\w\-\*]+$/',
            array_map('trim', $channels)
        )));
    }

    /**
     * @inheritDoc
     */
    public static function createControllerToken(string $group, array|string $channels): static
    {
        return new static([$group => ['ctl' => self::sanitizeChannels($channels)]]);
    }

    /**
     * @inheritDoc
     */
    public static function createPublisherToken(string $group, array|string $channels): static
    {
        return new static([$group => ['pub' => self::sanitizeChannels($channels)]]);
    }

    /**
     * @inheritDoc
     */
    public static function createListenerToken(string $group, array|string $channels): static
    {
        return new static([$group => ['sub' => self::sanitizeChannels($channels)]]);
    }

    /**
     * @inheritDoc
     */
    public static function createMessengerToken(string $group, array|string $pubChannels, array|string $subChannels): static
    {
        return new static([$group => [
            'pub' => self::sanitizeChannels($pubChannels),
            'sub' => self::sanitizeChannels($subChannels)
        ]]);
    }

    /**
     *  @inheritDoc
     */
    public static function mergeTokens(TokenInterface ...$tokens): static
    {
        $channels = array_merge_recursive(
            ...array_map(fn($token) => $token->toArray(), $tokens)
        );
        foreach ($channels as &$group)
            foreach ($group as &$typed)
                $typed = array_values(array_unique($typed));
        return new static($channels);
    }

    public function __construct(protected array $data) { }

    /**
     * @inheritDoc
     */
    public function toArray(): array
    {
        return $this->data;
    }

    /**
     * Iterated over groups managed by this token
     */
    public function getIterator(): Traversable
    {
        return new ArrayIterator($this->data);
    }

    /**
     * @inheritDoc
     */
    public function getPubChannels(string $group, int $channelFlags=0): ?array
    {
        $channels = $this->data[$group]['pub'] ?? null;
        return is_null($channels) ? null : self::filterChannels($channels, $channelFlags);
    }

    /**
     * @inheritDoc
     */
    public function getSubChannels(string $group, int $channelFlags=0): ?array
    {
        $channels = $this->data[$group]['sub'] ?? null;
        return is_null($channels) ? null : self::filterChannels($channels, $channelFlags);
    }

    /**
     * @inheritDoc
     */
    public function getCtlChannels(string $group, int $channelFlags=0): ?array
    {
        $channels = $this->data[$group]['ctl'] ?? null;
        return is_null($channels) ? null : self::filterChannels($channels, $channelFlags);
    }

    /**
     * @inheritDoc
     */
    public function authorizesPublisher(string $group, array $channels): bool
    {
        return $this->matchesChannels($this->getPubChannels($group, self::KEEP_ALL) ?? [], $channels);
    }

    /**
     * @inheritDoc
     */
    public function authorizesListener(string $group, array $channels): bool
    {
        return $this->matchesChannels($this->getSubChannels($group, self::KEEP_ALL) ?? [], $channels);
    }

    /**
     * @inheritDoc
     */
    public function authorizesController(string $group, array $channels): bool
    {
        return $this->matchesChannels($this->getCtlChannels($group, self::KEEP_ALL) ?? [], $channels);
    }

    /**
     * Filters a list of channels according to the rules.
     */
    protected static function filterChannels(array $channels, int $channelFlags): array
    {
        switch ($channelFlags & 0x03) {
            case 0: $pattern = '/^[\w\-]+$/'; break;
            case self::KEEP_SPECIAL:  $pattern = '/^(?:[\w\-]+\.)?[\w\-]+$/'; break;
            case self::KEEP_PATTERNS: $pattern = '/^[\w\-\*]+$/'; break;
            case 3: return $channels;
        }
        return array_values(preg_grep($pattern, $channels));
    }

    /**
     * A helper method that returns `true` if each channel matches at least one
     * of the provided patterns.
     */
    protected static function matchesChannels(array $patterns, array $channels): bool
    {
        try {
            // Accept all channels that are in the token
            $missing = array_diff($channels, $patterns);
            if (empty($missing)) return true;
            // Check the remaining channels against patterns
            $regexps = array_map(
                fn($pattern) => '/^' . strtr($pattern, ['.' => '\.', '*' => '[\w\-]*']) . '$/',
                array_filter($patterns, fn($channel) => str_contains($channel, '*'))
            );
            if (empty($regexps)) return false;
            foreach ($missing as $channel) {
                $unautorized = true;
                foreach ($regexps as $regexp) {
                    if (preg_match($regexp, $channel, $match)) {
                        $unautorized = false;
                        break;
                    }
                }
                if ($unautorized) return false;
            }
            return true;
        } catch (\Exception $e) {
            return false;
        }
    }
    
}
