<?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\Payment;

use Configuration;
use Context;
use Currency;
use Module;
use Order;
use SimplyIn\Front\BrandColors;
use SimplyIn\Security\TokenService;
use Tools;

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

/**
 * Renders the surface that lets a shopper pay an unpaid order again.
 *
 * Rendered SERVER-side from a display hook the shop fires inside the order's own detail block, the
 * same shape the returning-shopper offer uses: the theme owns the placement, so there is no
 * client-side anchor hunting and nothing to re-inject. The hook is fired from the logged-in order
 * page and from guest order tracking with the same payload, so one mount serves both audiences.
 *
 * Mounted a second time on the order-confirmation page a shopper lands on right after checkout -
 * the page a failed payment actually leaves them on, rather than one they would have to navigate to
 * order history to find. Both mounts call this same {@see render()}: the decision, the token and
 * the markup are identical either way, and only `$embedded` differs - the confirmation page passes
 * `true` so this surface folds into the live status card already on that page rather than standing
 * next to it as a second one saying the same thing twice.
 *
 * It is additive by construction. It renders beside the order's own detail, never over it, and
 * every path that cannot produce a usable surface returns nothing at all rather than a broken one -
 * a shopper whose retry we cannot offer is left exactly where they were, with the order intact and
 * every normal shop path open, which is the whole point of the fail-safe.
 *
 * Which of the three checkout variants the shop runs is irrelevant here, and that is deliberate:
 * neither the order detail page nor the order-confirmation page is a checkout page. Both are
 * themed, not variant-specific, so there is no per-variant mount to get right and none to leave out.
 */
final class OrderRetryRenderer
{
    /**
     * Verdicts this surface says nothing about, because the shop's own order page already states
     * the fact and a second sentence from us would only add noise:
     *
     * - nothing recorded: every order page fires this hook, so this is the ordinary case, and
     *   speaking here would put Simply.IN on orders that never went through it;
     * - settled: the order shows as paid;
     * - closed: the order shows as cancelled or refunded. Saying nothing IS the graceful handling -
     *   what matters is that no retry control is offered and nothing errors.
     *
     * @var list<string>
     */
    private const SILENT_DECISIONS = [
        PaymentRetryPolicy::NOTHING_TO_OFFER,
        PaymentRetryPolicy::ALREADY_SETTLED,
        PaymentRetryPolicy::ORDER_CLOSED,
    ];

    public function __construct(
        private readonly PaymentMethodsProvider $methodsProvider,
        private readonly PaywallRenderer $paywallRenderer,
        private readonly TokenService $tokenService,
        private readonly PaymentLogoResolver $logoResolver,
    ) {
    }

