/**
 * Copyright (c) 2012 Partners In Health.  All rights reserved.
 * The use and distribution terms for this software are covered by the
 * Eclipse Public License 1.0 (http://opensource.org/licenses/eclipse-1.0.php)
 * which can be found in the file epl-v10.html at the root of this distribution.
 * By using this software in any fashion, you are agreeing to be bound by
 * the terms of this license.
 * You must not remove this notice, or any other, from this software.
 **/
package org.pih.warehouse.shipping

import au.com.bytecode.opencsv.CSVWriter
import com.google.zxing.BarcodeFormat
import grails.gorm.transactions.Transactional
import grails.validation.ValidationException
import groovy.sql.Sql
import org.apache.commons.lang.text.StrSubstitutor

import org.pih.warehouse.core.Comment
import org.pih.warehouse.core.Document
import org.pih.warehouse.core.DocumentService
import org.pih.warehouse.core.DocumentType
import org.pih.warehouse.core.Event
import org.pih.warehouse.core.EventType
import org.pih.warehouse.core.Location
import org.pih.warehouse.core.MailService
import org.pih.warehouse.core.Person
import org.pih.warehouse.core.RoleType
import org.pih.warehouse.core.User
import org.pih.warehouse.inventory.Transaction
import org.pih.warehouse.inventory.TransactionException
import org.pih.warehouse.product.Product
import org.pih.warehouse.receiving.Receipt
import org.pih.warehouse.receiving.ReceiptItem

@Transactional
class ShipmentController {

    static scaffold = Shipment
    def shipmentService
    def userService
    def inventoryService
    MailService mailService
    def barcodeService
    def sessionFactory
    DocumentService documentService

    def redirect() {
        redirect(controller: "shipment", action: "showDetails", id: params.id)
    }

    def show() {
        redirect(action: "showDetails", params: ['id': params.id])
    }

    def list() {
        def startTime = System.currentTimeMillis()
        println "Get shipments: " + params

        params.max = Math.min(params.max ? params.int('max') : 100, 10000)

        Calendar calendar = Calendar.instance
        int dayOfMonth = calendar.get(Calendar.DAY_OF_MONTH)
        int firstDayOfMonth = calendar.getActualMinimum(Calendar.DAY_OF_MONTH)
        int lastDayOfMonth = calendar.getActualMaximum(Calendar.DAY_OF_MONTH)
        calendar.set(calendar.get(Calendar.YEAR), calendar.get(Calendar.MONTH), firstDayOfMonth)
        def lastUpdatedFromDefault = calendar.getTime()
        def lastUpdatedToDefault = calendar.getTime()

        boolean incoming = params?.type?.toUpperCase() == "INCOMING"
        def origin = incoming ? (params.origin ? Location.get(params.origin) : null) : Location.get(session.warehouse.id)
        def destination = incoming ? Location.get(session.warehouse.id) : (params.destination ? Location.get(params.destination) : null)
        def shipmentType = params.shipmentType ? ShipmentType.get(params.shipmentType) : null
        def statusCode = params.status ? Enum.valueOf(ShipmentStatusCode.class, params.status) : null
        def statusStartDate = params.statusStartDate ? Date.parse("MM/dd/yyyy", params.statusStartDate) : null
        def statusEndDate = params.statusEndDate ? Date.parse("MM/dd/yyyy", params.statusEndDate) : null
        def lastUpdatedFrom = params.lastUpdatedFrom ? Date.parse("MM/dd/yyyy", params.lastUpdatedFrom) : null
        def lastUpdatedTo = params.lastUpdatedTo ? Date.parse("MM/dd/yyyy", params.lastUpdatedTo) : null


        println "lastUpdatedFrom = " + lastUpdatedFrom + " lastUpdatedTo = " + lastUpdatedTo

        def shipments = shipmentService.getShipments(params.terms, shipmentType, origin, destination,
                statusCode, statusStartDate, statusEndDate, lastUpdatedFrom, lastUpdatedTo, params.max)

        println "List shipments: " + (System.currentTimeMillis() - startTime) + " ms"

        [
                shipments      : shipments,
                shipmentType   : shipmentType?.id,
                origin         : origin?.id,
                destination    : destination?.id,
                status         : statusCode?.name,
                lastUpdatedFrom: lastUpdatedFrom,
                lastUpdatedTo  : lastUpdatedTo,
                incoming       : incoming
        ]
    }


    def create() {
        def shipmentInstance = new Shipment()
        shipmentInstance.properties = params

        if (params.type == "incoming") {
            shipmentInstance.destination = session.warehouse
        } else if (params.type == "outgoing") {
            shipmentInstance.origin = session.warehouse
        }
        render(view: "create", model: [shipmentInstance: shipmentInstance,
                                       warehouses      : Location.list(), eventTypes: EventType.list()])
    }

    def save() {
        def shipmentInstance = new Shipment(params)

        if (shipmentInstance.save(flush: true)) {

            // Try to add the initial event
            def eventType = EventType.get(params.eventType.id)
            if (eventType) {
                def shipmentEvent = new Event(eventType: eventType, eventLocation: session.warehouse, eventDate: new Date())
                shipmentInstance.addToEvents(shipmentEvent).save(flush: true)
            }
            flash.message = "${warehouse.message(code: 'default.created.message', args: [warehouse.message(code: 'shipment.label', default: 'Shipment'), shipmentInstance.id])}"
            redirect(action: "showDetails", id: shipmentInstance.id)
        } else {
            render(view: "create", model: [shipmentInstance: shipmentInstance,
                                           warehouses      : Location.list(), eventTypes: EventType.list()])
        }
    }

