<?php

/**
 * Copyright © 2016 Filoblu S.r.l. All rights reserved.
 */

namespace FiloBlu\Esb\Model\Rest;

use Exception;
use FiloBlu\Esb\Api\Data\AcknowledgeInterface;
use FiloBlu\Esb\Api\ExternalCreditmemoInterface;
use FiloBlu\Esb\Helper\LoggerProvider;
use FiloBlu\OrderAdditionalData\Model\AdditionalDataFactory;
use Magento\Framework\Api\SearchCriteriaBuilder;
use Magento\Framework\App\Config\ScopeConfigInterface;
use Magento\Framework\App\Filesystem\DirectoryList;
use Magento\Framework\Exception\FileSystemException;
use Magento\Framework\Exception\LocalizedException;
use Magento\Framework\Filesystem;
use Magento\Framework\Filesystem\File\Write;
use Magento\Framework\Filesystem\File\WriteInterface;
use Magento\Framework\Phrase;
use Magento\Sales\Api\Data\CreditmemoInterface;
use Magento\Sales\Api\Data\OrderExtensionFactory;
use Magento\Sales\Api\Data\OrderInterface;
use Magento\Sales\Exception\CouldNotRefundException;
use Magento\Sales\Model\Order\CreditmemoFactory;
use Magento\Sales\Model\Order\Email\Container\CreditmemoIdentity;
use Magento\Sales\Model\Order\Item;
use Magento\Sales\Model\OrderRepository;
use Magento\Sales\Model\Service\CreditmemoService;
use Magento\Store\Model\ScopeInterface;
use Monolog\Logger;
use function array_key_exists;
use function count;
use function in_array;

/**
 * REST Api to save external creditmemos
 */
class ExternalCreditmemo implements ExternalCreditmemoInterface
{

    /**
     * @var OrderExtensionFactory
     */
    protected $orderRepository;

    /**
     * @var SearchCriteriaBuilder
     */
    protected $searchCriteriaBuilder;

    /**
     * @var ScopeConfigInterface
     */
    protected $scopeConfig;

    /**
     * @var OrderExtensionFactory
     */
    protected $orderExtensionFactory;

    /**
     * @var CreditmemoFactory
     */
    protected $creditMemoFactory;

    /**
     * @var CreditmemoService
     */
    protected $creditMemoService;

    /**
     * @var Filesystem
     */
    protected $filesystem;

    /**
     * @var Logger
     */
    protected $logger;

    /**
     * @var AcknowledgeInterface
     */
    protected $response;

    /**
     * @var AdditionalDataFactory
     */
    protected $additionalDataFactory;

    /**
     * @param ScopeConfigInterface $scopeConfig
     * @param OrderRepository $orderRepository
     * @param OrderExtensionFactory $orderExtensionFactory
     * @param CreditmemoFactory $creditMemoFactory
     * @param CreditmemoService $creditMemoService
     * @param Filesystem $filesystem
     * @param SearchCriteriaBuilder $searchCriteriaBuilder
     * @param LoggerProvider $loggerProvider
     * @param AcknowledgeInterface $response
     * @param AdditionalDataFactory $additionalDataFactory
     */
    public function __construct(
        ScopeConfigInterface  $scopeConfig,
        OrderRepository       $orderRepository,
        OrderExtensionFactory $orderExtensionFactory,
        CreditmemoFactory     $creditMemoFactory,
        CreditmemoService     $creditMemoService,
        Filesystem            $filesystem,
        SearchCriteriaBuilder $searchCriteriaBuilder,
        LoggerProvider        $loggerProvider,
        AcknowledgeInterface  $response,
        AdditionalDataFactory $additionalDataFactory
    ) {
        $this->scopeConfig = $scopeConfig;
        $this->orderRepository = $orderRepository;
        $this->orderExtensionFactory = $orderExtensionFactory;
        $this->creditMemoFactory = $creditMemoFactory;
        $this->creditMemoService = $creditMemoService;
        $this->filesystem = $filesystem;
        $this->searchCriteriaBuilder = $searchCriteriaBuilder;
        $this->logger = $loggerProvider->getLogger();
        $this->response = $response;
        $this->additionalDataFactory = $additionalDataFactory;
    }

