import net.kromhouts.HqlBuilder
import grails.orm.PagedResultList
import org.compass.core.engine.SearchEngineQueryParseException

/**
* Service class that encapsulates the business logic for InventoryItem searches.
*/
class InventoryItemSearchService {

    boolean transactional = false

    def dateUtilService
    def messageSource

    def paramsMax = 100000

    /**
    * Selects and returns the correct search results based on the supplied quickSearch.
    * @param params The request params, may contain params.quickSearch string to specify the search.
    * @param locale The locale to use when generating result.message.
    */
    def getQuickSearch(params, locale) {
        def result = [:]
        result.quickSearch = params.quickSearch ?: "all"

        def getMessage = { Map m ->
            messageSource.getMessage(m.code, m.args == null ? null : m.args.toArray(), locale)
        }

        switch (result.quickSearch) {
            case "inventoryBelowReorder":
                result.inventoryItemList = getInventoryBelowReorder(params)
                if(result.inventoryItemList.totalCount > 0)
                    result.message = getMessage(code:"inventoryItem.search.text.below.reorder.description")
                else
                    result.message = getMessage(code:"inventoryItem.search.text.below.reorder.none.found")
                break
            case "inventoryBelowReorderAll":
                result.inventoryItemList = getInventoryBelowReorder(params, false)
                if(result.inventoryItemList.totalCount > 0)
                    result.message = getMessage(code:"inventoryItem.search.text.below.reorder.all.description")
                else
                    result.message = getMessage(code:"inventoryItem.search.text.below.reorder.none.found")
                break
            case "recentlyUsed":
                result.daysBack = params.daysBack?.isInteger() ? params.daysBack.toInteger() : 14
                result.inventoryItemList = getRecentlyUsed(params, result.daysBack)
                if(result.inventoryItemList.totalCount > 0)
                    result.message = getMessage(code:"inventoryItem.search.text.recently.used.description", args:[result.daysBack])
                else
                    result.message = getMessage(code:"inventoryItem.search.text.recently.used.none.found", args:[result.daysBack])
                break
            default:
                result.inventoryItemList = getAll(params)
                if(result.inventoryItemList.totalCount > 0)
                    result.message = getMessage(code:"inventoryItem.search.text.all.description")
                else
                    result.message = getMessage(code:"inventoryItem.search.text.all.none.found")
                break
        } // switch.

        // Success.
        return result

    } // getQuickSearch

    /**
    * Get all inventory items.
    * @param params The request params.
    */
    def getAll(params) {
        params.max = Math.min(params?.max?.toInteger() ?: 10, paramsMax)
        params.offset = params?.offset?.toInteger() ?: 0
        params.sort = params?.sort ?: "name"
        params.order = params?.order ?: "asc"

        def inventoryItemList = InventoryItem.createCriteria().list(
            max: params.max,
            offset: params.offset,
            sort: params.sort,
            order: params.order) {
            } // createCriteria
    } // getAll

    /**
    * List inventory items that are below reorder point.
    * @param params The request params.
    * @param onlyReorderEnabled Only include items with reorder enabled, defaults to true.
    */
    def getInventoryBelowReorder(params, onlyReorderEnabled=true) {
        params.max = Math.min(params?.max?.toInteger() ?: 10, paramsMax)
        params.offset = params?.offset?.toInteger() ?: 0
        params.sort = params?.sort ?: "name"
        params.order = params?.order ?: "asc"

        def inventoryItemList = InventoryItem.createCriteria().list(
            max: params.max,
            offset: params.offset,
            sort: params.sort,
            order: params.order) {
                eq("isActive", true)
                if(onlyReorderEnabled)
                    eq("enableReorderListing", true)
                leProperty("unitsInStock", "reorderPoint")
            } // createCriteria
    } // getInventoryBelowReorder