    def update() {

        def shipmentInstance = Shipment.get(params.id)
        if (shipmentInstance) {
            if (params.version) {
                def version = params.version.toLong()
                if (shipmentInstance.version > version) {
                    shipmentInstance.errors.rejectValue("version", "default.optimistic.locking.failure", [warehouse.message(code: 'shipment.label', default: 'Shipment')] as Object[], "Another user has updated this Shipment while you were editing")
                    render(view: "editDetails", model: [shipmentInstance: shipmentInstance])
                    return
                }
            }

            // Bind request parameters
            shipmentInstance.properties = params

            // -- Processing shipment method  -------------------------
            log.info "autocomplete shipment method: " + params
            // Create a new shipment method if one does not exist
            def shipmentMethod = shipmentInstance.shipmentMethod
            if (!shipmentMethod) {
                shipmentMethod = new ShipmentMethod()
            }

            // If there's an ID but no name, it means we want to remove the shipper and shipper service
            if (!params.shipperService.name) {
                shipmentMethod.shipper = null
                shipmentMethod.shipperService = null
            }
            // Otherwise we set the selected accordingly
            else if (params.shipperService.id && params.shipperService.name) {
                def shipperService = ShipperService.get(params.shipperService.id)
                if (shipperService) {
                    shipmentMethod.shipperService = shipperService
                    shipmentMethod.shipper = shipperService.shipper
                }
            }
            // We work with and save the shipmentMethod instance in order to avoid a transient object exception
            // that occurs when setting the destination above and saving the shipment method within the shipment
            shipmentInstance.shipmentMethod = shipmentMethod
            shipmentInstance.shipmentMethod.save(flush: true)

            // -- Processing destination  -------------------------
            // Reset the destination to null
            if (!params.safeDestination.name) {
                shipmentInstance.destination = null
            }
            // Assign a destination if one was selected
            else if (params.safeDestination.id && params.safeDestination.name) {
                def destination = Location.get(params.safeDestination.id)
                if (destination && params.safeDestination.name == destination.name) // if it exists
                    shipmentInstance.destination = destination
            }

            // -- Processing carrier  -------------------------
            // This is necessary because Grails seems to be binding things incorrectly.  If we just let
            // Grails do the binding by itself, it tries to change the ID of the 'carrier' that is already
            // associated with the shipment, rather than changing the 'carrier' object associated with
            // the shipment.

            // Reset the carrier
            if (!params.safeCarrier.name) {
                shipmentInstance.carrier = null
            }
            // else if the person is found and different from the current one, then we use that person
            else if (params.safeCarrier.id && params.safeCarrier.name) {
                def safeCarrier = Person.get(params.safeCarrier.id)
                if (safeCarrier && safeCarrier?.name != shipmentInstance?.carrier?.name)
                    shipmentInstance.carrier = safeCarrier
            }
            // else if only the name is provided, we need to create a new person
            else {
                def safeCarrier = convertStringToPerson(params.safeCarrier.name)
                if (safeCarrier) {
                    safeCarrier.save(flush: true)
                    shipmentInstance.carrier = safeCarrier
                }
            }

            if (!shipmentInstance.hasErrors() && shipmentInstance.save(flush: true)) {
                flash.message = "${warehouse.message(code: 'default.updated.message', args: [warehouse.message(code: 'shipment.label', default: 'Shipment'), shipmentInstance.id])}"
                redirect(action: "showDetails", id: shipmentInstance.id)
            } else {
                render(view: "editDetails", model: [shipmentInstance: shipmentInstance])
            }
        } else {
            flash.message = "${warehouse.message(code: 'default.not.found.message', args: [warehouse.message(code: 'shipment.label', default: 'Shipment'), params.id])}"
            redirect(action: "list")
        }
    }

    def showDetails() {
        def shipmentInstance = Shipment.get(params.id)
        if (!shipmentInstance) {
            flash.message = "${warehouse.message(code: 'default.not.found.message', args: [warehouse.message(code: 'shipment.label', default: 'Shipment'), params.id])}"
            redirect(action: "list", params: [type: params.type])
        } else {

            // Redirect to stock movement details page
            if (shipmentInstance?.requisition && !params?.override) {
                redirect(controller: "stockMovement", action: "show", id: shipmentInstance?.requisition?.id)
                return
            }

            def eventTypes = EventType.list()
            def shipmentWorkflow = shipmentService.getShipmentWorkflow(shipmentInstance)
            [shipmentInstance: shipmentInstance, shipmentWorkflow: shipmentWorkflow, shippingEventTypes: eventTypes]
        }
    }

    def showTracking() {
        def shipmentInstance = Shipment.get(params.id)
        def trackingUrl
        def trackingUrlTemplate = shipmentInstance?.shipmentMethod?.shipper?.trackingUrl
        def trackingNumber = shipmentInstance?.shipmentMethod?.trackingNumber
        if (trackingNumber && trackingUrlTemplate?.contains("%s")) {
            trackingUrl = String.format(trackingUrlTemplate, trackingNumber)
        }
        else {
            trackingUrl = StrSubstitutor.replace(trackingUrlTemplate, [trackingNumber:trackingNumber])
        }

        render(template: "showTracking", model: [shipmentInstance: shipmentInstance, trackingUrl: trackingUrl])
    }

    def showTransactions() {
        def shipmentInstance = Shipment.get(params.id)
        render(template: "showTransactions", model: [shipmentInstance: shipmentInstance])
    }

    def syncTransactions() {
        def shipmentInstance = Shipment.get(params.id)
        shipmentService.synchronizeTransactions(shipmentInstance)
        redirect(action: "showDetails", id: params.id)
    }

    def editDetails() {

        def shipmentInstance = Shipment.get(params.id)
        if (!shipmentInstance) {
            flash.message = "${warehouse.message(code: 'default.not.found.message', args: [warehouse.message(code: 'shipment.label', default: 'Shipment'), params.id])}"
            redirect(action: "list", params: [type: params.type])
        } else {
            [shipmentInstance: shipmentInstance]
        }
    }

