<?php

/**
 * Copyright 2024-2025 Simply.IN Sp. z o.o.
 *
 * NOTICE OF LICENSE
 *
 * Licensed under the EUPL-1.2 or later.
 * You may not use this work except in compliance with the Licence.
 *
 * Copy of the Licence is available at:
 * https://joinup.ec.europa.eu/software/page/eupl
 * It is bundled with this package in the file LICENSE.txt
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the Licence is distributed on an as is basis,
 * without warranties or conditions of any kind, either express or implied.
 * Check the Licence for the specific language governing permissions
 * and limitations under the License.
 *
 * @author   Simply.IN Sp. z o.o.
 * @copyright 2024-2025 Simply.IN Sp. z o.o.
 * @license   https://joinup.ec.europa.eu/software/page/eupl
 */

declare(strict_types=1);

namespace SimplyIn\Service;

use Closure;
use Configuration;
use SimplyIn\Api\ApiException;
use SimplyIn\Api\ConnectionOutcome;
use SimplyIn\Api\SimplyInApiClient;
use SimplyIn\Webhook\WebhookSecretStore;
use Tools;

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

final class MerchantSettingsSync
{
    private static bool $called = false;
    private static ConnectionOutcome $lastOutcome = ConnectionOutcome::SUCCESS;

    /** @var Closure(?string): SimplyInApiClient */
    private Closure $clientFactory;

    /**
     * @param Closure(?string): SimplyInApiClient $clientFactory called with an optional merchantApiKey override
     */
    public function __construct(
        private readonly string $pluginVersion,
        Closure $clientFactory,
        private readonly WebhookSecretStore $webhookSecretStore,
    ) {
        $this->clientFactory = $clientFactory;
    }

    /**
     * @param string $newApiKey the key to sync with; both call sites always pass this explicitly
     *                          (there is no per-environment "current key" this class can safely
     *                          default to on its own) — an empty string means "no key available"
     *                          and is rejected below, never silently substituted
     */
    public function sync(string $newApiKey = '', bool $admin = false): ConnectionOutcome
    {

        if (self::$called && !$admin) {
            return self::$lastOutcome;
        }

        if ($newApiKey === '') {
            self::$lastOutcome = ConnectionOutcome::AUTH_REJECTED;

            return self::$lastOutcome;
        }

        $lastUpdateDate = Configuration::get('SIMPLYIN_LAST_UPDATE_INFO');
        if (!$admin && $lastUpdateDate) {
            $lastUpdateTimestamp = strtotime((string) $lastUpdateDate);
            if ($lastUpdateTimestamp !== false && $lastUpdateTimestamp > (time() - 3600)) {
                self::$lastOutcome = ConnectionOutcome::SUCCESS;

                return self::$lastOutcome;
            }
        }

        self::$called = true;

        $params = [
            'platform' => 'PrestaShop',
            'pluginVersion' => $this->pluginVersion,
            'shopName' => (string) Configuration::get('PS_SHOP_NAME'),
        ];

        $client = ($this->clientFactory)($newApiKey);

        try {
            $apiResponse = $client->request('GET', 'plugin-sync/info', params: $params);
        } catch (ApiException) {
            // Transport-level failure (network error, timeout, malformed JSON) — every throw site
            // in SimplyInApiClient sets statusCode=null here, so this is never an auth rejection.
            self::$lastOutcome = ConnectionOutcome::TECHNICAL_FAILURE;

            return self::$lastOutcome;
        }

        if ($apiResponse->rawBody === null || $apiResponse->rawBody === '') {
            self::$lastOutcome = ConnectionOutcome::fromStatusCode($apiResponse->statusCode);

            return self::$lastOutcome;
        }
        $response = json_decode($apiResponse->rawBody);
        // A non-2xx status has to gate this too: a body that happens to decode to an object
        // without a `code` property (e.g. a plain-text 500 wrapped as {"message": "..."}) would
        // otherwise read as success on the body shape alone.
        $isSuccessStatus = $apiResponse->statusCode >= 200 && $apiResponse->statusCode < 300;
        if (!$isSuccessStatus || !$response || isset($response->code)) {
            self::$lastOutcome = ConnectionOutcome::fromStatusCode($apiResponse->statusCode);

            return self::$lastOutcome;
        }

        Configuration::updateValue('SIMPLYIN_LAST_UPDATE_INFO', date('Y-m-d H:i:s'));

        // Inbound-webhook HMAC secret: present+non-empty = store; present+empty ('') = rotate off;
        // null or absent = leave untouched. Treating null as "no change" (not "clear") avoids
        // accidentally disabling HMAC when the backend sends null for an unchanged optional field.
        if (property_exists($response, 'paymentWebhookSigningSecret')) {
            $secret = $response->paymentWebhookSigningSecret;
            $this->webhookSecretStore->persist($secret === null ? null : (string) $secret);
        }

        if (isset($response->switchInfo->isChecked)) {
            $globalDefaultChecked = (bool) $response->switchInfo->isChecked;
            Configuration::updateValue('SIMPLYIN_CHECKBOX_FORCED_DISABLED', !$globalDefaultChecked);

            $merchantOverridden = (bool) Configuration::get('SIMPLYIN_SAVE_CHECKBOX_OVERRIDDEN');
            if (!$merchantOverridden && !Tools::isSubmit('submitSimplyinModule')) {
                Configuration::updateValue('SIMPLYIN_SAVE_CHECKBOX', !$globalDefaultChecked);
            }
        }

        if (isset($response->transactionHistoryLengthDays)) {
            Configuration::updateValue('SIMPLYIN_TRANS_HISTORY_LENGTH_DAY', $response->transactionHistoryLengthDays);
        }

        if (isset($response->switchInfo->lastChange)) {
            Configuration::updateValue('SIMPLYIN_SWITCH_LASTCHANGE', $response->switchInfo->lastChange);
        }

        if (isset($response->logisticsPartnersOrder->order)) {
            Configuration::updateValue(
                'SIMPLYIN_LOGISTICSPARTNERSORDER',
                (string) json_encode($response->logisticsPartnersOrder->order),
            );
        }

        if (!empty($response->docsInfo) && is_array($response->docsInfo)) {
            $docs = [];
            foreach ($response->docsInfo as $doc) {
                if (isset($doc->key, $doc->url)) {
                    $docs[$doc->key] = $doc->url;
                }
            }
            if ($docs !== []) {
                Configuration::updateValue('SIMPLYIN_DOCS_INFO', (string) json_encode($docs));
            }
        }

        self::$lastOutcome = ConnectionOutcome::SUCCESS;

        return self::$lastOutcome;
    }
}
