<?php
/**
 * 2021 CAWL Online Payments
 *
 * NOTICE OF LICENSE
 *
 * This source file is subject to the Academic Free License 3.0 (AFL-3.0).
 * It is also available through the world-wide-web at this URL: https://opensource.org/licenses/AFL-3.0
 *
 * @author    PrestaShop partner
 * @copyright 2021 CAWL Online Payments
 * @license   http://opensource.org/licenses/afl-3.0.php  Academic Free License (AFL 3.0)
 */

namespace WorldlineOP\PrestaShop\Builder;

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

use Language;
use OnlinePayments\Sdk\Domain\AmountOfMoney;
use OnlinePayments\Sdk\Domain\CardPaymentMethodSpecificInput;
use OnlinePayments\Sdk\Domain\CardPaymentMethodSpecificInputForHostedCheckout;
use OnlinePayments\Sdk\Domain\GPayThreeDSecure;
use OnlinePayments\Sdk\Domain\HostedCheckoutSpecificInput;
use OnlinePayments\Sdk\Domain\LineItem;
use OnlinePayments\Sdk\Domain\MobilePaymentMethodHostedCheckoutSpecificInput;
use OnlinePayments\Sdk\Domain\MobilePaymentProduct320SpecificInput;
use OnlinePayments\Sdk\Domain\Order;
use OnlinePayments\Sdk\Domain\OrderLineDetails;
use OnlinePayments\Sdk\Domain\OrderReferences;
use OnlinePayments\Sdk\Domain\PaymentProduct130SpecificInput;
use OnlinePayments\Sdk\Domain\PaymentProduct130SpecificThreeDSecure;
use OnlinePayments\Sdk\Domain\PaymentProductFilter;
use OnlinePayments\Sdk\Domain\PaymentProductFiltersHostedCheckout;
use OnlinePayments\Sdk\Domain\RedirectionData;
use OnlinePayments\Sdk\Domain\ShoppingCart;
use OnlinePayments\Sdk\Domain\ThreeDSecure;
use RandomLib\Factory;
use SecurityLib\Strength;
use WorldlineOP\PrestaShop\Configuration\Entity\PaymentMethodsSettings;
use WorldlineOP\PrestaShop\Configuration\Entity\PaymentSettings;
use WorldlineOP\PrestaShop\Utils\Tools;

/**
 * Class HostedPaymentRequestBuilder
 */
class HostedPaymentRequestBuilder extends AbstractRequestBuilder
{
    const GIFT_CARD_PRODUCT_TYPE_FOOD_DRINK = 'FoodAndDrink';
    const GIFT_CARD_PRODUCT_TYPE_HOME_GARDEN = 'HomeAndGarden';
    const GIFT_CARD_PRODUCT_TYPE_GIFT_FLOWERS = 'GiftAndFlowers';
    const GIFT_CARD_PRODUCT_TYPE_NONE = 'none';
    const TRANSACTION_RISK_ANALYSIS_EXEMPTION = 'transaction-risk-analysis';
    const LOW_VALUE_EXEMPTION = 'low-value';

    public const NO_CHALLENGE_REQUEST = 'no-challenge-request';
    const NO_CHALLENGE_REQUESTED_RISK_ANALYSIS_PERFORMED = 'no-challenge-requested-risk-analysis-performed';
    const NO_CHALLENGE_REQUESTED = 'no-challenge-requested';
    const MEALVOUCHER_PRODUCT_ID = 5402;

