--[[----------------------------------------------------------------------------

WFCatSearchOnlineRequest.lua

--------------------------------------------------------------------------------

ADOBE CONFIDENTIAL
------------------
Copyright 2015 Adobe Systems Incorporated
All Rights Reserved.

NOTICE: All information contained herein is, and remains the property of
Adobe Systems Incorporated and its suppliers, if any. The intellectual and
technical concepts contained herein are proprietary to Adobe Systems
Incorporated and its suppliers and are protected by all applicable
intellectual property laws, including trade secret and copyright laws.
Dissemination of this information or reproduction of this material is
strictly forbidden unless prior written permission is obtained from
Adobe Systems Incorporated.

------------------------------------------------------------------------------]]

	-- Rx
local Rx = require "Rx"
local Disposable = Rx.Disposable
local Subject = Rx.Subject
local ReplaySubject = Rx.ReplaySubject
local WFRxRunUntilDone = require 'WFRxRunUntilDone'

local KSJsonEncoder = require "KSJsonEncoder"
local KSJsonDecoder = require "KSJsonDecoder"
local KSTableUtils = require "KSTableUtils"
local WFTableArchiving = require 'WFTableArchiving'

local WFCatSearchAndSortConstants = require "WFCatSearchAndSortConstants"
local WFConfiguration = require "WFConfiguration"
local WFRxHttpClient = require 'WFRxHttpClient'

	-- WF.core.logging
local WFLogger = require 'WFLogger'
local WFSearchDebugLogging = require 'WFSearchDebugLogging'

local searchLogger = WFLogger( 'WFCatSearchOnlineRequest' )
local autoCompleteLogger = WFLogger( 'WFCatSearchOnlineAutoComplete' )
local facetsLogger = WFLogger( 'WFCatSearchOnlineFacets' )

--------------------------------------------------------------------------------
local CURRENT_ONLINE_ASSET_SOURCE = "lightroom"
local PAGE_SIZE = WFCatSearchAndSortConstants.PAGE_SIZE
--------------------------------------------------------------------------------
local OnlineSearchStatus = WFCatSearchAndSortConstants.OnlineSearchStatus

--============ Logging =======================
local _latestRequestCounter = 0
-- not sure of checking a logger active stauts, hence to prevent the unwanted logging process, this flag is used
local onlineRequestLoggingNeeded =  WFConfiguration.searchConfiguration.onlineRequestLoggingNeeded

--------------------------------------------------------------------------------
local function search_debugPrint( logger, messageType, ... )
	if messageType == "e" or onlineRequestLoggingNeeded then
		WFSearchDebugLogging.search_debugPrint( "WFCatSearchOnlineRequest", logger, messageType, ... )
	end
end

--------------------------------------------------------------------------------
local KSDate_timeNowForAnimation = require 'KSDate'.timeNowForAnimation
local function search_debug_getCurrentTime()
	return KSDate_timeNowForAnimation()
end

--------------------------------------------------------------------------------
local function search_debugPrintTimeConsumed( logger, operationIdentifier, sessionIdentifier , 
												expectedMaxTime , startTime, endTime, 
												additionalData )
	WFSearchDebugLogging.search_debugPrintTimeConsumed( 
		"WFCatSearchOnlineRequest", logger,
		operationIdentifier, sessionIdentifier,expectedMaxTime, 
		startTime, endTime, additionalData )

end

--------------------------------------------------------------------------------
local function getFormattedTimeForSearchDebug( )
	if onlineRequestLoggingNeeded then
		return WFSearchDebugLogging.getFormattedTimeForSearchDebug( "WFCatSearchAndSort" )
	else
		return ""
	end
end

--------------------------------------------------------------------------------

