<?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\ExternalInvoiceInterface;
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\App\ObjectManager;
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\InvoiceInterface;
use Magento\Sales\Api\Data\OrderExtensionFactory;
use Magento\Sales\Exception\CouldNotInvoiceException;
use Magento\Sales\Model\InvoiceOrder;
use Magento\Sales\Model\Order\Invoice;
use Magento\Sales\Model\OrderRepository;
use Magento\Sales\Model\Service\InvoiceService;
use Magento\Sales\Api\Data\InvoiceItemCreationInterfaceFactory;
use Magento\Store\Model\ScopeInterface;
use Monolog\Logger;
use \Magento\Framework\DB\TransactionFactory;
use Magento\Sales\Model\Order;

use function array_key_exists;
use function define;
use function defined;
use function sprintf;

if (!defined('DS')):
    define('DS', DIRECTORY_SEPARATOR);
endif;

/**
 * REST Api to save external invoices
 */
class ExternalInvoice implements ExternalInvoiceInterface
{
    /** @var string  */
    const XML_ESB_EXTERNAL_INVOICE_CREATE_INVOICE = 'esb/external_invoice/create_invoice';

    /** @var string  */
    const XML_ESB_EXTERNAL_INVOICE_SKIP_CREATION_IF_EXISTS = 'esb/external_invoice/skip_creation_if_exists';

    /** @var string  */
    const XML_ESB_PRINT_DOCUMENTS_DOCUMENT_FOLDER = 'esb/print_documents/document_folder';

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

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

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

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

    /**
     * @var InvoiceService
     */
    protected $_invoiceService;

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

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

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

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

    /**
     * @var TransactionFactory
     */
    protected $_transactionFactory;

    /**
     * Init plugin
     *
     * @param ScopeConfigInterface $scopeConfig
     * @param OrderRepository $orderRepository
     * @param OrderExtensionFactory $orderExtensionFactory
     * @param InvoiceOrder $invoiceOrder
     * @param InvoiceService $invoiceService
     * @param Filesystem $filesystem
     * @param SearchCriteriaBuilder $searchCriteriaBuilder
     * @param LoggerProvider $loggerProvider
     * @param AcknowledgeInterface $response
     * @param AdditionalDataFactory $additionalDataFactory
     */
    public function __construct(
        ScopeConfigInterface $scopeConfig,
        OrderRepository $orderRepository,
        OrderExtensionFactory $orderExtensionFactory,
        Filesystem $filesystem,
        SearchCriteriaBuilder $searchCriteriaBuilder,
        LoggerProvider $loggerProvider,
        AcknowledgeInterface $response,
        AdditionalDataFactory $additionalDataFactory,
        InvoiceService $invoiceService,  
        TransactionFactory $transactionFactory
    )
    {
        $this->_scopeConfig = $scopeConfig;
        $this->_orderRepository = $orderRepository;
        $this->_orderExtensionFactory = $orderExtensionFactory;
        $this->_filesystem = $filesystem;
        $this->_searchCriteriaBuilder = $searchCriteriaBuilder;
        $this->_logger = $loggerProvider->getLogger();
        $this->_response = $response;
        $this->_additionalDataFactory = $additionalDataFactory;
        $this->_invoiceService = $invoiceService;        
        $this->_transactionFactory=$transactionFactory;
    }

