import grails.util.GrailsUtil
import au.com.bytecode.opencsv.CSVWriter
import au.com.bytecode.opencsv.CSVReader
import org.apache.commons.lang.WordUtils

/**
 * Provides some csv import/export methods.
 * Requires the opencsv jar to be available which is included in the grails-export plugin.
 */
class PersonCsvService {

    boolean transactional = false

    def authService

    def g = new org.codehaus.groovy.grails.plugins.web.taglib.ApplicationTagLib()

    def sessionFactory
    def propertyInstanceMap = org.codehaus.groovy.grails.plugins.DomainClassGrailsPlugin.PROPERTY_INSTANCE_MAP

    /**
    * Import persons creating items as required.
    */
    def importPersons(request) {
        Person.withTransaction { status ->
            def result = [:]

            def kByteMultiplier = 1000
            def fileMaxSize = 800 * kByteMultiplier
            def logFileLink = g.link(controller: "appCore", action: "appLog") {"log"}

            def multiPartFile = request.getFile('file')

            InputStreamReader sr = new InputStreamReader(multiPartFile.inputStream)
            CSVReader reader = new CSVReader(sr)

            def fail = { Map m ->
                status.setRollbackOnly()
                reader.close()
                result.error = [ code: m.code, args: m.args ]
                return result
            }

            if(!multiPartFile || multiPartFile.isEmpty())
                return fail(code: "default.file.not.supplied")

            if (multiPartFile.getSize() > fileMaxSize)
                return fail(code: "default.file.over.max.size", args: [fileMaxSize/kByteMultiplier, "kB"])

            def line = []
            def lineNumber = 0
            def maxNumberOfColumns = 13
            def personParams = [:]
            def personProperties = ["loginName", "firstName", "lastName",
                                                    "ROLE_Manager", "ROLE_AppUser",
                                                    "ROLE_TaskManager", "ROLE_TaskUser",
                                                    "ROLE_InventoryManager", "ROLE_InventoryUser",
                                                    "ROLE_AssetManager", "ROLE_AssetUser",
                                                    "ROLE_ProductionManager", "ROLE_ProductionUser"]

            def personInstance
            def loginNamesAndPasswords = [:]

            def nextLine = {
                    line = reader.readNext()
                    lineNumber ++
                    log.info "Processing line: " + lineNumber
            }

            // Get first line.
            nextLine()

            // Check for header line 1.
            if(line != templateHeaderLine1) {
                log.error "Failed to find header line 1. "
                log.error "Required: " + templateHeaderLine1.toString()
                log.error "Supplied: " + line.toString()
                return fail(code: "default.file.no.header")
            }

            log.info "Header line found."

            // Prepare the first body line.
            nextLine()

            // Primary loop.
            while(line) {

                if(line.size() > maxNumberOfColumns) {
                    log.error "Too many columns on line: " + lineNumber
                    return fail(code: "person.import.failure", args: [lineNumber, logFileLink])
                }

                // Ignore comment lines.
                if(line.toString().toLowerCase().contains("comment")) {
                    log.info "Comment line found."
                    nextLine()
                    continue
                }

                // Ignore example lines.
                if(line.toString().toLowerCase().contains("example")) {
                    log.info "Example line found."
                    nextLine()
                    continue
                }

                // Parse the line into the params map.
                personParams = [:]
                line.eachWithIndex { it, j ->
                    personParams."${personProperties[j]}" = it.trim()
                }

                // Debug
                log.debug " Supplied params: "
                log.debug personParams

                // Ignore blank lines.
                if(personParams.loginName == '') {
                    log.info "No login name found."
                    nextLine()
                    continue
                }

                // Login Name.
                personParams.loginName = personParams.loginName.toLowerCase()

                // First Name.
                personParams.firstName = WordUtils.capitalizeFully(personParams.firstName)

                // First Name.
                personParams.lastName = WordUtils.capitalizeFully(personParams.lastName)

                // Password.
                personParams.pass = personParams.pass ?: authService.randomPassword

                // Debug
                log.debug "personParams: "
                log.debug personParams

                personInstance = Person.findByLoginName(personParams.loginName)

                if(!personInstance) {
                    log.info "Creating person with login name: " + personParams.loginName
                    personInstance = new Person(loginName: personParams.loginName,
                                                                                firstName: personParams.firstName,
                                                                                lastName: personParams.lastName,
                                                                                pass: personParams.pass,
                                                                                password: authService.encodePassword(personParams.pass))

                    // Save.
                    if(personInstance.hasErrors() || !personInstance.save()) {
                        log.error "Failed to create person on line: " + lineNumber
                        log.debug personInstance.errors
                        return fail(code: "person.import.failure", args: [lineNumber, logFileLink])
                    }

                    // Fill map with persons and passwords.
                    loginNamesAndPasswords."${personParams.loginName}" = personParams.pass
                }
                else
                    log.info "Person already exists with login name: " + personParams.loginName

                // Add Authorities.
                if('true'.equalsIgnoreCase(personParams.ROLE_Manager))
                    personInstance.addToAuthorities(Authority.get(2))
                if('true'.equalsIgnoreCase(personParams.ROLE_AppUser))
                    personInstance.addToAuthorities(Authority.get(3))
                if('true'.equalsIgnoreCase(personParams.ROLE_TaskManager))
                    personInstance.addToAuthorities(Authority.get(4))
                if('true'.equalsIgnoreCase(personParams.ROLE_TaskUser))
                    personInstance.addToAuthorities(Authority.get(5))
                if('true'.equalsIgnoreCase(personParams.ROLE_InventoryManager))
                    personInstance.addToAuthorities(Authority.get(6))
                if('true'.equalsIgnoreCase(personParams.ROLE_InventoryUser))
                    personInstance.addToAuthorities(Authority.get(7))
                if('true'.equalsIgnoreCase(personParams.ROLE_AssetManager))
                    personInstance.addToAuthorities(Authority.get(8))
                if('true'.equalsIgnoreCase(personParams.ROLE_AssetUser))
                    personInstance.addToAuthorities(Authority.get(9))
                if('true'.equalsIgnoreCase(personParams.ROLE_ProductionManager))
                    personInstance.addToAuthorities(Authority.get(10))
                if('true'.equalsIgnoreCase(personParams.ROLE_ProductionUser))
                    personInstance.addToAuthorities(Authority.get(11))

                if(lineNumber % 100 == 0)
                    cleanUpGorm()

                if(!result.error) nextLine()
            } //while(line)

            // Success.
            log.info "End of file."
            result.loginNamesAndPasswords = g.message(code: "person.import.success") + '\n'
            result.loginNamesAndPasswords += "Login names and passwords: " + '\n'
            result.loginNamesAndPasswords += '\n'
            loginNamesAndPasswords.each() { result.loginNamesAndPasswords += (it.key + ' : ' + it.value + '\n') }
            reader.close()
            return result

         } //end withTransaction
    } // end importPersons()

