<?php
// Copyright (c) Pickware GmbH. All rights reserved.
// This file is part of software that is released under a proprietary license.
// You must not copy, modify, distribute, make publicly available, or execute
// its contents or parts thereof without express permission by the copyright
// holder, unless otherwise permitted by law.

use Doctrine\ORM\NoResultException;
use Enlight_Event_EventArgs as EventArgs;
use Shopware\Components\Api\Exception as ApiException;
use Shopware\Components\Api\Manager as ResourceManager;
use Shopware\Models\Attribute\Document as OrderDocumentAttribute;
use Shopware\Models\Document\Document as DocumentType;
use Shopware\Models\Order\Document\Document as OrderDocument;
use Shopware\Models\Order\Order;
use Shopware\Plugins\ViisonCommon\Classes\Util\Document as DocumentUtil;
use Shopware\Plugins\ViisonCommon\Classes\Util\Util as ViisonCommonUtil;
use Shopware\Plugins\ViisonCommon\Components\FileStorage\FileStorage;
use Shopware\Plugins\ViisonPickwareCommon\Classes\RestApi\OrderDocumentResponding;
use Shopware\Plugins\ViisonPickwareCommon\Classes\RestApi\OrderDocumentUploading;
use Shopware\Plugins\ViisonPickwareCommon\Classes\Util;
use Shopware_Plugins_Core_ViisonPickwareERP_Bootstrap as PickwareErpBootstrap;

class Shopware_Controllers_Api_ViisonPickwareCommonOrders extends Shopware_Controllers_Api_Rest
{
    use OrderDocumentResponding;
    use OrderDocumentUploading;

    /**
     * Performs some additional rerouting for the send action and disables the post dispatch
     * and output buffering for those actions, which respond a PDF document.
     */
    public function init()
    {
        if ($this->Request()->getActionName() === 'postDocuments' && $this->Request()->getParam('subId') !== false && $this->Request()->getParam('send') !== null) {
            // Manually change the action name to trigger the send action on an order document
            $this->Request()->setActionName('postDocumentsSend');
        }
    }

    /**
     * Loads the document with the given document ID of the order with the ID.
     * Finally a JSON is responded containing ID, document type and document file data.
     *
     * GET /api/orders/{id}/documents/{documentId}
     */
    public function getDocumentsAction()
    {
        $orderId = $this->Request()->getParam('id');
        // Check if the order id parameter is set and a respective order exists. Throws exception otherwise.
        ResourceManager::getResource('order')->getOne($orderId);

        // Try to find the document
        $documentId = $this->Request()->getParam('subId');
        $document = $this->getOrderDocument($orderId, $documentId);

        $this->View()->assign([
            'success' => true,
            'data' => $this->makeOrderDocumentResponseData($document),
        ]);
    }

    /**
     * Creates a new document of the given 'typeId' for the order with the given id. That is, it either uploads a
     * document file named 'documentFile' and adds it to the order or creates a new Shopware document of the given type.
     * In the latter case, it is also possible to optionally upload a document named `attachmentFile` that will be
     * attached to the document created by Shopware.
     * It is also possible to create an additional document of a type, which was already created for that order by
     * setting the 'addAdditional' field in the 'options' to 'true'.
     * If the 'getFileData' parameter is given and evaluates to true, the created document is added as a base64 encoded
     * string to the response.
     *
     * POST /api/orders/{id}/documents
     */
    public function postDocumentsAction()
    {
        // Check the privileges
        ResourceManager::getResource('order')->checkPrivilege('update');

        // Try to get the order to check if the requested resource exists
        $orderId = $this->Request()->getParam('id', 0);
        $order = $this->get('models')->find(Order::class, $orderId);
        if (!$order) {
            throw new ApiException\NotFoundException(sprintf(
                'Order with ID "%d" does not exist.',
                $orderId
            ));
        }

        // Check the document type
        $typeId = $this->Request()->getParam('typeId', 0);
        if (!$typeId) {
            throw new ApiException\CustomValidationException('No document type ID given.');
        }
        $type = $this->get('models')->find(DocumentType::class, $typeId);
        if (!$type) {
            throw new ApiException\CustomValidationException(
                sprintf('Document type with ID "%s" does not exist.', $typeId)
            );
        }
        $document = null;
        $options = $this->Request()->getParam('options', []);
        $shouldAddAdditionalDocument = isset($options['addAdditional']) && $options['addAdditional'] === true;
        if (!$shouldAddAdditionalDocument) {
            // Check for an existing document with the given type first. This is necessary since the document component
            // won't create a document with the same type twice, but just update the existing instance and overwrite the
            // file. In this case we must not do anything but just return the existing document. This prevents errors in
            // the ViisonDATEV plugin, which throws an exception when trying to create an order invoice a second time,
            // as well as errors due to sending the same document via email more than once.
            $document = $this->get('models')->getRepository(OrderDocument::class)->findOneBy([
                'typeId' => $type->getId(),
                'order' => $order,
            ]);
        }

        if ($shouldAddAdditionalDocument || $document === null) {
            // Create or upload a document
            // Following our style guide, the $_FILES global is not allowed and hence must be ignored!
            // phpcs:ignore MySource.PHP.GetRequestData
            $documentFile = $_FILES['documentFile'];
            if ($documentFile) {
                // Create a new document using the uploaded file
                $document = $this->uploadDocument(
                    $order->getId(),
                    $documentFile,
                    $type,
                    $this->Request()->getParam('number')
                );
            } else {
                // Create a new Shopware document using the type
                // Following our style guide, the $_FILES global is not allowed and hence must be ignored!
                // phpcs:ignore MySource.PHP.GetRequestData
                $attachmentFile = $_FILES['attachmentFile'];
                $document = $this->createDocument(
                    $order,
                    $type,
                    $options,
                    $this->Request()->getParam('comment'),
                    $attachmentFile
                );
            }
        }

        // Prepare the response data
        $responseData = $this->makeOrderDocumentResponseData(
            $document,
            (bool)$this->Request()->getParam('getFileData')
        );
        $responseData['location'] = sprintf(
            '%sorders/%s/documents/%s',
            $this->apiBaseUrl,
            $orderId,
            $document->getId()
        );

        // Set response
        $this->Response()->setHttpResponseCode(201);
        $this->View()->assign([
            'success' => true,
            'data' => $responseData,
        ]);
    }

