<?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
 */

use PrestaShop\PrestaShop\Core\Payment\PaymentOption;

// The module targets PHP 8.1+ (readonly properties, enums, first-class syntax). Bail out
// quietly instead of fataling mid-request on an older runtime.
if (version_compare(PHP_VERSION, '8.1.0', '<')) {
    if (defined('_PS_VERSION_') && class_exists('PrestaShopLogger')) {
        PrestaShopLogger::addLog('Simply.IN requires PHP 8.1+ (running ' . PHP_VERSION . '); module not loaded.', 3);
    }

    return;
}

$simplyinAutoload = dirname(__FILE__) . '/vendor/autoload.php';
if (file_exists($simplyinAutoload)) {
    require_once $simplyinAutoload;
}

require_once dirname(__FILE__) . '/models/SimplyInInstallSqlTrait.php';
require_once dirname(__FILE__) . '/models/SimplyInPoint.php';
require_once dirname(__FILE__) . '/models/SimplyInOrder.php';
require_once dirname(__FILE__) . '/models/SimplyInOrderSync.php';
require_once dirname(__FILE__) . '/models/SimplyInEmailCache.php';
require_once dirname(__FILE__) . '/models/SimplyInPayments.php';

if (!defined('_PS_VERSION_')) {
    exit;
}
class Simplyin extends PaymentModule
{
    /** @var \SimplyIn\Session\SimplyInSessionInterface|null */
    public $simplyin_session;

    /** @var \SimplyIn\Api\SimplyInApiClient|null */
    public $apiClient;

    /**
     * Resolved once per request in {@see \SimplyIn\Bootstrap\ModuleBootstrap::bootstrap()} from
     * the stored environment setting ({@see \SimplyIn\BackOffice\Environment}), which is the only
     * source of truth for the actual URL — this property is never read before bootstrap has run,
     * so it starts empty rather than duplicating that URL here.
     */
    public $backend_url = '';

    /** @var \SimplyIn\BackOffice\Environment|null resolved alongside {@see Simplyin::$backend_url}; null only before bootstrap has run. */
    public ?\SimplyIn\BackOffice\Environment $environment = null;

    public string $merchantApiKey = '';

    /** @deprecated since 2.5 — use {@see Simplyin::$merchantApiKey}. Kept as public BC alias, mirrored in {@see Simplyin::loadObjects()}. */
    public string $merchand_api_key = '';

    public $version_ps;

    /** @var \SimplyIn\Front\OpcDetector|null */
    public $opcDetector;

    /** @var \SimplyIn\Front\AssetRegistrar|null */
    public $assetRegistrar;

    /** @var \SimplyIn\Front\JsHeaderDataBuilder|null */
    public $jsHeaderDataBuilder;

    /** @var \SimplyIn\Security\TokenService|null */
    public $tokenService;

    /** @var \SimplyIn\Payment\PaymentStatusChecker|null */
    public $paymentStatusChecker;

    /** @var \SimplyIn\Payment\PaymentReconciliationSweep|null */
    public $paymentReconciliationSweep;

    /** @var \SimplyIn\Payment\PaymentOptionsBuilder|null */
    public $paymentOptionsBuilder;

    /** @var \SimplyIn\Payment\OrderConfirmationRenderer|null */
    public $orderConfirmationRenderer;

    /** @var \SimplyIn\Payment\OrderRetryRenderer|null */
    public $orderRetryRenderer;

    /** @var \SimplyIn\Checkout\CheckoutSessionRepository|null */
    public $checkoutSessionRepository;

    /** @var \SimplyIn\Service\OrderSync\OrderCreationHandler|null */
    public $orderCreationHandler;

    /** @var \SimplyIn\Service\MerchantSettingsSync|null */
    public $merchantSettingsSync;

    /** @var \SimplyIn\Service\TrackingNumberSync|null */
    public $trackingNumberSync;

    /** @var \SimplyIn\Carrier\CarrierConfigRegistry|null */
    public $carrierConfigRegistry;

    /** @var \SimplyIn\Session\SessionHelper|null */
    public $sessionHelper;

    /** @var \SimplyIn\BackOffice\AdminOrderDisplay|null */
    public $adminOrderDisplay;