    /**
     * @param InvoiceInterface $invoice_data
     * @return AcknowledgeInterface
     * @throws CouldNotInvoiceException
     * @throws \Magento\Framework\Exception\FileSystemException
     * @throws \Exception
     */
    public function save(InvoiceInterface $invoice_data)
    {
        $invoiceExtensionAttributes = $invoice_data->getExtensionAttributes();

        if ($invoiceExtensionAttributes == null) {
            $message = __('Cannot find order to save external document: missing extension attributes');
            $this->_logger->critical($message->render());
            throw new CouldNotInvoiceException(new Phrase($message->render()));
        }

        if (!$invoiceExtensionAttributes->getOrderIncrementId()) {
            $message = __('Cannot find order to save external document: missing order increment id');
            $this->_logger->critical($message->render());
            throw new CouldNotInvoiceException(new Phrase($message->render()));
        }

        if (!$invoiceExtensionAttributes->getDocumentType()) {
            $message = __('Cannot find order to save external document: missing document type');
            $this->_logger->critical($message->render());
            throw new CouldNotInvoiceException(new Phrase($message->render()));
        }

        if ($invoice_data->getIncrementId() == null) {
            $message = __('Cannot find order to save external document: missing document increment id');
            $this->_logger->critical($message->render());
            throw new CouldNotInvoiceException(new Phrase($message->render()));
        }

        $filename = $this->saveDocument($invoice_data);

        $orderIncrementId = $invoiceExtensionAttributes->getOrderIncrementId();
        $invoiceIncrementId = $invoice_data->getIncrementId();
        $documentType = $invoiceExtensionAttributes->getDocumentType();

        $invoice = [
            'created_at'    => $invoice_data->getCreatedAt(),
            'increment_id'  => $invoice_data->getIncrementId(),
            'document_type' => $documentType,
            'grand_total'   => $invoice_data->getGrandTotal(),
            'document_pdf'  => $filename
        ];

        /*if (count($orderList) <> 1) {
            $message = __("Cannot find order %1", $invoiceExtensionAttributes->getOrderIncrementId());
            $this->_logger->critical($message->render());
            throw new CouldNotInvoiceException(new Phrase($message->render()));
        }*/

        try {

            $order = $this->getOrder($orderIncrementId);
            $this->createInvoiceIfRequired($order, $invoice_data);

            //$order = $orderList->getFirstItem();
            $original_status = $order->getStatus();
            $orderExtensionAttributes = $order->getExtensionAttributes();
            $additionalData = ['external_invoices' => [$invoice_data->getIncrementId() => $invoice]];
            $additionalDataObject=null;
            if ($orderExtensionAttributes) {
                if ($orderExtensionAttributes->getFbOad()) {
                    $additionalDataObject = $orderExtensionAttributes->getFbOad();
                    $additionalData = [];
                    foreach ($additionalDataObject->getCustomAttributes() as $customAttribute) {
                        $additionalData[$customAttribute->getAttributeCode()] = $customAttribute->getValue();
                    }
                    if (array_key_exists('external_invoices', $additionalData)) {
                        $additionalData['external_invoices'][$invoice_data->getIncrementId()] = $invoice;
                    } else {
                        $additionalData['external_invoices'] = [$invoice_data->getIncrementId() => $invoice];
                    }
                }else{
                    $additionalDataObject = $this->_additionalDataFactory->create();
                }
            } else {
                $orderExtensionAttributes = $this->_orderExtensionFactory->create();
                $additionalDataObject = $this->_additionalDataFactory->create();
            }
            foreach ($additionalData as $field => $value) {
                $additionalDataObject->setCustomAttribute($field, $value);
            }

            $orderExtensionAttributes->setFbOad($additionalDataObject);
            $order->setExtensionAttributes($orderExtensionAttributes);
            $order->addStatusHistoryComment(__('External document has been saved: %1 %2', $documentType, $invoiceIncrementId), 'invoiced');

            if ($original_status === 'sent_to_logistics') {
                $order->addStatusHistoryComment(__('Order has been sent to logistics'), 'sent_to_logistics');
            }

            if(($order->getState() === 'processing' || $order->getState() === 'complete') && !$order->canInvoice() && !$order->canShip()) {
                $order->setState('complete');
                $order->addStatusHistoryComment('', 'complete');
            }

            $this->_orderRepository->save($order);
            $this->_response->setStatus('success');
            $this->_response->setMessage(__('External document has been saved'));
            $this->_response->setFlow('create_document');
            $this->_response->setOrderIncrementId($order->getIncrementId());
            return $this->_response;
        } catch (Exception $e) {
            $this->_logger->critical($e->getMessage());
            throw $e;
        }
    }

    /**
     * @param int|null|string $scopeCode $scopeCode
     * @return bool
     */
    public function shouldCreateIvoice($scopeCode) {
        return $this->_scopeConfig->isSetFlag(self::XML_ESB_EXTERNAL_INVOICE_CREATE_INVOICE,
            ScopeInterface::SCOPE_STORE,
            $scopeCode);
    }

    /**
     * @param int|null|string $scopeCode $scopeCode
     * @return bool
     */
    public function shouldSkipCreation($scopeCode) {
        return $this->_scopeConfig->isSetFlag(self::XML_ESB_EXTERNAL_INVOICE_SKIP_CREATION_IF_EXISTS,
            ScopeInterface::SCOPE_STORE,
            $scopeCode);
    }

    /**
     * @param string $incrementId
     * @return \Magento\Sales\Model\Order
     * @throws \Magento\Sales\Exception\CouldNotInvoiceException
     */
    public function getOrder($incrementId) {
        // Sometimes the orderList has duplicates. Use raw loadByIncrementId in that case.

        $order = null;
        try{
            $searchCriteria = $this->_searchCriteriaBuilder
                ->addFilter('increment_id', $incrementId)->create();
            $orderList = $this->_orderRepository->getList($searchCriteria);
            $items = $orderList->getItems();
            $order = array_shift($items);
        }
        catch(Exception $e){
            $this->_logger->info($e->getMessage());
            $objectManager = ObjectManager::getInstance();
            $order = $objectManager->create(Order::class)->loadByIncrementId($incrementId);
        }

        if($order === null){
            $message = __('Cannot find order %1', $incrementId);
            $this->_logger->critical($message->render());
            throw new CouldNotInvoiceException(new Phrase($message->render()));
        }

        return $order;
    }