    def sendShipment() {
        def transactionInstance
        def shipmentInstance = Shipment.get(params.id)
        def shipmentWorkflow = shipmentService.getShipmentWorkflow(params.id)

        if (!shipmentInstance) {
            flash.message = "${warehouse.message(code: 'default.not.found.message', args: [warehouse.message(code: 'shipment.label', default: 'Shipment'), params.id])}"
            redirect(action: "list", params: [type: params.type])
        } else {
            // handle a submit
            if ("POST".equalsIgnoreCase(request.getMethod())) {
                // make sure a shipping date has been specified and that is not the future
                if (!params.actualShippingDate || Date.parse("MM/dd/yyyy HH:mm XXX", params.actualShippingDate) > new Date()) {
                    flash.message = "${warehouse.message(code: 'shipping.specifyValidShipmentDate.message')}"
                    render(view: "sendShipment", model: [shipmentInstance: shipmentInstance, shipmentWorkflow: shipmentWorkflow])
                    return
                }

                // create the list of email recipients
                def emailRecipients = new HashSet()
                params.emailRecipientId?.each({
                    emailRecipients = emailRecipients + Person.get(it)
                })
                try {
                    // send the shipment
                    shipmentService.sendShipment(shipmentInstance, params.comment, session.user, session.warehouse,
                            Date.parse("MM/dd/yyyy HH:mm XXX", params.actualShippingDate))
                    //triggerSendShipmentEmails(shipmentInstance, userInstance, emailRecipients)
                }
                catch (TransactionException e) {
                    transactionInstance = e.transaction
                    shipmentInstance = Shipment.get(params.id)
                    shipmentWorkflow = shipmentService.getShipmentWorkflow(params.id)
                    render(view: "sendShipment", model: [shipmentInstance: shipmentInstance, shipmentWorkflow: shipmentWorkflow, transactionInstance: transactionInstance])
                    return
                }

                if (!shipmentInstance?.hasErrors() && !transactionInstance?.hasErrors()) {
                    flash.message = "${warehouse.message(code: 'default.updated.message', args: [warehouse.message(code: 'shipment.label', default: 'Shipment'), shipmentInstance.id])}"
                    redirect(action: "showDetails", id: shipmentInstance?.id)
                }
            }

            // populate the model and render the page
            render(view: "sendShipment", model: [shipmentInstance: shipmentInstance, shipmentWorkflow: shipmentWorkflow])
        }
    }

    def refreshCurrentStatus() {
        shipmentService.refreshCurrentStatus(params.id)
        redirect(action: "showDetails", id: params?.id)
    }


    def rollbackLastEvent() {
        def shipmentInstance = Shipment.get(params.id)
        shipmentService.rollbackLastEvent(shipmentInstance)
        redirect(action: "showDetails", id: shipmentInstance?.id)
    }

    def deleteShipment() {
        def shipmentInstance = Shipment.get(params.id)
        if (!shipmentInstance) {
            flash.message = "${warehouse.message(code: 'default.not.found.message', args: [warehouse.message(code: 'shipment.label', default: 'Shipment'), params.id])}"
            redirect(controller: "dashboard", action: "index")
            return
        } else {
            if ("POST".equalsIgnoreCase(request.getMethod())) {
                shipmentService.deleteShipment(shipmentInstance)

                flash.message = "${warehouse.message(code: 'default.deleted.message', args: [warehouse.message(code: 'shipment.label', default: 'Shipment'), shipmentInstance.id])}"
                redirect(controller: "dashboard", action: "index")
                return
            }
        }
        [shipmentInstance: shipmentInstance]
    }

    def markAsReceived() {
        def shipmentInstance = Shipment.get(params.id)

        // actually process the receipt
        shipmentService.markAsReceived(shipmentInstance, session.warehouse)
        if (!shipmentInstance.hasErrors()) {
            flash.message = "${warehouse.message(code: 'default.updated.message', args: [warehouse.message(code: 'shipment.label', default: 'Shipment'), shipmentInstance.id])}"
        }
        redirect(controller: "shipment", action: "showDetails", id: shipmentInstance.id)
    }


    def bulkDeleteShipments() {
        def shipmentIds = params.list("shipment.id")

        log.info "Shipment ids: " + shipmentIds
        try {
            shipmentIds.each { shipmentId ->
                Shipment shipment = Shipment.get(shipmentId)
                shipmentService.deleteShipment(shipment)
            }
            flash.message = "Successfully deleted ${shipmentIds?.size()} shipments"

        } catch (Exception e) {
            flash.message = "Error occurred while bulk deleting shipments: " + e.message
        }
        redirect(action: "list", params: [type: params.type, status: params.status])
    }

    def bulkReceiveShipments() {
        def shipmentIds = params.list("shipment.id")
        try {
            shipmentService.receiveShipments(shipmentIds, null, session.user.id, session.warehouse.id, true)
            flash.message = "Successfully received shipments"

        } catch (Exception e) {
            flash.message = "Error occurred while bulk receiving shipments: " + e.message
        }
        redirect(action: "list", params: [type: params.type, status: params.status])
    }


    def bulkMarkAsReceived() {
        def shipmentIds = params.list("shipment.id")
        Location location = Location.load(session.warehouse.id)
        try {
            shipmentIds.each { shipmentId ->
                Shipment shipment = Shipment.load(shipmentId)
                shipmentService.markAsReceived(shipment, location)
            }
            flash.message = "Successfully received shipments"

        } catch (Exception e) {
            flash.message = "Error occurred while bulk receiving shipments: " + e.message
        }
        redirect(action: "list", params: [type: params.type, status: params.status])
    }


    def bulkRollbackShipments() {
        def shipmentIds = params.list("shipment.id")
        try {
            shipmentService.rollbackShipments(shipmentIds)
            flash.message = "Successfully rolled back last event for selected shipments"

        } catch (Exception e) {
            flash.message = "Error occurred while bulk receiving shipments: " + e.message
        }
        redirect(action: "list", params: [type: params.type, status: params.status])
    }