    /**
    * Search for inventory items that are below reorder point.
    * @param params The request params.
    * @param locale The locale to use when generating result.message.
    */
    def getReorderSearch(params, locale) {
        def result = [:]

        def getMessage = { Map m ->
            messageSource.getMessage(m.code, m.args == null ? null : m.args.toArray(), locale)
        }

        params.max = Math.min(params?.max?.toInteger() ?: 10, paramsMax)
        params.offset = params?.offset?.toInteger() ?: 0

        def sort = "inventoryItem." + (params?.sort ?: "name")
        def order = params?.order == "desc" ? "desc" : "asc"

        def q = new HqlBuilder(max: params.max, offset: params.offset).query {
            select 'count(distinct inventoryItem) as inventoryItemCount'
            from 'InventoryItem as inventoryItem',
                    'left join inventoryItem.alternateSuppliers as alternateSupplier'
            where 'inventoryItem.unitsInStock <= inventoryItem.reorderPoint'
                    and 'inventoryItem.isActive = true'
                    and 'inventoryItem.isObsolete = false'

                    if(!params.includeReorderListingDisabled)
                        and "inventoryItem.enableReorderListing = true"

                    if(params.selectedSupplier?.isLong()) {
                        namedParams.supplier = Supplier.get(params.selectedSupplier.toLong())
                        if(params.includeAlternateSuppliers)
                            and "(inventoryItem.preferredSupplier = :supplier or alternateSupplier = :supplier)"
                        else
                            and "inventoryItem.preferredSupplier = :supplier"
                    } // if selectedSupplier

                    if(params.selectedGroups) {
                        namedParams.selectedGroupIds = params.selectedGroups
                        and  "inventoryItem.inventoryGroup.id in(:selectedGroupIds)"
                    }

                    if(!params.includeOnBackOrder) {
                        // Subquery!
                        def onBackOrder = new HqlBuilder().query {
                            from "InventoryItemPurchase p"
                            where "p.inventoryItem = inventoryItem"
                                    and "p.inventoryItem = inventoryItem"
                                    and "p.inventoryItemPurchaseType.id = 1" // Order Placed.
                                    and "p.receivedComplete = false"
                                    and "p.date > :oneMonthAgo"
                        }

                        namedParams.oneMonthAgo = new Date() - 30
                        and "not exists ($onBackOrder.query)"
                    }

        } // query

        def totalCount = InventoryItem.executeQuery(q.query, q.namedParams)[0].toInteger()

        q.select = 'distinct inventoryItem'
        q.order = "by $sort $order, inventoryItem.id asc"
        def list = InventoryItem.executeQuery(q.query, q.namedParams, q.paginateParams)

        result.inventoryItemList = new PagedResultList(list, totalCount)

        // Get the result message.
        if(result.inventoryItemList.totalCount > 0)
            result.message = getMessage(code:"inventoryItem.search.text.below.reorder.description")
        else
            result.message = getMessage(code:"inventoryItem.search.text.below.reorder.none.found")

        // Success.
        return result

    } // getReorderSearch

    /**
    * Get a list of recently used inventory items.
    * @param params The request params.
    * @param daysBack The number of days back to get results for.
    */
    def getRecentlyUsed(params, daysBack) {
        def paginateParams = [:]
        paginateParams.max = Math.min(params?.max?.toInteger() ?: 10, paramsMax)
        paginateParams.offset = params?.offset?.toInteger() ?: 0

        def sort = "inventoryItem." + (params?.sort ?: "name")
        def order = params?.order == "desc" ? "desc" : "asc"
        def orderBy = " order by " + sort + ' ' + order

        def namedParams = [:]
        namedParams.startOfDay = dateUtilService.today - daysBack

        def baseQuery = "from InventoryItem as inventoryItem \
                                        left join inventoryItem.inventoryMovements as inventoryMovement \
                                        where (inventoryItem.isActive = true \
                                                    and inventoryMovement.date > :startOfDay \
                                                    and inventoryMovement.inventoryMovementType = 1 \
                                                    )"

        def searchQuery = "select distinct inventoryItem " + baseQuery + orderBy
        def list = InventoryItem.executeQuery(searchQuery, namedParams, paginateParams)

        def countQuery = "select count(distinct inventoryItem) as inventoryItemCount " + baseQuery
        def totalCount = InventoryItem.executeQuery(countQuery, namedParams)[0].toInteger()

        def inventoryItemInstanceList = new PagedResultList(list, totalCount)
        return inventoryItemInstanceList
    } // getRecentlyUsed

