<?php
declare(strict_types=1);

namespace FiloBlu\Esb\Model\Rest;

use DateTime;
use DateInterval;
use Exception;
use FiloBlu\Esb\Api\Data\AcknowledgeInterface;
use FiloBlu\Esb\Api\ExternalCreditmemoInterface;
use FiloBlu\Esb\Api\ExternalCreditmemoV2Interface;
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\AlreadyExistsException;
use Magento\Framework\Exception\FileSystemException;
use Magento\Framework\Exception\InputException;
use Magento\Framework\Exception\LocalizedException;
use Magento\Framework\Exception\NoSuchEntityException;
use Magento\Framework\Filesystem;
use Magento\Framework\Filesystem\File\Write;
use Magento\Framework\Filesystem\File\WriteInterface;
use Magento\Framework\Phrase;
use Magento\Sales\Api\CreditmemoManagementInterface;
use Magento\Sales\Api\Data\CreditmemoInterface;
use Magento\Sales\Api\Data\InvoiceInterface;
use Magento\Sales\Api\Data\OrderExtensionFactory;
use Magento\Sales\Api\Data\OrderInterface;
use Magento\Sales\Api\OrderRepositoryInterface;
use Magento\Sales\Exception\CouldNotRefundException;
use Magento\Sales\Model\Order;
use Magento\Sales\Model\Order\CreditmemoFactory;
use Magento\Sales\Model\Order\Invoice;
use Magento\Sales\Model\OrderRepository;
use Magento\Sales\Model\Service\CreditmemoService;
use Monolog\Logger;
use function array_key_exists;
use function in_array;

/**
 * REST Api to save external creditmemos
 */
class ExternalCreditmemoV2 implements ExternalCreditmemoV2Interface
{

    /**
     * @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;
    /**
     * @var CreditmemoManagementInterface
     */
    private $creditmemoManagement;

    /**
     * @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
     * @param CreditmemoManagementInterface $creditmemoManagement
     */
    public function __construct(ScopeConfigInterface          $scopeConfig,
                                OrderRepositoryInterface      $orderRepository,
                                OrderExtensionFactory         $orderExtensionFactory,
                                CreditmemoFactory             $creditMemoFactory,
                                CreditmemoService             $creditMemoService,
                                Filesystem                    $filesystem,
                                SearchCriteriaBuilder         $searchCriteriaBuilder,
                                LoggerProvider                $loggerProvider,
                                AcknowledgeInterface          $response,
                                AdditionalDataFactory         $additionalDataFactory,
                                CreditmemoManagementInterface $creditmemoManagement)
    {
        $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;
        $this->creditmemoManagement = $creditmemoManagement;
    }

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

        $documentType = $creditMemoExtensionAttributes ? $creditMemoExtensionAttributes->getDocumentType() : null;

        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 ($creditmemoData->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 = $creditmemoData->getIncrementId();
        $order = $this->getOrder($creditmemoData);

        $orderGrandTotal = $order->getGrandTotal();
        if($creditmemoData->getGrandTotal() > $orderGrandTotal){
            $this->logger->info('Capping grand total to order total: Credit Memo Grand Total : ' . $creditmemoData->getGrandTotal() . ' Order Grand Total: ' . $orderGrandTotal);
            $creditmemoData->setGrandTotal($orderGrandTotal);
        }
        $this->createCreditMemoAndRefund($order, $creditmemoData);
        $this->storeAdditionalDataIntoOrder($order, $creditmemoData);

        $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', $creditmemoData->getIncrementId(), $creditmemoData->getGrandTotal()));
        $this->response->setFlow('create_refund');
        $this->response->setOrderIncrementId($order->getIncrementId());
        return $this->response;

    }