    /**
    * Build a persons template csv file.
    * This template can then be populated for import.
    * @returns The template as a String in csv format.
    */
    def buildPersonsTemplate() {

        StringWriter sw = new StringWriter()
        CSVWriter writer = new CSVWriter(sw)

        writeTemplateLines(writer)

        writer.close()
        return sw.toString()
    }

    private writeTemplateLines(writer) {
        writer.writeNext(templateHeaderLine1 as String[])
        writer.writeNext()
        writer.writeNext("Comment: The header line is required.")
        writer.writeNext("Comment: Required columns are marked with a (*) in the header line.")
        writer.writeNext("Comment: Lists of items in a column must be separated by a semicolon (;), not a comma.")
        writer.writeNext("Comment: Role columns must be 'true' or 'false'.")
        writer.writeNext("Comment: Identical and existing names will be considered as the same item.")
        writer.writeNext("Comment: Lines containing 'comment' will be ignored.")
        writer.writeNext("Comment: Lines containing 'example' will be ignored.")
        writer.writeNext("Comment: This file must be saved as a CSV file before import.")
        writer.writeNext()
    }

    private getTemplateHeaderLine1() {
            ["Login Name*", "First Name*", "Last Name*",
            "Role: Business Manager*", "Role: Application User*",
            "Role: Task Manager*", "Role: Task User*",
            "Role: Inventory Manager*", "Role: Inventory User*",
            "Role: Asset Manager*", "Role: Asset User*",
            "Role: Production Manager*", "Role: Production User*"]
    }

    /**
    * This cleans up the hibernate session and a grails map.
    * For more info see: http://naleid.com/blog/2009/10/01/batch-import-performance-with-grails-and-mysql/
    * The hibernate session flush is normal for hibernate.
    * The map is apparently used by grails for domain object validation errors.
    * A starting point for clean up is every 100 objects.
    */
    def cleanUpGorm() {
        def session = sessionFactory.currentSession
        session.flush()
        session.clear()
        propertyInstanceMap.get().clear()
    }

} // end class