    /**
     * @return HostedCheckoutSpecificInput|null
     *
     * @throws \Exception
     */
    public function buildHostedCheckoutSpecificInput()
    {
        $hostedCheckoutSpecificInput = new HostedCheckoutSpecificInput();
        $hostedCheckoutSpecificInput->setShowResultPage(
            $this->settings->advancedSettings->displayPaymentConfirmationPage
        );
        if ($this->settings->paymentMethodsSettings->redirectTemplateFilename) {
            $hostedCheckoutSpecificInput->setVariant($this->settings->paymentMethodsSettings->redirectTemplateFilename);
        }
        $cartIsoLang = Language::getIsoById($this->context->cart->id_lang);
        $hostedCheckoutSpecificInput->setLocale(str_replace('-', '_', Language::getLocaleByIso($cartIsoLang)));
        $hostedCheckoutSpecificInput->setReturnUrl(
            $this->context->link->getModuleLink($this->module->name, 'redirect', ['action' => 'redirectReturnHosted'])
        );
        $productFilterHostedCheckout = new PaymentProductFiltersHostedCheckout();

        if ($this->idProduct) {
            $productFilter = new PaymentProductFilter();
            $productFilter->setProducts([(int)$this->idProduct]);
            $productFilterHostedCheckout->setRestrictTo($productFilter);
        }

        // exclude meal vouchers
        $excludeMealVoucherFilter = new PaymentProductFilter();
        $excludeMealVoucherFilter->setProducts([self::MEALVOUCHER_PRODUCT_ID]);
        $productFilterHostedCheckout->setExclude($excludeMealVoucherFilter);

        $hostedCheckoutSpecificInput->setPaymentProductFilters($productFilterHostedCheckout);

        if (false !== $this->tokenValue) {
            $hostedCheckoutSpecificInput->setTokens($this->tokenValue);
        }
        if (true === $this->settings->advancedSettings->groupCardPaymentOptions) {
            $cardPaymentMethodSpecificInputForHC = new CardPaymentMethodSpecificInputForHostedCheckout();
            $cardPaymentMethodSpecificInputForHC->setGroupCards(true);
            $hostedCheckoutSpecificInput->setCardPaymentMethodSpecificInput($cardPaymentMethodSpecificInputForHC);
        }

        return $hostedCheckoutSpecificInput;
    }

