<?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\Esb\Model\Order\OrderWriteLock;
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_EXTERNAL_INVOICE_PROTECTED_STATUSES = 'esb/external_invoice/protected_statuses';

    /** @var string  */
    const ORDER_STATUS_INVOICED = 'invoiced';

    /** @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;

    /**
     * @var OrderWriteLock
     */
    protected $_orderWriteLock;

    /**
     * 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
     * @param OrderWriteLock $orderWriteLock
     */
    public function __construct(
        ScopeConfigInterface $scopeConfig,
        OrderRepository $orderRepository,
        OrderExtensionFactory $orderExtensionFactory,
        Filesystem $filesystem,
        SearchCriteriaBuilder $searchCriteriaBuilder,
        LoggerProvider $loggerProvider,
        AcknowledgeInterface $response,
        AdditionalDataFactory $additionalDataFactory,
        InvoiceService $invoiceService,
        TransactionFactory $transactionFactory,
        OrderWriteLock $orderWriteLock
    )
    {
        $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;
        $this->_orderWriteLock = $orderWriteLock;
    }

    /**
     * @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 {

            // Talend chiama /V1/acknowledge/save e /V1/externalinvoice/save in parallelo sullo
            // stesso ordine e non puo' serializzarle a monte: load, modifica e save devono
            // stare tutti dentro il lock, altrimenti si sovrascrivono a vicenda.
            $order = $this->_orderWriteLock->execute(
                $orderIncrementId,
                function () use ($orderIncrementId, $invoice_data, $invoice, $documentType, $invoiceIncrementId) {
                    return $this->applyExternalDocument(
                        $orderIncrementId,
                        $invoice_data,
                        $invoice,
                        $documentType,
                        $invoiceIncrementId
                    );
                }
            );

            $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;
        }
    }

    /**
     * Carica l'ordine, gli attacca il documento contabile e lo salva.
     *
     * Va invocato SEMPRE dentro OrderWriteLock::execute(): contiene il read-modify-write che
     * va reso atomico rispetto agli altri endpoint REST che scrivono sullo stesso ordine.
     *
     * @param string $orderIncrementId
     * @param InvoiceInterface $invoice_data
     * @param array $invoice
     * @param string $documentType
     * @param string $invoiceIncrementId
     * @return \Magento\Sales\Model\Order
     * @throws CouldNotInvoiceException
     * @throws \Exception
     */
    private function applyExternalDocument(
        $orderIncrementId,
        InvoiceInterface $invoice_data,
        array $invoice,
        $documentType,
        $invoiceIncrementId
    ) {
        $order = $this->getOrder($orderIncrementId);
        $this->createInvoiceIfRequired($order, $invoice_data);

        // La creazione dell'invoice e' una transazione a se' e puo' durare: nel frattempo
        // l'ack del flusso di fulfillment puo' aver portato l'ordine avanti nel flusso.
        // Ricarico perche' OrderRepository::save() riscrive tutta la riga a partire da
        // questo oggetto, quindi uno snapshot vecchio si porta dietro un lost update.
        // getOrder() usa getList(), che rilegge dal DB e non passa dalla identity map.
        $order = $this->getOrder($orderIncrementId);

        //$order = $orderList->getFirstItem();
        $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);
        $documentStatus = $this->resolveDocumentStatus($order);

        $order->addStatusHistoryComment(
            __('External document has been saved: %1 %2', $documentType, $invoiceIncrementId),
            $documentStatus
        );

        // La chiusura dell'ordine ha senso solo se il documento lo ha effettivamente portato a
        // invoiced: se lo status era protetto non va sovrascritto nemmeno qui, altrimenti la
        // guardia sopra verrebbe aggirata da questo blocco (es. complete_in_store -> complete).
        if ($documentStatus === self::ORDER_STATUS_INVOICED
            && ($order->getState() === 'processing' || $order->getState() === 'complete')
            && !$order->canInvoice() && !$order->canShip()) {
            $order->setState('complete');
            $order->addStatusHistoryComment('', 'complete');
        }

        $this->_orderRepository->save($order);

        return $order;
    }

    /**
     * @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);
    }

    /**
     * Status che nel flusso vengono DOPO 'invoiced' e che il salvataggio del documento
     * contabile non deve riportare indietro. Configurabile perche' l'ordine degli status
     * cambia da progetto a progetto.
     *
     * @param int|null|string $scopeCode
     * @return string[]
     */
    public function getProtectedStatuses($scopeCode) {
        return preg_split(
            '/\s*,\s*/',
            (string)$this->_scopeConfig->getValue(self::XML_ESB_EXTERNAL_INVOICE_PROTECTED_STATUSES,
                ScopeInterface::SCOPE_STORE,
                $scopeCode),
            -1,
            PREG_SPLIT_NO_EMPTY);
    }

    /**
     * Decide quale status scrivere insieme al commento del documento contabile.
     *
     * Se l'ordine e' gia' avanti nel flusso restituisce false: Magento aggiunge il commento
     * in history usando lo status corrente SENZA chiamare setStatus(). Perche' funzioni,
     * $order deve essere stato ricaricato dal DB poco prima (vedi save()): su uno snapshot
     * vecchio il save riscriverebbe comunque lo status stale.
     *
     * @param \Magento\Sales\Model\Order $order
     * @return string|false
     */
    private function resolveDocumentStatus($order) {
        $currentStatus = $order->getStatus();

        if (in_array($currentStatus, $this->getProtectedStatuses($order->getStoreId()), true)) {
            $this->_logger->info(sprintf(
                'ExternalInvoice: order %s is in status "%s", not moving it back to "%s"',
                $order->getIncrementId(),
                $currentStatus,
                self::ORDER_STATUS_INVOICED));
            return false;
        }

        return self::ORDER_STATUS_INVOICED;
    }

    /**
     * @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();
    }
}