    def downloadLabels() {

        Shipment shipmentInstance = Shipment.get(params.id)
        response.contentType = 'application/pdf'
        response.setHeader('Content-disposition', 'attachment; filename="barcodes.pdf";')

        def shipmentItems = []

        final shipmentNumberBarcode = new ByteArrayOutputStream()
        barcodeService.renderImage(shipmentNumberBarcode, shipmentInstance?.shipmentNumber, 180, 50, BarcodeFormat.CODE_128)

        shipmentInstance?.shipmentItems?.each { shipmentItem ->
            final lotNumberBarcode = new ByteArrayOutputStream()
            barcodeService.renderImage(lotNumberBarcode, shipmentItem?.inventoryItem?.lotNumber, 180, 50, BarcodeFormat.CODE_128)
            final productCodeBarcode = new ByteArrayOutputStream()
            barcodeService.renderImage(productCodeBarcode, shipmentItem?.inventoryItem?.product?.productCode, 180, 50, BarcodeFormat.CODE_128)
            shipmentItems << [
                    productCode     : shipmentItem?.inventoryItem?.product?.productCode,
                    productName     : shipmentItem?.inventoryItem?.product?.displayName ?: shipmentItem?.inventoryItem?.product?.name,
                    lotNumber       : shipmentItem?.inventoryItem?.lotNumber,
                    lotNumberBytes  : lotNumberBarcode.toByteArray(),
                    productCodeBytes: productCodeBarcode.toByteArray()]
        }
        renderPdf(template: 'barcodeLabel', model: [shipmentInstance: shipmentInstance, shipmentItems: shipmentItems, shipmentNumberBytes: shipmentNumberBarcode.toByteArray()])
    }

    def showPutawayLocations() {
        def location = Location.get(session.warehouse.id)
        ReceiptItem receiptItem = ReceiptItem.load(params.id)

        Product productInstance = receiptItem.inventoryItem?.product // Product.load(params.id)
        def binLocations = inventoryService.getProductQuantityByBinLocation(location, productInstance)

        render template: "showPutawayLocations", model: [product: productInstance, binLocations: binLocations]
    }


    def splitReceiptItem() {
        ReceiptItem receiptItem1 = ReceiptItem.load(params.id)
        ReceiptItem receiptItem2 = new ReceiptItem(receiptItem1.properties)
        receiptItem2.quantityReceived = 0
        receiptItem1.receipt.addToReceiptItems(receiptItem2)

        Shipment shipment = receiptItem1?.receipt?.shipment
        flash.message = "${warehouse.message(code: 'default.updated.message', args: [warehouse.message(code: 'shipment.label', default: 'Shipment'), shipment.id])}"
        redirect(controller: "shipment", action: "receiveShipment", id: shipment?.id)
    }

    def deleteReceiptItem() {
        ReceiptItem receiptItem = ReceiptItem.load(params.id)
        Shipment shipmentInstance = receiptItem?.receipt?.shipment

        if (receiptItem) {
            // FIXME Prevent delete of the last receipt item for a shipment item (kind of a hack). There should be a
            // way to represent one receipt item as the primary so we don't even show the delete button in the UI.
            if (receiptItem.shipmentItem.receiptItems.size() <= 1) {
                shipmentInstance?.receipt?.errors?.reject("shipping.mustHaveAtLeastOneReceiptItemPerShimentItem")
                render(view: "receiveShipment", model: [shipmentInstance: shipmentInstance, receiptInstance: shipmentInstance.receipt])
                return
            } else {
                shipmentInstance?.receipt.removeFromReceiptItems(receiptItem)
                receiptItem.shipmentItem.removeFromReceiptItems(receiptItem)
                receiptItem.delete()
                flash.message = "${warehouse.message(code: 'default.updated.message', args: [warehouse.message(code: 'shipment.label', default: 'Shipment'), shipmentInstance.id])}"
            }
        } else {
            flash.message = "${warehouse.message(code: 'default.not.found.message', args: [warehouse.message(code: 'receiptItem.label', default: 'Receipt Item'), params.id])}"
        }
        redirect(controller: "shipment", action: "receiveShipment", id: shipmentInstance?.id)
    }

    def deleteReceipt() {
        Receipt receiptInstance = Receipt.get(params.id)
        Shipment shipmentInstance = receiptInstance?.shipment
        if (shipmentInstance) {
            shipmentInstance.receipt = null // FIXME This seems absurd
        }
        receiptInstance.delete()
        redirect(controller: "shipment", action: "showDetails", id: shipmentInstance?.id)
    }

    def validateReceipt() {
        Receipt receiptInstance = Receipt.get(params.id)
        Shipment shipmentInstance = receiptInstance?.shipment
        if (shipmentService.validateReceipt(receiptInstance)) {
            flash.message = "Receipt is valid"
        }

        redirect(controller: "shipment", action: "receiveShipment", id: shipmentInstance?.id)
    }


    def receiveShipment(ReceiveShipmentCommand command) {
        log.info "params: " + params
        def receiptInstance
        def location = Location.get(session.warehouse.id)
        def shipmentInstance = Shipment.get(params.id)
        def userInstance = User.get(session.user.id)

        if (!shipmentInstance) {
            flash.message = "${warehouse.message(code: 'default.not.found.message', args: [warehouse.message(code: 'shipment.label', default: 'Shipment'), params.id])}"
            redirect(action: "list", params: [type: 'incoming'])
            return
        }

        // Process receive shipment form
        if ("POST".equalsIgnoreCase(request.method)) {

            // FIXME Somewhat unreadable code here ... need to clean it up a bit, but basically we're just checking
            // to see if there's already a receipt on the shipment. If not we create one
            receiptInstance = shipmentInstance.receipt
            if (!receiptInstance) {
                receiptInstance = new Receipt(params)
                shipmentInstance.addToReceipts(receiptInstance)
            } else {
                receiptInstance.properties = params
            }

            // check for errors
            if (receiptInstance.hasErrors() || !receiptInstance.validate()) {
                render(view: "receiveShipment", model: [shipmentInstance: shipmentInstance, receiptInstance: receiptInstance])
                return
            }


            if (params.saveButton == 'receiveShipment') {
                // For now, we'll always credit stock on receipt of shipment
                //def creditStockOnReceipt = params.creditStockOnReceipt=='yes'
                def creditStockOnReceipt = true
                // actually process the receipt
                try {
                    shipmentService.receiveShipment(shipmentInstance.id, params.comment, session?.user?.id, session.warehouse?.id, creditStockOnReceipt)
                    // If there were no errors we can trigger shipment emails to be sent
                    if (!shipmentInstance.hasErrors()) {
                        def recipients = new HashSet()
                        triggerReceiveShipmentEmails(shipmentInstance, userInstance, recipients)
                        flash.message = "${warehouse.message(code: 'default.received.message', args: [warehouse.message(code: 'shipment.label', default: 'Shipment'), shipmentInstance.shipmentNumber ?: shipmentInstance?.id])}"
                        redirect(action: "showDetails", id: shipmentInstance?.id)
                        return
                    }
                } catch (ValidationException e) {
                    shipmentInstance = Shipment.read(params.id)
                    receiptInstance.errors = e.errors
                    render(view: "receiveShipment", model: [shipmentInstance: shipmentInstance, receiptInstance: receiptInstance])
                    return
                }
            } else {

                flash.message = "${warehouse.message(code: 'default.updated.message', args: [warehouse.message(code: 'shipment.label', default: 'Shipment'), shipmentInstance.shipmentNumber ?: shipmentInstance?.id])}"
                if (params.saveButton == 'saveAndExit') {
                    redirect(controller: "shipment", action: "showDetails", id: shipmentInstance?.id)
                } else {
                    redirect(controller: "shipment", action: "receiveShipment", id: shipmentInstance?.id, fragment: "tabs-details")
                }
                return

            }
        }

        // Display form
        else {

            if (shipmentInstance?.destination != location) {
                flash.message = "${g.message(code: 'shipping.mustBeLoggedIntoDestinationToReceive.message', args: [shipmentInstance?.destination])}"
            }

            receiptInstance = shipmentService.findOrCreateReceipt(shipmentInstance)

        }

        render(view: "receiveShipment", model: [shipmentInstance: shipmentInstance, receiptInstance: receiptInstance])
    }


