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

use Configuration;
use Context;
use Country;
use Module;
use SimplyIn\BackOffice\ConfigurationKey;
use SimplyIn\BackOffice\ConfigurationRepository;
use SimplyIn\BackOffice\SaveCheckboxState;
use SimplyIn\Security\TokenService;
use SimplyIn\Session\SimplyInSessionInterface;
use SimplyInEmailCache;
use SimplyInPayments;
use SimplyInPoint;
use Tools;

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

final class JsHeaderDataBuilder
{
    /** Value of the front-end `surface` field on a page that renders the pay-again surface. */
    public const SURFACE_ORDER_RETRY = 'order-retry';

    private OpcDetector $opcDetector;

    private TokenService $tokenService;

    private SimplyInSessionInterface $session;

    private ConfigurationRepository $config;

    public function __construct(
        OpcDetector $opcDetector,
        TokenService $tokenService,
        SimplyInSessionInterface $session,
        ConfigurationRepository $config,
    ) {
        $this->opcDetector = $opcDetector;
        $this->tokenService = $tokenService;
        $this->session = $session;
        $this->config = $config;
    }

    /**
     * @param array<string, mixed> $override
     *
     * @return array<string, mixed>
     */
    public function build(Module $module, array $override = []): array
    {
        $context = Context::getContext();
        $customer = $context->customer ?? null;

        $hidePhoneField = false;
        $newAccountPhone = false;
        if ($customer && !empty($customer->email)) {
            $hidePhoneField = SimplyInEmailCache::checkUserExist($customer->email);
            $newAccountPhone = $this->resolveNewAccountPhone();
        }

        $point = ($context->cart && $context->cart->id)
            ? SimplyInPoint::getByIdCart((int) $context->cart->id)
            : new SimplyInPoint();

        $idAddressDelivery = $this->session->get('simplyin_id_address_delivery', false);
        $authToken = (string) $this->session->get('simplyin_auth_token', '');
        $apiLogged = trim($authToken) !== '';
        $idCarrier = (int) ($context->cart->id_carrier ?? 0);
        $checkout = $this->opcDetector->detect();
        $needLogin = $hidePhoneField && !$apiLogged;
        $controller = (string) Tools::getValue('controller');

        $return = [
            'simplyin' => [
                'checkout' => $checkout,
                'controller' => $controller,
                // Where the OneClickOffer force_login pivot (cart page) sends the shopper once the
                // native shop login completes: that pivot is never itself on the
                // checkout controller, so front.js cannot just reload() the current page like it does
                // for a login started on the checkout page. front.js never reads this on the checkout
                // controller itself, so skip the URL lookup there.
                'checkout_url' => $controller !== 'order' ? $context->link->getPageLink('order') : null,
                'ajax_token' => $this->tokenService->getAjaxToken(),
                'cart_token' => (string) $module->getCartToken(),
                'files_url' => _MODULE_DIR_ . $module->name . '/',
                'id_carrier' => $idCarrier,
                // Bridges the checkout address form's numeric id_country (PrestaShop's
                // internal country id, all the ".js-country" select carries) to the
                // ISO alpha-2 code the phone widget's own country dropdown is keyed
                // on, so the widget can track the selected address country.
                'country_iso_by_id' => $this->buildCountryIsoById(),
                'controllers' => [
                    'ajax' => $context->link->getModuleLink('simplyin', 'ajax'),
                    'payment_ajax' => $context->link->getModuleLink('simplyin', 'paymentajax'),
                ],
                'customer_email' => $this->resolveCustomerEmail(),
                'customer' => [
                    'need_login' => $needLogin,
                    'has_selected_address' => (bool) $idAddressDelivery,
                    'is_guest' => $customer ? $customer->isGuest() : false,
                    'logged' => $customer ? $customer->isLogged() : false,
                    'has_token' => $apiLogged,
                    'email' => $customer ? $customer->email : '',
                    'point' => [
                        'label' => $point->label,
                        'locker_id' => $point->lockerId,
                        'address' => $point->address,
                        'address_name' => $point->addressName,
                    ],
                ],
                'currentLanguage' => $context->language,
                'display' => [
                    'bar' => (bool) Configuration::get('SIMPLYIN_BAR_ENABLED'),
                    'bar_color' => (bool) Configuration::get('SIMPLYIN_BAR_COLOR'),
                    'badge_color' => (bool) Configuration::get('SIMPLYIN_BADGE_COLOR'),
                    'accent_color' => BrandColors::accent(),
                    'font_color' => BrandColors::font(),
                ],
                'labels' => $this->buildLabels($module),
                'hide_phone_field' => $hidePhoneField,
                'new_account_phone' => $newAccountPhone,
                'save_checkbox' => SaveCheckboxState::isCheckedByDefault(),
                'select_customer_privacy' => false,
                'select_psgdpr' => false,
                'submit_guest_form' => false,
            ],
        ];

        if ($controller === 'orderconfirmation') {
            $return = $this->applyOrderConfirmationOverride($module, $return, $checkout);
        }

        if (Configuration::get('SIMPLYIN_PAYMENTS')) {
            $return = $this->applyPaymentsChannel($module, $return);
        }

        if (!empty($override)) {
            $return = array_merge_recursive($return, $override);
        }

        return $return;
    }

