<?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
 */

namespace Prestashop\Module\AddonPayments\Operations;

use addonpayments;
use Cart;
use ComerciaGlobalPayments\AddonPayments\SDK\Response\OperationInterface;
use ComerciaGlobalPayments\AddonPayments\SDK\Response\Transaction;
use Configuration;
use Customer;
use OrderSlip;
use Order;
use OrderHistory;
use Prestashop\Module\AddonPayments\OperationContext;
use PrestaShopLogger;

/**
 * Performs order validation and placement.
 */
class ValidateOrder
{

    public const OPERATION_DEBIT = 'DEBIT';

    public const OPERATION_CREDIT = 'CREDIT';

    public const OPERATION_VOID = 'VOID';

    public const OPERATION_REFUND = 'REFUND';

    public const OPERATION_REBATE = 'REBATE';

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

    /**
     * ValidateOrder constructor.
     *
     * @param \addonpayments $module
     */
    public function __construct(addonpayments $module)
    {
        $this->module = $module;
    }

    /**
     * Executes the order validation.
     *
     * @param \ComerciaGlobalPayments\AddonPayments\SDK\Response\Transaction $transaction
     * @param \Prestashop\Module\AddonPayments\OperationContext              $operationContext
     *
     * @return \Order|null
     *
     * @throws \PrestaShopDatabaseException
     * @throws \PrestaShopException
     */
    public function __invoke(Transaction $transaction, OperationContext $operationContext): ?Order {

        $cart = new Cart($operationContext->getCartOrOrderId());
        $order_id = Order::getOrderByCartId($cart->id);
        $order = $order_id ? new Order($order_id) : null;
        $paymentSolution = $transaction->getPaymentSolution();
        $customer = new Customer($cart->id_customer);
        $extraVars = [
            'payment_solution'  => $paymentSolution,
            'transaction_id'    => $transaction->getPayFrexTransactionId(),
            'number_operations' => $transaction->getSortedOrder(),
            'guest'             => $customer->isGuest(),
        ];

        if (null !== $order) {
            return $this->updateOrder($order, $transaction, $extraVars);
        }

        return $this->placeOrder($cart, $transaction, $extraVars);
    }

    /**
     * Place newly confirmed order.
     *
     * The order is placed only if the received status is positive.
     *
     * @param Cart $cart
     * @param Transaction $transaction
     * @param array $extraVars
     *
     * @return Order|null
     * @throws \PrestaShopDatabaseException
     * @throws \PrestaShopException
     */
    private function placeOrder(Cart $cart, Transaction $transaction, array $extraVars): ?Order {
        switch ($transaction->getStatus()) {
            case OperationInterface::STATUS_SUCCESS:
                $id_order_state = (int)Configuration::get('PS_OS_PAYMENT');
                break;
            case OperationInterface::STATUS_ERROR:
            case OperationInterface::STATUS_FAIL:
                $id_order_state = (int)Configuration::get('PS_OS_ERROR');
                break;
            case OperationInterface::STATUS_CANCELLED:
                $id_order_state = (int)Configuration::get('PS_OS_CANCELED');
                break;
            case OperationInterface::STATUS_PENDING:
                $id_order_state = (int)Configuration::get(addonpayments::PS_OS_EASYPAYMENTGATEWAY_PENDING);

                if ('bizum' === $transaction->getPaymentSolution() && null != $transaction->getRedirectionTarget()) {
                    $id_order_state = (int)Configuration::get(addonpayments::PS_OS_EASYPAYMENTGATEWAY_PROCESSING);
                }

                break;
            case OperationInterface::STATUS_AWAITING_PAYSOL:
            case OperationInterface::STATUS_REDIRECTED:
            default:
                $id_order_state = (int)Configuration::get(addonpayments::PS_OS_EASYPAYMENTGATEWAY_PROCESSING);
        }

        // When placing the order if the status is ERROR, FAIL or CANCELLED
        // we are dealing with a cart and payment with redirection, thus
        // the user surely gets redirected to KO page and the cart
        // cannot be placed as order.
        if ($id_order_state === (int)Configuration::get('PS_OS_ERROR')
            || $id_order_state === (int)Configuration::get('PS_OS_CANCELED')) {
            PrestaShopLogger::addLog(
                "The payment {$transaction->getPayFrexTransactionId()} failed with {$transaction->getMessage()}",
                3,
                0,
                'Cart',
                $cart->id
            );

            return null;
        }

        $this->module->validateOrder(
            $cart->id,
            $id_order_state,
            $cart->getOrderTotal(),
            sprintf( $this->module->l('Payment by %s', 'validateorder'), $transaction->getPaymentSolution() ?: $this->module->displayName),
            null,
            $extraVars,
            $cart->id_currency,
            true,
            $cart->secure_key
        );

        return new Order(Order::getOrderByCartId($cart->id));
    }

