<?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\Configuration\Repository;

use InPost\International\Configuration\EnvironmentConfiguration;
use InPost\International\Encryption\CryptoInterface;
use InPost\International\Environment\EnvironmentInterface;
use InPost\International\Environment\EnvironmentRegistry;
use InPost\International\Environment\SandboxEnvironment;
use InPost\International\OAuth2\Authentication\ClientCredentials;
use InPost\International\OAuth2\Authentication\ClientCredentialsInterface;
use InPost\International\OAuth2\Authentication\ClientCredentialsRepositoryInterface;
use InPost\International\OAuth2\Token\AccessTokenInterface;
use InPost\International\OAuth2\Token\AccessTokenRepositoryInterface;
use InPost\International\OAuth2\Token\BearerToken;
use InPost\International\Serializer\SafeDeserializerTrait;
use PrestaShop\PrestaShop\Core\Domain\Configuration\ShopConfigurationInterface;
use PrestaShop\PrestaShop\Core\Domain\Shop\ValueObject\ShopConstraint;
use Symfony\Component\Serializer\SerializerInterface;

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

final class ApiConfigurationRepository implements ApiConfigurationRepositoryInterface, ClientCredentialsRepositoryInterface, AccessTokenRepositoryInterface
{
    use SafeDeserializerTrait;

    private const ENVIRONMENT_ID_KEY = 'INPOST_INTL_ENVIRONMENT_ID';

    private const OAUTH2_CLIENT_ID_KEY_PATTERN = 'INPOST_INTL_{ENV}_OAUTH2_CLIENT_ID';
    private const OAUTH2_CLIENT_SECRET_KEY_PATTERN = 'INPOST_INTL_{ENV}_OAUTH2_CLIENT_SECRET';
    private const GEO_WIDGET_TOKEN_KEY_PATTERN = 'INPOST_INTL_{ENV}_GEO_WIDGET_TOKEN';

    private const ACCESS_TOKEN_KEY_PATTERN = 'INPOST_INTL_{ENV}_ACCESS_TOKEN';

    /**
     * @var ShopConfigurationInterface
     */
    private $configuration;

    /**
     * @var EnvironmentRegistry
     */
    private $envRegistry;

    /**
     * @var CryptoInterface
     */
    private $crypto;

    /**
     * @var array{
     *     env?: EnvironmentInterface,
     *     configuration: array<string, EnvironmentConfiguration>,
     *     token: array<string, AccessTokenInterface|null>,
     * }
     */
    private $cache = [
        'configuration' => [],
        'token' => [],
    ];

    public function __construct(ShopConfigurationInterface $configuration, EnvironmentRegistry $envRegistry, SerializerInterface $serializer, CryptoInterface $crypto)
    {
        $this->configuration = $configuration;
        $this->envRegistry = $envRegistry;
        $this->serializer = $serializer;
        $this->crypto = $crypto;
    }

    public function getCurrentEnvironment(): EnvironmentInterface
    {
        if (array_key_exists('env', $this->cache)) {
            return $this->cache['env'];
        }

        return $this->cache['env'] = $this->loadEnvironment();
    }

    public function setCurrentEnvironment(EnvironmentInterface $environment, ShopConstraint $shops = null): void
    {
        $this->setEnvironment($environment, $shops);

        if (null === $shops) {
            $this->cache['env'] = $environment;
        } else {
            unset($this->cache['env']);
        }
    }

    public function getAllConfigurations(): array
    {
        return array_map(function (EnvironmentInterface $environment) {
            return $this->getConfiguration($environment->getId());
        }, $this->envRegistry->getAll());
    }

    /**
     * {@inheritDoc}
     */
    public function hasConfiguration(string $env = null): bool
    {
        $configuration = $this->getConfiguration($env);

        if (null === $configuration->getClientCredentials()) {
            return false;
        }

        return null !== $configuration->getGeoWidgetToken();
    }

    public function getConfiguration(string $env = null): EnvironmentConfiguration
    {
        $environment = $env ? $this->envRegistry->get($env) : $this->getCurrentEnvironment();

        if (array_key_exists($env = $environment->getId(), $this->cache['configuration'])) {
            return $this->cache['configuration'][$env];
        }

        return $this->cache['configuration'][$env] = new EnvironmentConfiguration(
            $environment,
            $this->loadClientCredentials($env),
            $this->loadGeoWidgetToken($env)
        );
    }

    public function saveConfiguration(EnvironmentConfiguration $configuration, ShopConstraint $shops = null): void
    {
        $env = $configuration->getEnvironment()->getId();

        $this->setClientCredentials($configuration->getClientCredentials(), $env, $shops);
        $this->setGeoWidgetToken($configuration->getGeoWidgetToken(), $env, $shops);

        $this->deleteAccessToken($env, $shops);

        if (null === $shops) {
            $this->cache['configuration'][$env] = $configuration;
        } else {
            unset($this->cache['configuration'][$env]);
        }
    }

    public function getClientCredentials(string $env = null): ?ClientCredentialsInterface
    {
        return $this->getConfiguration($env)->getClientCredentials();
    }