    /**
     * Sends the document with the given document id of the order with the id to the posted email address.
     *
     * POST /api/orders/{id}/documents/{documentId}/send
     */
    public function postDocumentsSendAction()
    {
        // Check the privileges
        ResourceManager::getResource('order')->checkPrivilege('update');

        // Try to get the order to check if the requested resource exists
        $orderId = $this->Request()->getParam('id');
        $order = $this->get('models')->find(Order::class, $orderId);
        if (!$order) {
            throw new ApiException\NotFoundException(
                sprintf('Order by ID %d not found', $orderId)
            );
        }

        // Try to find the document
        $documentId = $this->Request()->getParam('subId');
        $document = $this->getOrderDocument($orderId, $documentId);

        // Try to find the targeted email address
        $emailAddress = $this->Request()->getParam('email');
        if (!$emailAddress) {
            throw new ApiException\ParameterMissingException('email');
        }

        // Allow other plugins to overwrite the parameters used for creating and sending the email
        $eventArgs = new EventArgs([
            'order' => $order,
            'document' => $document,
            'emailAddress' => $emailAddress,
            'templateName' => $this->getEmailTemplateNameForDocumentType(DocumentUtil::getDocumentTypeFromOrderDocument($document)),
            'context' => $this->createOrderDocumentEmailContext($document),
            'config' => [],
            'shop' => $order->getLanguageSubShop(),
        ]);
        $this->get('events')->notify(
            'Shopware_Plugins_ViisonPickwareCommon_API_SendOrderDocument_BeforeSend',
            $eventArgs
        );

        // Send the document to the given email address
        $mail = $this->get('pickware.erp.document_mailing_service')->createDocumentMailFromTemplate(
            $eventArgs->get('templateName'),
            $eventArgs->get('context'),
            $document,
            $eventArgs->get('shop'),
            $eventArgs->get('config')
        );
        $mail->addTo(trim($eventArgs->get('emailAddress')));
        $mail->send();

        // Set response
        $this->Response()->setHttpResponseCode(200);
        $this->View()->success = true;
    }