    /** @var \SimplyIn\Payment\OrderStateUpdater|null */
    public $orderStateUpdater;

    /** @var \SimplyIn\Checkout\AccountUpdateHandler|null */
    public $accountUpdateHandler;

    /** @var \SimplyIn\BackOffice\OrdersGridDecorator|null */
    public $ordersGridDecorator;

    /** @var \SimplyIn\Service\OrderSync\OrderSyncService|null */
    public $orderSyncService;

    /** @var \SimplyIn\Auth\RsaSigner|null */
    public $rsaSigner;

    /** @var \SimplyIn\Auth\RsaRegistrar|null */
    public $rsaRegistrar;

    /**
     * Per-request memo for {@see isOneClickOfferAvailable()}; null until first decided.
     *
     * @var bool|null
     */
    private $oneClickOfferAvailable;

    public function __construct()
    {
        $this->name = 'simplyin';
        $this->tab = 'administration';
        $this->version = '8.1.45';
        $this->author = 'Simply.IN';
        $this->need_instance = 1;
        $this->bootstrap = true;
        parent::__construct();
        $this->controllers = ['ajax', 'cron', 'paymentajax', 'paymentretry', 'validation', 'webhook'];

        $this->displayName = $this->l('Simply.IN');
        $this->description = $this->l('Simply.IN Super easy and fast 1-click shopping');
        $this->confirmUninstall = $this->l('Are you sure you want to uninstall Simply.IN?');
        $this->ps_versions_compliancy = ['min' => '8.0.0', 'max' => '9.99.99'];
        // Two buckets only: the module requires PS >= 8.0.0, so the 1.6/1.7 branches were dead —
        // and worse, the old chain labelled every 8.0.1+ shop (8.1, 8.2) as "9".
        $this->version_ps = Tools::version_compare(_PS_VERSION_, '9.0.0', '>=') ? '9' : '8';
        if ($this->apiClient === null) {
            $this->loadObjects();
        }
    }

    public function install()
    {
        // A fresh, clean install can never be in the legacy-conflict state — clear any stale
        // flag so the back-office warning disappears once the merchant reinstalls cleanly.
        Configuration::deleteByName('SIMPLYIN_LEGACY_CONFLICT');

        return parent::install() && (new \SimplyIn\Install\Installer($this))->installAll();
    }

    public function uninstall()
    {
        (new \SimplyIn\Install\Uninstaller($this))->uninstallAll();

        return parent::uninstall();
    }

    public function getContent()
    {
        return (new \SimplyIn\BackOffice\ConfigurationScreen($this))->render();
    }

    public function hookActionOrderStatusPostUpdate($params)
    {
        $this->adminOrderDisplay->onOrderStatusPostUpdate($params);
    }

    public function hookDisplayAdminOrderMain($params)
    {
        return $this->adminOrderDisplay->renderOrderPanel($this, $params, __FILE__);
    }

    public function hookDisplayBackOfficeHeader()
    {
        // First: if this resolves a pending adoption, it rebuilds $this->environment,
        // $this->rsaSigner and everything else via loadObjects() — everything below must see
        // that resolved state, not the pre-resolution default, or the Test warning could be
        // skipped on the very page load that resolves to Test, and RSA registration could run
        // against the wrong backend with the wrong signer.
        $this->attemptEnvironmentAdoption();
        $this->warnAboutLegacyConflict();
        $this->warnAboutTestEnvironment();
        $this->informAboutMissingPayments();
        $this->adminOrderDisplay->registerAdminAssets($this);
        // Best-effort RSA key registration, throttled to avoid repeating the handshake on every request.
        $this->attemptRsaRegistration();
    }

    /**
     * Whether the current back-office request is this module's own configuration screen
     * (AdminModulesController rendering it via {@see getContent()}), as opposed to any other
     * back-office page. Same idiom (not the same code) as the inline check
     * {@see \SimplyIn\BackOffice\AdminOrderDisplay::registerAdminAssets()} uses to scope its own
     * asset loading.
     */
    private function isOwnConfigurationScreen(): bool
    {
        return Tools::getValue('configure') === $this->name;
    }