    /**
     * @return CardPaymentMethodSpecificInput|false
     *
     * @throws \Exception
     */
    public function buildCardPaymentMethodSpecificInput()
    {
        if (false !== $this->idProduct) {
            $product = $this->settings->paymentMethodsSettings->findRedirectPMByProductId((int) $this->idProduct);
            if (false === $product || PaymentMethodsSettings::PAYMENT_METHOD_CARD !== $product->type) {
                return false;
            }
        }

        $cardPaymentMethodSpecificInput = new CardPaymentMethodSpecificInput();
        if (self::PRODUCT_ID_INTERSOLVE == $this->idProduct || self::PRODUCT_ID_PLEDG == $this->idProduct) {
            $cardPaymentMethodSpecificInput->setAuthorizationMode(PaymentSettings::TRANSACTION_TYPE_IMMEDIATE);
        } else {
            $cardPaymentMethodSpecificInput->setAuthorizationMode(
                $this->settings->advancedSettings->paymentSettings->transactionType
            );
        }
        if (false !== $this->tokenValue) {
            $cardPaymentMethodSpecificInput->setToken($this->tokenValue);
        }
        $cardPaymentMethodSpecificInput->setReturnUrl(
            $this->context->link->getModuleLink($this->module->name, 'redirect', ['action' => 'redirectReturnHosted'])
        );
        if (false !== $this->idProduct) {
            $cardPaymentMethodSpecificInput->setPaymentProductId($this->idProduct);
        }
        $threeDSecure = new ThreeDSecure();
        $threeDSecure->setSkipAuthentication(true);
        if (self::PRODUCT_ID_MAESTRO == $this->idProduct || true === $this->settings->advancedSettings->force3DsV2) {
            $threeDSecure->setSkipAuthentication(false);
        }
        $orderTotalInEuros = Tools::getAmountInEuros(
            $this->context->cart->getOrderTotal(),
            new \Currency($this->context->cart->id_currency)
        );
        $threeDSExemptedValue = $this->settings->advancedSettings->threeDSExemptedValue;
        $threeDSExemptedType = (string) $this->settings->advancedSettings->threeDSExemptedType;

        $is3dsOptionEnabled = (true === $this->settings->advancedSettings->threeDSExempted)
            && ($threeDSExemptedValue >= $orderTotalInEuros);

        $isNoChallengePreference = $is3dsOptionEnabled && ($threeDSExemptedType === self::NO_CHALLENGE_REQUEST);

        if ($is3dsOptionEnabled) {
            if ($isNoChallengePreference) {
                // Preference: No challenge request (NOT an exemption request)
                $threeDSecure->setSkipAuthentication(false);
                $threeDSecure->setChallengeIndicator(self::NO_CHALLENGE_REQUESTED);
            } else {
                // Existing exemption logic
                $threeDSecure->setSkipAuthentication(true);
                $threeDSecure->setExemptionRequest($threeDSExemptedType);
                $threeDSecure->setSkipSoftDecline(false);
            }
        }

        if (true === $this->settings->advancedSettings->enforce3DS) {
            $threeDSecure->setChallengeIndicator(self::CHALLENGE_INDICATOR_REQUIRED);
        }

        $cardPaymentMethodSpecificInput->setThreeDSecure($threeDSecure);

        $shoppingCartPresented = $this->shoppingCartPresenter->present($this->context->cart);
        $numberOfItems = min(count($shoppingCartPresented['products']), self::MAX_NUMBER_OF_ITEMS);

        if (true === $this->settings->advancedSettings->force3DsV2) {
            $paymentProduct130SpecificInput = new PaymentProduct130SpecificInput();
            $paymentProduct130ThreeDSecure = new PaymentProduct130SpecificThreeDSecure();
            $paymentProduct130ThreeDSecure->setUsecase('single-amount');
            $paymentProduct130ThreeDSecure->setNumberOfItems($numberOfItems);

            if (!$this->settings->advancedSettings->threeDSExempted) {
                $paymentProduct130ThreeDSecure->setAcquirerExemption(false);
            } elseif ($this->settings->advancedSettings->threeDSExempted) {
                if ($this->settings->advancedSettings->threeDSExemptedValue >= $orderTotalInEuros) {
                    if ($threeDSExemptedType === self::NO_CHALLENGE_REQUEST) {
                        $paymentProduct130ThreeDSecure->setAcquirerExemption(false);

                        $threeDSecure->setSkipAuthentication(false);
                        $threeDSecure->setChallengeIndicator(self::NO_CHALLENGE_REQUESTED);
                    } else {
                        $paymentProduct130ThreeDSecure->setAcquirerExemption(
                            $threeDSExemptedType === self::TRANSACTION_RISK_ANALYSIS_EXEMPTION
                        );

                        $threeDSecure->setSkipAuthentication(false);
                        $threeDSecure->setExemptionRequest($threeDSExemptedType);
                        $threeDSecure->setSkipSoftDecline(false);
                        switch ($threeDSExemptedType) {
                            case self::TRANSACTION_RISK_ANALYSIS_EXEMPTION:
                                $threeDSecure->setChallengeIndicator(self::NO_CHALLENGE_REQUESTED_RISK_ANALYSIS_PERFORMED);
                                break;
                            case self::LOW_VALUE_EXEMPTION:
                                $threeDSecure->setChallengeIndicator(self::NO_CHALLENGE_REQUESTED);
                                break;
                            default:
                                $threeDSecure->setChallengeIndicator(self::NO_CHALLENGE_REQUESTED);
                                break;
                        }
                    }
                } else {
                    $paymentProduct130ThreeDSecure->setAcquirerExemption(false);
                }
            }
            $paymentProduct130SpecificInput->setThreeDSecure($paymentProduct130ThreeDSecure);
            $cardPaymentMethodSpecificInput->setPaymentProduct130SpecificInput($paymentProduct130SpecificInput);
        }

        if (false === $this->tokenValue) {
            $cardPaymentMethodSpecificInput->setUnscheduledCardOnFileRequestor(self::CARD_ON_FILE_REQUESTOR_FIRST);
            $cardPaymentMethodSpecificInput->setUnscheduledCardOnFileSequenceIndicator(self::CARD_ON_FILE_SEQUENCE_INDICATOR_FIRST);
        } else {
            $cardPaymentMethodSpecificInput->setUnscheduledCardOnFileRequestor(self::CARD_ON_FILE_REQUESTOR_SUBSEQUENT);
            $cardPaymentMethodSpecificInput->setUnscheduledCardOnFileSequenceIndicator(self::CARD_ON_FILE_SEQUENCE_INDICATOR_SUBSEQUENT);
        }

        return $cardPaymentMethodSpecificInput;
    }