    /**
     *
     * @param shipmentInstance
     * @param userInstance
     * @param recipients
     */
    void triggerReceiveShipmentEmails(Shipment shipmentInstance, User userInstance, Set<Person> recipients) {
        if (!userInstance) userInstance = User.get(session.user.id)
        if (!shipmentInstance.hasErrors()) {
            if (!recipients) recipients = new HashSet<Person>()

            // add all admins to the email
            def adminList = userService.findUsersByRoleType(RoleType.ROLE_SHIPMENT_NOTIFICATION)
            adminList.each { adminUser ->
                recipients.add(adminUser)
            }

            // add the current user to the list of email recipients
            if (userInstance?.email) {
                recipients.add(userInstance)
            }

            // add all shipment recipients
            shipmentInstance?.recipients?.each { recipient ->
                recipients.add(recipient)
            }

            def shipmentName = "${shipmentInstance.name}"
            def shipmentType = "${format.metadata(obj: shipmentInstance.shipmentType)}"
            def shipmentDate = "${formatDate(date: shipmentInstance?.actualDeliveryDate, format: 'MMMMM dd yyyy')}"
            def subject = "${warehouse.message(code: 'shipment.hasBeenReceived.message', args: [shipmentType, shipmentName, shipmentDate])}"
            def body = g.render(template: "/email/shipmentReceived", model: [shipmentInstance: shipmentInstance, userInstance: userInstance])
            def toList = recipients?.collect { it?.email }?.unique()
            log.info("Mailing shipment emails to ${toList} with subject ${subject}")

            try {
                mailService.sendHtmlMail(subject, body.toString(), toList)
            } catch (Exception e) {
                log.error "Error triggering receive shipment emails " + e.message
            }
        }
    }

    def renderReceivedEmail() {
        def shipmentInstance = Shipment.get(params.id)
        def userInstance = User.get(session.user.id)
        render(template: "/email/shipmentReceived", model: [shipmentInstance: shipmentInstance, userInstance: userInstance])
    }

    def renderShippedEmail() {
        def shipmentInstance = Shipment.get(params.id)
        def userInstance = User.get(session.user.id)
        render(template: "/email/shipmentShipped", model: [shipmentInstance: shipmentInstance, userInstance: userInstance])
    }


    def showPackingList() {
        def shipmentInstance = Shipment.get(params.id)
        if (!shipmentInstance) {
            flash.message = "${warehouse.message(code: 'default.not.found.message', args: [warehouse.message(code: 'shipment.label', default: 'Shipment'), params.id])}"
            redirect(action: "list", params: [type: params.type])
        } else {
            [shipmentInstance: shipmentInstance]
        }
    }

    def downloadPackingList() {
        def shipmentInstance = Shipment.get(params.id)
        if (!shipmentInstance) {
            flash.message = "${warehouse.message(code: 'default.not.found.message', args: [warehouse.message(code: 'shipment.label', default: 'Shipment'), params.id])}"
            redirect(action: "list", params: [type: params.type])
        } else {
            String query = """
				select  
					container.name,  
					container.height, 
					container.width, 
					container.length, 
					container.volume_units, 
					container.weight, 
					container.weight_units,
					shipment_item.quantity,
					product.name,
					shipment_item.serial_number
				from shipment, container, shipment_item, product
				where shipment.id = container.shipment_id
				and shipment_item.container_id = container.id
				and shipment_item.product_id = product.id 
				and shipment.id = ${params.id}"""

            StringWriter sw = new StringWriter()
            CSVWriter writer = new CSVWriter(sw)
            Sql sql = new Sql(sessionFactory.currentSession.connection())

            String[] colArray = new String[6]
            colArray.putAt(0, "unit")
            colArray.putAt(1, "dimensions")
            colArray.putAt(2, "weight")
            colArray.putAt(3, "qty")
            colArray.putAt(4, "item")
            colArray.putAt(5, "serial number")
            writer.writeNext(colArray)
            sql.eachRow(query) { row ->

                def rowArray = new String[6]
                rowArray.putAt(0, row[0])
                rowArray.putAt(1, (row[1]) ? row[1] : "0" + "x" + (row[2]) ? row[2] : "0" + "x" + (row[3]) ? row[3] : "0" + " " + (row[4]) ? row[4] : "")
                rowArray.putAt(2, row[5] + " " + row[6])
                rowArray.putAt(3, row[7])
                rowArray.putAt(4, row[8])
                rowArray.putAt(5, row[9])
                writer.writeNext(rowArray)
            }
            log.info "results: " + sw.toString()
            response.setHeader("Content-disposition", "attachment; filename=\"PackingList.csv\"")
            render(contentType: "text/csv", text: sw.toString())
            sql.close()
        }
    }


