<?php
/**
 * 2007-2021 PrestaShop
 *
 * NOTICE OF LICENSE
 *
 * This source file is subject to the Academic Free License (AFL 3.0)
 * that is bundled with this package in the file LICENSE.txt.
 * It is also available through the world-wide-web at this URL:
 * http://opensource.org/licenses/afl-3.0.php
 * If you did not receive a copy of the license and are unable to
 * obtain it through the world-wide-web, please send an email
 * to license@prestashop.com so we can send you a copy immediately.
 *
 * DISCLAIMER
 *
 * Do not edit or add to this file if you wish to upgrade PrestaShop to newer
 * versions in the future. If you wish to customize PrestaShop for your
 * needs please refer to http://www.prestashop.com for more information.
 *
 * @author    PrestaShop SA <contact@prestashop.com>
 * @copyright 2007-2021 PrestaShop SA
 * @license   http://opensource.org/licenses/afl-3.0.php  Academic Free License (AFL 3.0)
 *  International Registered Trademark & Property of PrestaShop SA
 */

// @codingStandardsIgnoreStart
/* @noinspection AutoloadingIssuesInspection */
/* @noinspection PhpMultipleClassDeclarationsInspection */
/* @noinspection PhpIllegalPsrClassPathInspection */
/* @noinspection PhpDeprecationInspection */
/* @noinspection ReturnTypeCanBeDeclaredInspection */
// @codingStandardsIgnoreEnd

use ComerciaGlobalPayments\AddonPayments\SDK\Api\EPGJs;
use ComerciaGlobalPayments\AddonPayments\SDK\Request\Charge;
use ComerciaGlobalPayments\AddonPayments\SDK\Request\Enum\ChargeTypeEnum;
use ComerciaGlobalPayments\AddonPayments\SDK\Response\OperationInterface;
use ComerciaGlobalPayments\AddonPayments\SDK\Response\ResponseInterface;
use ComerciaGlobalPayments\AddonPayments\SDK\Response\Transaction;
use Prestashop\Module\AddonPayments\AddonPaymentsConfig;
use Prestashop\Module\AddonPayments\Exception\CheckoutException;
use Prestashop\Module\AddonPayments\Exception\GatewayException;
use Prestashop\Module\AddonPayments\Helpers\_3DSecureV2Trait;
use Prestashop\Module\AddonPayments\Helpers\BNPLHelper;
use Prestashop\Module\AddonPayments\Helpers\FrontControllerRedirectionsTrait;
use Prestashop\Module\AddonPayments\Helpers\SecurityTrait;
use Prestashop\Module\AddonPayments\OperationContext;
use Prestashop\Module\AddonPayments\Operations\ProcessTransaction;
use Prestashop\Module\AddonPayments\Operations\ValidateOrder;
use Prestashop\Module\AddonPayments\Services\ApiFactory;

/**
 * Handle payment validation during checkout.
 */
class AddonpaymentsValidationModuleFrontController extends ModuleFrontController
{

    use _3DSecureV2Trait;
    use SecurityTrait;
    use FrontControllerRedirectionsTrait;

    /**
     * @var string
     */
    public const PREPAY_TOKEN_KEY = 'prepayToken';

    /**
     * @var string
     */
    public const CUSTOMER_ID_KEY = 'customerId';

    /**
     * @var bool
     */
    public $ssl = _PS_USE_SSL_;

    /**
     * @var \addonpayments
     */
    public $module;

    /**
     * @var OperationContext
     */
    private $operationContext;

    public function __construct()
    {
        parent::__construct();
        $this->registerTranslations();
    }

    public function registerTranslations()
    {
        $this->module->l('Error processing payment. The order could not be completed. Try make the payment again if the problem persist, please contact our customer service.', 'validation');
        $this->module->l('Your session has expired for the selected payment method.', 'validation');
        $this->module->l('The payment operation could not be completed.', 'validation');
        $this->module->l('The payment operation could not be completed. An error occurred while trying to redirect you o your selected payment method.', 'validation');
        $this->module->l('You have cancelled the payment. The order could not be completed.', 'validation');
        $this->module->l('These price conditions are not allowed. The price may be too low or too high.', 'validation');
    }

