<?php

/**
 * Copyright © Resurs Bank AB. All rights reserved.
 * See LICENSE for license details.
 */

declare(strict_types=1);

namespace Resursbank\MerchantApi\Config;

use Address;
use Context;
use Country;
use DateTime;
use Defuse\Crypto\Exception\CryptoException;
use Exception;
use Module;
use PrestaShop\PrestaShop\Adapter\Configuration as Adapter;
use PrestaShop\PrestaShop\Core\Domain\Shop\ValueObject\ShopConstraint;
use Resursbank\Ecom\Lib\Api\Environment;
use Resursbank\MerchantApi\Crypto\Openssl;
use Resursbank\MerchantApi\Traits\Config as ConfigTrait;
use Resursbank\MerchantApi\Util\Log;
use Shop;

/**
 * Interact with the configuration table. This class provide methods to read and
 * write all values related to our module.
 */
class Config
{
    use ConfigTrait;

    /**
     * @var ?Context
     */
    private ?Context $context;

    /** @var false|Module */
    private false|Module $module;

    private Log $log;

    /**
     * Default setting for part payment widget.
     */
    private const DEFAULT_PART_PAYMENT_ENABLED = true;

    /**
     * Default part payment threshold.
     */
    private const DEFAULT_PART_PAYMENT_THRESHOLD = 150;

    /**
     * Default setting for order management enabled/disabled.
     */
    private const DEFAULT_ORDER_MANAGEMENT_ENABLE = true;

    /**
     * Default callback TTL value.
     */
    private const DEFAULT_CALLBACK_TTL = 120;

    /**
     * Default XDebug session name.
     */
    private const DEFAULT_XDEBUG_SESSION = '';

    /**
     * @param Adapter $adapter
     * @param Openssl $openssl
     */
    public function __construct(
        private readonly Adapter $adapter,
        private readonly Openssl $openssl
    ) {
        $this->context = Context::getContext();
        $this->module = Module::getInstanceByName(module_name: 'resursbank');
        $this->log = new Log();
    }

    /**
     * @param ShopConstraint|null $shopConstraint
     *
     * @return Environment
     */
    public function getEnvironment(
        ShopConstraint $shopConstraint = null
    ): Environment {
        $raw = (string)$this->adapter->get(
            key: $this->getId(key: 'environment'),
            default: Environment::TEST->value,
            shopConstraint: $shopConstraint
        );

        return Environment::from(value: $raw);
    }

    /**
     * @param Environment $value
     * @param ShopConstraint|null $shopConstraint
     *
     * @throws Exception
     */
    public function setEnvironment(
        Environment $value,
        ShopConstraint $shopConstraint = null
    ): void {
        $this->adapter->set(
            key: $this->getId(key: 'environment'),
            value: $value->value,
            shopConstraint: $shopConstraint
        );
    }

    /**
     * @param ShopConstraint|null $shopConstraint
     * @return string
     */
    public function getStoreId(
        ShopConstraint $shopConstraint = null
    ): string {
        return (string)$this->adapter->get(
            key: $this->getId(key: 'store_id'),
            default: '',
            shopConstraint: $shopConstraint
        );
    }

    /**
     * @throws Exception
     */
    public function setStoreId(
        string $value,
        ShopConstraint $shopConstraint = null
    ): void {
        $this->adapter->set(
            key: $this->getId(key: 'store_id'),
            value: $value,
            shopConstraint: $shopConstraint
        );
    }

    /**
     * @param Environment $environment
     * @param ShopConstraint|null $shopConstraint
     *
     * @return string
     *
     */
    public function getClientId(
        Environment $environment,
        ShopConstraint $shopConstraint = null
    ): string {
        return (string)$this->adapter->get(
            key: $this->getId(key: "client_id_$environment->value"),
            default: null,
            shopConstraint: $shopConstraint
        );
    }

    /**
     * @param string $value
     * @param Environment $environment
     * @param ShopConstraint|null $shopConstraint
     *
     * @throws Exception
     */
    public function setClientId(
        string $value,
        Environment $environment,
        ShopConstraint $shopConstraint = null
    ): void {
        $this->adapter->set(
            key: $this->getId(key: "client_id_$environment->value"),
            value: $value,
            shopConstraint: $shopConstraint
        );
    }