    def editContents() {
        def shipmentInstance = Shipment.get(params.id)
        def containerInstance = Container.get(params?.container?.id)

        if (!shipmentInstance) {
            flash.message = "${warehouse.message(code: 'default.not.found.message', args: [warehouse.message(code: 'shipment.label', default: 'Shipment'), params.id])}"
            redirect(action: "list", params: [type: params.type])
        } else {

            if (!containerInstance && shipmentInstance?.containers) {
                containerInstance = shipmentInstance.containers.iterator().next()
            }
            [shipmentInstance: shipmentInstance, containerInstance: containerInstance]
        }
    }


    def editContainer() {

        def shipmentInstance = Shipment.get(params.shipmentId)
        def containerInstance = Container.get(params.containerId)
        if (containerInstance) {

            containerInstance.properties = params

            Iterator iter = containerInstance.shipmentItems.iterator()
            while (iter.hasNext()) {
                def item = iter.next()
                log.info item.product.name + " " + item.quantity

                if (item.quantity == 0) {
                    item.delete()
                    iter.remove()
                }
            }

            // If the user removed the recipient, we need to make sure that the whole object is removed (not just the ID)
            for (def shipmentItem : containerInstance?.shipmentItems) {
                if (!shipmentItem?.recipient?.id) {
                    log.info("item recipient: " + shipmentItem?.recipient?.id)
                    shipmentItem.recipient = null
                }
            }

            if (!containerInstance.hasErrors() && containerInstance.save(flush: true)) {
                flash.message = "${warehouse.message(code: 'default.updated.message', args: [warehouse.message(code: 'container.label', default: 'Container'), containerInstance.id])}"
                redirect(action: "editContents", id: shipmentInstance.id, params: ["container.id": params.containerId])
            } else {
                flash.message = "${warehouse.message(code: 'shipping.couldNotEditContainer.message')}"
                redirect(action: "showDetails", id: shipmentInstance.id, params: ["containerId": params.containerId])
                //render(view: "edit", model: [containerInstance: containerInstance])
            }
        } else {
            flash.message = "${warehouse.message(code: 'default.not.found.message', args: [warehouse.message(code: 'container.label', default: 'Container'), params.containerId])}"
            redirect(action: "showDetails", id: shipmentInstance.id, params: ["containerId": params.containerId])
        }
    }


    def copyContainer() {
        def container = Container.get(params.id)
        def shipment = Shipment.get(params.shipmentId)

        if (container && shipment) {
            def numCopies = (params.copies) ? Integer.parseInt(params.copies) : 1
            int index = (shipment?.containers) ? (shipment.containers.size()) : 1

            while (numCopies-- > 0) {
                def containerCopy = new Container(container.properties)
                containerCopy.id = null
                containerCopy.name = "" + (++index)
                containerCopy.containerType = container.containerType
                containerCopy.weight = container.weight
                containerCopy.shipmentItems = null
                containerCopy.save(flush: true)

                container.shipmentItems.each {
                    containerCopy.shipment.addToShipmentItems(shipmentItem).save(flush: true)
                }
                shipment.addToContainers(containerCopy).save(flush: true)
            }
            flash.message = "${warehouse.message(code: 'shipping.copiedContainerSuccessfully.message')}"
        } else {
            flash.message = "${warehouse.message(code: 'shipping.unableToCopyPackage.message')}"
        }

        redirect(action: 'showDetails', id: params.shipmentId)
    }


    def addDocument() {
        Shipment shipmentInstance = Shipment.get(params.id)
        Document documentInstance = Document.get(params?.document?.id)
        List<DocumentType> documentTypes = documentService.getNonTemplateDocumentTypes()

        if (!documentInstance) {
            documentInstance = new Document()
        }
        if (!shipmentInstance) {
            flash.message = "${warehouse.message(code: 'default.not.found.message', args: [warehouse.message(code: 'shipment.label', default: 'Shipment'), params.id])}"
            redirect(action: "list")
        }
        render(view: "addDocument", model: [
                shipmentInstance: shipmentInstance,
                documentInstance: documentInstance,
                documentTypes: documentTypes
        ])
    }

    def editDocument() {
        Shipment shipmentInstance = Shipment.get(params?.shipmentId)
        Document documentInstance = Document.get(params?.documentId)
        List<DocumentType> documentTypes = documentService.getNonTemplateDocumentTypes()

        if (!shipmentInstance) {
            flash.message = "${warehouse.message(code: 'default.not.found.message', args: [warehouse.message(code: 'shipment.label', default: 'Shipment'), params.shipmentId])}"
            redirect(action: "list")
        }
        if (!documentInstance) {
            flash.message = "${warehouse.message(code: 'default.not.found.message', args: [warehouse.message(code: 'document.label', default: 'Document'), params.documentId])}"
            redirect(action: "showDetails", id: shipmentInstance?.id)
        }
        render(view: "addDocument", model: [
                shipmentInstance: shipmentInstance,
                documentInstance: documentInstance,
                documentTypes: documentTypes
        ])
    }


    def addComment() {
        log.debug "params " + params
        def shipmentInstance = Shipment.get(params.id)
        render(view: "addComment", model: [shipmentInstance: shipmentInstance, comment: new Comment()])
    }

    /**
     * This action is used to render the form page used to add a
     * new package/container to a shipment.
     */
    def addPackage() {
        def shipmentInstance = Shipment.get(params.id)
        def containerName = (shipmentInstance?.containers) ? String.valueOf(shipmentInstance?.containers?.size() + 1) : "1"
        def containerInstance = new Container(name: containerName)

        render(view: "addPackage", model: [shipmentInstance: shipmentInstance, containerInstance: containerInstance])
    }