    /**
     * Back-office warning while the shop is connected to the Test environment — the control that
     * prevents a live shop from sitting on Test, sending buyer data to a non-production
     * environment, unnoticed. Scoped to the module's own configuration screen: there is no way to
     * dismiss it short of switching the environment in the Simply.IN settings, so showing it on
     * every back-office page would just train merchants to ignore it.
     */
    private function warnAboutTestEnvironment()
    {
        if (!$this->isOwnConfigurationScreen() || !$this->isTestEnvironment()) {
            return;
        }
        $this->context->controller->warnings[] = $this->l('Simply.IN: this shop is configured for the TEST environment, not Production. No live checkout traffic should go through Test — switch to Production in the Simply.IN settings now if this is a live shop.');
    }

    /**
     * Back-office upsell for a shop that connected Simply.IN (Test or Production) but has never
     * enabled its payment methods — the add-on's checkout upgrade is left on the table. Reuses
     * {@see \SimplyIn\BackOffice\ConfigurationKey::PAYMENTS_ENABLED} / `SIMPLYIN_PAYMENTS`, the same flag
     * {@see \SimplyIn\Payment\PaymentOptionsBuilder} gates the actual payment options on, and is
     * scoped to the module's own configuration screen the same way as
     * {@see warnAboutTestEnvironment()}.
     *
     * Queued as an `informations[]` notice, not `warnings[]`: nothing is actually at risk here (the
     * opposite of {@see warnAboutTestEnvironment()}'s live-traffic-on-Test danger), so it must not
     * share the same yellow alert box or inflate PrestaShop's "N warnings" count with a message that
     * is not a warning.
     */
    private function informAboutMissingPayments()
    {
        if (!$this->isOwnConfigurationScreen() || !Configuration::get('SIMPLYIN_CONNECTED') || Configuration::get('SIMPLYIN_PAYMENTS')) {
            return;
        }
        $this->context->controller->informations[] = $this->l("To get more from Simply.IN's 1-click checkout add-on, enable payments — at no extra cost. Configure it at merchant.simply.in.");
    }

    private function isTestEnvironment(): bool
    {
        return $this->environment === \SimplyIn\BackOffice\Environment::TEST;
    }

    /**
     * Keeps warning in the back office while a legacy PrestaShop 1.7 Simply.IN install is still
     * present. The flag is set by upgrade-8.1.0.php when an in-place upgrade from that line is
     * refused, and cleared on a clean (re)install. Non-fatal — a persistent warning banner only.
     */
    private function warnAboutLegacyConflict()
    {
        $legacyVersion = (string) Configuration::get('SIMPLYIN_LEGACY_CONFLICT');
        if ($legacyVersion === '') {
            return;
        }
        $this->context->controller->warnings[] = sprintf(
            $this->l('Simply.IN: an older PrestaShop 1.7 version of this module (v%s) was detected and its in-place upgrade was blocked. That version is not compatible with PrestaShop 8/9+. Please UNINSTALL it completely (including its data), then install the dedicated "Simply.IN for PrestaShop 8/9+" module on a clean slate. Checkout will not work correctly until this is resolved.'),
            $legacyVersion
        );
    }

    /**
     * Throttled RSA key-registration attempt; no-op once already registered for the current API key.
     */
    private function attemptRsaRegistration(int $throttleSeconds = 300)
    {
        if ($this->rsaRegistrar === null || $this->merchantApiKey === '') {
            return;
        }
        $last = (int) Configuration::get(\SimplyIn\Auth\RsaRegistrar::LAST_REGISTER_KEY);
        if ($last > 0 && (time() - $last) < $throttleSeconds) {
            return;
        }
        Configuration::updateValue(\SimplyIn\Auth\RsaRegistrar::LAST_REGISTER_KEY, (string) time());
        $this->rsaRegistrar->ensureRegistered();
    }

