<?php
/**
 * Copyright since 2025 InPost S.A.
 *
 * For the full license information, please view the LICENSE file bundled with the module.
 *
 * @author InPost S.A.
 * @copyright since 2025 InPost S.A.
 * @license MIT
 */

declare(strict_types=1);

namespace InPost\International\Geocoding;

use InPost\International\Configuration\Repository\GeocodingConfigurationRepositoryInterface;
use Psr\Cache\CacheItemPoolInterface;
use Psr\Container\ContainerInterface;
use Psr\Log\LoggerInterface;
use Symfony\Contracts\Service\ServiceSubscriberInterface;

if (!defined('_PS_VERSION_')) {
    exit;
}

/**
 * @phpstan-type LoggerOptions array{enabled?: bool}
 * @phpstan-type CacheOptions array{enabled?: bool, ttl?: int|null}
 */
final class GeocoderFactory implements GeocoderFactoryInterface, ServiceSubscriberInterface
{
    /**
     * @var ContainerInterface
     */
    private $container;

    /**
     * @var ContainerInterface
     */
    private $factoryLocator;

    /**
     * @var array<string, mixed>
     */
    private $defaultOptions;

    /**
     * @param array<string, mixed> $defaultOptions
     */
    public function __construct(ContainerInterface $container, ContainerInterface $factoryLocator, array $defaultOptions = [])
    {
        $this->container = $container;
        $this->factoryLocator = $factoryLocator;
        $this->defaultOptions = $defaultOptions;
    }

    public static function getSubscribedServices(): array
    {
        return [
            GeocodingConfigurationRepositoryInterface::class,
            '?' . LoggerInterface::class,
            '?' . CacheItemPoolInterface::class,
        ];
    }

    /**
     * @param array{type: string, logger?: LoggerOptions, cache?: CacheOptions, ...} $options
     */
    public function create(array $options): GeocoderInterface
    {
        if (empty($options['type'])) {
            throw new \LogicException('A geocoder type identifier is required.');
        }

        $options = array_merge_recursive($this->defaultOptions, $options);

        if (!$this->factoryLocator->has($type = (string) $options['type'])) {
            throw new \LogicException(sprintf('Unsupported geocoder type identifier: "%s".', $type));
        }

        $loggerOptions = $options['logger'] ?? [];
        $cacheOptions = $options['cache'] ?? false;
        unset($options['type'], $options['logger'], $options['cache']);

        /** @var GeocoderFactoryInterface $factory */
        $factory = $this->factoryLocator->get($type);
        $geocoder = $factory->create($options);

        if ($loggerOptions['enabled'] ?? false) {
            $geocoder = $this->logging($geocoder, $loggerOptions);
        }

        if ($cacheOptions['enabled'] ?? false) {
            $geocoder = $this->caching($geocoder, $cacheOptions);
        }

        return $geocoder;
    }

    public function createDefault(): GeocoderInterface
    {
        $configuration = $this->get(GeocodingConfigurationRepositoryInterface::class);

        if (null === $type = $configuration->getGeocoderTypeId()) {
            return new NullGeocoder();
        }

        $options = $configuration->getGeocoderOptions($type) ?? [];
        $options['type'] = $type;

        return $this->create($options);
    }

    /**
     * @param LoggerOptions $options
     */
    private function logging(GeocoderInterface $geocoder, array $options): GeocoderInterface
    {
        if (!$this->container->has(LoggerInterface::class)) {
            throw new \LogicException('To create a logging geocoder, a logger service must be available.');
        }

        $logger = $this->get(LoggerInterface::class);

        return new LoggingGeocoder($geocoder, $logger);
    }

    /**
     * @param CacheOptions $options
     */
    private function caching(GeocoderInterface $geocoder, array $options): GeocoderInterface
    {
        if (!$this->container->has(CacheItemPoolInterface::class)) {
            throw new \LogicException('To create a caching geocoder, a cache service must be available.');
        }

        $cache = $this->get(CacheItemPoolInterface::class);

        return new CachingGeocoder($geocoder, $cache, $options['ttl'] ?? null);
    }

    /**
     * @template T of object
     *
     * @param class-string<T> $id
     *
     * @return T
     */
    private function get(string $id): object
    {
        return $this->container->get($id);
    }
}
