<?php

declare(strict_types=1);

namespace CpCreation\AltchaBundle\Scope;

use CpCreation\AltchaBundle\Contract\ScopeProviderInterface;
use CpCreation\AltchaBundle\Model\ScopeDefinition;
use CpCreation\AltchaBundle\Model\ScopeManifest;

final readonly class ScopeRegistry
{
    /** @var array<string, string> */
    private array $scopes;

    /**
     * @param array<array-key, mixed> $scopes
     * @param iterable<mixed>         $providers
     */
    public function __construct(array $scopes, iterable $providers = [], private string $source = 'symfony')
    {
        $normalized = [];
        foreach ($scopes as $name => $label) {
            if (is_int($name)) {
                $name = $label;
            }
            if (!is_string($name) || !is_string($label)) {
                throw new \InvalidArgumentException('Scope names and labels must be strings.');
            }

            ScopeName::assertValid($name);
            $normalized[$name] = '' !== trim($label) ? trim($label) : $name;
        }

        foreach ($providers as $provider) {
            if (!$provider instanceof ScopeProviderInterface) {
                throw new \InvalidArgumentException('ALTCHA scope providers must implement ScopeProviderInterface.');
            }
            /** @var iterable<mixed> $definitions */
            $definitions = $provider->scopes();
            foreach ($definitions as $definition) {
                if (!$definition instanceof ScopeDefinition) {
                    throw new \InvalidArgumentException('ALTCHA scope providers must yield ScopeDefinition objects.');
                }
                ScopeName::assertValid($definition->name);
                if (isset($normalized[$definition->name]) && $normalized[$definition->name] !== $definition->label) {
                    throw new \InvalidArgumentException(sprintf('Conflicting labels for ALTCHA scope "%s".', $definition->name));
                }
                $normalized[$definition->name] = $definition->label;
            }
        }

        ksort($normalized);
        $this->scopes = $normalized;
    }

    public function contains(string $scope): bool
    {
        return isset($this->scopes[$scope]);
    }

    public function assertDeclared(string $scope): void
    {
        ScopeName::assertValid($scope);
        if (!$this->contains($scope)) {
            throw new \InvalidArgumentException(sprintf('ALTCHA scope "%s" is not declared.', $scope));
        }
    }

    public function manifest(): ScopeManifest
    {
        $definitions = [];
        foreach ($this->scopes as $name => $label) {
            $definitions[] = new ScopeDefinition($name, $label);
        }

        return new ScopeManifest($definitions, $this->source);
    }
}