    /**
     * @return MobilePaymentMethodHostedCheckoutSpecificInput|false
     * @throws \Exception
     */
    public function buildMobilePaymentMethodSpecificInput()
    {
        if (false !== $this->idProduct) {
            $product = $this->settings->paymentMethodsSettings->findRedirectPMByProductId((int) $this->idProduct);
            if (false === $product || PaymentMethodsSettings::PAYMENT_METHOD_MOBILE !== $product->type) {
                return false;
            }
        }

        $mobilePaymentMethodSpecificInput = new MobilePaymentMethodHostedCheckoutSpecificInput();
        if (false !== $this->idProduct) {
            $mobilePaymentMethodSpecificInput->setPaymentProductId((int)$this->idProduct);
        }
        $mobilePaymentMethodSpecificInput->setAuthorizationMode(
            $this->settings->advancedSettings->paymentSettings->transactionType
        );

        $paymentProduct320SpecificInput = new MobilePaymentProduct320SpecificInput();
        $gPayThreeDSecure = new GPayThreeDSecure();

        // evaluate no-challenge preference (same gating as existing exemption logic)
        $orderTotalInEuros = Tools::getAmountInEuros(
            $this->context->cart->getOrderTotal(),
            new \Currency($this->context->cart->id_currency)
        );
        $threeDSExemptionType = (string) $this->settings->advancedSettings->threeDSExemptedType;
        $threeDSExemptionValue = $this->settings->advancedSettings->threeDSExemptedValue;
        $isNoChallengePreference = (true === $this->settings->advancedSettings->threeDSExempted)
            && ($threeDSExemptionValue >= $orderTotalInEuros)
            && ($threeDSExemptionType === self::NO_CHALLENGE_REQUEST);

        if (!$this->settings->advancedSettings->force3DsV2) {
            $gPayThreeDSecure->setSkipAuthentication(true);
        } else {
            if ($isNoChallengePreference) {
                // Client requirement: include product320 3DS params for no-challenge preference
                $gPayThreeDSecure->setChallengeIndicator(self::NO_CHALLENGE_REQUESTED);
                $gPayThreeDSecure->setSkipAuthentication(false);
            } else {
                if (
                    !$this->settings->advancedSettings->enforce3DS
                    && !$this->settings->advancedSettings->threeDSExempted
                ) {
                    $gPayThreeDSecure->setChallengeIndicator(self::CHALLENGE_INDICATOR_NO_PREFERENCE);
                    $gPayThreeDSecure->setSkipAuthentication(false);
                } elseif ($this->settings->advancedSettings->enforce3DS) {
                    $gPayThreeDSecure->setChallengeIndicator(self::CHALLENGE_INDICATOR_REQUIRED);
                    $gPayThreeDSecure->setSkipAuthentication(false);
                } elseif ($this->settings->advancedSettings->threeDSExempted) {
                    $gPayThreeDSecure->setSkipAuthentication(false);

                    if ($threeDSExemptionValue >= $orderTotalInEuros) {
                        $gPayThreeDSecure->setExemptionRequest($threeDSExemptionType);
                        switch ($threeDSExemptionValue) {
                            case self::TRANSACTION_RISK_ANALYSIS_EXEMPTION:
                                $gPayThreeDSecure->setChallengeIndicator(self::NO_CHALLENGE_REQUESTED_RISK_ANALYSIS_PERFORMED);
                                break;
                            case self::LOW_VALUE_EXEMPTION:
                                $gPayThreeDSecure->setChallengeIndicator(self::NO_CHALLENGE_REQUESTED);
                                break;
                            default:
                                $gPayThreeDSecure->setChallengeIndicator(self::NO_CHALLENGE_REQUESTED);
                                break;
                        }
                    }
                }
            }
            $gPayRedirectionData = new RedirectionData();
            $gPayRedirectionData->setReturnUrl($this->context->link->getModuleLink(
                $this->module->name, 'redirect', ['action' => 'redirectReturnHosted']));
            $gPayThreeDSecure->setRedirectionData($gPayRedirectionData);
        }

        $paymentProduct320SpecificInput->setThreeDSecure($gPayThreeDSecure);
        $mobilePaymentMethodSpecificInput->setPaymentProduct320SpecificInput($paymentProduct320SpecificInput);

        return $mobilePaymentMethodSpecificInput;
    }