    /**
     * The header data for a page that renders the pay-again surface.
     *
     * Only what the paywall's own bundle reads: which surface this is, the two tokens its
     * method-selection and BLIK requests are authenticated with, the endpoint they go to, and the
     * labels its client-side messages come from. Everything the checkout payload carries beyond that
     * describes checkout state an order page does not have - and some of it (the shopper's e-mail,
     * the shop's country table) has no reason to be emitted on an order page at all.
     *
     * The surface is DECLARED here rather than left for the bundle to infer from the markup. The
     * server already knows which surface it is rendering; making the bundle re-derive that from a
     * CSS class would mean a template rename silently disabling the page's behaviour.
     *
     * The cart token is minted with no cart, exactly as the checkout payload mints it and exactly as
     * the receiving endpoint verifies it - those requests carry no cart id, so the token they are
     * checked against is the cart-agnostic one. An order page has no cart, and this token was never
     * about one. Taken from the token service directly rather than through the module, which only
     * forwards to it and would substitute the context cart if it were given one.
     *
     * Named `retry_*` rather than reusing `ajax_token`/`cart_token`: on the order-confirmation page
     * this payload is merged alongside the checkout-shaped one {@see applyOrderConfirmationOverride}
     * builds for the live payment-status poll, which needs the CART-SPECIFIC pair that poll is
     * already verified against. Reusing the plain names there would make one pair overwrite the
     * other in the single `window.simplyin` object client-side, breaking whichever surface lost.
     *
     * @return array<string, mixed>
     */
    public function buildPaymentRetryData(Module $module): array
    {
        return [
            'simplyin' => [
                'surface' => self::SURFACE_ORDER_RETRY,
                'retry_ajax_token' => $this->tokenService->getAjaxToken(),
                'retry_cart_token' => $this->tokenService->getCartToken(),
                'controllers' => [
                    'payment_ajax' => Context::getContext()->link->getModuleLink('simplyin', 'paymentajax'),
                ],
                'labels' => $this->buildPaywallLabels($module),
            ],
        ];
    }

    /**
     * The labels the paywall bundle actually reads.
     *
     * A subset, not the checkout map: the full map is mostly checkout-flow copy the paywall never
     * asks for, and every entry costs a translation lookup and inline bytes on the page. Keep this
     * list in step with the l() calls in the paywall bundle - anything missing degrades to the
     * untranslated source string rather than breaking.
     *
     * @return array<string, string>
     */
    private function buildPaywallLabels(Module $module): array
    {
        $labels = $this->buildLabels($module);

        return array_intersect_key($labels, array_flip([
            'By paying, I accept the terms and [conditions] and [privacy policy] of %s',
            'Please select a payment method',
            'BLIK code rejected',
            'Invalid BLIK code',
            'Invalid tokenization response',
            'Problem processing card',
            'Problem with BLIK processing',
        ]));
    }