    public function getAccessToken(string $env = null): ?AccessTokenInterface
    {
        $env = $env ?? $this->getCurrentEnvironment()->getId();

        if (array_key_exists($env, $this->cache['token'])) {
            return $this->cache['token'][$env];
        }

        $value = $this->getEnvSpecific(self::ACCESS_TOKEN_KEY_PATTERN, $env);

        if (null === $value) {
            return $this->cache['token'][$env] = null;
        }

        try {
            $value = $this->crypto->decrypt($value);
        } catch (\Exception $e) {
            return $this->cache['token'][$env] = null;
        }

        return $this->cache['token'][$env] = $this->deserialize($value, BearerToken::class);
    }

    public function saveAccessToken(AccessTokenInterface $accessToken, string $env = null): void
    {
        $env = $env ?? $this->getCurrentEnvironment()->getId();

        $value = $this->serializer->serialize($accessToken, 'json');
        $value = $this->crypto->encrypt($value);

        $this->setEnvSpecific(self::ACCESS_TOKEN_KEY_PATTERN, $env, $value);

        $this->cache['token'][$env] = $accessToken;
    }

    public function deleteAccessToken(string $env = null, ShopConstraint $shops = null): void
    {
        $env = $env ?? $this->getCurrentEnvironment()->getId();

        $this->setEnvSpecific(self::ACCESS_TOKEN_KEY_PATTERN, $env, null, $shops);

        if (null === $shops) {
            $this->cache['token'][$env] = null;
        } else {
            unset($this->cache['token'][$env]);
        }
    }

    /**
     * @internal
     */
    public function specializeTokenRepository(string $env): AccessTokenRepositoryInterface
    {
        if (!$this->envRegistry->has($env)) {
            throw new \DomainException(sprintf('Environment "%s" does not exist.', $env));
        }

        return new class($this, $env) implements AccessTokenRepositoryInterface {
            /**
             * @var ApiConfigurationRepository
             */
            private $repository;

            /**
             * @var string
             */
            private $env;

            public function __construct(ApiConfigurationRepository $repository, string $env)
            {
                $this->repository = $repository;
                $this->env = $env;
            }

            public function getAccessToken(): ?AccessTokenInterface
            {
                return $this->repository->getAccessToken($this->env);
            }

            public function saveAccessToken(AccessTokenInterface $accessToken): void
            {
                $this->repository->saveAccessToken($accessToken, $this->env);
            }

            public function deleteAccessToken(): void
            {
                $this->repository->deleteAccessToken($this->env);
            }
        };
    }

    private function loadEnvironment(): EnvironmentInterface
    {
        $environmentId = $this->configuration->get(self::ENVIRONMENT_ID_KEY);

        if (null === $environmentId || !$this->envRegistry->has((string) $environmentId)) {
            $environmentId = SandboxEnvironment::ID;
        }

        return $this->envRegistry->get($environmentId);
    }

    private function loadClientCredentials(string $env): ?ClientCredentialsInterface
    {
        if (null === $clientId = $this->getEnvSpecific(self::OAUTH2_CLIENT_ID_KEY_PATTERN, $env)) {
            return null;
        }

        $clientSecret = $this->getEnvSpecific(self::OAUTH2_CLIENT_SECRET_KEY_PATTERN, $env);

        try {
            $clientSecret = $this->crypto->decrypt($clientSecret);
        } catch (\Exception $e) {
            return null;
        }

        return new ClientCredentials($clientId, $clientSecret);
    }

    private function loadGeoWidgetToken(string $env): ?string
    {
        return $this->getEnvSpecific(self::GEO_WIDGET_TOKEN_KEY_PATTERN, $env);
    }

    private function setEnvironment(EnvironmentInterface $environment, ?ShopConstraint $shops): void
    {
        $this->configuration->set(self::ENVIRONMENT_ID_KEY, $environment->getId(), $shops);
    }

    private function setClientCredentials(?ClientCredentialsInterface $credentials, string $env, ?ShopConstraint $shops): void
    {
        $clientId = $credentials ? $credentials->getClientId() : null;
        $clientSecret = $credentials ? $credentials->getClientSecret() : null;

        if (null !== $clientSecret) {
            $clientSecret = $this->crypto->encrypt($clientSecret);
        }

        $this->setEnvSpecific(self::OAUTH2_CLIENT_ID_KEY_PATTERN, $env, $clientId, $shops);
        $this->setEnvSpecific(self::OAUTH2_CLIENT_SECRET_KEY_PATTERN, $env, $clientSecret, $shops);
    }

    private function setGeoWidgetToken(?string $token, string $env, ?ShopConstraint $shops): void
    {
        $this->setEnvSpecific(self::GEO_WIDGET_TOKEN_KEY_PATTERN, $env, $token, $shops);
    }

    private function getEnvSpecific(string $keyPattern, string $env)
    {
        $key = self::getEnvKey($keyPattern, $env);

        return $this->configuration->get($key);
    }

    private function setEnvSpecific(string $keyPattern, string $env, $value, ShopConstraint $shops = null): void
    {
        $key = self::getEnvKey($keyPattern, $env);

        $this->configuration->set($key, $value, $shops);
    }

    private static function getEnvKey(string $pattern, string $env): string
    {
        return strtr($pattern, ['{ENV}' => strtoupper($env)]);
    }
}