    public function postProcess()
    {
        try {
            if (false === $this->checkIfContextIsValid()) {
                throw new CheckoutException('The context is not valid', CheckoutException::PRESTASHOP_CONTEXT_INVALID);
            }

            if (false === $this->checkIfPaymentOptionIsAvailable()) {
                throw new CheckoutException(
                    'This payment method is not available.',
                    CheckoutException::PRESTASHOP_PAYMENT_UNAVAILABLE
                );
            }

            $prepayResponse = Tools::getValue('prepayResponse');
            $prepayResponse = json_decode($prepayResponse, true);

            if (empty($prepayResponse) || JSON_ERROR_NONE !== json_last_error()) {
                throw new CheckoutException(
                    'The prepay token is missing.', CheckoutException::API_PREPAY_TOKEN_MISSING
                );
            }

            $this->operationContext = new OperationContext(
                $this->context->cart->id,
                $this->context->cart->getOrderTotal(),
                $this->context->cart->id_currency,
                $this->context->customer->secure_key
            );

            $response = $this->callCharge(
                $prepayResponse[self::PREPAY_TOKEN_KEY],
                $prepayResponse[self::CUSTOMER_ID_KEY],
                $prepayResponse['paymentMethod'],
                $prepayResponse['rememberMe']
            );

            $this->processResponse($response, $this->operationContext);

            return true;
        } catch (Exception $e) {
            $this->handleException($e);
        }

        return false;
    }

    /**
     * Check if the context is valid and if the module is active
     */
    private function checkIfContextIsValid(): bool
    {
        return true === (bool)$this->module->active
            && true === Validate::isLoadedObject($this->context->cart)
            && true === Validate::isUnsignedInt($this->context->cart->id_customer)
            && true === Validate::isUnsignedInt($this->context->cart->id_address_delivery)
            && true === Validate::isUnsignedInt($this->context->cart->id_address_invoice);
    }

    /**
     * Check that this payment option is still available in case the customer changed
     * his address just before the end of the checkout process
     */
    private function checkIfPaymentOptionIsAvailable(): bool
    {
        $modules = Module::getPaymentModules();

        if (empty($modules)) {
            return false;
        }

        foreach ($modules as $module) {
            if (isset($module['name']) && $this->module->name === $module['name']) {
                return true;
            }
        }

        return false;
    }

    /**
     * Perform charge API call.
     *
     * @param string|null $operationType *
     *
     * @return \ComerciaGlobalPayments\AddonPayments\SDK\Response\OperationInterface|\ComerciaGlobalPayments\AddonPayments\SDK\Response\ErrorResponseInterface
     *
     * @throws \Exception
     */
    protected function callCharge(
        string $prepayToken,
        string $customerId,
        string $paymentMethod,
        bool $rememberMe,
        string $operationType = ChargeTypeEnum::DEBIT
    ): ResponseInterface {

        $address = new Address($this->context->cart->id_address_invoice);

        /** @var EPGJs $apiClient */
        $apiClient = ApiFactory::createInstance(EPGJs::NAME);
        $merchantTransactionId = ApiFactory::getTransactionIdFromContext(
            $this->context
        );

        $transactionsCount = \Transactions::countAttemptsByOrder(
            $this->context->cart->id
        );
        $this->operationContext->setTransactionCount($transactionsCount);
        $this->operationContext->setPaySol($paymentMethod);

        $request = (new Charge())
            ->setApiVersion(3)
            ->setCustomerId($customerId)
            ->setMerchantTransactionId($merchantTransactionId)
            ->setCountry(Country::getIsoById($address->id_country))
            ->setCurrency((new Currency($this->context->cart->id_currency))->iso_code)
            ->setOperationType($operationType)
            ->setAmount($this->context->cart->getOrderTotal())
            ->setPrepayToken($prepayToken)
            ->setProductId((int)Configuration::get(AddonPaymentsConfig::EPG_PRODUCT_ID))
            ->setStatusUrl(urlencode($this->getStatusUrl($merchantTransactionId)))
            ->setSuccessUrl(urlencode($this->getCallbackUrl(addonpayments::PAYMENT_STATUS_SUCCESS)))
            ->setCancelUrl(urlencode($this->getCallbackUrl(addonpayments::PAYMENT_STATUS_CANCELLED)))
            ->setErrorUrl(urlencode($this->getCallbackUrl(addonpayments::PAYMENT_STATUS_FAILED)))
            ->setAwaitingURL(urlencode($this->getCallbackUrl(addonpayments::PAYMENT_STATUS_AWAITING)));

        if (addonpayments::BNPL_NAME === $paymentMethod) {
            $customer = $this->context->customer;
            $birthday_arr = explode("-", $customer->birthday);

            if(checkdate($birthday_arr[1], $birthday_arr[2], $birthday_arr[0])) {
                 $request->setDob($customer->birthday);
                $birthday_timestamp = strtotime($customer->birthday);
                $request->setDob(date("d-m-Y", $birthday_timestamp));
            }

            $request
                ->setpaymentSolution($paymentMethod)
                ->setCustomerCountry(Country::getIsoById($address->id_country))
                ->setCustomerEmail($customer->email)
                ->setFirstName($customer->firstname)
                ->setLastName($customer->lastname)
                ->setLanguage(Language::getIsoById($customer->id_lang))
                ->setIpAddress(Tools::getRemoteAddr())
                ->setRememberMe($rememberMe)
                ->setAccount($customer->id)
                ->setpaysolExtendedData(json_encode($this->getPaysolExtendedData()));

        } else {
            $request
                ->add3DSecureV2Object($this->getRiskValidation())
                ->add3DSecureV2Object($this->getCardHolderAccountInfo())
                ->add3DSecureV2Object($this->getShippingAddress());
        }

        if (true === (bool)Configuration::get(AddonPaymentsConfig::SETTLE)) {
            $request->setAutoCapture(true);
            return $apiClient->charge($request);
        }

        $request->setAutoCapture(false);
        return $apiClient->preAuth($request);
    }