    /**
     * This closure is used to process the 'add package' form.
     */
    def savePackage() {

        log.info "params " + params

        def shipmentInstance = Shipment.get(params.shipmentId)
        def parentContainerInstance = Container.get(params?.parentContainer?.id)

        def containerInstance = new Container(params)
        if (containerInstance && shipmentInstance) {
            shipmentInstance.addToContainers(containerInstance)
            if (!shipmentInstance.hasErrors() && shipmentInstance.save(flush: true)) {
                flash.message = "${warehouse.message(code: 'default.updated.message', args: [warehouse.message(code: 'container.label', default: 'Container'), containerInstance.id])}"
                if (parentContainerInstance) {
                    parentContainerInstance.addToContainers(containerInstance).save(flush: true)
                }
                redirect(action: "editContents", id: shipmentInstance.id, params: ["container.id": containerInstance.id])
            } else {
                render(view: "addPackage", model: [shipmentInstance: shipmentInstance, containerInstance: containerInstance])
            }
        } else {
            redirect(action: 'showDetails', id: params.shipmentId)
        }
    }


    def saveComment() {
        User recipient = User.get(params.recipientId)
        shipmentService.addShipmentComment(params.shipmentId, params.comment, session.user, recipient)
        flash.message = "${warehouse.message(code: 'shipping.addedCommentToShipment.message', args: [params.comment, params.shipmentId])}"
        redirect(action: 'showDetails', id: params.shipmentId)
    }


    def editItem() {
        def item = ShipmentItem.get(params.id)
        def container = item.getContainer()
        def shipmentId = container.getShipment().getId()
        if (item) {
            item.quantity = Integer.parseInt(params.quantity)
            item.save()
            flash.message = "${warehouse.message(code: 'shipping.addedCommentToShipment.message', args: [params.id, container.name])}"
            redirect(action: 'editContents', id: shipmentId)
        } else {
            flash.message = "${warehouse.message(code: 'shipping.couldNotEditItemFromContainer.message', args: [params.id])}"
            redirect(action: 'showDetails', id: shipmentId, params: [container.id, container.id])
        }
    }


    def deleteDocument() {
        def document = Document.get(params.id)
        def shipment = Shipment.get(params.shipmentId)
        if (shipment && document) {
            shipment.removeFromDocuments(document)
            document.delete()
            shipment.merge(flush: true)
            flash.message = "${warehouse.message(code: 'shipping.deletedDocumentFromShipment.message', args: [params.id])}"
        } else {
            flash.message = "${warehouse.message(code: 'shipping.couldNotRemoveDocumentFromShipment.message', args: [params.id])}"
        }
        redirect(controller: "stockMovement", action: "show", id: params.shipmentId)
    }

    def deleteEvent() {
        def event = Event.get(params.id)
        def shipment = Shipment.get(params.shipmentId)
        if (shipment && event) {   // not allowed to delete a "created" event
            shipment.removeFromEvents(event)
            event.delete()
            shipment.save()
            flash.message = "${warehouse.message(code: 'shipping.deletedEventFromShipment.message', args: [params.id])}"
        } else {
            flash.message = "${warehouse.message(code: 'shipping.couldNotRemoveEventFromShipment.message', args: [params.id])}"
        }
        redirect(action: 'showDetails', id: params.shipmentId)
    }

    def deleteContainer() {
        def container = Container.get(params.id)
        def shipment = Shipment.get(params.shipmentId)

        if (shipment && container) {
            container.delete()
            flash.message = "${warehouse.message(code: 'shipping.deletedContainerFromShipment.message', args: [params.id])}"
        } else {
            flash.message = "${warehouse.message(code: 'shipping.couldNotRemoveContainerFromShipment.message', args: [params.id])}"
        }

        redirect(action: 'showDetails', id: params.shipmentId)
    }

    def deleteItem() {
        def shipmentItem = ShipmentItem.get(params.id)
        def container = shipmentItem.getContainer()
        def shipmentId = container.getShipment().getId()
        if (item) {
            container.removeFromShipmentItems(shipmentItem)
            flash.message = "${warehouse.message(code: 'shipping.deletedShipmentItemFromContainer.message', args: [params.id, container.name])}"
            redirect(action: 'showDetails', id: shipmentId)
        } else {
            flash.message = "${warehouse.message(code: 'shipping.couldNotRemoveItemFromContainer.message', args: [params.id])}"
            redirect(action: 'showDetails', id: shipmentId)
        }
    }

    def deleteComment() {
        def comment = Comment.get(params.id)
        def shipment = Shipment.get(params.shipmentId)
        if (shipment && comment) {
            shipment.removeFromComments(comment).save(flush: true)
            comment.delete()
            flash.message = "${warehouse.message(code: 'shipping.deletedCommentFromShipment.message', args: [comment, params.shipmentId])}"
            redirect(action: 'showDetails', id: params.shipmentId)
        } else {
            flash.message = "${warehouse.message(code: 'shipping.couldNotRemoveCommentFromShipment.message', args: [params.id])}"
            redirect(action: 'showDetails', id: params.shipmentId)
        }
    }


    def editEvent() {
        def eventInstance = Event.get(params.id)
        def shipmentInstance = Shipment.get(params.shipmentId)

        if (!eventInstance) {
            flash.message = "${warehouse.message(code: 'default.not.found.message', args: [warehouse.message(code: 'shipmentEvent.label', default: 'ShipmentEvent'), params.id])}"
            redirect(action: "showDetails", id: params.shipmentId)
        }

        render(view: "editEvent", model: [shipmentInstance: shipmentInstance, eventInstance: eventInstance])
    }


    def addEvent() {
        def shipmentInstance = Shipment.get(params.id)

        if (!shipmentInstance) {
            flash.message = "${warehouse.message(code: 'default.not.found.message', args: [warehouse.message(code: 'shipmentEvent.label', default: 'ShipmentEvent'), params.id])}"
            redirect(action: "list")
        }

        def eventInstance = new Event(params)
        render(view: "editEvent", model: [shipmentInstance: shipmentInstance, eventInstance: eventInstance])
    }