    /**
     * Throttled retry of the one-off environment adoption (see upgrade/upgrade-8.1.24.php): a
     * shop upgrading from the old per-environment build with a legacy key and no environment
     * setting yet gets one attempt at upgrade time, but that attempt's outcome cannot reliably
     * make PrestaShop re-run it later (see the upgrade file's own docblock) — so this admin-only,
     * throttled backstop is the actual retry mechanism for the rare case where the one-off attempt
     * could not get a definitive answer from either environment. No-op once resolved, or when
     * there is nothing to adopt (a fresh install always has an explicit environment already).
     */
    private function attemptEnvironmentAdoption(int $throttleSeconds = 300)
    {
        $config = new \SimplyIn\BackOffice\ConfigurationRepository();
        if (!$config->isEnvironmentAdoptionPending()) {
            return;
        }
        $last = (int) Configuration::get(\SimplyIn\Install\EnvironmentAdopter::LAST_ATTEMPT_KEY);
        if ($last > 0 && (time() - $last) < $throttleSeconds) {
            return;
        }
        Configuration::updateValue(\SimplyIn\Install\EnvironmentAdopter::LAST_ATTEMPT_KEY, (string) time());

        $resolved = (new \SimplyIn\Install\EnvironmentAdopter($config))->adopt(
            (string) Configuration::get('SIMPLYIN_SECRET_KEY'),
            (string) $this->context->shop->getBaseURL(true),
            (string) $this->version,
            (string) Configuration::get('PS_SHOP_NAME'),
        );
        if ($resolved) {
            // Everything resolved at bootstrap used the pre-adoption default state — rebuild it
            // now so the rest of THIS request (and the page this hook is rendering into) sees the
            // just-adopted environment/key instead of stale bootstrap-time values.
            $this->loadObjects();
        }
    }

    /**
     * Activates RSA request signing immediately for a freshly validated API key, instead of
     * waiting for the periodic back-office/cron check to pick it up.
     */
    public function registerRsaForApiKey($apiKey): \SimplyIn\Api\ConnectionOutcome
    {
        $apiKey = (string) $apiKey;
        if ($apiKey === '') {
            return \SimplyIn\Api\ConnectionOutcome::AUTH_REJECTED;
        }
        if ($this->rsaSigner === null) {
            return \SimplyIn\Api\ConnectionOutcome::TECHNICAL_FAILURE;
        }
        $registrar = new \SimplyIn\Auth\RsaRegistrar(
            $this->rsaSigner,
            rtrim((string) $this->backend_url, '/') . '/',
            $apiKey,
            (string) $this->context->shop->getBaseURL(true),
        );
        // Mark the throttle so this same request cycle does not repeat the handshake right after
        // we have just performed it.
        Configuration::updateValue(\SimplyIn\Auth\RsaRegistrar::LAST_REGISTER_KEY, (string) time());

        if ($registrar->ensureRegistered()) {
            return \SimplyIn\Api\ConnectionOutcome::SUCCESS;
        }

        if ($registrar->wasAlreadyRegistered()) {
            return \SimplyIn\Api\ConnectionOutcome::ALREADY_REGISTERED;
        }

        return $registrar->lastAttemptWasAuthRejected()
            ? \SimplyIn\Api\ConnectionOutcome::AUTH_REJECTED
            : \SimplyIn\Api\ConnectionOutcome::TECHNICAL_FAILURE;
    }

    /**
     * @param array<string, mixed> $params
     */
    public function hookActionOrderGridDefinitionModifier(array $params): void
    {
        $this->ordersGridDecorator->modifyDefinition($params);
    }

    /**
     * @param array<string, mixed> $params
     */
    public function hookActionOrderGridQueryBuilderModifier(array $params): void
    {
        $this->ordersGridDecorator->modifyQuery($params);
    }

    public function checkController()
    {
        return $this->opcDetector->isCheckoutController();
    }

    public function getOPC()
    {
        return $this->opcDetector->detect();
    }

    public function hookDisplayHeader()
    {
        if (!Configuration::get('SIMPLYIN_CONNECTED')) {
            return;
        }
        if ($this->opcDetector->isOrderDetailController()) {
            // A deliberately small payload rather than the checkout one: an order page has no
            // checkout state to describe, and the full header data carries the shopper's e-mail and
            // the shop's whole country table, neither of which belongs on an order page.
            if (Configuration::get('SIMPLYIN_PAYMENTS')) {
                Media::addJsDef($this->jsHeaderDataBuilder->buildPaymentRetryData($this));
            }

            return;
        }
        if (!$this->opcDetector->isCheckoutController()) {
            return;
        }

        Media::addJsDef($this->getJsDefaultHeader());
    }