    /**
     * @param Environment $environment
     * @param ShopConstraint|null $shopConstraint
     *
     * @return string
     */
    public function getClientSecret(
        Environment $environment,
        ShopConstraint $shopConstraint = null
    ): string {
        $envClientSecret = (string)$this->adapter->get(
            key: $this->getId(key: "client_secret_$environment->value"),
            default: null,
            shopConstraint: $shopConstraint
        );

        try {
            $result = $envClientSecret !== '' ? $this->openssl->decrypt(data: $envClientSecret) : '';
        } catch (CryptoException $error) {
            // The below notes is still important to remember.
            //
            // @todo If your secret key changes you cannot decrypt the value.
            // @todo If an Exception is thrown the Configuration page won't render.
            // @todo We cannot implement the logger since it requires this class (circular dependency).
            //
            // The clientSecret fields are always empty, for security reasons, even when there is a value.
            // So there is no simple way to notify the client something went wrong in this scenario.
            // Not sure how to handle this yet.
            $this->log->error(message: $error);
            $result = $envClientSecret;
        }

        return $result;
    }

    /**
     * @param string $value
     * @param Environment $environment
     * @param ShopConstraint|null $shopConstraint
     *
     * @throws Exception
     */
    public function setClientSecret(
        string $value,
        Environment $environment,
        ShopConstraint $shopConstraint = null
    ): void {
        try {
            $clientSecretValue = $this->openssl->encrypt(data: $value);
        } catch (Exception $error) {
            // Fail over if Symfony and OpenSSL fails. Note: We still can't log this.
            $this->log->error(message: $error);
            $clientSecretValue = $value;
        }

        $this->adapter->set(
            key: $this->getId(key: "client_secret_$environment->value"),
            value: $clientSecretValue,
            shopConstraint: $shopConstraint
        );
    }

    /**
     * @param ShopConstraint|null $shopConstraint
     *
     * @return bool
     */
    public function isDebugEnabled(
        ShopConstraint $shopConstraint = null
    ): bool {
        return (bool)$this->adapter->get(
            key: $this->getId(key: 'debug'),
            default: $shopConstraint
        );
    }

    /**
     * @param bool $value
     * @param ShopConstraint|null $shopConstraint
     *
     * @throws Exception
     */
    public function setIsDebugEnabled(
        bool $value,
        ShopConstraint $shopConstraint = null
    ): void {
        // NOTE: Regarding $value ternary, PrestaShop evaluates false as NULL.
        $this->adapter->set(
            key: $this->getId(key: 'debug'),
            value: ($value === true ? 1 : 0),
            shopConstraint: $shopConstraint
        );
    }

    /**
     * @param ShopConstraint|null $shopConstraint
     *
     * @return string
     *
     * @todo We still don't use any shopConstraint at this point, but probably should when we work with multistores.
     */
    public function getCountry(
        ShopConstraint $shopConstraint = null
    ): string {
        // Initially, we always returned 'se' here. For now on we return the shop address country instead.
        // As we returned this data in lowercase, at this moment we will continue doing that.
        return strtolower(string: $this->getShopAddressCountry());
    }

    /**
     * Get ISO based country from current shop.
     *
     * This data is retrieved from Improve => International => Localization => Default Country.
     * What we currently do not know, is if this covers a multi store setup where each store has
     * its own default country.
     *
     * @return string
     *
     * @since 1.0.0
     */
    public function getShopAddressCountry(): string
    {
        // Worst case scenario will return nothing.
        $return = '';
        if ($this->context->shop instanceof Shop) {
            $shopAddress = $this->context->shop->getAddress();
            // For some reason this instance must point at \Address if it wants
            // to be found.
            if ($shopAddress instanceof Address) {
                $return = Country::getIsoById(idCountry: $shopAddress->id_country);
            }
        }

        return $return;
    }

    /**
     * @param Environment $env
     * @param ShopConstraint|null $shopConstraint
     *
     * @return bool
     */
    public function hasCredentials(
        Environment $env,
        ShopConstraint $shopConstraint = null
    ): bool {
        return
            $this->getClientId(environment: $env,
                shopConstraint: $shopConstraint) !== '' &&
            $this->getClientSecret(environment: $env,
                shopConstraint: $shopConstraint) !== '';
    }