    /**
     * @return array<string, string>
     */
    private function buildLabels(Module $module): array
    {
        return [
            'By paying, I accept the terms and [conditions] and [privacy policy] of %s' => $module->l('By paying, I accept the terms and [conditions] and [privacy policy] of %s'),
            'Please select a payment method' => $module->l('Please select a payment method'),
            'BLIK code rejected' => $module->l('BLIK code rejected'),
            'bar_caption' => $module->l('Here you complete your purchase with'),
            'cancel' => $module->l('Cancel'),
            'change' => $module->l('Change'),
            'code_invalid' => $module->l('Invalid code'),
            'confirm_delete' => $module->l('Confirm delete address?'),
            'confirm_profile' => $module->l('Confirm profile'),
            'data_completed' => $module->l('Data completed'),
            'delete' => $module->l('Delete'),
            'delete_header' => $module->l('Delete address'),
            'code_consumed' => $module->l('Something went wrong finishing sign-in. Please request a new code.'),
            'edit' => $module->l('Edit'),
            'generic_error' => $module->l('Something went wrong. Please try again.'),
            'Invalid BLIK code' => $module->l('Invalid BLIK code'),
            'Invalid tokenization response' => $module->l('Invalid tokenization response'),
            'Problem processing card' => $module->l('Problem processing card'),
            'Problem with BLIK processing' => $module->l('Problem with BLIK processing'),
            // The confirmation-heading override's own copy, rather than PrestaShop core's theme
            // translation catalog: the merchant's theme owns "Your order is confirmed" and this
            // module has no business rewriting a translation key it does not ship.
            'Order placed' => $module->l('Order placed'),
            'Order confirmed' => $module->l('Order confirmed'),
            // The live payment-status poll's narration (core.js, setupOrderStatusCheck). The two
            // waiting-state messages are looked up by status rather than compared against it, so
            // they carry no tpl-side twin - see PaymentPollerStatusContractTest for why the source
            // string, not the status name, is what may appear here.
            'Waiting for the bank to confirm your payment...' => $module->l('Waiting for the bank to confirm your payment...'),
            'Confirm the payment in your banking app to finish.' => $module->l('Confirm the payment in your banking app to finish.'),
            // The definitive-failure reason, one entry per reason this plugin has safe, non-fraud
            // shopper copy for (payment_return.tpl's own {if}/{elseif} chain renders the same four on
            // first load; kept in step with core.js's REJECT_REASON_MESSAGES). Every other reason -
            // known to SIN.CORE or not - degrades to the generic sentence below.
            'This order has not been paid.' => $module->l('This order has not been paid.'),
            'The BLIK code you entered was incorrect.' => $module->l('The BLIK code you entered was incorrect.'),
            'Your bank did not confirm the card.' => $module->l('Your bank did not confirm the card.'),
            'This payment exceeds an allowed transaction limit.' => $module->l('This payment exceeds an allowed transaction limit.'),
            'The payment took too long and timed out.' => $module->l('The payment took too long and timed out.'),
        ];
    }

    /**
     * @return array<int, string>
     */
    private function buildCountryIsoById(): array
    {
        $idLang = (int) Context::getContext()->language->id;
        $map = [];
        foreach (Country::getCountries($idLang, true) as $country) {
            $map[(int) $country['id_country']] = (string) $country['iso_code'];
        }

        return $map;
    }