    /**
     * Update the order with the payment gateway response.
     *
     * @param Order $order
     * @param Transaction $transaction
     * @param array $extraVars
     *
     * @return \Order|null
     */
    private function updateOrder(Order $order, Transaction $transaction, array $extraVars): ?Order {
        // If order exists and its current state is final, skip.
        $statuses = [
            (int) Configuration::get('PS_OS_PAYMENT'), // 2
            (int) Configuration::get('PS_OS_ERROR'), // 8
            (int) Configuration::get('PS_OS_CANCELED'), // 6
            (int) Configuration::get('PS_OS_REFUND'), // 6
            (int)Configuration::get(addonpayments::PS_OS_EASYPAYMENTGATEWAY_REBATE), // 6
        ];

        $operation_type = [
            self::OPERATION_REBATE,
            self::OPERATION_REFUND
        ];

        // Final status PS_OS_PAYMENT can change with refund and rebate operations
        if (in_array((int) $order->getCurrentState(), $statuses, true) &&
            (!in_array($transaction->getOperationType(), $operation_type, true) ||
             (in_array( $transaction->getOperationType(), $operation_type, true) &&
              $transaction->getStatus() !== OperationInterface::STATUS_SUCCESS))
            ) {
            PrestaShopLogger::addLog(
                'The order state cannot be changed',
                3,
                0,
                'Order',
                $order->id
            );

            return $order;
        }

        // El estado reembolso parcial o reembolso total depende de la cantidad devuelta
        // con el pago con tarjeta, el estado de los reembolsos funciona correctamente porque
        // la propia transacción devuelve el estado correcto
        // No funciona correctamente cuando se realizar multiples rebates hasta acabar con el amount
        $id_order_state = $this->getNewOrderState($order, $transaction);

        // Como se comporta esto con tarjeta??
        if ($transaction->getOperationType() === self::OPERATION_REBATE
            || $transaction->getOperationType() === self::OPERATION_REFUND
        ) {
            $this->createOrderSlip($order, $transaction);

            $remaining_amount = $this->getRemainingAmount($order, $transaction);

            if ($remaining_amount <= 0) {
                $id_order_state = (int)Configuration::get('PS_OS_REFUND');
            }
        }

        // If the transaction does not change the order state, skip.
        if ($id_order_state === (int) $order->getCurrentState()) {
            return $order;
        }

        $use_existing_payment = false;
        // Add payment if the status is success.
        if ($id_order_state === (int)Configuration::get('PS_OS_PAYMENT')) {
            $use_existing_payment = $order->addOrderPayment(
                $transaction->getAmount(),
                $this->module->name,
                $transaction->getPayFrexTransactionId()
            );
        }

        // Set the order status
        $new_history = new OrderHistory();
        $new_history->id_order = $order->id;
        $new_history->changeIdOrderState((int)$id_order_state, $order, $use_existing_payment);
        $new_history->addWithemail(true, $extraVars);

        return $order;
    }

    /**
     * Get the order new state.
     *
     * @param \Order                                                         $order
     * @param \ComerciaGlobalPayments\AddonPayments\SDK\Response\Transaction $transaction
     *
     * @return int
     */
    private function getNewOrderState(Order $order, Transaction $transaction): int {
        $map = $this->getOrderStatesMap();

        if (array_key_exists($transaction->getStatus(), $map)) {
            return $map[$transaction->getStatus()];
        }

        // @TODO Add constants to OperationInterface
        // Handle CAPTURE
        $paymentOperations = [
            self::OPERATION_DEBIT,
            self::OPERATION_CREDIT
        ];

        if (OperationInterface::STATUS_SUCCESS === $transaction->getStatus()
            && in_array($transaction->getOperationType(), $paymentOperations,true)) {
            return (int)Configuration::get('PS_OS_PAYMENT');
        }

        if (OperationInterface::STATUS_PENDING === $transaction->getStatus()
            && in_array($transaction->getOperationType(), $paymentOperations,
                true)) {
            return $map[OperationInterface::STATUS_PENDING];
        }

        // Handle VOID
        if (OperationInterface::STATUS_SUCCESS === $transaction->getStatus()
            && self::OPERATION_VOID === $transaction->getOperationType()) {
            return $map[OperationInterface::STATUS_CANCELLED];
        }

        // Handle REFUND
        if (OperationInterface::STATUS_SUCCESS === $transaction->getStatus()
            && self::OPERATION_REFUND === $transaction->getOperationType()) {
            return (int)Configuration::get('PS_OS_REFUND');
        }

        // Handle REBATE (partial refund)
        if (OperationInterface::STATUS_SUCCESS === $transaction->getStatus()
            && self::OPERATION_REBATE === $transaction->getOperationType()) {
            return (int)Configuration::get(addonpayments::PS_OS_EASYPAYMENTGATEWAY_REBATE);
        }

        return (int)Configuration::get(addonpayments::PS_OS_EASYPAYMENTGATEWAY_PROCESSING);
    }