    /**
    * Get a list of inventory items by search text.
    * @param params The request params.
    * @param locale The locale to use when generating result.message.
    */
    def getTextSearch(params, locale) {
        def result = [:]
        result.searchText = params.searchText.trim() ?: "" // User supplied text.
        result.queryString = "" // Modified string that will be passed to searchable query.

        def getMessage = { Map m ->
            messageSource.getMessage(m.code, m.args == null ? null : m.args.toArray(), locale)
        }

        params.max = Math.min(params?.max?.toInteger() ?: 10, paramsMax)
        params.offset = params?.offset?.toInteger() ?: 0
        params.sort = params?.sort ?: "id"
        params.order = params?.order ?: "asc"

        // Build searchableParams.
        // Do not include params.sort, since not all properites are indexed.
        def searchableParams = [:]
        searchableParams.max = params.max
        searchableParams.offset = params.offset
        searchableParams.reload = true
        searchableParams.defaultOperator =  'or'
        def properitesList = []
        if(params.searchName)
            properitesList << '$/InventoryItem/name'
        if(params.searchDescription)
            properitesList << '$/InventoryItem/description'
        if(params.searchComment)
            properitesList << '$/InventoryItem/comment'
        if(params.searchLocation)
            properitesList << '$/InventoryItem/inventoryLocation/name'
        if(params.searchGroup)
            properitesList << '$/InventoryItem/inventoryGroup/name'
        if(params.searchSpareFor) {
            properitesList << '$/InventoryItem/spareFor/name'
            properitesList << '$/InventoryItem/spareFor/description'
            properitesList << '$/InventoryItem/spareFor/comment'
        }
        if(properitesList)
            searchableParams.properties = properitesList

        // Check searchText for key words and modifiers.
        def hasIsActive = result.searchText.contains('isActive')
        def hasIsObsolete = result.searchText.contains('isObsolete')
        def hasBracket = result.searchText.contains('(') || result.searchText.contains(')')
        def containsModifier = { s ->
            s.contains('"') ||
            s.contains('~') ||
            s.contains('*') ||
            s.contains('(') ||
            s.contains(')') ||
            s.contains('+') ||
            s.contains('-') ||
            s.contains('^') ||
            s.contains('OR') ||
            s.contains('AND') ||
            s.contains('NOT') ||
            s.contains('TO') ||
            s.contains('isObsolete') ||
            s.contains('isActive')
        }

        // Expand search with wildcards.
        def addWildcards = { text ->
            text = text.tokenize().collect { token ->
                if(!containsModifier(token))
                    '*'+token+'*'
                else
                    token
            }.join(' ')
            return text
        }

        // Default isActive and isObsolete.
        def addDefaultFlags = { text ->
            if(!hasBracket)
                text = '( '+text+' )'
            if(!hasIsActive)
                text = text + ' AND isActive:"true" '
            if(!hasIsObsolete)
                text = text + ' AND isObsolete:"false" '
            return text
        }

        result.queryString = addWildcards(result.searchText)
        result.queryString = addDefaultFlags(result.queryString)

        // Perform the searchable query.
        try {
            result.inventoryItemList = InventoryItem.search(result.queryString, searchableParams)

            // Would be nice if this worked.
//             result.inventoryItemList = InventoryItem.search(result.searchText, searchableParams) {
//                 must(term("isActive", true))
//                 must(term("isObsolete", false))
//             }

        } catch (e) {
            log.error e
            result.inventoryItemList = [:]
            result.inventoryItemList.results = []
            result.inventoryItemList.total = 0
        }

        // Sort the returned instances.
        if(params.sort != 'id') {
            if(params.order == 'asc') {
                if(params.sort == 'name' || params.sort == 'description')
                    result.inventoryItemList.results.sort { p1, p2 -> p1[params.sort].compareToIgnoreCase(p2[params.sort]) }
                else if(params.sort == 'inventoryGroup') {
                    result.inventoryItemList.results.sort { p1, p2 ->
                        p1.inventoryGroup.name.compareToIgnoreCase(p2.inventoryGroup.name)
                    }
                }
                else if(params.sort == 'unitsInStock')
                    result.inventoryItemList.results.sort {p1, p2 -> p1[params.sort]  <=> p2[params.sort] }
            } // asc.
            else {
                if(params.sort == 'name' || params.sort == 'description')
                    result.inventoryItemList.results.sort { p1, p2 -> p2[params.sort].compareToIgnoreCase(p1[params.sort]) }
                else if(params.sort == 'inventoryGroup') {
                    result.inventoryItemList.results.sort { p1, p2 ->
                        p2.inventoryGroup.name.compareToIgnoreCase(p1.inventoryGroup.name)
                    }
                }
                else if(params.sort == 'unitsInStock')
                    result.inventoryItemList.results.sort {p1, p2 -> p2[params.sort] <=> p1[params.sort]}
            } // desc.
        } // sort.

        // Create a PagedResultList.
        result.inventoryItemList = new PagedResultList(result.inventoryItemList.results, result.inventoryItemList.total)

        // Get the result message.
        if(result.inventoryItemList.totalCount > 0)
            result.message = getMessage(code:"inventoryItem.search.text.found", args: [result.queryString])
        else
            result.message = getMessage(code:"inventoryItem.search.text.none.found", args: [result.queryString])

        // Success.
        return result

    } // getTextSearch()

} // end class