    /**
     * @throws \PrestaShopException
     */
    protected function processResponse(ResponseInterface $response, OperationContext $operationContext): void
    {
        switch (true) {
            case $response instanceof OperationInterface && $response->needsRedirection():
                $this->setOrder($response, $operationContext);
                $this->redirectCustomerToPaymentSolutionSite($response->getOperations());
                break;

            case $response instanceof OperationInterface && $response->isCancelled():
                $this->registerTransactions($response->getOperations(), $operationContext);

                throw new GatewayException(
                    "Transaction {$this->context->cart->id} was cancelled by the user",
                    GatewayException::OPERATION_CANCELLED
                );

            case $response instanceof OperationInterface && $response->hasFailed():
                $this->registerTransactions($response->getOperations(), $operationContext);
                $transaction = $response->getTransaction();
                $message = "Transaction {$this->context->cart->id} failed";

                if(addonpayments::BNPL_NAME === $transaction->getPaymentSolution()) {
                    $message = $transaction->getMessage();
                }

                throw new GatewayException(
                    $message,
                    GatewayException::OPERATION_FAILED
                );

            case $response instanceof OperationInterface
                 && OperationInterface::STATUS_SUCCESS === $response->getStatus()
                 && OperationInterface::STATUS_REDIRECTED === $response->getTransaction()->getStatus()
                 && $response->getTransaction()->getPaymentSolution() === addonpayments::BNPL_NAME:

                $this->setOrder($response, $operationContext);

                $transaction = $response->getTransaction();
                $extraDetails =  $transaction->getPaymentDetails()->extraDetails;
                $paymentResponse = [];

                if(property_exists($extraDetails, 'entry')) {
                    $paymentExtraDetails = $extraDetails->entry;

                    foreach ($paymentExtraDetails as $detail) {
                        $paymentResponse[$detail->key] = $detail->value;
                    }

                }else if(property_exists($extraDetails, 'nemuruAuthToken')) {
                    $paymentResponse = (array) $extraDetails;

                }else {
                    throw new GatewayException(
                        "The API response does not contains a valid JSON data",
                        GatewayException::INVALID_REDIRECT_RESPONSE
                    );
                }

                $paymentResponse['merchantTrxId'] = $transaction->getMerchantTransactionId();
                $paymentResponse['callback'] = $this->getCallbackUrl(AddonPayments::PAYMENT_STATUS_UNKNOWN);

                $this->ajaxDie(Tools::jsonEncode($paymentResponse));

                break;
            case $response instanceof OperationInterface
                // @FIXME Operation::hasSucceed does not properly check success status.
                && ((OperationInterface::STATUS_SUCCESS === $response->getStatus()
                        && OperationInterface::STATUS_SUCCESS === $response->getTransaction()->getStatus())
                    || $response->isPending()):

                $order = $this->setOrder($response, $operationContext);

                $newOperationContext = array_values($operationContext->toArray());
                $newOperationContext['transactionId'] =
                    $response->getTransaction()->getPayFrexTransactionId();
                $newOperationContext =
                    OperationContext::createFromArray($newOperationContext);
                $this->redirectCustomer($newOperationContext, $order);
                break;
            default:
                // At this point if it s a response means is not a valid response
                // just record the transaction and set the status to error.
                if ($response instanceof OperationInterface) {
                    $this->registerTransactions($response->getOperations(), $operationContext);
                    throw new GatewayException("Transaction {$this->context->cart->id} returned an unknown status");
                }
        }

        // If we came this far is because the response is not an operation but an error.
        if (null === $response->getHttpResponse()) {
            throw new GatewayException($response->getReason(), GatewayException::UNKNOWN);
        }

        $errorContent = json_decode((string)$response->getHttpResponse()->getBody(), true);

        if (empty($errorContent) || JSON_ERROR_NONE !== json_last_error()) {
            throw new GatewayException($response->getReason(), GatewayException::UNKNOWN);
        }

        if (strpos($errorContent['errorMessage'], 'Prepay Token and Charge Request does not match')) {
            throw new GatewayException($errorContent['errorMessage'], GatewayException::PREPAY_CHARGE_MISMATCH);
        }

        if (strpos($errorContent['errorMessage'], 'The provided prepay token is invalid or expired')) {
            throw new GatewayException($errorContent['errorMessage'], GatewayException::PREPAY_TOKEN_EXPIRED);
        }

        // If we have come this far is because its an unknown error.
        throw new GatewayException($errorContent['errorMessage'], GatewayException::UNKNOWN);
    }