    public function hookActionFrontControllerSetMedia()
    {
        if (!Configuration::get('SIMPLYIN_CONNECTED')) {
            return;
        }
        if (!$this->opcDetector->isCheckoutController() && !$this->opcDetector->isOrderDetailController()) {
            return;
        }

        $this->assetRegistrar->registerHeaderAssets(
            $this->context->controller,
            $this->name,
            (string) $this->version,
        );
    }

    public function hookDisplayPaymentTop()
    {
        if (!Configuration::get('SIMPLYIN_CONNECTED')) {
            return '';
        }

        return $this->testEnvironmentWarningHtml()
            . $this->oneClickOfferHtml()
            . '<div id="simplyin-phone-prerender" style="display:none">'
            . (new \SimplyIn\Front\NewAccountFormRenderer($this->context, $this))->render((string) $this->getOPC())
            . '</div>';
    }

    /**
     * Cart-page counterpart to the offer rendered by {@see hookDisplayPaymentTop()}. The shopper
     * this offer exists for is already shop-authenticated when they reach the cart, so the cart is
     * the earliest place it can help them.
     *
     * Rendered from `displayExpressCheckout`, the theme's own extension point right after the
     * "Proceed to checkout" button inside the order-summary card (`_partials/cart-detailed-actions.tpl`)
     * — so the offer sits under the button instead of full-width below the whole cart-grid row.
     */
    public function hookDisplayExpressCheckout()
    {
        if (!Configuration::get('SIMPLYIN_CONNECTED')) {
            return '';
        }

        return $this->oneClickOfferHtml();
    }

    /**
     * Compatibility shim: an already-installed shop keeps whatever hooks it was registered with
     * (see {@see \SimplyIn\Install\HookRegistry}), so removing this method outright would silently
     * drop the cart badge on every such shop until the cut-time upgrade script unregisters this
     * hook and registers `displayExpressCheckout` in its place. Keeps the old hook rendering the
     * exact same offer in the meantime. Safe to delete once that migration has shipped and is
     * confirmed on all shops.
     */
    public function hookDisplayShoppingCartFooter()
    {
        return $this->hookDisplayExpressCheckout();
    }

    /**
     * Address-step counterpart to the offer above: a shopper who signs into the SHOP with its own
     * password skips the e-mail-recognition step entirely and lands straight on address selection,
     * so this hook - the theme's own extension point at the bottom of that step - is the earliest
     * place on that path the offer can still reach them.
     */
    public function hookDisplayAddressSelectorBottom()
    {
        if (!Configuration::get('SIMPLYIN_CONNECTED')) {
            return '';
        }

        return $this->oneClickOfferHtml();
    }

    /**
     * The returning-shopper 1-click offer, or '' when it must not be shown.
     *
     * Rendered SERVER-side, from a display hook, deliberately: the theme owns the placement, so
     * there is no client-side anchor hunting, no re-injection and no injection-idempotency
     * machinery. It is also the shape that ports to the sibling plugins' own layout/hook seams
     * instead of fighting their client-side re-renders.
     *
     * The cart-page copy (`displayExpressCheckout`, inside `_partials/cart-detailed-actions.tpl`)
     * DOES get destroyed and re-rendered on every AJAX quantity update — the whole `cart-summary`
     * card is replaced wholesale by the theme's own cart-refresh JS. That is safe by the same
     * property that lets it render at multiple hooks on one page load (see below): the replacement
     * is a fresh, self-contained copy, front.js re-binds nothing per-instance (one document-delegated
     * click handler), and the old node is simply gone, not left behind as a stale duplicate. The
     * classic checkout step hooks, by contrast, render once per page load and only move the
     * `-current` class.
     *
     * A shopper with no linked Simply.IN account gets nothing rendered at all, rather than a
     * control that could only open an empty popup.
     *
     * Deliberately renders at every hook that calls it, even the address-step and payment-step
     * hooks that can both fire on the same classic-checkout page load: the card carries no id, only
     * classes, and its JS (front.js) only ever acts on `.simplyin-one-click*` class selectors with a
     * single document-delegated click handler, so N copies in the DOM behave exactly like one - each
     * hides/removes/disables all of them together, and only the copy on the step CSS actually shows
     * (via `-current`) is ever visible to the shopper. Suppressing all but the first hook to fire
     * would risk suppressing the only one that ends up visible, e.g. for a returning shopper with a
     * saved address who is auto-advanced past the (now hidden) address step straight to payment.
     */
    private function oneClickOfferHtml(): string
    {
        if (!$this->isOneClickOfferAvailable()) {
            return '';
        }

        $this->context->smarty->assign('module_path', $this->getPathUri());

        return $this->fetch('module:simplyin/views/templates/hook/one_click_offer.tpl');
    }