    /**
     * `$return['simplyin']['controller']` reads 'orderconfirmation' by the time this returns,
     * whichever branch below ran: build() sets it in the base array before ever calling this method,
     * this method's only caller gates on that exact value, and the $idModule-matched branch below
     * only replaces the array wholesale for a SIMPLYIN order - it does not clear the key for every
     * other case. Restated here because the dispatcher in core.js keys the order-confirmation wiring
     * off this field, so a reader checking that it holds needs it spelled out at the point that
     * matters, not re-derived from build()'s scope two calls away.
     *
     * @param array<string, mixed> $return
     *
     * @return array<string, mixed>
     */
    private function applyOrderConfirmationOverride(Module $module, array $return, string $checkout): array
    {
        $ajaxSend = true;
        $idCart = (int) Tools::getValue('id_cart');
        $idModule = (int) Tools::getValue('id_module');

        $ajaxToken = (string) $module->getAjaxToken();
        $cartToken = (string) $module->getCartToken($idCart);
        $paymentCheckToken = (string) $module->getPaymentCheckToken($ajaxToken, $cartToken);

        if ($idModule) {
            $paymentInfo = SimplyInPayments::getPaymentInfo($idCart);
            $status = $paymentInfo ? $paymentInfo->status : null;

            $instance = Module::getInstanceById($idModule);
            if ($instance && $instance->name === 'simplyin') {
                $ajaxSend = false;
                $return['simplyin'] = [
                    'controller' => 'orderconfirmation',
                    'checkout' => $checkout,
                    'order_sended' => true,
                    'controllers' => [
                        'payment_ajax' => Context::getContext()->link->getModuleLink('simplyin', 'paymentajax'),
                    ],
                    'files_url' => _MODULE_DIR_ . $module->name . '/',
                    'id_cart' => $idCart,
                    'id_module' => $idModule,
                    'key' => Tools::getValue('key'),
                    'ajax_token' => $ajaxToken,
                    'cart_token' => $cartToken,
                    'payment_check_token' => $paymentCheckToken,
                    'payment_status' => $status,
                    // Merchant-toggleable, default ON: only ever read on a confirmed
                    // Simply.IN order (this branch), never on a page reached with a stale/foreign
                    // id_module - core.js must not touch a theme heading for an order that is not
                    // ours to speak about.
                    'confirmation_heading_override' => $this->config->getBool(
                        ConfigurationKey::CONFIRMATION_HEADING_OVERRIDE,
                        true,
                    ),
                ];
            }
        }

        if ($idCart && $ajaxSend && $this->isSimplyInAuthenticated()) {
            $return['simplyin']['order'] = [
                'id_cart' => $idCart,
                'order_token' => $this->tokenService->getOrderToken($idCart),
            ];
        }

        // The pay-again surface {@see OrderRetryRenderer} may render on this same page - gated the
        // SAME way that renderer gates itself ($module->active && SIMPLYIN_PAYMENTS), independently
        // of the $idModule branch above. That branch answers "did id_module in the URL name this
        // module", which the renderer's own decision never asks - it reads the Order alone - so a
        // confirmation page reached with no or a stale id_module (a case the $ajaxSend fallback
        // above already anticipates) must not leave the rendered form without the tokens its own
        // method-selection/BLIK/card requests need. Reuses buildPaymentRetryData()'s values, the same
        // source the order-detail page's payload uses, rather than a second hand-rolled copy that
        // could drift from it. Merged under the `retry_*` names (not `ajax_token`/`cart_token` above):
        // those are already spoken for by the live payment-status poll this page also runs, and that
        // poll's cart-specific token must survive untouched.
        if ($module->active && Configuration::get('SIMPLYIN_PAYMENTS')) {
            $retryData = $this->buildPaymentRetryData($module)['simplyin'];
            $return['simplyin']['retry_ajax_token'] = $retryData['retry_ajax_token'];
            $return['simplyin']['retry_cart_token'] = $retryData['retry_cart_token'];
            // Merged, not replaced. front.js also runs on this page whenever `order_sended` is not
            // set - which is exactly the no/stale-id_module case the branch above anticipates - and
            // it reads label keys the paywall subset does not carry. Overwriting the map would turn
            // those lookups into undefined and blank out its copy on the one page this surface is
            // for. Retry keys win on collision: where both define a key they hold the same string,
            // and the paywall's own copy is the authoritative one for the form being rendered.
            // The full label catalog, not just the paywall subset: the confirmation-heading
            // override (below) reads 'Order placed'/'Order confirmed' from this same map, and
            // buildPaywallLabels()'s allowlist deliberately does not carry non-paywall copy.
            $return['simplyin']['labels'] = array_merge(
                (array) ($return['simplyin']['labels'] ?? []),
                $this->buildLabels($module),
                $retryData['labels'],
            );
        }

        return $return;
    }

    /**
     * @param array<string, mixed> $return
     *
     * @return array<string, mixed>
     */
    private function applyPaymentsChannel(Module $module, array $return): array
    {
        $context = Context::getContext();
        $cartId = (int) ($context->cart->id ?? 0);
        $ajaxToken = $module->getAjaxToken();
        $cartToken = $module->getCartToken($cartId);

        $return['simplyin'][''] = [
            'check_payment_ajax' => $context->link->getModuleLink('simplyin', 'paymentajax'),
            'id_cart' => $cartId,
            'id_module' => $module->id,
            'ajax_token' => $ajaxToken,
            'cart_token' => $cartToken,
            'payment_check_token' => $this->tokenService->getPaymentCheckToken($ajaxToken, $cartToken),
            'status' => SimplyInPayments::getPaymentInfo($cartId),
        ];

        return $return;
    }

    private function resolveNewAccountPhone(): mixed
    {
        $phone = $this->session->get('new_account_phone');

        return empty($phone) ? false : $phone;
    }

    private function resolveCustomerEmail(): string
    {
        $email = $this->session->get('simplyin_email');
        $token = $this->session->get('simplyin_auth_token');
        if (!empty($email) && !empty($token)) {
            return (string) $email;
        }

        return '';
    }

    private function isSimplyInAuthenticated(): bool
    {
        $token = $this->session->get('simplyin_auth_token');
        if (!empty($token)) {
            return true;
        }
        $phone = $this->session->get('new_account_phone');

        return !empty($phone);
    }
}