    /**
     * @throws \PrestaShopDatabaseException
     * @throws \PrestaShopException
     */
    private function setOrder(OperationInterface $response, $operationContext) {
        $this->registerTransactions(
            $response->getOperations(),
            $operationContext
        );
        $order = (new ValidateOrder($this->module))(
            $response->getTransaction(),
            $operationContext
        );
        if (null !== $order) {
            Transactions::setOrderByCart($order->id_cart, $order->id);
        }

        return $order;
    }

    /**
     * @param Transaction[] $transactions
     * @param OperationContext $context
     */
    private function registerTransactions(array $transactions, OperationContext $context): void {
        $operations = count($transactions);
        foreach ($transactions as $transaction) {
            (new ProcessTransaction($context, $this->module))(
                $transaction,
                $transaction->getSortedOrder() === $operations
            );
        }
    }

    /**
     * @param \ComerciaGlobalPayments\AddonPayments\SDK\Response\Transaction[] $transactions
     */
    private function redirectCustomerToPaymentSolutionSite(array $transactions): void
    {
        foreach ($transactions as $transaction) {
            $target = $transaction->getRedirectionTarget();

            if (null === $target) {
                continue;
            }

            $target->setBaseUrl(ApiFactory::getRedirectionTargetUrl());
            PrestaShopLogger::addLog(
                "Transaction {$transaction->getPayFrexTransactionId()} redirects customer to {$target->__toString()}"
            );
            Tools::redirect((string)$target);
        }
        throw new GatewayException(
            GatewayException::INVALID_REDIRECT_RESPONSE,
            'All operations were parsed and none contained a valid redirection payload'
        );
    }

    /**
     * Redirects the user to the order confirmation page.
     */
    protected function redirectCustomer(OperationContext $operationContext, Order $order = null): void
    {
        if (null === $order) {
            PrestaShopLogger::addLog(
                'The cart could not be placed.',
                3,
                0,
                'Cart',
                $operationContext->getCartOrOrderId()
            );
            Tools::redirect($this->getErrorUrl());
        }

        switch ($order->getCurrentState()) {
            case (int)Configuration::get('PS_OS_ERROR'):
                Tools::redirect($this->getErrorUrl());
                break;
            case (int)Configuration::get('PS_OS_CANCELED'):
                Tools::redirect($this->getCancelUrl());
                break;
            default:
                Tools::redirect($this->getSuccessUrl());
        }
    }