    /**
     * Memoized per request. The decision genuinely cannot change within one request: its inputs
     * are the shop customer, the account-recognition lookup (deterministic and DB-cached per
     * request) and the Simply.IN auth token, which is only ever written by an AJAX handler, never
     * during a page render. Keeps the decision cheap to re-ask across the up to three hooks that can
     * call it in a single request. {@see \SimplyIn\Front\OneClickOffer::isAvailable()} holds the
     * rule itself.
     */
    private function isOneClickOfferAvailable(): bool
    {
        if ($this->oneClickOfferAvailable === null) {
            $customer = $this->context->customer ?? null;
            // Trimmed here so the same notion of "has a usable e-mail" drives both the lookup and
            // the predicate: a whitespace-only address passes !empty() but is rejected by
            // OneClickOffer::isAvailable(), which would spend a DB lookup to reach a decision
            // already known.
            $email = $customer ? trim((string) $customer->email) : '';

            $this->oneClickOfferAvailable = \SimplyIn\Front\OneClickOffer::isAvailable(
                $customer,
                $email !== '' && \SimplyInEmailCache::checkUserExist($email),
                trim((string) $this->simplyin_session->get('simplyin_auth_token', '')) !== ''
            );
        }

        return $this->oneClickOfferAvailable;
    }

    /**
     * Checkout-visible counterpart to {@see warnAboutTestEnvironment()} — the control that
     * prevents a live shop from sitting on the Test environment unnoticed (requirement: visible
     * both in the shop admin and in the checkout). Plain, static markup with no script controlling
     * its visibility, so there is nothing for a buyer or merchant to dismiss.
     */
    private function testEnvironmentWarningHtml(): string
    {
        if (!$this->isTestEnvironment()) {
            return '';
        }

        return '<div style="background:#b91c1c;color:#fff;text-align:center;padding:10px 16px;font:600 14px/1.4 -apple-system,BlinkMacSystemFont,\'Segoe UI\',Roboto,sans-serif;">'
            . $this->l('Simply.IN: this checkout is connected to the TEST environment, not Production. Real orders should never go through Test — switch to Production in the Simply.IN settings if this is a live shop.')
            . '</div>';
    }

    public function hookActionModuleRegisterCronJobs()
    {
        return [
            [
                'task' => 'cron',
                'module' => $this->name,
                'freq' => '*/15 * * * *',
            ],
        ];
    }

    public function getAllAvailableShippingMethods()
    {
        return $this->carrierConfigRegistry->getAllAvailableShippingMethods((int) $this->context->language->id);
    }

    public function getAsyncToken()
    {
        return $this->tokenService->getAsyncToken(_COOKIE_IV_);
    }

    public function getAvailableCarriers()
    {
        return $this->carrierConfigRegistry->getAvailableCarriers();
    }

    public function getCarriersKey()
    {
        return $this->carrierConfigRegistry->getCarrierKeys();
    }

    public function getJsDefaultHeader($override = [])
    {
        return $this->jsHeaderDataBuilder->build($this, $override);
    }

    public function checkPaymentAjax()
    {
        return $this->paymentStatusChecker->check($this);
    }

    public function hookActionAdminOrdersTrackingNumberUpdate($params)
    {
        $this->trackingNumberSync->handle($params);
    }

    public function hookActionObjectOrderCarrierUpdateAfter($params)
    {
        if ($this->trackingNumberSync !== null) {
            $this->trackingNumberSync->handleOrderCarrier($params);
        }
    }

    public function getBackendUrl()
    {
        return $this->backend_url;
    }