    /**
     * Creates a new document of the given type for the order with the given id.
     * If the documentData contains the 'sendViaEmail' and 'receiverEmail' fields,
     * it automatically sends the created document to that email address.
     * Finally the document entity is returned.
     *
     * @param Order $order
     * @param DocumentType $type
     * @param array $options
     * @param string|null $comment
     * @param array|null $attachmentFile
     * @return OrderDocument
     * @throws ApiException\CustomValidationException If the given document type does not exist.
     */
    protected function createDocument(
        Order $order,
        DocumentType $type,
        array $options,
        $comment = null,
        $attachmentFile = null
    ) {
        $options = ($options) ?: [];

        $shouldAddAdditionalDocument = isset($options['addAdditional']) && $options['addAdditional'] === true;
        if ($shouldAddAdditionalDocument) {
            // Register temporary hooks that allow to create more than one document of the same type for the same order
            $this->get('viison_common.document_component_listener_service')->allowNextDocumentToHaveTypeOfExistingDocument();
        }

        try {
            if ($attachmentFile !== null) {
                $attachmentFilePath = self::moveUploadedFile($attachmentFile);
                $this->get('viison_pickware_common.document_component_listener_service')->appendFileToNextRenderedDocument($attachmentFilePath);
            }

            // Render and save the document
            $document = $this->get('pickware.erp.order_document_creation_service')->createOrderDocument(
                $order,
                $type,
                [
                    'docComment' => (mb_strlen($comment) > 0) ? $comment : null,
                ]
            );
        } finally {
            if ($attachmentFilePath !== null) {
                unlink($attachmentFilePath);
            }
        }

        // Refresh the document entity to ensure that all its values are loaded. This is necessary because e.g.
        // `documentId` (aka `docID`) would otherwise be `0`.
        $this->get('models')->refresh($document);

        // Check whether the document shall be sent via email
        if ($options['sendViaEmail'] === true && !empty($options['emailReceiver'])) {
            // Send the document to the given email address
            $mail = $this->get('pickware.erp.document_mailing_service')->createDocumentMailFromTemplate(
                self::getEmailTemplateNameForDocumentType($type),
                $this->createOrderDocumentEmailContext($document),
                $document
            );
            $mail->addTo(trim($options['emailReceiver']));
            $mail->send();
        }

        return $document;
    }

    /**
     * Creates a new order document by uploading the given file, moving it to Shopware's
     * documents directory and creating creating a new order document entity, which is added to
     * the order with the given ID. Finally, if the uploaded document is an invoice or cancellation
     * invoice and the ViisonPickwareERP invoice archive is configured, the document is sent
     * via email to the archive.
     *
     * @param int orderId
     * @param array $documentFile
     * @param DocumentType $type
     * @param int|null $documentNumber
     * @return \Shopware\Models\Order\Document\Document
     * @throws ApiException\CustomValidationException If uploading the file failed.
     */
    private function uploadDocument($orderId, $documentFile, DocumentType $type, $documentNumber = null)
    {
        try {
            $tempFileName = self::moveUploadedFile($documentFile);

            // Create a new document entity (and attribute)
            /** @var Order $order */
            $order = $this->get('models')->find(Order::class, $orderId);
            $document = new OrderDocument();
            $document->fromArray([
                'date' => new \DateTime(),
                'type' => DocumentUtil::getDocumentTypeForOrderDocumentModel($type),
                'order' => $order,
                'customerId' => $order->getCustomer()->getId(),
                'hash' => DocumentUtil::generateDocumentHash(),
                'amount' => $order->getInvoiceAmount(),
                'documentId' => ($documentNumber) ?: $order->getNumber(),
            ]);
            $documentAttribute = new OrderDocumentAttribute();
            $document->setAttribute($documentAttribute);

            /** @var FileStorage $documentFileStorage */
            $documentFileStorage = $this->get('viison_common.document_file_storage_service');
            $documentFileStorage->writeFileContents(
                DocumentUtil::getDocumentFileName($document),
                file_get_contents($tempFileName)
            );
        } finally {
            unlink($tempFileName);
        }

        $order->getDocuments()->add($document);
        $this->get('models')->persist($document);
        $this->get('models')->flush($document);

        // Send uploaded invoices and cancellation invoices to the invoice archive
        if (in_array($type->getId(), [1, 4])) {
            try {
                $this->get('pickware.erp.invoice_archive_service')->sendDocumentToInvoiceArchive($document);
            } catch (\Exception $e) {
                // Just log email archive exceptions, but don't throw them, since the upload itself succeeded
                $this->get('pluginlogger')->error('Failed to send uploaded document to Pickware invoice archive', [
                    'plugin' => 'ViisonPickwareCommon',
                    'exception' => $e,
                ]);
            }
        }

        // Allow other plugins to perform additional business logic after a
        // document has been uploaded successfully
        $eventArgs = new EventArgs([
            'order' => $order,
            'document' => $document,
        ]);
        $this->get('events')->notify(
            'Shopware_Plugins_ViisonPickwareCommon_API_UploadDocument_AfterUpload',
            $eventArgs
        );

        return $document;
    }