    /**
     * `$embedded` folds this surface into the order-confirmation page's own consolidated status
     * card instead of standing it up as a second card next to it: no separate logo, no border of
     * its own - `$simplyin_retry_embedded` tells the template to leave that chrome to the card
     * already wrapping it. Order-detail calls this with the default `false`: that page has no
     * status card to embed into, so the surface still needs to be a card in its own right there.
     */
    public function render(Module $module, Order $order, bool $embedded = false): ?string
    {
        $vars = $this->computeVars($module, $order, $embedded);
        if ($vars === null) {
            return null;
        }

        $vars['simplyin_retry_embedded'] = $embedded;
        Context::getContext()->smarty->assign($vars);

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

    /**
     * The decision and every variable the template needs, computed once so every {@see render()}
     * call - embedded or not - can never drift onto two different verdicts for the same order.
     *
     * @return array<string, mixed>|null null for every {@see SILENT_DECISIONS} verdict, or when
     *                                    payments are unavailable at all - the caller renders
     *                                    nothing in either case
     */
    private function computeVars(Module $module, Order $order, bool $embedded): ?array
    {
        if (!$module->active || !Configuration::get('SIMPLYIN_PAYMENTS')) {
            return null;
        }

        $decision = OrderPaymentRetry::decisionFor($order);
        if (in_array($decision, self::SILENT_DECISIONS, true)) {
            return null;
        }

        $context = Context::getContext();
        $vars = [
            'simplyin_retry_decision' => $decision,
            'simplyin_retry_paywalls' => [],
            'simplyin_retry_unavailable' => false,
            'module_path' => _MODULE_DIR_ . $module->name . '/',
            'accent_color' => BrandColors::accent(),
            'font_color' => BrandColors::font(),
            'simplyin_retry_id_order' => 0,
            'simplyin_retry_token' => '',
        ];

        // The order-confirmation mount's live payment-status poll re-asks this same decision once it
        // learns of a definitive failure, because by then this render can already be stale (the
        // decline is often recorded a couple of seconds after this page renders). Carried only for
        // the two decisions a re-ask can ever change something for - `payment_in_progress` may still
        // become `retry_allowed`, and `retry_allowed` needs the token below regardless.
        // `attempts_exhausted` never does: the ceiling it reflects cannot un-reach itself, so it gets
        // nothing to carry.
        if (in_array($decision, [PaymentRetryPolicy::PAYMENT_IN_PROGRESS, PaymentRetryPolicy::RETRY_ALLOWED], true)) {
            $vars['simplyin_retry_id_order'] = (int) $order->id;
            $vars['simplyin_retry_token'] = $this->tokenService->getPaymentRetryToken(
                (int) $order->id,
                (string) $order->secure_key,
            );
        }

        if ($decision === PaymentRetryPolicy::RETRY_ALLOWED) {
            // Only reached for an order that may actually be paid again, which is what keeps the
            // remote method lookup and the module link off every other order page.
            //
            // The order id is passed through so a session that holds no authorization - the
            // confirmation page does not reliably run in the one the order was placed in - can fall
            // back to the authorization that order itself was placed with.
            $idOrder = (int) $order->id;
            $methods = $this->methodsProvider->getAvailableMethods($idOrder);

            // Diagnostic only, no value ever logged: saved cards are still missing on the live shop
            // even though the shopper's Simply.IN authorization now survives checkout. A boolean and
            // a count are enough to tell "no authorization was there to save to" apart from "one was
            // there but the methods call still came back without a saved card" the next time this
            // paywall is built for a failed order.
            \PrestaShopLogger::addLog(
                sprintf(
                    'SimplyIn: pay-again paywall built for order %d - Simply.IN authorization present: %s, methods returned: %d',
                    $idOrder,
                    $this->paywallRenderer->canSaveCard($idOrder) ? 'yes' : 'no',
                    count($methods),
                ),
                1,
                null,
                'Order',
                $idOrder,
            );

            $vars['simplyin_retry_paywalls'] = $this->paywallRenderer->blocks(
                $module,
                $methods,
                // The ORDER's currency, not the visitor's current one: the retry pays the order as
                // it was placed, so a shopper who has since switched the shop's currency must not
                // be offered a method the order's own currency cannot settle in.
                (string) Currency::getIsoCodeById((int) $order->id_currency),
                [],
                $idOrder,
            );
            // A transient failure fetching the payment methods empties the paywall while the verdict
            // still says the order is payable. That must not read as the definitive "we cannot take a
            // payment for this order" - it is "not right now", and a reload may well fix it.
            $vars['simplyin_retry_unavailable'] = $vars['simplyin_retry_paywalls'] === [];
            $vars['simplyin_retry_failed'] = (bool) Tools::getValue('simplyin_retry_error');
            $vars['simplyin_retry_action'] = $context->link->getModuleLink($module->name, 'paymentretry', [], true);
            // Same resolver the checkout's own payment option icon uses ({@see PaymentOptionsBuilder}),
            // fed the same methods list, so the standalone card's header (order-detail) shows the
            // same Simply.IN + operator artwork checkout does rather than the bare wordmark. Only the
            // template's non-embedded branch ever reads this - the embedded (order-confirmation) card
            // suppresses its own logo and relies on the wrapping status card's, so resolving one there
            // would be pure waste, and resolve() can reach out over HTTPS for an uncached provider logo.
            $vars['simplyin_retry_logo'] = $embedded ? null : $this->logoResolver->resolve($methods);
        }

        return $vars;
    }
}