    /**
     * Applies an environment selection to this request: the environment, the backend URL and the
     * request signer's storage scope always change together, so this is the one place that does
     * it, instead of every caller having to remember all three. Called both at bootstrap and when
     * the settings screen switches environment mid-request — a signer left bound to the previous
     * environment would otherwise persist its credential into that environment's slot.
     */
    public function setEnvironment(\SimplyIn\BackOffice\Environment $environment): void
    {
        $this->environment = $environment;
        $this->backend_url = $environment->backendUrl();
        $this->rsaSigner = new \SimplyIn\Auth\RsaSigner(
            new \SimplyIn\Auth\SecretCipher(_COOKIE_KEY_),
            $environment->rsaSlotSuffix(),
        );
    }

    public function simplyInUpdateMerchantSettings($new_api_key = '', $admin = false): \SimplyIn\Api\ConnectionOutcome
    {
        return $this->merchantSettingsSync->sync((string) $new_api_key, (bool) $admin);
    }

    public function hookActionValidateOrder($params)
    {
        $this->orderCreationHandler->handle($params);
    }

    public function checkUserExist($user_email)
    {
        return SimplyInEmailCache::checkUserExist($user_email);
    }

    public function getCronSecureKey()
    {
        return $this->tokenService->getCronSecureKey();
    }

    public function execCron()
    {

        // The cron key derivation changed in 8.1.20; keep accepting the previous one so an
        // already-scheduled crontab entry does not silently stop working after the upgrade.
        $providedKey = (string) Tools::getValue('secure_key');
        if (!hash_equals((string) $this->getCronSecureKey(), $providedKey)
            && !hash_equals($this->tokenService->getLegacyCronSecureKey(), $providedKey)
        ) {
            return false;
        }
        // Zero-touch RSA registration backstop + webhook-dedup housekeeping.
        if ($this->rsaRegistrar !== null) {
            $this->rsaRegistrar->ensureRegistered();
        }
        (new \SimplyIn\Webhook\WebhookDeduplicator())->prune();

        $this->orderSyncService->refreshPending();
        $this->orderSyncService->synchronizePending();

        // An attempt whose outcome we never learned is left honestly unresolved
        // (AWAITING_CONFIRMATION) rather than guessed at; this is what keeps such an attempt from
        // sitting unresolved forever if the webhook that would settle it never arrives.
        $this->paymentReconciliationSweep->run();

        return true;
    }

    public function hookActionCarrierUpdate($params)
    {
        $carrier = $params['carrier'] ?? null;
        if ($carrier === null) {
            return;
        }
        $this->carrierConfigRegistry->handleCarrierUpdate((int) $carrier->id);
    }

    public function getCartCarrierCost()
    {
        return $this->carrierConfigRegistry->getCartCarrierCost($this->context);
    }

    public function getAjaxToken()
    {
        return $this->tokenService->getAjaxToken();
    }

    /**
     * Found while closing the payment retry loop: this used to substitute the CURRENT context cart
     * for whatever id a caller explicitly passed, on the mistaken belief that "no id" and "context
     * cart" were the same request. They are not, and every caller already passes exactly what it
     * means: a real id_cart when the token must be bound to a specific cart (the order-confirmation
     * poll, minted and checked from that same real id on both ends - see {@see
     * \SimplyIn\Front\JsHeaderDataBuilder::applyOrderConfirmationOverride()} and {@see
     * \SimplyIn\Payment\PaymentStatusChecker::tokensValid()}), or nothing at all when the token is
     * meant to be cart-AGNOSTIC (the checkout's own token, and the retry surface's - see {@see
     * \SimplyIn\Front\JsHeaderDataBuilder::buildPaymentRetryData()}, which mints with no cart on
     * purpose). Substituting context->cart on the no-id path broke that second contract - it made a
     * token meant to prove nothing about any cart start binding itself to whatever cart the CURRENT
     * request happened to carry, which almost never matches on the order-confirmation page (that
     * cart is already spent by the time this runs). No substitution is the correct behaviour either
     * way: this is a thin, stateless pass-through, and the one place that wants "the current cart's
     * id" already resolves and passes it explicitly (see applyPaymentsChannel() below).
     */
    public function getCartToken($id_cart = false)
    {
        return $this->tokenService->getCartToken($id_cart);
    }

