<?php

declare(strict_types=1);

namespace Gls\GlsPoland\PrestaShop\Hook;

use Gls\GlsPoland\DependencyInjection\ServiceSubscriberInterface;
use Gls\GlsPoland\PrestaShop\Installer\AvailableHooksProviderInterface;
use Psr\Container\ContainerInterface;
use Psr\Container\NotFoundExceptionInterface;

final class HookExecutor implements ServiceSubscriberInterface, HookExecutorInterface, AvailableHooksProviderInterface
{
    private $locator;
    private $psVersion;

    public function __construct(ContainerInterface $locator, string $psVersion)
    {
        $this->locator = $locator;
        $this->psVersion = $psVersion;
    }

    public static function getSubscribedServices(): array
    {
        return [
            Common\ActionDispatcherBefore::HOOK_NAME => Common\ActionDispatcherBefore::class,

            /* admin orders */
            Admin\DisplayAdminOrderMain::HOOK_NAME => '?' . Admin\DisplayAdminOrderMain::class,
            Admin\ActionOrderGridDefinitionModifier::HOOK_NAME => '?' . Admin\ActionOrderGridDefinitionModifier::class,
            Admin\DisplayAdminOrderTabContent::HOOK_NAME => '?' . Admin\DisplayAdminOrderTabContent::class,
            Admin\DisplayAdminOrderTabLink::HOOK_NAME => '?' . Admin\DisplayAdminOrderTabLink::class,

            /* PS 1.7.6 admin orders */
            Admin\Legacy\DisplayAdminOrderContentShip::HOOK_NAME => '?' . Admin\Legacy\DisplayAdminOrderContentShip::class,
            Admin\Legacy\DisplayAdminOrdersListBefore::HOOK_NAME => '?' . Admin\Legacy\DisplayAdminOrdersListBefore::class,
            Admin\Legacy\DisplayAdminOrderTabShip::HOOK_NAME => '?' . Admin\Legacy\DisplayAdminOrderTabShip::class,

            /* checkout */
            Front\ActionFrontControllerInitAfter::HOOK_NAME => '?' . Front\ActionFrontControllerInitAfter::class,
            Front\ActionFrontControllerSetMedia::HOOK_NAME => '?' . Front\ActionFrontControllerSetMedia::class,
            Front\ActionObjectOrderAddAfter::HOOK_NAME => '?' . Front\ActionObjectOrderAddAfter::class,
            Front\ActionValidateOrder::HOOK_NAME => '?' . Front\ActionValidateOrder::class,
            Front\Checkout\ActionCarrierProcess::HOOK_NAME => '?' . Front\Checkout\ActionCarrierProcess::class,
            Front\Checkout\ActionValidateStepComplete::HOOK_NAME => '?' . Front\Checkout\ActionValidateStepComplete::class,
            Front\Checkout\DisplayBeforeBodyClosingTag::HOOK_NAME => '?' . Front\Checkout\DisplayBeforeBodyClosingTag::class,
            Front\Checkout\DisplayCarrierExtraContent::HOOK_NAME => '?' . Front\Checkout\DisplayCarrierExtraContent::class,
            Front\Checkout\DisplayOrderConfirmation::HOOK_NAME => '?' . Front\Checkout\DisplayOrderConfirmation::class,

            /* deprecated hooks */
            Front\Checkout\DisplayAfterCarrier::HOOK_NAME => '?' . Front\Checkout\DisplayAfterCarrier::class,
        ];
    }

    public function execute(string $hookName, array $parameters)
    {
        return $this
            ->getHook($hookName)
            ->execute($parameters);
    }

    public function getAvailableHooks(): array
    {
        $hookNames = [];

        foreach (self::getSubscribedServices() as $hookName => $serviceName) {
            $class = '?' === $serviceName[0] ? \Tools::substr($serviceName, 1) : $serviceName;

            if (is_subclass_of($class, AliasedHookInterface::class)) {
                foreach ($this->getAliases($class) as $alias) {
                    $hookNames[] = $alias;
                }
            } elseif (
                !is_subclass_of($class, PrestaShopVersionAwareHookInterface::class)
                || $class::getVersionRange()->contains($this->psVersion)
            ) {
                $hookNames[] = $hookName;
            }
        }

        return $hookNames;
    }

    private function getHook(string $hookName): HookInterface
    {
        try {
            return $this->locator->get($hookName);
        } catch (NotFoundExceptionInterface $e) {
            $services = self::getSubscribedServices();

            if (!isset($services[$hookName])) {
                throw HookNotFoundException::notImplemented($hookName);
            }

            if ('?' === $services[$hookName][0]) {
                throw HookNotFoundException::notAvailable($hookName, $e);
            }

            throw HookNotFoundException::notFound($hookName, $e);
        }
    }

    /**
     * @param class-string<AliasedHookInterface> $class
     */
    private function getAliases(string $class): array
    {
        $aliases = [];

        foreach ($class::getAliases() as $alias => $versionRange) {
            if (null === $versionRange || $versionRange->contains($this->psVersion)) {
                $aliases[] = $alias;
            }
        }

        return $aliases;
    }
}