    /**
     * Tries to find the order document with the given ID in the order with the given ID.
     *
     * @param int $orderId
     * @param int $documentId
     * @return \Shopware\Models\Order\Document\Document
     * @throws ApiException\NotFoundException If the document does not exists or isn't part of the order.
     */
    private function getOrderDocument($orderId, $documentId)
    {
        // Prepare the query
        $repository = Shopware()->Models()->getRepository(OrderDocument::class);
        $builder = $repository->createQueryBuilder('document');
        $builder->where('document.orderId = :orderId')
                ->andWhere('document.id = :documentId')
                ->setParameter('orderId', $orderId)
                ->setParameter('documentId', $documentId);

        // Get the document
        try {
            $document = $builder->getQuery()->getSingleResult();
        } catch (NoResultException $e) {
            throw new ApiException\NotFoundException(
                sprintf('Document with ID %d for order with ID %d not found.', $documentId, $orderId)
            );
        }

        return $document;
    }

    /**
     * @param OrderDocument $document
     * @return array
     */
    private function createOrderDocumentEmailContext(OrderDocument $document)
    {
        $templateVars = $this->get('db')->fetchRow(
            'SELECT
                `s_order`.`id` AS `orderId`,
                `s_order`.`ordernumber` AS `orderNumber`,
                `s_order`.`language` AS `shopId`,
                `s_user`.`salutation` AS `userSalutation`,
                `s_user`.`firstname` AS `userFirstName`,
                `s_user`.`lastname` AS `userLastName`
            FROM `s_order`
            LEFT JOIN `s_user`
                ON `s_user`.`id` = `s_order`.`userID`
            LEFT JOIN `s_order_documents`
                ON `s_order_documents`.`orderID` = `s_order`.`id`
            WHERE `s_order_documents`.`hash` = :documentHash',
            [
                'documentHash' => $document->getHash(),
            ]
        );

        // Trigger the creation of an order mail to save the default context as an internal state of the
        // template mail component and can be retrieved
        $this->get('modules')->getModule('Order')->createStatusMail($templateVars['orderId'], 1, 'sORDERSTATEMAIL1');
        $shopwareDefaultContext = $this->get('templatemail')->getStringCompiler()->getContext();
        $templateVars = array_merge($shopwareDefaultContext, $templateVars);

        return $templateVars;
    }

    /**
     * Determines the email template name that should be used to send a document of the given $type. For documents of
     * type 'invoice' (ID 1), 'delivery note' (ID 2), 'refund' (ID 3) and 'cancellation invoice' (ID 4) their respective
     * Pickware ERP email template is used. For all other documents the Pickware ERP invoice email template is used.
     *
     * @param DocumentType $documentType
     * @return string
     * @throws Exception
     */
    private function getEmailTemplateNameForDocumentType(DocumentType $documentType)
    {
        // Select a mail template that exists and try in the following order:
        //   1. A mail template from the ERP document mailer.
        //   2. A mail template from the Shopware document mailer for this document type.
        //   3. The default mail template from the Shopware document mailer.
        // This is required because the migration
        // \Shopware\Plugins\ViisonPickwareERP\Migrations\DocumentMailTemplatesToBuiltInMailTemplatesMigration
        // removes the ERP document mailer templates and migrates them to the Shopware document mailer templates.
        // See issue: https://github.com/VIISON/ShopwarePickwareMobile/issues/300
        $namesOfPossibleMailTemplateCandidates = [];
        $namesOfPossibleMailTemplateCandidates[] = PickwareErpBootstrap::DOCUMENT_MAILER_MAIL_TEMPLATE_NAME . '_DocType' . $documentType->getId();
        if (method_exists($documentType, 'getKey')) {
            $namesOfPossibleMailTemplateCandidates[] = 'document_' . $documentType->getKey();
        }
        $namesOfPossibleMailTemplateCandidates[] = 'sORDERDOCUMENTS';

        /** @var Zend_Db_Adapter_Abstract $db */
        $db = $this->get('db');
        $existingMailTemplates = $db->fetchCol(
            'SELECT `name`
            FROM `s_core_config_mails`
            WHERE `name` IN (' . implode(',', array_fill(0, count($namesOfPossibleMailTemplateCandidates), '?')) . ')',
            $namesOfPossibleMailTemplateCandidates
        );

        // Select the first existing mail template
        foreach ($namesOfPossibleMailTemplateCandidates as $nameOfPossibleMailTemplateCandidate) {
            if (in_array($nameOfPossibleMailTemplateCandidate, $existingMailTemplates, true)) {
                return $nameOfPossibleMailTemplateCandidate;
            }
        }

        throw new Exception('None of the following mail templates exist: ' . implode(',', $namesOfPossibleMailTemplateCandidates));
    }
}