    public function getCustomerEmail()
    {
        return $this->sessionHelper->getCustomerEmail();
    }

    public function hookActionCustomerAccountUpdate($params)
    {
        $this->accountUpdateHandler->handle($params);
    }

    public function getCheckoutSessionData()
    {
        return $this->checkoutSessionRepository->getCheckoutSessionData((int) $this->context->cart->id);
    }

    public function upgradeSessionData($data, $redirect = true)
    {
        $this->checkoutSessionRepository->upgradeSessionData($this->context, $redirect);
    }

    public function hookActionCustomerLogoutBefore($params)
    {
        $customer = $params['customer'] ?? null;
        if ($customer instanceof Customer && !$customer->is_guest) {
            $this->logout();
        }
    }

    public function logout()
    {
        $this->sessionHelper->logout();
    }

    public function getOrderToken($id_cart)
    {
        return $this->tokenService->getOrderToken($id_cart);
    }

    public function getNewAccountPhone()
    {
        return $this->sessionHelper->getNewAccountPhone();
    }

    public function isSimplyInAuth()
    {
        return $this->sessionHelper->isAuthenticated();
    }

    public function hookPaymentOptions($params)
    {
        return $this->paymentOptionsBuilder->build($this, $params['cart']);
    }

    /**
     * Fired by PrestaShop right after checkout, on the page the shopper actually lands on. Renders
     * ONE consolidated card: the live payment-status widget with the pay-again surface folded into
     * its own error state ({@see OrderConfirmationRenderer::render()}, which embeds
     * {@see OrderRetryRenderer::render()} itself) rather than the two separate cards an
     * earlier change mounted side by side - a shopper whose payment just failed can still retry
     * right here, without navigating to their order history, but is now told so once, not twice.
     *
     * A missing or unusable payload returns nothing at all, for the same reason as below: this hook
     * belongs to the shop, and a theme or another module calling it differently must not be able to
     * break the page it renders on.
     */
    public function hookDisplayOrderConfirmation($params)
    {
        $order = $this->resolveHookOrder($params);
        if ($order === null) {
            return '';
        }

        return (string) $this->orderConfirmationRenderer->render($this, $order);
    }

    /**
     * Fired by PrestaShop inside the order's own detail block, from the customer's order page and
     * from guest order tracking alike, with the same ['order' => Order] payload.
     *
     * A missing or unusable payload returns nothing at all: this hook belongs to the shop, so a
     * theme or another module calling it differently must not be able to break the page it renders
     * on. The same goes for the renderer's own decisions - every one of them returns null rather
     * than a partial surface.
     */
    public function hookDisplayOrderDetail($params)
    {
        $order = $this->resolveHookOrder($params);
        if ($order === null) {
            return '';
        }

        return (string) $this->orderRetryRenderer->render($this, $order);
    }

    /**
     * The ['order' => Order] payload both display hooks above share, validated once: a theme or
     * another module firing either hook with something missing or unloaded must not be able to
     * fatal the page it renders on.
     */
    private function resolveHookOrder(array $params): ?Order
    {
        $order = $params['order'] ?? null;
        if (!($order instanceof Order) || !Validate::isLoadedObject($order)) {
            return null;
        }

        return $order;
    }

    public function getPaymentCheckToken($ajax_token, $cart_token)
    {
        return $this->tokenService->getPaymentCheckToken((string) $ajax_token, (string) $cart_token);
    }

    public function getPaymentRetryToken($id_order, $secure_key)
    {
        return $this->tokenService->getPaymentRetryToken((int) $id_order, (string) $secure_key);
    }

    public function upgradeModule($version)
    {
        return (new \SimplyIn\Install\UpgradeRunner())->run($this, (string) $version);
    }

    public function updateOrderState($order, $new_state, $callback)
    {
        $this->orderStateUpdater->update($order, (int) $new_state, (string) $callback);
    }

    public function loadObjects()
    {
        (new \SimplyIn\Bootstrap\ModuleBootstrap($this))->bootstrap();
    }
    // Do not remove translations for objects
    // $this->l('blik');
    // $this->l('card');
    // $this->l('visa');
    // $this->l('mastercard');
    // $this->l('amex');
    // $this->l('discover');
    // $this->l('diners');
}