    /**
     * Get map for API responses status and order states.
     *
     * @return int[]
     */
    private function getOrderStatesMap(): array
    {
        return [
            OperationInterface::STATUS_ERROR => (int)Configuration::get('PS_OS_ERROR'),
            OperationInterface::STATUS_FAIL => (int)Configuration::get('PS_OS_ERROR'),
            OperationInterface::STATUS_CANCELLED => (int)Configuration::get('PS_OS_CANCELED'),
            OperationInterface::STATUS_PENDING => (int)Configuration::get(addonpayments::PS_OS_EASYPAYMENTGATEWAY_PENDING),
            OperationInterface::STATUS_AWAITING_PAYSOL => (int)Configuration::get(addonpayments::PS_OS_EASYPAYMENTGATEWAY_PENDING_CONFIRMATION),
            OperationInterface::STATUS_REDIRECTED => (int)Configuration::get(addonpayments::PS_OS_EASYPAYMENTGATEWAY_PROCESSING),
            OperationInterface::STATUS_REJECTED => (int)Configuration::get('PS_OS_CANCELED'),
            OperationInterface::STATUS_PENDING_CONFIRMATION => (int)Configuration::get(addonpayments::PS_OS_EASYPAYMENTGATEWAY_PENDING_CONFIRMATION),
            OperationInterface::STATUS_ERROR3DS => (int)Configuration::get('PS_OS_ERROR'),
        ];
    }


    /**
     * @param Order $order
     * @param $transaction
     *
     * @return bool|int
     */
    private function createOrderSlip($order, $transaction) {
        $product_list = [];
        $amount = 0;
        $shipping = 0;
        $amount_chosen = false;

        if(addonpayments::BNPL_NAME !== $transaction->getPaymentSolution() && 'nemuru' !== $transaction->getPaymentSolution()) {
            return false;
        }

        if(empty($transaction->getDetails())) {
            return false;
        }

        $details = json_decode($transaction->getDetails());
        $refunded_items = property_exists($details, 'refunded_items') ? $details->refunded_items : [];

        if(!empty($refunded_items)) {
            $order_products = $order->getProducts();
            $order_products_references = array_column($order_products, 'product_reference', 'id_order_detail');

            foreach ($refunded_items as $item) {

                if ('shipping_fee' === $item->article->category) {
                    $shipping = $item->total_price_with_tax->amount;
                    continue;
                }

                $key = array_search($item->article->reference, $order_products_references, true);

                // TODO: tax?
                $product_list[$key] = [
                    'quantity' => $item->units,
                    'id_order_detail' => $key,
                    'amount' => $item->total_price_with_tax->amount,
                    'unit_price' => $item->total_price_with_tax->amount / $item->units,
                ];
            }
        }

        if( property_exists($details, 'refunded_amount') && !empty($details->refunded_amount)) {
            $amount = $details->refunded_amount;
            $amount_chosen = true;
        }

        if (_PS_MODE_DEV_) {
            set_error_handler(['ControllerCore', 'myErrorHandler']);
        }

        if(empty($product_list) && empty($shipping)) {
            $order_slip = $this->createPartialOrderSlip(
                $order,
                $amount,
                $shipping,
                $order->getOrderDetailList()
            );

        }else {
            $order_slip = OrderSlip::create(
                $order,
                $product_list,
                $shipping,
                $amount,
                $amount_chosen,
                false
            );
        }

        if (_PS_MODE_DEV_) {
            restore_error_handler();
        }

        return $order_slip;
    }

    /**
     * Function copied from OrderSlip::createPartialOrderSlip but without error :)
     *
     * @param Order $order
     * @param float $amount
     * @param float $shipping_cost_amount
     * @param array $order_detail_list
     *
     * @return bool
     * @throws \PrestaShopDatabaseException
     * @throws \PrestaShopException
     */
    private function createPartialOrderSlip($order, $amount, $shipping_cost_amount, $order_detail_list)
    {
        $currency = new \Currency($order->id_currency);
        $orderSlip = new OrderSlip();
        $orderSlip->id_customer = (int)$order->id_customer;
        $orderSlip->id_order = (int)$order->id;
        $orderSlip->amount = (float)$amount;
        $orderSlip->shipping_cost = false;
        $orderSlip->shipping_cost_amount = (float)$shipping_cost_amount;
        $orderSlip->conversion_rate = $currency->conversion_rate;
        $orderSlip->partial = 1;

        // Added to prevent error
        $orderSlip->total_products_tax_excl = 0;
        $orderSlip->total_products_tax_incl = 0;
        $orderSlip->total_shipping_tax_excl = 0;
        $orderSlip->total_shipping_tax_incl = 0;

        // type 2 for chosen amount
        $orderSlip->order_slip_type = 2;

        if (!$orderSlip->add()) {
            return false;
        }

        $orderSlip->addPartialSlipDetail($order_detail_list);

        return true;
    }

    /**
     * @param Order $order
     * @param $transaction
     *
     * @return float|int|mixed
     */
    private function getRemainingAmount($order, $transaction)
    {
        $remaining_amount = $transaction->getRemainingAmount();

        if(null !== $transaction->getRemainingAmount()) {
            return $remaining_amount;
        }

        $order_slips = $order->getOrderSlipsCollection()->getResults();
        $total_refunded = 0;

        if (!empty($order_slips)) {
            foreach ($order_slips as $order_slip) {
                /** @var OrderSlip $order_slip */
                $total_refunded += $order_slip->amount;
            }

            $remaining_amount = round($order->total_paid - $total_refunded, 2);
        }

        return $remaining_amount;
    }
}