    /**
     * @param Environment $env
     * @param ShopConstraint|null $shopConstraint
     * @return bool
     */
    public function hasClientSecret(Environment $env, ShopConstraint $shopConstraint = null): bool
    {
        $secret = (string)$this->adapter->get(
            key: $this->getId(key: "client_secret_$env->value"),
            default: '',
            shopConstraint: $shopConstraint
        );
        return $secret !== '';
    }

    /**
     * @return int
     */
    public function getMigrationVersion(): int
    {
        return (int)$this->adapter->get(key: $this->getId(key: 'migration_version'), default: 0);
    }

    /**
     * @param int $value
     *
     * @throws Exception
     */
    public function setMigrationVersion(int $value): void
    {
        $this->adapter->set(key: $this->getId(key: 'migration_version'), value: $value);
    }

    /**
     * Set callback test triggered value.
     *
     * @return void
     * @throws Exception
     */
    public function setCallbackTestTriggered(): void
    {
        $time = new DateTime();
        $this->adapter->set(
            key: $this->getId(key: 'CALLBACK_TEST_SENT'),
            value: $time->format(format: 'c')
        );
    }

    /**
     * Get callback test triggered value.
     *
     * @return string|null
     */
    public function getCallbackTestTriggered(): ?string
    {
        return $this->adapter->get(
            key: $this->getId(key: 'CALLBACK_TEST_SENT')
        );
    }

    /**
     * Set callback test received value.
     *
     * @return void
     * @throws Exception
     */
    public function setCallbackTestReceived(): void
    {
        $time = new DateTime();
        $this->adapter->set(
            key: $this->getId(key: 'CALLBACK_TEST_RECEIVED'),
            value: $time->format(format: 'c')
        );
    }

    /**
     * Get callback test received value.
     *
     * @return string|null
     */
    public function getCallbackTestReceived(): ?string
    {
        return $this->adapter->get(
            key: $this->getId(key: 'CALLBACK_TEST_RECEIVED')
        );
    }

    /**
     * Set part payment enabled/disabled.
     *
     * @param bool $state
     * @param ShopConstraint|null $shopConstraint
     * @return void
     * @throws Exception
     */
    public function setPartPaymentEnabled(
        bool $state,
        ShopConstraint $shopConstraint = null
    ): void {
        $this->adapter->set(
            key: $this->getId(key: 'PART_PAYMENT_ENABLED'),
            value: $state,
            shopConstraint: $shopConstraint
        );
    }

    /**
     * Get part payment enabled/disabled.
     *
     * @param ShopConstraint|null $shopConstraint
     * @return bool
     */
    public function getPartPaymentEnabled(
        ShopConstraint $shopConstraint = null
    ): bool {
        return (bool)$this->adapter->get(
            key: $this->getId(key: 'PART_PAYMENT_ENABLED'),
            default: self::DEFAULT_PART_PAYMENT_ENABLED,
            shopConstraint: $shopConstraint
        );
    }

    /**
     * Set part payment payment method.
     *
     * @param string $methodId
     * @param ShopConstraint|null $shopConstraint
     * @return void
     * @throws Exception
     */
    public function setPartPaymentPaymentMethod(
        string $methodId,
        ShopConstraint $shopConstraint = null
    ): void {
        $this->adapter->set(
            key: $this->getId(key: 'PART_PAYMENT_METHOD_ID'),
            value: $methodId,
            shopConstraint: $shopConstraint
        );
    }

    /**
     * Get part payment payment method.
     *
     * @param ShopConstraint|null $shopConstraint
     * @return string|null
     */
    public function getPartPaymentPaymentMethod(
        ShopConstraint $shopConstraint = null
    ): ?string {
        return $this->adapter->get(
            key: $this->getId(key: 'PART_PAYMENT_METHOD_ID'),
            shopConstraint: $shopConstraint
        );
    }

    /**
     * Set part payment annuity period.
     *
     * @param int $period
     * @param ShopConstraint|null $shopConstraint
     * @return void
     * @throws Exception
     */
    public function setPartPaymentAnnuityPeriod(
        int $period,
        ShopConstraint $shopConstraint = null
    ): void {
        $this->adapter->set(
            key: $this->getId(key: 'PART_PAYMENT_ANNUITY_PERIOD'),
            value: $period,
            shopConstraint: $shopConstraint
        );
    }