local WFCatSearchOnlineRequest = {
	_catUser = false,
	
	_url = false,
	_searchOptions = false,
	_onlineSearchTerms = false,
	_isValidQuery = false,
	
	_isSearchActive = false,
	_currentStart = false,
	_limit = false,
	
	_totalAvailable = false,
	_totalReceived = false,
	_receivedAssetsMap = false,
	_currentReceivedCount = false,
	
	_searchResultSubject = false,
	_executionStatus = false,
	
	Disposable,
}
--============== Local methods ================--
-------------------- getLightroomDataFromSearchResult ---------------------------
local function getLightroomDataFromSearchResult( searchResult, headers, requestTimeCounter )
	local resultSets = searchResult and searchResult.result_sets
	if resultSets then
		for _,resultSet in pairs( resultSets ) do
			local name = resultSet.name
			if name == CURRENT_ONLINE_ASSET_SOURCE then
			
				local curentItemsCount = ( resultSet.items and #resultSet.items ) or 0
				local totalCount = resultSet.total_items
					
				if onlineRequestLoggingNeeded then	
					
					local message = string.format( 
						"Search: request-id: %s, current page count: %s, total items: %s", 
							headers[ 'x-request-id' ], curentItemsCount, totalCount )
					search_debugPrint( searchLogger, "d", message, 
									requestTimeCounter, getFormattedTimeForSearchDebug(), "\n") 
				end
		
				return resultSet.items,  curentItemsCount, totalCount
			end
		end
	else
		if onlineRequestLoggingNeeded then	
			search_debugPrint( searchLogger, "d", "Search : WFCatSearchOnlineRequest: no result set V2" , 
							requestTimeCounter, getFormattedTimeForSearchDebug(), "\n" ) 
		end
	end
end

-------------------- addNextDataRangeTerms ---------------------------
local function addNextDataRangeTerms( self )
	self._onlineSearchTerms["start_index" ] = self._currentStart
	self._onlineSearchTerms["limit" ] = self._limit 
end

--------------------------------------------------------------------------------
local function getSortOrderType( onlineQuery )
	
	-- Use `relevancy` if there is a text search, otherwise default to `capture_date`
	local sortType = "capture_date"
	
	if onlineQuery[ "q" ] then
		sortType = "relevancy"
	end
	
	return sortType
	
end

-------------------- initializeCommonTerms ---------------------------
local function initializeCommonTerms( self )

	self._onlineSearchTerms[ "scope" ] 				= 	{ CURRENT_ONLINE_ASSET_SOURCE  }
	self._onlineSearchTerms[ "sort_order" ] 		=  	"desc"
	self._onlineSearchTerms[ "sort_orderby" ] 		=  	getSortOrderType( self._onlineSearchTerms )
	self._onlineSearchTerms[ "fields_excluded" ] 	= 	{ "links", "tags" }
	
end

-------------------- analyseSearchResultData ---------------------------
local function analyseSearchResultData( self, searchResult, headers, requestTimeCounter )
	
	local newResults = {}
	local dataReceived = false
	local metrics = searchResult.metrics
	if metrics then
	
		if onlineRequestLoggingNeeded then	
			local searchResultMetricsData = WFTableArchiving.tableToString( metrics )
			search_debugPrint( searchLogger, "d", "Search: request-id: " .. headers[ 'x-request-id' ] .. " | result metrics " .. searchResultMetricsData, 
							requestTimeCounter, getFormattedTimeForSearchDebug() ,"\n") 
		end
		
		-- since we have only one search scope,  metrics.total_hits is our total_hits
		-- hence not changing this to the page's totalCount
		if self._totalAvailable == false then
			self._totalAvailable = metrics.total_hits
		end
	end
	
	if self._totalAvailable > 0 then
		local lightroomResults, currentCount = getLightroomDataFromSearchResult( searchResult, headers, requestTimeCounter )
		currentCount = currentCount or 0
		self._currentReceivedCount = currentCount
		
		if currentCount > 0 then
			dataReceived = true
			self._totalReceived = self._totalReceived + currentCount
			
			if not self._receivedAssetsMap then
				-- currently there are some issues with Federer results
				-- in the successive page requests, we get the repeated data
				-- this results in empty grid.
				-- we are preventing the same here, by checking against present
				-- itemsMap
				self._receivedAssetsMap = {}
			end
			
			for _, assetDetails in ipairs( lightroomResults ) do
			
				local assetId = assetDetails[ "asset_id" ]
				
				if not self._receivedAssetsMap[ assetId ]  then
					
					local relevancyScore = 100
					if type( assetDetails[ "score" ] ) == "number" then
						relevancyScore = assetDetails[ "score" ]
					end
					
					self._receivedAssetsMap[ assetId ] = true
					table.insert( newResults, { assetId, relevancyScore } )
					
				else
					search_debugPrint( searchLogger, "d", "Search: received duplicate asset ".. assetId, 
							requestTimeCounter, getFormattedTimeForSearchDebug() ,"\n")
				end
				
			end
			
		end
		
	end
	-- if current count is 0, we report nil and handle this as completedUnexpectedly case 
	-- if total received is not same as total available
	return dataReceived, newResults
end

-------------------- isFurtherRequestPossible ---------------------------
local function isFurtherRequestPossible( self )
	
	local totalReceived = self._totalReceived
	if ( self._currentReceivedCount % PAGE_SIZE ~= 0 ) then
		-- we have an issue of one item missing
		-- so temp fixing
		totalReceived = totalReceived + 1
	end
	
	return totalReceived < self._totalAvailable and self._searchResultSubject ~= false
	
end

-------------------- setNewSearchDataRange ---------------------------
local function setNewSearchDataRange( self )
	if self._totalAvailable  == false then
		-- we have not recieved the total count
		-- so let us initialize the detais
		self._currentStart = 0
		self._limit = PAGE_SIZE
		self._totalReceived = 0
	else
		if isFurtherRequestPossible( self ) then
			-- we have more elements to obtain
			self._currentStart = self._totalReceived  
			local endLimit = PAGE_SIZE -- math.min( self._totalReceived+PAGE_SIZE, self._totalAvailable )
			self._limit = endLimit
		end
	end
	self._currentReceivedCount  = 0
	addNextDataRangeTerms( self ) 
end

--------------------------------------------------------------------------------

local function sendSearchStatus( self, status, maybeNewResults )
	
	if not self._searchResultSubject then
		-- The request is cancelled. No need send the status
		return
	end
	
	self._executionStatus = status
	
	local statusData = self:getCurrentStatus()
	
	statusData.newResults = maybeNewResults
	
	self._searchResultSubject:onNext( statusData )
	
	if status ~= OnlineSearchStatus.InProgress then
		self._isSearchActive = false
		self._searchResultSubject:onCompleted(  )
	end
end

--------------------------------------------------------------------------------
-- [NMB-28267] Client needs to send the `sort` data from previos response in the next request
-- in order to get more than 10k results when not sorted by `relevancy`
-- Ref: https://wiki.corp.adobe.com/x/ksrRw

local function updateSortValue( self, searchResult )
	
	local resultSets = searchResult and searchResult.result_sets
	
	for _, resultSet in pairs( resultSets or {} ) do
		
		if resultSet.name == CURRENT_ONLINE_ASSET_SOURCE then
			
			self._onlineSearchTerms.sort = resultSet.sort
			break
			
		end
	end
	
end

-------------------------- startNextSetSearch -------------------------------------------
local function startNextSetSearch( self, currentCounter )
	
	if not self._searchResultSubject then
		-- The request is cancelled. No need to make further requests
		return
	end
	
	if self._catUser then
		setNewSearchDataRange( self )
		
		local searchOptions = KSTableUtils.deepCopy( self._searchOptions )
		searchOptions.requestPriority = WFRxHttpClient.requestPriorityHighest
		searchOptions.requestCost = WFRxHttpClient.requestCostMaximizeBandwidth
			-- Prioritize this HTTP request.
		
		-- update the request headers to use the user-provided request-id
		local maybeRequestId = self._onlineSearchTerms.requestId
		if maybeRequestId then
		
			self._onlineSearchTerms.requestId = nil -- reset the requestId so it isn't used for subsequent calls.
			local requestHeaders = searchOptions.requestHeaders or {}
			table.insert( requestHeaders, { header = 'X-Request-Id', value = maybeRequestId } )
			searchOptions.requestHeaders = requestHeaders
			
		end
		
		local searchTerms = KSTableUtils.deepCopy( self._onlineSearchTerms )
		
		if onlineRequestLoggingNeeded then
			local startString = "\n----------------------------------\n"
			
			if currentCounter == 1 then
				search_debugPrint( searchLogger, "d", "Search: url " .. self._url, 
							currentCounter, getFormattedTimeForSearchDebug(), "\n" ) 
			end
			
			local searchInputData = WFTableArchiving.tableToString( searchTerms )
			if maybeRequestId then
				searchInputData = string.format( "requestId: %s \n", maybeRequestId ) .. searchInputData
			end
			
			search_debugPrint( searchLogger, "d", startString .. "Search: requestData \n" .. searchInputData .. "\n", 
							currentCounter, getFormattedTimeForSearchDebug(), "\n" ) 
							
		end
		
		local jsonData = KSJsonEncoder.prettyPrint( searchTerms )
		
		local dataObservable = self._catUser:put( self._url, jsonData, searchOptions )
		
		dataObservable : map( function( searchResult, headers )
			local didResultAnalysisSucceed, message = pcall ( function ()
				if  self._isSearchActive then
					local success, searchResultTable = pcall( KSJsonDecoder.decodeWebSafeJson, searchResult )
					if success then
						local dataReceived, newResults = analyseSearchResultData( self, searchResultTable, headers, currentCounter ) 
						if dataReceived then
							
							-- we received the data, so sending progress
							sendSearchStatus( self, OnlineSearchStatus.InProgress, newResults )
							
							if isFurtherRequestPossible( self ) then
								updateSortValue( self, searchResultTable )
								-- we have more data, so place the next page request
								startNextSetSearch( self, currentCounter )
							else
								-- all data receieved completely
								sendSearchStatus( self, OnlineSearchStatus.Completed )
							end
						else
							
							-- no data received
							-- if available vs received if there is an issue, let us send CompletedUnexpectedly
							if self._totalAvailable ~= self._totalReceived then
								sendSearchStatus( self, OnlineSearchStatus.CompletedUnexpectedly )
							else
								sendSearchStatus( self, OnlineSearchStatus.Completed )
							end
							
						end
						
					else
						sendSearchStatus( self, OnlineSearchStatus.CompletedUnexpectedly )
					end
				end
			end )
			if not didResultAnalysisSucceed then
				search_debugPrint( searchLogger, "d","\n********** IMP ERROR IN PROCESSING ***************\n Search: processing result error \n" .. message .. "\n", 
							currentCounter, getFormattedTimeForSearchDebug(), "\n" ) 
			end
			
		end )
		:tapOnError( function( response, headers )
			
			if headers then
				local decoded, responseTable = pcall( KSJsonDecoder.decodeWebSafeJson, response )
			
				if decoded and type( responseTable ) == 'table' then
					
					local searchResponseData = WFTableArchiving.tableToString( responseTable )
					local startString = "\n----------------------------------\n"
					search_debugPrint( searchLogger, "e", startString .. "Search: responseError \n" .. searchResponseData .. "\n", 
								headers['x-request-id'], getFormattedTimeForSearchDebug(), "\n" ) 

				end
				
			end
			
			sendSearchStatus( self, OnlineSearchStatus.CompletedUnexpectedly )

		end)
		:call( WFRxRunUntilDone ) 
	end
	
	return self._searchResultSubject
end

--------------------------------------------------------------------------------

local function invalidQueryResult( self )
	
	self._totalAvailable = 0
	self._totalReceived = 0
	self._searchResultSubject = ReplaySubject( 1 )
	
	-- directly send completed, with 0 results
	sendSearchStatus( self, OnlineSearchStatus.Completed )
	
	return self._searchResultSubject
	
end

--------------------------------------------------------------------------------
local function startFacetValuesSearch( self )
	
	self._searchResultSubject = ReplaySubject( 1 )
	
	if not self._catUser or not self._isValidQuery then
		
		-- send empty facets list for the requested facets fields
		local facetsMap = {}
		
		for _, key in ipairs( self._onlineSearchTerms.facets_fields or {} ) do
			facetsMap[ key ] = { }
		end
		
		self._searchResultSubject:onNext( facetsMap )
		self._searchResultSubject:onCompleted()
		
	else
		
		-- Set limit to 0 to ignore items in the response
		self._limit = 0	
		self._currentStart = 0
		addNextDataRangeTerms( self )
		
		local searchOptions = KSTableUtils.deepCopy( self._searchOptions )
		local searchTerms = KSTableUtils.deepCopy( self._onlineSearchTerms )
		
		if onlineRequestLoggingNeeded then
			local startString = "\n----------------------------------\n"
			
			search_debugPrint( facetsLogger, "d", "FacetedSearch: url " .. self._url, 
							_latestRequestCounter, getFormattedTimeForSearchDebug(), "\n" ) 
			
			local searchInputData = WFTableArchiving.tableToString( searchTerms )
			
			search_debugPrint( facetsLogger, "d", startString .. "FacetedSearch: requestData \n" .. searchInputData .. "\n", 
							_latestRequestCounter, getFormattedTimeForSearchDebug(), "\n" ) 
		
		end
		
		local jsonData = KSJsonEncoder.prettyPrint( searchTerms )
		local startTime = search_debug_getCurrentTime()
		
		searchOptions.requestPriority = WFRxHttpClient.requestPriorityHighest
		searchOptions.requestCost = WFRxHttpClient.requestCostMaximizeBandwidth
			-- Prioritize this HTTP request.
		
		local dataObservable = self._catUser:put( self._url, jsonData, searchOptions )
		
		dataObservable : map( function( searchResult, headers )
			local success, searchResultTable = pcall( KSJsonDecoder.decodeWebSafeJson, searchResult )
			local facetsMap = {}
			
			if success then
			
				local resultSets = searchResultTable.result_sets
				if resultSets then
					for _, resultSet in ipairs( resultSets ) do
						local name = resultSet.name
						if name == CURRENT_ONLINE_ASSET_SOURCE then
							
							local facetsArray = resultSet.facets or {}
							for _, facetData in ipairs( facetsArray ) do
								facetsMap[ facetData.facet_field ] = facetData.facet_values
							end
							
							if onlineRequestLoggingNeeded then
								local totalTime = search_debug_getCurrentTime() - startTime
								local startString = "\n----------------------------------\n"
								local firstLine = string.format( "FacetedSearch: response { req id: %s } %s sec\n", headers[ 'x-request-id' ], totalTime )
								local facetsData = WFTableArchiving.tableToString( facetsMap )
								
								search_debugPrint( facetsLogger, "d", startString .. firstLine .. facetsData .. "\n",
										getFormattedTimeForSearchDebug(), "\n" )
								
							end
						end
					end
				end
			end
			
			self._searchResultSubject:onNext( facetsMap )
			self._searchResultSubject:onCompleted()
			
		end )
		
		:tapOnError( function( response, headers )
			
			if headers then
				local decoded, responseTable = pcall( KSJsonDecoder.decodeWebSafeJson, response )
			
				if decoded then
					local startString = "\n----------------------------------\n"
					local searchResponseData = WFTableArchiving.tableToString( responseTable )
					
					search_debugPrint( facetsLogger, "e", startString .. "FacetedSearch: error \n" .. searchResponseData .. "\n", 
							headers['x-request-id'], getFormattedTimeForSearchDebug(), "\n" )
					
				end
			end
			
			self._searchResultSubject:onNext( {} )
			self._searchResultSubject:onCompleted()
			
		end)
		:call( WFRxRunUntilDone ) 
	end

	return self._searchResultSubject

end


-------------------------- getMetadataSearch -------------------------------------------
local function getMetadataSearch( self )
	
	self._searchResultSubject = Subject( )
	
	if self._catUser then
		
		-- set METADATA_ONLY flag to restrict the response
		local resultType = { }
		resultType["core_resultType"] = "METADATA_ONLY"
		self._onlineSearchTerms[ "hints" ] = self._onlineSearchTerms[ "hints" ] or { }
		table.insert( self._onlineSearchTerms[ "hints" ], resultType )
		
		local searchOptions = KSTableUtils.deepCopy( self._searchOptions )
		local searchTerms = KSTableUtils.deepCopy( self._onlineSearchTerms )
		
		if onlineRequestLoggingNeeded then
			local startString = "\n----------------------------------\n"
			local searchInputData = WFTableArchiving.tableToString( searchTerms )
			search_debugPrint( facetsLogger, "d", startString .. "Search: metadata request\n" .. searchInputData .. "\n", 
							_latestRequestCounter, getFormattedTimeForSearchDebug(), "\n" ) 
		
		end
		
		local jsonData = KSJsonEncoder.prettyPrint( searchTerms )
		
		searchOptions.requestPriority = WFRxHttpClient.requestPriorityHighest
		searchOptions.requestCost = WFRxHttpClient.requestCostMaximizeBandwidth
			-- Prioritize this HTTP request.
		
		local dataObservable = self._catUser:put( self._url, jsonData, searchOptions )
		
		dataObservable : map( function( searchResult )
			local success, searchResultTable = pcall( KSJsonDecoder.decodeWebSafeJson, searchResult )
			local metadataMap = {}
			if success then
			
				local resultSets = searchResultTable.result_sets
				if resultSets then
					for _, resultSet in ipairs( resultSets ) do
						local name = resultSet.name
						if name == CURRENT_ONLINE_ASSET_SOURCE then
							
							local metadataArray = resultSet.meta and resultSet.meta.field_info or {}
							
							for _, metadata in ipairs( metadataArray ) do
								metadataMap[ metadata.canonical_name ] = metadata.display_names
							end

							if onlineRequestLoggingNeeded then
								local startString = "\n----------------------------------\n"
								local metadataStr = WFTableArchiving.tableToString( metadataMap )
								search_debugPrint( facetsLogger, "d", startString .. "Search: metadata response \n" .. metadataStr .. "\n", 
										getFormattedTimeForSearchDebug(), "\n" )
							end
						end
					end
				end
			end
			
			self._searchResultSubject:onNext( metadataMap )
			self._searchResultSubject:onCompleted()
			
		end )
		:tapOnError( function( response, headers )
			
			if headers then
				local decoded, responseTable = pcall( KSJsonDecoder.decodeWebSafeJson, response )
			
				if decoded then
					
					local startString = "\n----------------------------------\n"
					local searchResponseData = WFTableArchiving.tableToString( responseTable )
					
					search_debugPrint( facetsLogger, "e", startString .. "Search: metadata error \n" .. searchResponseData .. "\n", 
							headers['x-request-id'], getFormattedTimeForSearchDebug(), "\n" )
				end
			end
			
			self._searchResultSubject:onNext( {} )
			self._searchResultSubject:onCompleted()
			
		end)
		:call( WFRxRunUntilDone ) 
	end

	return self._searchResultSubject

end

--------------------------------------------------------------------------------
local function startAutoComplete( self )

	self._searchResultSubject = Subject( )
	
	if self._catUser then
		
		local startTime = search_debug_getCurrentTime()
		
		local searchOptions = KSTableUtils.deepCopy( self._searchOptions )
		
		searchOptions.requestPriority = WFRxHttpClient.requestPriorityHighest
		searchOptions.requestCost = WFRxHttpClient.requestCostMaximizeBandwidth
			-- Prioritize this HTTP request.
			
		local dataObservable = self._catUser:put( self._url, "", searchOptions )
		
		dataObservable : map( function( searchResult, headers )
			local _, searchResultTable = pcall( KSJsonDecoder.decodeWebSafeJson, searchResult )

			if onlineRequestLoggingNeeded then
				local startString = "\n----------------------------------\n"
				local autoCompleteStr = WFTableArchiving.tableToString( searchResultTable )
				search_debugPrint( autoCompleteLogger, "d", startString .. "Autocomplete: response \n" .. autoCompleteStr .. "\n", 
						getFormattedTimeForSearchDebug(), "\n" )
			end
			
			local suggestions = searchResultTable.suggested_completions or {}
			
			local endTime = search_debug_getCurrentTime()
			local expectedTime
			local suggestionCount = string.format( "%s suggestions | requestId: %s\n", #suggestions, headers[ 'x-request-id' ] )
			search_debugPrintTimeConsumed( autoCompleteLogger, "Autocomplete: URL: ", self._url, expectedTime, startTime, endTime, suggestionCount )
			
			local results = {}
			for _, autoCompleteValue in ipairs( suggestions ) do
				
				local facet_key = autoCompleteValue.facet_key or "search"
				local values = results[ facet_key ] or {}
				
				local facetValue = {}
				facetValue[ 'href' ] = autoCompleteValue.href
				facetValue[ 'score' ] = autoCompleteValue.score
				facetValue[ 'display_name' ] = autoCompleteValue.name
				facetValue[ 'value' ] = autoCompleteValue.value
				
				-- if "value" field was empty, try "id" field ( applicable for people facet )
				if facetValue[ 'value' ] == nil then
					facetValue[ 'value' ] = autoCompleteValue.id
				end
				
				-- if "id" field was empty, use "name" field
				if facetValue[ 'value' ] == nil then
					facetValue[ 'value' ] = autoCompleteValue.name
				end
				
				-- [anmurark]: the value could be number or string depending on the facet type
				-- if it can be represented as number, change it to number because it will be
				-- 1. consistent with the facet values API response from USS
				-- 2. easy to sort based on number if required.
				facetValue[ 'value' ] = tonumber( facetValue[ 'value' ] ) or facetValue[ 'value' ]
				
				values[ #values + 1 ] = facetValue
				
				results[ facet_key ] = values
				
			end
			
			local metadata = {}
			metadata[ 'requestId' ] = headers['x-request-id']
			metadata[ 'url' ] = WFCatSearchAndSortConstants.getAutoCompleteEndpoint()
			metadata[ 'count' ] = #suggestions
			metadata[ 'responseTime' ] = endTime - startTime
			
			local response = {}
			
			response[ 'suggestions' ] = results
			response[ 'metadata' ] = metadata
			
			self._searchResultSubject:onNext( response )
			self._searchResultSubject:onCompleted()
			
		end )
		
		:tapOnError( function( response, headers )
			
			-- ignore response if there is a newer request
			if response == "canceledByNewRequest" then
				return
			end
			
			local decoded, responseTable = pcall( KSJsonDecoder.decodeWebSafeJson, response )
			
			local startString = "\n----------------------------------\n"
			local requestId = headers and headers['x-request-id'] or '<no request id>'
			local searchResponseData = decoded and WFTableArchiving.tableToString( responseTable ) or tostring( response )
			local logInfo = string.format( "%s Autocomplete: error \n URL: %s \n Response: %s \n", startString, self._url, searchResponseData )
			search_debugPrint( autoCompleteLogger, "e", logInfo, requestId, getFormattedTimeForSearchDebug(), "\n" )
			
			self._searchResultSubject:onNext( {} )
			self._searchResultSubject:onCompleted()
		end)
		
		:call( WFRxRunUntilDone ) 
			
	end
	
	return self._searchResultSubject

end

--------------------------------------------------------------------------------

local function findIntersection( array1, array2 )
	
	local intersectionArray = {}
	
	local set2 = KSTableUtils.mapArrayToSet( array2 )
	
	for _, value in ipairs( array1 ) do
		
		if set2[ value ] then
			table.insert( intersectionArray, value )
		end
		
	end
	
	return intersectionArray
	
end

--------------------------------------------------------------------------------

local function _validator( filterValues )
	
	local isValidTerm = true
	local combinedValues = filterValues
	
	-- if it's an array of arrays
	if type( filterValues ) == "table"
			and #filterValues >= 1 
			and type( filterValues[ 1 ] ) == "table" then
		
		for i, values in ipairs( filterValues ) do
			
			if i == 1 then
				combinedValues = table.shallowcopy( values )
				
			else
				
				local intersectionList = findIntersection( combinedValues, values )
				
				-- if the intersection is empty, then consider it as invalid term
				if #combinedValues > 0
						and #values > 0
						and #intersectionList == 0 then
						
					combinedValues = intersectionList
					isValidTerm = false
					break
				end
				
				combinedValues = intersectionList
			end
		end
	end
	
	return isValidTerm, combinedValues
	
end

--------------------------------------------------------------------------------
-- Following fields do not support AND based querying.
-- Ref: https://wiki.corp.adobe.com/x/7Acevg
local _requiresValidation = {}
_requiresValidation[ "rating" ] = true
_requiresValidation[ "lightroom_commented_on" ] = true
_requiresValidation[ "lightroom_favorited" ] = true
_requiresValidation[ "lightroom_depthmap" ] = true
_requiresValidation[ "lightroom_flash" ] = true
_requiresValidation[ "lightroom_edited" ] = true

--------------------------------------------------------------------------------
-- Some fields (listed in _requiresValidation) do not support AND-based filtering.
-- For such fields, we need to merge the query into a single 1-d array.
-- If the query can't be merged, we set `_isValidQuery` as false
-- and directly return 0 results to the client without going to USS.
-- Example 1: rating = { { 3, 4, 5 }, { 4 } } 
--				-> isValid = true, rating = { 4 }
-- Example 2: rating = { { 4, 5 }, { 3 } } 
--				-> isValid = false, rating = <doesn't matter>

local function validateUserGivenSearchTerm( userGivenSearchTerms )
	
	local isValidTerm = true
	local validatedSearchTerm = {}
	
	for key, filterValues in pairs( userGivenSearchTerms ) do
		
		if _requiresValidation[ key ] then
			
			local isCurrentTermValid, newValue = _validator( filterValues )
			
			isValidTerm = isValidTerm and isCurrentTermValid
			
			validatedSearchTerm[ key ] = newValue
			
			if not isValidTerm then
				
				local message = string.format( "Search: Invalid search request: %s", WFTableArchiving.tableToString( userGivenSearchTerms ) )
				search_debugPrint( searchLogger, "d", message, "\n")
				break
			
			end
			
		else
			
			validatedSearchTerm[ key ] = filterValues
		
		end
		
	end
	
	return isValidTerm, validatedSearchTerm
	
end

--============== API's ================--
------------------- __init --------------------------------------
function WFCatSearchOnlineRequest:__init( userGivenSearchTerms )
	
	self._isValidQuery, self._onlineSearchTerms = validateUserGivenSearchTerm( userGivenSearchTerms or {} )
	initializeCommonTerms( self )
	self._totalReceived = 0
	
end

--[[ wiki links : 
V2: https://wiki.corp.adobe.com/display/adobesearch/Lightroom+Search+V2+Request+and+Response

V3: https://wiki.corp.adobe.com/display/adobesearch/Lightroom+Facets+Design
    https://wiki.corp.adobe.com/display/adobesearch/Facet+Field+Info+Design

]]
---------------------- executeOnlineSearch ----------------------------------
function WFCatSearchOnlineRequest:executeOnlineSearch( catUser, url, searchOptions )
	self._catUser = catUser
	self._url = url
	self._searchOptions = KSTableUtils.deepCopy( searchOptions )
	self._isSearchActive = true
	
	_latestRequestCounter = _latestRequestCounter + 1
	
	if self._isValidQuery then
		
		self._searchResultSubject = Subject( )
		return startNextSetSearch( self, _latestRequestCounter )
		
	else
		return invalidQueryResult( self )
	end
	
end

---------------------- executeFacetValuesSearch ----------------------------------
function WFCatSearchOnlineRequest:executeFacetValuesSearch( catUser, url, searchOptions )
	self._catUser = catUser
	self._url = url
	self._searchOptions = KSTableUtils.deepCopy( searchOptions )
	
	return startFacetValuesSearch( self )
end

---------------------- categoryKeysSearch ----------------------------------
function WFCatSearchOnlineRequest:executeCategoryKeysSearch( catUser, url, searchOptions )
	self._catUser = catUser
	self._url = url
	self._searchOptions = KSTableUtils.deepCopy( searchOptions )

	return getMetadataSearch( self )
	
end

---------------------- executeAutoComplete ----------------------------------
function WFCatSearchOnlineRequest:executeAutoComplete( catUser, url, searchOptions )
	self._catUser = catUser
	self._url = url
	self._searchOptions = KSTableUtils.deepCopy( searchOptions )
	
	return startAutoComplete( self )
end

-------------------------- closeRequest -------------------------------------
function WFCatSearchOnlineRequest:closeRequest(  )
	self._onlineSearchTerms = false
	self._currentStart = false
	self._limit = false
	self._totalAvailable = false
	self._totalReceived = 0
	self._url = false
	self._searchOptions = false
	self._searchResultSubject:dispose()
	self._searchResultSubject = false
end

-------------------------- getCurrentStatus -------------------------------------
function WFCatSearchOnlineRequest:getCurrentStatus(  )
	local status = {}
	status.totalAvailable 		=  self._totalAvailable
	status.executionStatus		=  self._executionStatus
	
	return status
end

return Rx.Class 'WFCatSearchOnlineRequest'( WFCatSearchOnlineRequest )