    /**
     * @param Exception $exception
     */
    private function handleException(Exception $exception): void
    {
        $exceptionMessageForCustomer = $this->module->l(
            'Error processing payment. The order could not be completed. Try make the payment again if the problem persist, please contact our customer service.', 'validation'
        );

        $notifyCustomerService = true;

        switch (true) {
            case ($exception instanceof CheckoutException)
                && CheckoutException::PRESTASHOP_PAYMENT_UNAVAILABLE === $exception->getCode():
                $exceptionMessageForCustomer = $this->module->l('This payment method is unavailable.', 'validation');
                $notifyCustomerService = false;
                break;
            case ($exception instanceof CheckoutException)
                && CheckoutException::PRESTASHOP_CONTEXT_INVALID === $exception->getCode():
                $exceptionMessageForCustomer = $this->module->l('Cart is invalid.','validation');
                $notifyCustomerService = false;
                break;
            case ($exception instanceof GatewayException)
                && GatewayException::PREPAY_TOKEN_EXPIRED === $exception->getCode():
                $exceptionMessageForCustomer = $this->module->l(
                    'Your session has expired for the selected payment method.',
                    'validation'
                );
                $notifyCustomerService = false;
                break;
            case ($exception instanceof GatewayException)
                && GatewayException::PREPAY_CHARGE_MISMATCH === $exception->getCode():
                $exceptionMessageForCustomer = $this->module->l('The payment operation could not be completed.', 'validation');
                break;
            case ($exception instanceof GatewayException)
                && GatewayException::OPERATION_FAILED === $exception->getCode():

                $message = $this->module->mapErrorMessage($exception->getMessage());

                if(!empty($message)) {
                    $exceptionMessageForCustomer = $message;
                }

                $notifyCustomerService = false;
                break;
            case ($exception instanceof GatewayException)
                && GatewayException::INVALID_REDIRECT_RESPONSE === $exception->getCode():
                $exceptionMessageForCustomer = $this->module->l(
                    'The payment operation could not be completed. An error occurred while trying to redirect you o your selected payment method.',
                    'validation'
                );
                break;
            case ($exception instanceof GatewayException)
                && GatewayException::OPERATION_CANCELLED === $exception->getCode():
                $exceptionMessageForCustomer = $this->module->l(
                    'You have cancelled the payment. The order could not be completed.',
                    'validation'
                );
                break;
            default:
        }

        $type = basename(str_replace('\\', '/', get_class($exception)));
        PrestaShopLogger::addLog(sprintf('%s: %s', $type,
            $exception->getMessage()), 3, $exception->getCode());

        if (true === $notifyCustomerService) {
            $this->notifyCustomerService($exception);
        }

        $params = [
            addonpayments::PAYMENT_STATUS_KEY  => addonpayments::PAYMENT_STATUS_FAILED,
            addonpayments::PAYMENT_MESSAGE_KEY => base64_encode($exceptionMessageForCustomer)
        ];

        if(!addonpayments::is17()) {
            $params['step'] = 3;
        }

        $redirect = $this->context->link->getPageLink('order', null, null, $params);

        if(addonpayments::BNPL_NAME === $this->operationContext->getPaySol()) {
            die(Tools::jsonEncode([
                'hasError' => true,
                'error_message' =>  $exceptionMessageForCustomer,
                'redirect' =>  $redirect
            ]));
        }

        Tools::redirect($redirect);
    }

    /**
     * @param \Exception $exception
     *
     * @return void
     */
    private function notifyCustomerService(Exception $exception): void
    {
        PrestaShopLogger::addLog('TODO: Implement ' . __FUNCTION__ . ', notified ' . $exception->getMessage());
    }

    /**
     * @param int $status
     *
     * @return string
     */
    private function getCallbackUrl(int $status): string
    {
        $idLanguage = $this->context->language->id;
        $paymentHash = $this->encode(
            $this->context->cart->id,
            $this->context->customer->id,
            $this->operationContext->getTransactionCount(),
            $status
        );

        return $this->context->link->getModuleLink(
            $this->module->name,
            'callback',
            [
                addonpayments::PAYMENT_STATUS_KEY => $paymentHash,
            ],
            null,
            $idLanguage
        );
    }

    /**
     * @param string $merchantTrxId
     *
     * @return string
     */
    private function getStatusUrl(string $merchantTrxId): string
    {
        return $this->context->link->getModuleLink(
            $this->module->name,
            'confirmation',
            [
                AddonpaymentsConfirmationModuleFrontController::OPERATION_PARAMETER => $merchantTrxId,
            ]
        );
    }

    /**
     * @throws \PrestaShopDatabaseException
     * @throws \PrestaShopException
     * @throws \Exception
     */
    private function getPaysolExtendedData(): array
    {
        $bnplHelper = new BNPLHelper();

        $cart = $this->context->cart;
        $carrier = new Carrier($cart->id_carrier);
        $this->context->cart->getCarrierCost($cart->id_carrier);

        $itemsProduct = $bnplHelper->getItems($cart);
        $itemsCarrier = $bnplHelper->getCarrierItems($carrier);
        $itemsDiscount = $bnplHelper->getDiscountItems();
        $items = array_merge($itemsProduct, $itemsCarrier, $itemsDiscount);

        return [
          'billing' => $bnplHelper->getBillingAddress(),
          'cart' => [
                'currency' => $this->context->currency->iso_code,
                'items' => $items,
                "reference" => (string)$this->context->cart->id,
                "total_price_with_tax" => $this->context->cart->getOrderTotal(),
            ],
          'product' => Configuration::get(AddonPaymentsConfig::FINANCING_OPTIONS) ?: AddonPaymentsConfig::FINANCING_DEFAULT_OPTION,
          'shipping' => $bnplHelper->getShippingAddress($carrier),
          'confirmation_cart_data' => new stdClass(),
        ];
    }

}
