<?php

declare(strict_types=1);

namespace Qameta\Allure\PHPUnit\Internal;

use Qameta\Allure\Hook\LifecycleHookInterface;
use Qameta\Allure\PHPUnit\Setup\ThreadDetectorInterface;
use Qameta\Allure\Setup\LinkTemplateInterface;
use RuntimeException;

use function class_exists;
use function is_a;
use function is_array;
use function is_callable;
use function is_int;
use function is_string;

final class Config implements ConfigInterface
{
    public function __construct(
        private readonly array $data,
    ) {
    }

    #[\Override]
    public function getOutputDirectory(): ?string
    {
        $key = 'outputDirectory';
        if (!isset($this->data[$key])) {
            return null;
        }

        /** @psalm-var mixed $outputDirectory */
        $outputDirectory = $this->data[$key];

        return is_string($outputDirectory)
            ? $outputDirectory
            : throw new RuntimeException("Config key \"{$key}\" should contain a string");
    }

    /**
     * @return array<string, LinkTemplateInterface>
     */
    #[\Override]
    public function getLinkTemplates(): array
    {
        $key = 'linkTemplates';
        $linkTemplates = [];
        /** @psalm-var mixed $linkTemplateSource */
        foreach ($this->getArrayFromData($key) as $linkKey => $linkTemplateSource) {
            if (!is_string($linkKey)) {
                throw new RuntimeException(
                    "Config key \"{$key}\" should contain an array with string keys only",
                );
            }
            $linkTemplates[$linkKey] = $this->buildObject(
                "{$key}/{$linkKey}",
                $linkTemplateSource,
                LinkTemplateInterface::class,
            );
        }

        return $linkTemplates;
    }

    private function getArrayFromData(string $key): array
    {
        /** @psalm-var mixed $source */
        $source = $this->data[$key] ?? [];

        return is_array($source)
            ? $source
            : throw new RuntimeException("Config key \"{$key}\" should contain an array");
    }

    /**
     * @template T
     * @param string          $key
     * @param mixed           $source
     * @param class-string<T> $expectedClass
     * @return T
     * @psalm-suppress MixedMethodCall
     */
    private function buildObject(string $key, mixed $source, string $expectedClass): object
    {
        return match (true) {
            $source instanceof $expectedClass => $source,
            $this->isExpectedClassName($source, $expectedClass) => new $source(),
            is_callable($source) => $this->buildObject($key, $source(), $expectedClass),
            default => throw new RuntimeException(
                "Config key \"{$key}\" contains invalid source of {$expectedClass}",
            ),
        };
    }

    /**
     * @template T
     * @param mixed  $source
     * @param class-string<T> $expectedClass
     * @return bool
     * @psalm-assert-if-true class-string<T> $source
     */
    private function isExpectedClassName(mixed $source, string $expectedClass): bool
    {
        return $this->isClassName($source) && is_a($source, $expectedClass, true);
    }

    /**
     * @psalm-assert-if-true class-string $source
     */
    private function isClassName(mixed $source): bool
    {
        return is_string($source) && class_exists($source);
    }

    #[\Override]
    public function getSetupHook(): ?callable
    {
        $key = 'setupHook';
        /** @psalm-var mixed $source */
        $source = $this->data[$key] ?? null;

        return isset($source)
            ? $this->buildCallable($key, $source)
            : null;
    }

    /**
     * @psalm-suppress MixedMethodCall
     */
    private function buildCallable(string $key, mixed $source): callable
    {
        return match (true) {
            is_callable($source) => $source,
            $this->isClassName($source) => $this->buildCallable($key, new $source()),
            default => throw new RuntimeException("Config key \"{$key}\" should contain a callable"),
        };
    }

    #[\Override]
    public function getThreadDetector(): ?ThreadDetectorInterface
    {
        $key = 'threadDetector';
        /** @var mixed $threadDetector */
        $threadDetector = $this->data[$key] ?? null;

        return isset($threadDetector)
            ? $this->buildObject($key, $threadDetector, ThreadDetectorInterface::class)
            : null;
    }

    /**
     * @return list<LifecycleHookInterface>
     */
    #[\Override]
    public function getLifecycleHooks(): array
    {
        $key = 'lifecycleHooks';
        $hooks = [];
        /** @psalm-var mixed $hookSource */
        foreach ($this->getArrayFromData($key) as $index => $hookSource) {
            if (!is_int($index)) {
                throw new RuntimeException(
                    "Config key \"{$key}\" should contain an array with integer keys only",
                );
            }
            $hooks[] = $this->buildObject(
                "{$key}/{$index}",
                $hookSource,
                LifecycleHookInterface::class,
            );
        }

        return $hooks;
    }
}