    def saveEvent() {
        def shipmentInstance = Shipment.get(params.shipmentId)
        def eventInstance = Event.get(params.eventId) ?: new Event()

        bindData(eventInstance, params)

        // check for errors
        if (eventInstance.hasErrors()) {
            flash.message = "${warehouse.message(code: 'shipping.unableToEditEvent.message', args: [format.metadata(obj: eventInstance?.eventType)])}"
            eventInstance?.errors.allErrors.each {
                log.error "${it}"
            }
            render(view: "editEvent", model: [shipmentInstance: shipmentInstance, eventInstance: eventInstance])
        }

        // save (or add) the event
        if (params.eventId) {
            eventInstance.save(flush: true)
        } else {
            shipmentInstance.addToEvents(eventInstance).save(flush: true)
        }

        redirect(action: 'showDetails', id: shipmentInstance.id)
    }

    def addShipmentItem() {
        log.info "parameters: " + params

        [shipmentInstance : Shipment.get(params.id),
         containerInstance: Container.get(params?.containerId),
         itemInstance     : new ShipmentItem()]
    }

    def addReferenceNumber() {
        def referenceNumber = new ReferenceNumber(params)
        def shipment = Shipment.get(params.shipmentId)
        shipment.addToReferenceNumbers(referenceNumber)
        flash.message = "${warehouse.message(code: 'shipping.addedReferenceNumber.message')}"
        redirect(action: 'show', id: params.shipmentId)
    }

    def form() {
        [shipments: Shipment.list()]
    }

    def view() {}

    def generateDocuments() {
        def shipmentInstance = Shipment.get(params.id)
        def shipmentWorkflow = shipmentService.getShipmentWorkflow(shipmentInstance)

        if (shipmentWorkflow.documentTemplate) {
            render(view: "templates/$shipmentWorkflow.documentTemplate", model: [shipmentInstance: shipmentInstance])
        } else {
            // just go back to the show details page if there is no templaet associated with this shipment workflow
            redirect(action: "showDetails", params: ['id': shipmentInstance.id])
        }
    }

    Person convertStringToPerson(String name) {
        def person = new Person()
        if (name) {
            def nameArray = name.split(" ")
            nameArray.each {
                if (it.contains("@")) {
                    person.email = it
                } else if (!person.firstName) {
                    person.firstName = it
                } else if (!person.lastName) {
                    person.lastName = it
                } else {
                    person.lastName += " " + it
                }
            }
        }
        return person
    }


    def addToShipment() {

        // Get product IDs and convert them to String
        def productIds = params.list('product.id')
        productIds = productIds.collect { String.valueOf(it) }

        Location location = Location.get(session.warehouse.id)
        def commandInstance = shipmentService.getAddToShipmentCommand(productIds, location)

        [commandInstance: commandInstance]
    }


    def addToShipmentPost(ItemListCommand command) {

        println "add to shipment post " + params.shipmentContainerKey

        if (!params?.shipmentContainerKey) {
            command.errors.rejectValue("items", "addToShipment.container.invalid")
            render(view: "addToShipment", model: [commandInstance: command])
            return
        }

        def shipmentContainer = params?.shipmentContainerKey?.split(":")
        if (shipmentContainer) {

            def shipment = Shipment.get(shipmentContainer[0])
            def container = Container.get(shipmentContainer[1])

            if (!shipment) {
                command.errors.rejectValue("items", "addToShipment.container.invalid")
                render(view: "addToShipment", model: [commandInstance: command])
                return
            }

            command.items.each {
                it.shipment = shipment
                it.container = container
            }

            try {
                boolean atLeastOneUpdate = shipmentService.addToShipment(command)
                if (atLeastOneUpdate) {
                    flash.message = "${warehouse.message(code: 'shipping.shipmentItemsHaveBeenAdded.message')}"
                    redirect(controller: "createShipmentWorkflow", action: "createShipment",
                            id: shipment.id, params: ["skipTo": "Packing", "containerId": container?.id])
                    return
                } else {
                    flash.message = "${warehouse.message(code: 'shipping.noShipmentItemsHaveBeenUpdated.message')}"
                }
            } catch (ShipmentItemException e) {
                flash['errors'] = e.shipmentItem.errors
                render(view: "addToShipment", model: [commandInstance: command])
                return
            } catch (ValidationException e) {
                flash['errors'] = e.errors
                render(view: "addToShipment", model: [commandInstance: command])
                return
            }
        }

        redirect(controller: "inventory", action: "browse")
    }


    def exportPackingList() {
        log.info "Export packing list for shipment " + params
        Shipment shipment = Shipment.get(params.id)
        if (!shipment) {
            throw new Exception("Could not locate shipment with ID " + params.id)
        }

        OutputStream outputStream = null
        try {
            // Write the file to the response
            ByteArrayOutputStream baos = new ByteArrayOutputStream()
            response.contentType = "application/vnd.ms-excel"
            response.setHeader 'Content-disposition', "attachment; filename=\"Shipment ${shipment?.shipmentNumber} - Packing List.xls\""
            shipmentService.exportPackingList(params.id, baos)
            response.outputStream << baos.toByteArray()
            response.outputStream.flush()
            return

        } catch (IOException e) {
            flash.message = "Failed to export packing list due to the following error: " + e.message
        } catch (Exception e) {
            log.warn("Failed to export packing list due to the following error: " + e.message, e)
            flash.message = "Failed to export packing list due to the following error: " + e.message
        } finally {
            if (outputStream != null) {
                try {
                    outputStream.close()
                } catch (IOException e) {
                    log.error('IOException occurred while closing output stream', e)
                }
            }
        }
        redirect(controller: "shipment", action: "showDetails", id: params.id)
    }
}

class ReceiveShipmentCommand implements Serializable {

    String comments
    Person recipient
    Receipt receipt
    Shipment shipment
    Transaction transaction
    Boolean creditStockOnReceive = true
    Date actualDeliveryDate

    static constraints = {
        receipt(nullable: true)
        shipment(nullable: false, validator: { value, obj -> obj.shipment.hasShipped() && !obj.shipment.wasReceived() })
        transaction(nullable: true)
        recipient(nullable: false)
        comments(nullable: true)
        creditStockOnReceive(nullable: false)
        actualDeliveryDate(nullable: false)
    }

}