    /**
     * Save External Credit Memo data. Save PDF file and additional data on Sales Order.
     *
     * @param CreditmemoInterface $creditmemo_data
     * @return AcknowledgeInterface
     * @throws CouldNotRefundException
     * @throws FileSystemException
     * @throws LocalizedException
     * @throws Exception
     */
    public function save(CreditmemoInterface $creditmemo_data): AcknowledgeInterface
    {
        $creditMemoExtensionAttributes = $creditmemo_data->getExtensionAttributes();

        if ($creditMemoExtensionAttributes === null) {
            $message = __('Missing extension attributes');
            $this->logger->critical($message->render());
            throw new CouldNotRefundException(new Phrase($message->render()));
        }
        if (!$creditMemoExtensionAttributes->getOrderIncrementId()) {
            $message = __('Missing order increment id');
            $this->logger->critical($message->render());
            throw new CouldNotRefundException(new Phrase($message->render()));
        }
        if (!$creditMemoExtensionAttributes->getDocumentType()) {
            $message = __('Missing document type');
            $this->logger->critical($message->render());
            throw new CouldNotRefundException(new Phrase($message->render()));
        }
        if ($creditmemo_data->getIncrementId() === null) {
            $message = __('Credit memo increment id is missing.');
            $this->logger->critical($message->render());
            throw new CouldNotRefundException(new Phrase($message->render()));
        }
        $orderIncrementId = $creditMemoExtensionAttributes->getOrderIncrementId();
        $creditMemoIncrementId = $creditmemo_data->getIncrementId();
        $documentType = $creditMemoExtensionAttributes->getDocumentType();
        $filename = "order_{$orderIncrementId}_{$documentType}_$creditMemoIncrementId.pdf";
        $filename = preg_replace('/[^0-9A-Za-z.]+/', '_', $filename);
        $documentFolder = $this->scopeConfig->getValue('esb/print_documents/document_folder');
        $writer = $this->filesystem->getDirectoryWrite(DirectoryList::VAR_DIR);
        /** @var WriteInterface|Write $file */
        $file = $writer->openFile($documentFolder . DIRECTORY_SEPARATOR . $filename, 'w');
        try {
            $file->lock();
            try {
                $file->write(base64_decode($creditMemoExtensionAttributes->getDocumentPdf()));
            } finally {
                $file->unlock();
            }
        } finally {
            $file->close();
        }

        $refundOnline = $creditMemoExtensionAttributes->getRefundOnline();
        $creditMemo = [
            'increment_id'  => $creditmemo_data->getIncrementId(),
            'document_type' => $creditMemoExtensionAttributes->getDocumentType(),
            'grand_total'   => $creditmemo_data->getGrandTotal(),
            'document_pdf'  => $filename,
            'refund_online' => $refundOnline,
        ];

        $searchCriteria = $this->searchCriteriaBuilder->addFilter(
            'increment_id',
            $creditMemoExtensionAttributes->getOrderIncrementId()
        )->create();
        $orderList = $this->orderRepository->getList($searchCriteria);

        if (count($orderList) !== 1) {
            $message = __('Cannot find order %1', $creditMemoExtensionAttributes->getOrderIncrementId());
            $this->logger->critical($message->render());
            throw new CouldNotRefundException(new Phrase($message->render()));
        }
        try {
            $items = $orderList->getItems();
            $order = array_shift($items);
            $itemsToSkip = $this->getItemsToSkip($order);

            if (!$order->canCreditmemo()) {
                $message = __(
                    'Cannot create credit memo for order %1',
                    $creditMemoExtensionAttributes->getOrderIncrementId()
                );
                $this->logger->critical($message->render());
                throw new CouldNotRefundException(new Phrase($message->render()));
            }
            $quantities = [];
            $creditMemoItems = $creditmemo_data->getItems();

            $mustSendEmail = false;

            foreach ($order->getItems() as $orderItem) {

                if (in_array($orderItem->getSku(), $itemsToSkip, true)) {
                    continue;
                }

                foreach (($creditMemoItems ?? []) as $creditMemoItem) {
                    if ($orderItem->getTypeId() !== 'configurable' && $orderItem->getTypeId() !== 'bundle' && $creditMemoItem['sku'] === $orderItem->getSku()) {
                        $quantities[$orderItem->getId()] = $creditMemoItem['qty'];
                        break;
                    }
                }

                $mustSendEmail = true;
            }
            if (empty($quantities)) {
                foreach ($order->getItems() as $orderItem) {
                    if ($orderItem->getTypeId() !== 'configurable' && $orderItem->getTypeId() !== 'bundle') {
                        $quantities[$orderItem->getId()] = 0;
                    }
                }
            }
            $shippingAmount = $creditmemo_data->getShippingInclTax();

            if (abs($shippingAmount - $order->getShippingInclTax()) < 0.5) {
                $shippingAmount = $order->getShippingInclTax();
            }

            $paymentFeeAmount = (float)$creditMemoExtensionAttributes->getPaymentFeeAmount();
            $paymentTaxAmount = $creditMemoExtensionAttributes->getPaymentTaxAmount();

            if ($paymentTaxAmount !== null && $paymentTaxAmount > 0) {
                $paymentFeeAmount += (float)$paymentTaxAmount;
            }

            $orderPaymentFeeAmount = $order->getMspCodAmount();

            if (abs($paymentFeeAmount - $orderPaymentFeeAmount) < 0.5) {
                $paymentFeeAmount = $orderPaymentFeeAmount;
            }

            $initData = [
                'qtys'                => $quantities,
                'shipping_amount'     => $shippingAmount,
                'base_msp_cod_amount' => $paymentFeeAmount
            ];

            $totalAdjustments = 0.0;
            $adjustment_positive = $creditmemo_data->getBaseAdjustmentPositive();
            if ($adjustment_positive) {
                $this->logger->info("Adjustment Positive: $adjustment_positive");
                /** @noinspection PhpCastIsUnnecessaryInspection */
                $totalAdjustments += (float)$adjustment_positive;
                $initData['adjustment_positive'] = $adjustment_positive;
            }

            $adjustment_negative = $creditmemo_data->getBaseAdjustmentNegative();
            if ($adjustment_negative) {
                $this->logger->info("Adjustment Negative: $adjustment_negative");
                /** @noinspection PhpCastIsUnnecessaryInspection */
                $totalAdjustments -= (float)$adjustment_negative;
                $initData['adjustment_negative'] = $adjustment_negative;
            }

            $items = [];
            $newSubtotal = 0;
            $newDiscount = 0;
            $newGrandTotal = 0;

            $processed = [];

            if ($itemsToSkip) {
                /** @var Item $item */
                foreach ($order->getAllItems() as $item) {

                    if (in_array($item->getSku(), $itemsToSkip)) {
                        if (isset($initData['qtys'][$item->getItemId()])) {
                            unset($initData['qtys'][$item->getItemId()]);
                        }
                        continue;
                    }

                    $items[] = $item;

                    if (!in_array($item->getSku(), $processed)) {
                        $newSubtotal =+ $item->getRowTotalInclTax();
                        $newDiscount =+ $item->getDiscountAmount();
                        $processed[] = $item->getSku();
                    }

                }

                if ($newSubtotal) {
                    $newGrandTotal = ($newSubtotal + $shippingAmount) - $newDiscount;
                }

            }


            if ($newGrandTotal) {
                $grandTotal = $newGrandTotal;
                $creditmemo_data->setGrandTotal($grandTotal);
                $creditMemo['grand_total'] = $grandTotal;
            }

            if ($items) {
                $order->setItems($items);
            }

            $creditMemoObject = $this->creditMemoFactory->createByOrder($order, $initData);

            //add Netsuite credit memo id to send into Adyen transaction
            $creditMemoObject->setData('netsuite_increment_id', $creditMemo['increment_id']);
            $grandTotal = (float)($creditmemo_data->getGrandTotal()) + $totalAdjustments;

            /** @noinspection PhpCastIsUnnecessaryInspection */
            if (abs((float)$creditMemoObject->getGrandTotal() - $grandTotal) > 0.1) {
                $message = __('Grand Total is not matching: %1 <> %2', $creditMemoObject->getGrandTotal(), $grandTotal);
                $this->logger->critical($message->render());
                throw new CouldNotRefundException(new Phrase($message->render()));
            }

            if ($refundOnline) {
                $invoices = $order->getInvoiceCollection();
                if ($invoices->getSize() !== 1) {
                    $message = __('No invoices to refund.');
                    $this->logger->critical($message->render());
                    throw new CouldNotRefundException(new Phrase($message->render()));
                }
                $invoice_obj = $invoices->getFirstItem();
                $creditMemoObject->setInvoice($invoice_obj);
                $creditMemo = $this->creditMemoService->refund($creditMemoObject);
            } else {
                $creditMemo = $this->creditMemoService->refund($creditMemoObject, true);
            }

            $additionalDataObject = $this->additionalDataFactory->create();
            $orderExtensionAttributes = $order->getExtensionAttributes();
            $additionalData = ['external_creditmemos' => [$creditmemo_data->getIncrementId() => $creditMemo]];
            if ($orderExtensionAttributes) {
                if ($orderExtensionAttributes->getFbOad()) {
                    $additionalDataObject = $orderExtensionAttributes->getFbOad();
                    $additionalData = [];
                    foreach ($additionalDataObject->getCustomAttributes() as $customAttribute) {
                        $additionalData[$customAttribute->getAttributeCode()] = $customAttribute->getValue();
                    }
                    if (array_key_exists('external_creditmemos', $additionalData)) {
                        $additionalData['external_creditmemos'][$creditmemo_data->getIncrementId()] = $creditMemo;
                    } else {
                        $additionalData['external_creditmemos'] = [$creditmemo_data->getIncrementId() => $creditMemo];
                    }
                }
            } else {
                $orderExtensionAttributes = $this->orderExtensionFactory->create();
            }

            foreach ($additionalData as $field => $value) {
                $additionalDataObject->setCustomAttribute($field, $value);
            }

            $orderExtensionAttributes->setFbOad($additionalDataObject);
            $order->setExtensionAttributes($orderExtensionAttributes);
            $order->addStatusHistoryComment(
                __('External refund has been saved: %1 %2', $documentType, $creditMemoIncrementId)
            );
            $order->addStatusHistoryComment(__('Order %1 has been refunded', $order->getIncrementId()));
            $this->orderRepository->save($order);

            if ($mustSendEmail) {
                $this->creditMemoService->notify($creditMemo->getEntityId());
            }

            $this->response->setStatus('success');
            $this->response->setCode($documentType . '_' . $creditMemoIncrementId);
            $this->response->setMessage(
                __(
                    'Order %1 has been refunded:  %2 %3 has been created',
                    $order->getIncrementId(),
                    $documentType,
                    $creditMemoIncrementId
                )
            );
            $this->response->setDescription(
                __(
                    'Magento Creditmemo: %1. Refunded Amount: %2',
                    $creditMemoObject->getIncrementId(),
                    $creditMemoObject->getGrandTotal()
                )
            );
            $this->response->setFlow('create_refund');
            $this->response->setOrderIncrementId($order->getIncrementId());
            return $this->response;
        } catch (Exception $e) {
            $this->logger->critical($e->getMessage());
            throw $e;
        }
    }

    /**
     * Returns a  list of sku to skip
     * @param OrderInterface $order
     * @return array
     */
    public function getItemsToSkip(OrderInterface $order): array
    {
        return [];
    }
}