    /**
     * Save Credit Memo PDF file to the filesystem
     * @throws FileSystemException
     */
    public function saveCreditMemoFile(CreditmemoInterface $creditmemo)
    {
        $extensionAttributes = $creditmemo->getExtensionAttributes();

        if (!$extensionAttributes) {
            throw RuntimeException('Missing extension attributes on credit memo');
        }
        $orderIncrementId = $extensionAttributes->getOrderIncrementId();
        $creditMemoIncrementId = $creditmemo->getIncrementId();
        $documentType = $extensionAttributes->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($extensionAttributes->getDocumentPdf()));
            } catch (FileSystemException $e) {
                $this->logger->critical(__('Error writing credit memo file: %1', $e->getMessage()));
            } finally {
                $file->unlock();
            }
        } finally {
            $file->close();
        }

        return $filename;
    }

    /**
     * Get Order from Credit Memo
     *
     * @param CreditmemoInterface $creditmemo
     * @return OrderInterface|Order
     * @throws NoSuchEntityException
     * @throws CouldNotRefundException
     */
    public function getOrder(CreditmemoInterface $creditmemo): OrderInterface
    {
        $extensionAttributes = $creditmemo->getExtensionAttributes();

        if (!$extensionAttributes) {
            throw new NoSuchEntityException(__('Missing extension attributes on credit memo'));
        }

        $searchCriteria = $this->searchCriteriaBuilder->addFilter('increment_id', $extensionAttributes->getOrderIncrementId())->create();

        $orderList = $this->orderRepository->getList($searchCriteria);

        if ($orderList->getTotalCount() === 0) {
            throw new NoSuchEntityException(__('Order not found: %1', $extensionAttributes->getOrderIncrementId()));
        }

        $list = $orderList->getItems();
        $order = array_shift($list);

        if (!$order->canCreditmemo()) {
            $message = __('Cannot create credit memo for order %1', $order->getIncrementId());
            $this->logger->critical($message->render());
            throw new CouldNotRefundException(new Phrase($message->render()));
        }

        return $order;
    }

    /**
     * @param OrderInterface|Order $order
     * @param CreditmemoInterface $creditmemoData
     * @return CreditmemoInterface[]
     * @throws FileSystemException
     */
    public function createCreditMemoAndRefund(OrderInterface $order, CreditmemoInterface $creditmemoData): array
    {
        $itemsToSkip = $this->getItemsToSkip($order);

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

        $offlineRequested = false;

        if ($creditmemoData->getExtensionAttributes() && !$creditmemoData->getExtensionAttributes()->getRefundOnline()) {
            $offlineRequested = true;
        }

        foreach ($creditmemoData->getItems() as $item) {
            $creditMemoItems[$item->getSku()] = $item->getQty();
        }

        $mustSendEmail = false;

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

            $sku = $orderItem->getSku();

            if (in_array($sku, $itemsToSkip, true)) {
                continue;
            }

            if (!isset($creditMemoItems[$sku])) {
                continue;
            }

            $initData['qtys'][$orderItem->getId()] = $creditMemoItems[$sku];
            $mustSendEmail = true;
        }

        if ($this->isSendEmailDisallowedByConfiguration($creditmemoData)) {
            $mustSendEmail = false;
        }

        /** @var InvoiceInterface | Invoice $invoice */
        $invoice = $order->getInvoiceCollection()->getFirstItem();
        $creditMemoObject = $this->creditMemoFactory->createByOrder($order, $initData);

        // Add Netsuite credit memo id to send into Adyen transaction
        $creditMemoObject->setData('netsuite_increment_id', $creditmemoData->getIncrementId());
        $creditMemoObject->setInvoice($invoice);

        $orderRateToBase = 1.0 / $order->getBaseToOrderRate() ?? 1.0;

        /** @see \Magento\GiftCardAccount\Observer\CreditmemoSaveAfter::refundToGiftCardAccount */
        $giftCardsAmount = null;
        if ($creditmemoData->getExtensionAttributes() && $creditmemoData->getExtensionAttributes()->getGiftCardsAmount()) {
            $giftCardsAmount = $creditmemoData->getExtensionAttributes()->getGiftCardsAmount();
        }
        
        $grandTotal = $creditmemoData->getGrandTotal();

        if($creditmemoData->getBaseAdjustmentNegative() > 0) {
            $this->logger->info('Refund with adjustment negative: ' . $creditmemoData->getBaseAdjustmentNegative());
            $creditMemoObject->setAdjustmentNegative($creditmemoData->getBaseAdjustmentNegative());
            $creditMemoObject->setBaseAdjustmentNegative($orderRateToBase * $creditmemoData->getBaseAdjustmentNegative());

            // check how charge the adjustment negative
            $grandTotal = $creditmemoData->getGrandTotal();
            $this->logger->info('Grand Total: ' . $grandTotal);
            if($grandTotal >= $creditmemoData->getBaseAdjustmentNegative()) {
                $grandTotal = $grandTotal - $creditmemoData->getBaseAdjustmentNegative();
                $this->logger->info('Grand Total after adjustment negative: ' . $grandTotal);
            }
            else {
                $this->logger->info('Grand Total is less than the adjustment');
                $chargeToGiftCard = $creditmemoData->getBaseAdjustmentNegative();
                if($grandTotal > 0) {
                    $adjustmentNegative = $creditmemoData->getBaseAdjustmentNegative() - $grandTotal;
                    $chargeToGiftCard = $adjustmentNegative;
                    $grandTotal = 0;
                    $this->logger->info('Grand Total after adjustment negative: ' . $grandTotal);
                }
                if($giftCardsAmount !== null && $giftCardsAmount > 0 && $chargeToGiftCard > 0) {
                    $this->logger->info('Adjustment to be charged to the gift card: ' . $chargeToGiftCard);
                    if($giftCardsAmount >= $chargeToGiftCard) {
                        $giftCardsAmount = $giftCardsAmount - $chargeToGiftCard;
                    } else {
                        $giftCardsAmount = 0;
                    }
                    $this->logger->info('Gift card amount after adjustment negative: ' . $giftCardsAmount);
                }
            }
        }

        $orderGrandTotal = $order->getGrandTotal();
        if($grandTotal > $orderGrandTotal){
            $this->logger->info('Capping grand total to order total: Credit Memo Grand Total : ' . $grandTotal . ' Order Grand Total: ' . $orderGrandTotal);
            $grandTotal = $orderGrandTotal;
        }

        // Update credit memo data with the one provided by the API
        $creditMemoObject->setShippingAmount($creditmemoData->getShippingAmount());
        $creditMemoObject->setBaseShippingAmount($orderRateToBase * $creditmemoData->getShippingAmount());
        $creditMemoObject->setGrandTotal($grandTotal);
        $creditMemoObject->setBaseGrandTotal($orderRateToBase * $grandTotal);
        $creditMemoObject->setSubtotal($creditmemoData->getSubtotal());
        $creditMemoObject->setBaseSubtotal($orderRateToBase * $creditmemoData->getSubtotal());
        $creditMemoObject->setTaxAmount($creditmemoData->getTaxAmount());
        $creditMemoObject->setBaseTaxAmount($orderRateToBase * $creditmemoData->getTaxAmount());
        $creditMemoObject->setSubtotalInclTax($creditmemoData->getSubtotal());
        $creditMemoObject->setBaseSubtotalInclTax($orderRateToBase * $creditmemoData->getSubtotal());

        if(!$mustSendEmail) {
            $creditmemoData->setEmailSent(true);
            $creditmemoData->setSendEmail(true);
        }

        if($giftCardsAmount) {
            $creditMemoObject->setGiftCardsAmount($giftCardsAmount);
            $creditMemoObject->setBaseGiftCardsAmount($orderRateToBase * $giftCardsAmount);
        }

        $this->creditmemoManagement->refund($creditMemoObject, $offlineRequested);

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

        $this->saveCreditMemoFile($creditmemoData);

        return [$creditMemoObject];
    }

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

    /**
     * @param OrderInterface $order
     * @param CreditmemoInterface $creditmemoData
     * @return void
     * @throws NoSuchEntityException
     * @throws AlreadyExistsException
     * @throws InputException
     */
    public function storeAdditionalDataIntoOrder(OrderInterface $order, CreditmemoInterface $creditmemoData)
    {
        $extensionAttributes = $creditmemoData->getExtensionAttributes();

        if (!$extensionAttributes) {
            throw new NoSuchEntityException(__('Missing extension attributes on credit memo'));
        }

        $filename = '';
        $refundOnline = '';
        $creditMemo = ['increment_id' => $creditmemoData->getIncrementId(), 'document_type' => $extensionAttributes->getDocumentType(), 'grand_total' => $creditmemoData->getGrandTotal(), 'document_pdf' => $filename, 'refund_online' => $refundOnline];
        $additionalDataObject = $this->additionalDataFactory->create();
        $orderExtensionAttributes = $order->getExtensionAttributes();
        $additionalData = ['external_creditmemos' => [$creditmemoData->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'][$creditmemoData->getIncrementId()] = $creditMemo;
                } else {
                    $additionalData['external_creditmemos'] = [$creditmemoData->getIncrementId() => $creditMemo];
                }
            }
        } else {
            $orderExtensionAttributes = $this->orderExtensionFactory->create();
        }

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

        $orderExtensionAttributes->setFbOad($additionalDataObject);
        $order->setExtensionAttributes($orderExtensionAttributes);
        $order->addCommentToStatusHistory(__('External refund has been saved: %1 %2', $extensionAttributes->getDocumentType(), $creditmemoData->getIncrementId()));
        $order->addCommentToStatusHistory(__('Order %1 has been refunded', $order->getIncrementId()));
        $this->orderRepository->save($order);
    }

    /**
     * @param CreditmemoInterface $creditmemo
     * @return bool
     */
    public function isSendEmailDisallowedByConfiguration(CreditmemoInterface $creditmemo): bool
    {
        $disableEmailNotification = (int)$this->scopeConfig->getValue('esb/external_creditmemo/disable_email_notification');

        if ($disableEmailNotification == 0) { // No
            $this->logger->info('Email notification enabled by configuration (No).');
            return false;
        }

        if ($disableEmailNotification == 1) { // Yes
            $this->logger->info('Email notification disabled by configuration (Yes).');
            return true;
        }

        if ($disableEmailNotification == 2) { // Only for older than days
            $olderThanDays = (int)$this->scopeConfig->getValue('esb/external_creditmemo/older_than_days');
            $this->logger->info('Email notification check for older than days: ' . $olderThanDays);
            if ($olderThanDays > 0) {
                try {
                    $creditmemoCreatedAt = new DateTime($creditmemo->getCreatedAt());
                    $days = (new DateTime())->diff($creditmemoCreatedAt)->format('%d');

                    if ((int)$days > $olderThanDays) {
                        $this->logger->info('Credit memo is older than ' . $olderThanDays . ' days. Disabling email.');
                        return true;
                    }
                    $this->logger->info('Credit memo is not older than ' . $olderThanDays . ' days. Enabling email.');
                } catch (Exception $e) {
                    $this->logger->error('Error comparing credit memo date: ' . $e->getMessage());
                    return false;
                }
            }
        }

        // Default to sending email
        return false;
    }
}
