<?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 Context;
use Module;
use SimplyIn\Session\SimplyInSessionInterface;

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

/**
 * Renders the Simply.IN paywall: the whole pipeline, not a piece of it.
 *
 * Two surfaces show this paywall - the checkout's payment option, and the surface that re-offers
 * payment on an unpaid order. Everything about how it is built is here, so that "the paywall" is
 * one thing with one behaviour rather than two that merely started out alike: which methods are
 * excluded for the order's currency, how methods are split per provider, which variables the
 * partial receives, and the partial's own path.
 *
 * Lifting only the variable shaping was not enough. It left both callers spelling out the same four
 * steps, and it left the partial's contract as an implicit bag of Smarty globals that either caller
 * could under-fill without a word - so the next variable added to the partial would have gone
 * missing on one surface silently. That is the drift the extraction existed to prevent.
 *
 * What legitimately differs between the surfaces is passed IN, rather than each caller deciding it
 * around a shared helper: the currency the paywall is offered against (the visitor's in the
 * checkout, the order's own when re-paying an order placed earlier) and any extra variables only
 * one surface needs.
 */
final class PaywallRenderer
{
    private const TEMPLATE = 'module:simplyin/views/templates/hook/simplyin_payment_info.tpl';

    /** BLIK settles in Polish zloty only, so it is not offered against any other currency. */
    private const BLIK_CURRENCY = 'PLN';

    public function __construct(
        private readonly SimplyInSessionInterface $session,
    ) {
    }

    /**
     * One rendered paywall block per payment provider, in provider order.
     *
     * For callers that need only the markup. A caller that must also post-process each block (the
     * checkout wraps each in a payment option and needs that provider's logo) walks
     * {@see prepare()} and calls {@see renderBlock()} itself - same steps, same owner, no second
     * copy of the pipeline.
     *
     * @param array<int, object>   $methods     the methods the payment service offers this shopper
     * @param string               $currencyIso the currency the paywall is offered against
     * @param array<string, mixed> $extraVars   variables only the calling surface's partial needs
     * @param null|int             $idOrder     the order this paywall re-pays, so {@see canSaveCard()}
     *                                          can fall back to that order's own stored authorization
     *                                          when the session holds none. Omitted for the
     *                                          checkout's own paywall, which has no order yet.
     *
     * @return array<int, string> rendered blocks; empty when there is nothing payable to show
     */
    public function blocks(Module $module, array $methods, string $currencyIso, array $extraVars = [], ?int $idOrder = null): array
    {
        $blocks = [];
        foreach ($this->prepare($methods, $currencyIso) as $providerMethods) {
            $blocks[] = $this->renderBlock($module, $providerMethods, $extraVars, $idOrder);
        }

        return $blocks;
    }

    /**
     * The methods this paywall may offer, grouped into the blocks they will be rendered as.
     *
     * @param array<int, object> $methods
     *
     * @return array<int|string, array<int, object>> provider id => that provider's methods
     */
    public function prepare(array $methods, string $currencyIso): array
    {
        if ($methods === []) {
            return [];
        }

        if ($currencyIso !== self::BLIK_CURRENCY) {
            $methods = self::withoutBlik($methods);
        }

        return self::groupByProvider($methods);
    }

    /**
     * One provider's block. The partial reads Smarty globals, so the complete variable set is
     * assigned here rather than by the caller - that is what stops a surface from under-filling it.
     *
     * @param array<int, object>   $providerMethods one group from {@see prepare()}
     * @param array<string, mixed> $extraVars       variables only the calling surface needs
     * @param null|int             $idOrder         forwarded to {@see canSaveCard()}
     */
    public function renderBlock(Module $module, array $providerMethods, array $extraVars = [], ?int $idOrder = null): string
    {
        Context::getContext()->smarty->assign(
            $extraVars + self::templateVars($providerMethods, $this->canSaveCard($idOrder)),
        );

        return (string) $module->fetch(self::TEMPLATE);
    }

    /**
     * Whether saving a card may be offered: only to a shopper who holds a Simply.IN authorization,
     * because without one there is nothing to save it to. Asked here so both surfaces answer it the
     * same way - a change to how an authorization is recognized must not fix one and leave the
     * other offering a control that cannot work. {@see AuthorizationResolver} is the single answer
     * to that question, shared with {@see PaymentMethodsProvider}.
     */
    public function canSaveCard(?int $idOrder = null): bool
    {
        return AuthorizationResolver::resolve($this->session, $idOrder) !== '';
    }

    /**
     * @param array<int, object> $methods
     *
     * @return array<int, object>
     */
    public static function withoutBlik(array $methods): array
    {
        return array_values(array_filter(
            $methods,
            static fn (object $method): bool => ($method->typeName ?? null) !== 'BLIK',
        ));
    }

    /**
     * One block per provider: each carries its provider's own terms, so methods from two providers
     * cannot share one. A method with no provider is dropped - the partial reads the provider's
     * terms and logo out of it, so it could only render a block with empty links.
     *
     * @param array<int, object> $methods
     *
     * @return array<int|string, array<int, object>>
     */
    public static function groupByProvider(array $methods): array
    {
        $grouped = [];
        foreach ($methods as $method) {
            $providerId = $method->paymentProvider->id ?? null;
            if ($providerId === null) {
                continue;
            }
            $grouped[$providerId][] = $method;
        }

        return $grouped;
    }

    /**
     * The complete set of variables the partial reads, stated in one place so neither surface can
     * supply a partial set. The single-method slots hold an object or false, never a list, because
     * the partial branches on them.
     *
     * @param array<int, object> $methods one provider's methods
     *
     * @return array<string, mixed>
     */
    public static function templateVars(array $methods, bool $showSaveCard): array
    {
        $addCard = false;
        $blik = false;
        $cards = false;
        foreach ($methods as $method) {
            $type = $method->type ?? null;
            if ($type === 'add_card') {
                $addCard = $method;
            } elseif ($type === 'blik') {
                $blik = $method;
            } elseif ($type === 'card') {
                if ($cards === false) {
                    $cards = [];
                }
                $cards[] = $method;
            }
        }

        $providerLogoUrl = '';
        if ($methods !== []) {
            $first = reset($methods);
            $providerLogoUrl = (string) ($first->paymentProvider->logoUrl ?? '');
        }

        return [
            'add_card' => $addCard,
            'blik' => $blik,
            'cards' => $cards,
            'show_save_card' => $showSaveCard,
            'provider_logo_url' => $providerLogoUrl,
        ];
    }
}