    /**
     * Get part payment annuity period.
     *
     * @param ShopConstraint|null $shopConstraint
     * @return int
     */
    public function getPartPaymentAnnuityPeriod(ShopConstraint $shopConstraint = null): int
    {
        return (int)$this->adapter->get(
            key: $this->getId(key: 'PART_PAYMENT_ANNUITY_PERIOD'),
            shopConstraint: $shopConstraint
        );
    }

    /**
     * Set part payment threshold.
     *
     * @param float $value
     * @param ShopConstraint|null $shopConstraint
     * @return void
     * @throws Exception
     */
    public function setPartPaymentThreshold(
        float $value,
        ShopConstraint $shopConstraint = null
    ): void {
        $this->adapter->set(
            key: $this->getId(key: 'PART_PAYMENT_THRESHOLD'),
            value: $value,
            shopConstraint: $shopConstraint
        );
    }

    /**
     * Get part payment threshold threshold.
     *
     * @param ShopConstraint|null $shopConstraint
     * @return int
     */
    public function getPartPaymentThreshold(
        ShopConstraint $shopConstraint = null
    ): float {
        return (float)$this->adapter->get(
            key: $this->getId(key: 'PART_PAYMENT_THRESHOLD'),
            default: self::DEFAULT_PART_PAYMENT_THRESHOLD,
            shopConstraint: $shopConstraint
        );
    }

    /**
     * @param ShopConstraint|null $shopConstraint
     *
     * @return bool
     */
    public function isAftershopEnabled(
        ShopConstraint $shopConstraint = null
    ): bool {
        return (bool)$this->adapter->get(
            key: $this->getId(key: 'AFTERSHOP_ENABLED'),
            default: self::DEFAULT_ORDER_MANAGEMENT_ENABLE,
            shopConstraint: $shopConstraint
        );
    }

    /**
     * @param bool $value
     * @param ShopConstraint|null $shopConstraint
     *
     * @throws Exception
     */
    public function setIsAftershopEnabled(
        bool $value,
        ShopConstraint $shopConstraint = null
    ): void {
        // NOTE: Regarding $value ternary, PrestaShop evaluates false as NULL.
        $this->adapter->set(
            key: $this->getId(key: 'AFTERSHOP_ENABLED'),
            value: ($value === true ? 1 : 0),
            shopConstraint: $shopConstraint
        );
    }

    /**
     * Resolve configured callbacks TTL value from table.
     *
     * @param ShopConstraint|null $shopConstraint
     * @return int
     */
    public function getCallbackTtl(
        ShopConstraint $shopConstraint = null
    ): int {
        return (int)$this->adapter->get(
            key: $this->getId(key: 'callback_ttl'),
            default: self::DEFAULT_CALLBACK_TTL,
            shopConstraint: $shopConstraint
        );
    }

    /**
     * Store configured callbacks TTL value in table.
     *
     * @param int $value
     * @param ShopConstraint|null $shopConstraint
     * @return void
     * @throws Exception
     */
    public function setCallbackTtl(
        int $value,
        ShopConstraint $shopConstraint = null
    ): void {
        $this->adapter->set(
            key: $this->getId(key: 'callback_ttl'),
            value: $value,
            shopConstraint: $shopConstraint
        );
    }

    /**
     * Get configured Xdebug session name.
     *
     * @param ShopConstraint|null $shopConstraint
     * @return string
     */
    public function getXdebugSession(
        ShopConstraint $shopConstraint = null
    ): string {
        return (string)$this->adapter->get(
            key: $this->getId(key: 'xdebug_session'),
            default: self::DEFAULT_XDEBUG_SESSION,
            shopConstraint: $shopConstraint
        );
    }

    /**
     * Set Xdebug session name.
     *
     * @param string $value
     * @param ShopConstraint|null $shopConstraint
     * @return void
     * @throws Exception
     */
    public function setXdebugSession(
        string $value,
        ShopConstraint $shopConstraint = null
    ): void {
        $this->adapter->set(
            key: $this->getId(key: 'xdebug_session'),
            value: $value,
            shopConstraint: $shopConstraint
        );
    }
}