    /**
     * @param \Magento\Sales\Api\Data\InvoiceInterface $invoiceData
     * @return array|string|string[]|null
     * @throws \Magento\Framework\Exception\FileSystemException
     * @throws \Magento\Sales\Exception\CouldNotInvoiceException
     */
    public function saveDocument(InvoiceInterface $invoiceData){

        $invoiceExtensionAttributes = $invoiceData->getExtensionAttributes();

        if ($invoiceExtensionAttributes === null) {
            $message = __('Cannot find order to save external document: missing extension attributes');
            $this->_logger->critical($message->render());
            throw new CouldNotInvoiceException(new Phrase($message->render()));
        }

        $orderIncrementId = $invoiceExtensionAttributes->getOrderIncrementId();
        $invoiceIncrementId = $invoiceData->getIncrementId();
        $documentType = $invoiceExtensionAttributes->getDocumentType();
        $filename = sprintf('order_%s_%s_%s.pdf',$orderIncrementId, $documentType, $invoiceIncrementId);
        $filename = preg_replace('/[^0-9A-Za-z.]+/', '_', $filename);
        $documentFolder = $this->_scopeConfig->getValue(self::XML_ESB_PRINT_DOCUMENTS_DOCUMENT_FOLDER);
        $writer = $this->_filesystem->getDirectoryWrite(DirectoryList::VAR_DIR);

        /** @var WriteInterface|Write $file */
        $file = $writer->openFile($documentFolder . DS . $filename, 'w');
        try {
            $file->lock();
            try {
                $file->write(base64_decode($invoiceExtensionAttributes->getDocumentPdf()));
            } finally {
                $file->unlock();
            }
        } finally {
            $file->close();
        }

        return $filename;
    }

    /**
     * @param \Magento\Sales\Api\Data\OrderInterface|\Magento\Sales\Model\Order $order
     * @param \Magento\Sales\Api\Data\InvoiceInterface $invoiceData
     * @return void
     * @throws \Magento\Framework\Exception\LocalizedException
     * @throws \Exception
     */
    public function createInvoiceIfRequired($order, InvoiceInterface $invoiceData)
    {
        $orderIncrementId = $order->getIncrementId();

        if(!$this->shouldCreateIvoice($order->getStoreId())) {
            return;
        }

        if(!$order->canInvoice()) {
            // Order has already an invoice
            if($this->shouldSkipCreation($order->getStoreId())) {
                $this->_logger->info("Invoice creation skipped for order {$order->getIncrementId()}");
                return;
            }

            $message = __('Cannot create invoice for order %1', $orderIncrementId);
            $this->_logger->critical($message->render());
            throw new CouldNotInvoiceException(new Phrase($message->render()));
        }

        $invoiceExtensionAttributes = $invoiceData->getExtensionAttributes();

        if ($invoiceExtensionAttributes === null) {
            $message = __('Cannot find order to create invoice: missing extension attributes');
            $this->_logger->critical($message->render());
            throw new CouldNotInvoiceException(new Phrase($message->render()));
        }

        $captureCase = $invoiceExtensionAttributes->getCaptureCase();

        $qtys = [];
        $invoice_items = $invoiceData->getItems();
        foreach ($order->getItems() as $order_item) {
            foreach ($invoice_items ?? [] as $invoice_item) {
                if ($order_item->getTypeId() !== 'configurable' && $order_item->getTypeId() !== 'bundle' && $invoice_item['sku'] === $order_item->getSku()) {
                    $qtys[$order_item->getId()] = $invoice_item['qty'];
                    break;
                    //unset($invoice_items[$key]);
                }
            }
        }

        $invoice_object = $this->_invoiceService->prepareInvoice($order, $qtys);

        if (!$invoice_object) {
            throw new LocalizedException(__('We can\'t save the invoice right now.'));
        }

        if (!$invoice_object->getTotalQty()) {
            throw new LocalizedException(__('You can\'t create an invoice without products.'));
        }

        $shipping_amount = $invoiceData->getShippingInclTax();

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

        $invoice_object->setShippingInclTax($shipping_amount);

        if (!$captureCase) {
            $captureCase= Invoice::CAPTURE_OFFLINE;
        }

        $this->_logger->info("CAPTURE CASE FOR ORDER $orderIncrementId: $captureCase");
        $invoice_object->setRequestedCaptureCase($captureCase);
        $invoice_object->register();
        $invoice_object->getOrder()->setCustomerNoteNotify(false);
        $invoice_object->getOrder()->setIsInProcess(true);
        //$order->addStatusHistoryComment('Automatically INVOICED', false);
        $transactionSave = $this->_transactionFactory->create()->addObject($invoice_object)->addObject($invoice_object->getOrder());
        $transactionSave->save();
    }
}

