<?php

namespace App\Services\Communications\Placeholders;

use App\ValueObjects\Communications\PlaceholderContext;
use App\ValueObjects\Communications\PlaceholderDefinition;
use Closure;

class PlaceholderRegistry
{
    /** @var array<string, PlaceholderDefinition> */
    protected array $definitions = [];

    public function register(
        string $token,
        ?string $appliesToType,
        Closure $resolver,
        ?string $fallback = null,
        ?string $description = null,
        bool $isHtml = false,
    ): void {
        $this->definitions[$token] = new PlaceholderDefinition(
            token: $token,
            appliesToType: $appliesToType,
            resolver: $resolver,
            fallback: $fallback,
            description: $description,
            isHtml: $isHtml,
        );
    }

    public function get(string $token): ?PlaceholderDefinition
    {
        return $this->definitions[$token] ?? null;
    }

    public function has(string $token): bool
    {
        return isset($this->definitions[$token]);
    }

    /**
     * @return array<int, PlaceholderDefinition>
     */
    public function availableFor(?string $appliesToType): array
    {
        return array_values(array_filter(
            $this->definitions,
            fn (PlaceholderDefinition $d) => $d->matches($appliesToType),
        ));
    }

    /**
     * Resolve a token against a context.  Returns [value, found].
     * Fallback semantics: if resolver returns null/empty, the definition's
     * default fallback is used; if the caller passed a pipe-fallback
     * ({{ token|theirs }}), that takes precedence over the definition default.
     */
    public function resolve(string $token, PlaceholderContext $ctx, ?string $callerFallback = null): array
    {
        $def = $this->definitions[$token] ?? null;

        if ($def === null) {
            return [$callerFallback ?? '', false];
        }

        try {
            $value = ($def->resolver)($ctx);
        } catch (\Throwable) {
            $value = null;
        }

        if ($value === null || $value === '') {
            $value = $callerFallback ?? $def->fallback ?? '';
        }

        return [(string) $value, true];
    }

    /**
     * @return array<int, string>
     */
    public function tokens(): array
    {
        return array_keys($this->definitions);
    }
}
