<?php

declare(strict_types=1);

namespace Apexl\Io\Jobs\Dtos;

use InvalidArgumentException;

/**
 * Which handlers a `jobs:queue` run is talking about.
 *
 * With nothing said either way it is the whole queue: a failed job is a row somebody has to
 * decide about whatever wrote it. `--handler` narrows that to one handler, or a few, when
 * only one thing is wrong, and an app can narrow the *default* through
 * `jobs.default_handlers` when its queue has a part that matters more than the rest.
 */
final readonly class QueueHandlerScope
{
    private const string EVERYTHING = 'all';

    /**
     * @param class-string[]|null $handlers Null is every handler — no filter at all.
     * @param string $label The handlers named, for output. Empty when there is no filter.
     * @param bool $explicit Whether this was asked for on the command line, or is the
     *                       default the app configured. A default that hides part of the
     *                       queue has to say so; one that was typed does not.
     */
    private function __construct(
        public ?array $handlers,
        public string $label,
        public bool $explicit,
    ) {}

    /**
     * The scope of a run with no `--handler`: whatever the app configured, or everything.
     *
     * @param class-string[]|null $handlers
     */
    public static function defaults(?array $handlers = null): self
    {
        return $handlers === null || $handlers === []
            ? new self(null, '', false)
            : new self(array_values($handlers), self::describe($handlers), false);
    }

    /**
     * @param string[] $known Handler class-strings present in the queue — what short names
     *                        are matched against, and what an unknown name is reported with.
     *
     * @throws InvalidArgumentException When a name matches nothing.
     */
    public static function resolve(?string $option, array $known): self
    {
        if ($option === null || trim($option) === '') {
            return self::defaults();
        }

        if (strcasecmp(trim($option), self::EVERYTHING) === 0) {
            return new self(null, '', true);
        }

        $handlers = array_values(array_unique(array_map(
            static fn (string $name): string => self::matchName(trim($name), $known),
            explode(',', $option),
        )));

        return new self($handlers, self::describe($handlers), true);
    }

    /** @param string[] $handlers */
    private static function describe(array $handlers): string
    {
        return implode(', ', array_map(static fn (string $h): string => self::shortName($h), $handlers));
    }

    /**
     * A handler already in the queue, matched on its short class name, or any class that
     * exists. Matching what is queued is the half that matters — nobody types a fully
     * qualified handler name while things are broken — but a class-string still resolves, so
     * a handler with nothing queued can be asked about.
     *
     * @param string[] $known
     */
    private static function matchName(string $name, array $known): string
    {
        foreach ($known as $handler) {
            if (strcasecmp($handler, $name) === 0 || strcasecmp(self::shortName($handler), $name) === 0) {
                return $handler;
            }
        }

        if (class_exists($name)) {
            return $name;
        }

        throw new InvalidArgumentException(sprintf(
            'No handler matching "%s". In the queue: %s',
            $name,
            $known === [] ? '(nothing)' : self::describe($known),
        ));
    }

    public static function shortName(string $handler): string
    {
        $parts = explode('\\', $handler);

        return end($parts);
    }
}