    /**
     * @return Order
     *
     * @throws \Exception
     */
    public function buildOrder()
    {
        /** @var Order $order */
        $order = parent::buildOrder();
        $factory = new Factory();
        $generator = $factory->getGenerator(new Strength(Strength::LOW));
        $orderReferences = new OrderReferences();
        $orderReferences->setMerchantReference(
            $this->context->cart->id . '-' . $generator->generateString(7, self::REFERENCE_CHARS)
        );

        $fixedSoftDescriptor = $this->settings->paymentMethodsSettings->fixedSoftDescriptor;
        if ((int) $this->idProduct === self::PRODUCT_ID_PLEDG) {
            $descriptor = empty($fixedSoftDescriptor)
                ? substr($this->context->shop->name, 0, 15)
                : $fixedSoftDescriptor;
        } else {
            $descriptor = (empty($fixedSoftDescriptor) || !empty($this->idProduct))
                ? null
                : $fixedSoftDescriptor;
        }
        $orderReferences->setDescriptor($descriptor);

        $order->setReferences($orderReferences);
        try {
            $productId = (int)$this->idProduct === self::MEALVOUCHER_PRODUCT_ID ? self::MEALVOUCHER_PRODUCT_ID : null;
            $shoppingCartPresented = $this->shoppingCartPresenter->present($this->context->cart, $productId);
        } catch (\Exception $e) {
            return $order;
        }

        if ((int)$this->idProduct !== self::MEALVOUCHER_PRODUCT_ID) {
            $shipping = $order->getShipping();
            $shipping->setShippingCost((int)(string)$shoppingCartPresented['shipping']['priceDiscountedWithoutTax']);
            $shipping->setShippingCostTax((int)(string)$shoppingCartPresented['shipping']['tax']);
            $order->setShipping($shipping);
        }

        $shoppingCart = new ShoppingCart();
        $items = $this->buildGroupedLineItems($shoppingCartPresented);
        if ((int)$this->idProduct === self::MEALVOUCHER_PRODUCT_ID && !empty($items)) {
            $orderAmount = $order->getAmountOfMoney()->getAmount();
            $shippingTax = (int) (string) $shoppingCartPresented['shipping']['tax'];

            $mergedItem = $items[0];
            $lineDetails = $mergedItem->getOrderLineDetails();
            $taxAmount = $lineDetails->getTaxAmount() + $shippingTax;
            $lineDetails->setTaxAmount($taxAmount);
            $lineDetails->setProductPrice($orderAmount - $taxAmount);
            $mergedItem->getAmountOfMoney()->setAmount($orderAmount);
        }

        $shoppingCart->setItems($items);
        if (!$this->settings->advancedSettings->omitOrderItemDetails) {
            $order->setShoppingCart($shoppingCart);
        }

        return $order;
    }

    private function buildGroupedLineItems(array $shoppingCartPresented): array
    {
        $itemsByGroupKey = [];

        foreach ($shoppingCartPresented['products'] as $product) {
            $productId = $product['productId'];
            $price = (int)(string)$product['productPrice'];
            $tax = (int)(string)$product['tax'];
            $discount = (int)(string)$product['discountPrice'];

            // Composite key to distinguish products with different pricing
            $groupKey = "{$productId}_{$price}";

            if (!isset($itemsByGroupKey[$groupKey])) {
                // Create new LineItem
                $item = new LineItem();
                $itemAmount = new AmountOfMoney();
                $itemAmount->setAmount((int)(string)$product['totalWithTax']);
                $itemAmount->setCurrencyCode(
                    Tools::getIsoCurrencyCodeById($shoppingCartPresented['cart']->id_currency)
                );
                $item->setAmountOfMoney($itemAmount);

                $itemLineDetails = new OrderLineDetails();
                $itemLineDetails->setProductPrice($price);
                $itemLineDetails->setDiscountAmount($discount);
                $itemLineDetails->setProductCode($product['productCode']);
                $itemLineDetails->setProductName($product['productName']);
                $itemLineDetails->setProductType($product['productType']);
                $itemLineDetails->setQuantity(1);
                $itemLineDetails->setTaxAmount($tax);
                $itemLineDetails->setUnit('piece');

                $item->setOrderLineDetails($itemLineDetails);

                $itemsByGroupKey[$groupKey] = $item;
            } else {
                // Update existing LineItem
                $existingItem = $itemsByGroupKey[$groupKey];

                $existingItem->getOrderLineDetails()->setQuantity(
                    $existingItem->getOrderLineDetails()->getQuantity() + 1
                );

                $existingAmount = $existingItem->getAmountOfMoney()->getAmount();
                $existingItem->getAmountOfMoney()->setAmount(
                    $existingAmount + (int)(string)$product['totalWithTax']
                );
            }
        }

        return array_values($itemsByGroupKey);
    }
}
