--=============================================================================
local LuaOrm = {
	Date = require "LuaOrm.Date"
}
local pl = {
	tablex = require "pl.tablex"
}
local reporter = {
	slices_helper = require "reporter.slices_helper"
}

local scheduler = require "reporter.scheduler"

--=============================================================================

--
-- Having the function before the table does not seem natural
-- so we create a wrapper around it
local function _map(tbl, fn)
	return pl.tablex.map(fn, tbl)
end

-- just a shortcut
local _foreach = pl.tablex.foreach

-- inserts `v` at the end of `tbl` and returns `tbl`
local function _append(tbl, v)
	table.insert(tbl, v)
	return tbl
end

-- merge `tbl1` and `tbl2`, `tbl2` may overwrite values in `tbl1`
local function _union(tbl1, tbl2)
	return pl.tablex.merge(tbl1, tbl2, true)
end

local function addDays(date, amount)
	return date:add(0, 0, amount) -- year, month, day
end

-- returns the day after `date`
local function nextDay(date)
	return addDays(date, 1)
end

--
-- `dateStr` must be something like "2010-01-01 [00:00:00]", the time part is ignored
local function parseDate(dateStr)
	local y,m,d = string.match(dateStr, "^(%d%d%d%d)-(%d%d)-(%d%d)")
	if not (y and m and d) then
		return nil, ("String '%s' could not be interpreted as a date."):format(dateStr)
	end
	return LuaOrm.Date({ year = y, month = m, day = d, hour = 0 })
end

local function parseIndicator(input, defaultField)
	if type(input) == "string" then
		return { [defaultField] = input }
	end
	return {
		name        = input.name or input.dimension,
		label       = input.label or input.dimensionData and input.dimensionData.label,
		description = input.description or input.dimensionData and input.dimensionData.description,
		indicatorId = input.indicator,
		extended    = input.extended,
		composite   = input.composite,
		offset      = input.offset,
		type        = input.type or input.dimensionData and input.dimensionData.type,
		domain		= input.domain or input.dimensionData and input.dimensionData.domain,
	}
end

--
-- Converts `projectedFields` into something we can actually use
local function parseIndicators(projectedFields)
	return _map(projectedFields, function(elem)
		return parseIndicator(elem, "name")
		-- TODO: Some crazy stuff (only if required)
	end)
end

--
-- Parses the dates represented as strings, uses last handle to
-- replace start date when required
local function buildDateRange(startDateStr, endDateStr, lastHandle)
	local startDate, endDate, err

	startDate, err = parseDate(lastHandle or startDateStr)
	if not startDate then
		return nil, nil, err
	end
	endDate, err = parseDate(endDateStr)
	if not endDate then
		return nil, nil, err
	end
	if lastHandle then
		startDate = nextDay(startDate)
	end
	return startDate, endDate
end

--
-- fixed indicators, campaign, vcc, date, etc
local function getValueForFakeIndicator(connection, indicatorName, campaignId, date)
	if indicatorName == "Date" then
		return date:toString("%Y-%m-%d")
	end
	if indicatorName == "Campaign" then
		return campaignId:match("[^@]+")
	end
	return "Unknown: ".. tostring(indicatorName)
end

--
-- helper
local function getFromCassandra(connection, key, pred)
	local columns = connection:GetSliceRange(key, "InteractionCounters", "", pred)
	for _, column in ipairs(columns) do
		return column.value_as_number -- there's only one value
	end
end

--[[
-- Example indicator:
--  [default@Historical] get InteractionCounters['ReceivedMailMessages:kamp1@wilderkrieger:ByDay']
--   => (counter=2014-04-25 00:00:00.000, value=1)
--  [default@Historical] get InteractionCounters['ReceivedMailMessages:MAIL:kamp1@wilderkrieger:ByDay']
--   => (counter=2014-04-25 00:00:00.000, value=1)
--]]
local function getPlainIndicatorValue(connection, params)
	local date = addDays(params.date, 0) -- make sure to use a copy

	local key = params.indicator.indicatorId
	if params.initiative then
		key = key ..":".. params.initiative
	end
	if params.interactionType then
		key = key ..":".. params.interactionType
	end
	key = key ..":".. params.campaignId ..":ByDay"

	if params.indicator.offset then
		date = addDays(date, params.indicator.offset.value)
	end

	local sliceColumn = date:toString("%Y-%m-%d 00:00:00.000")

	local value = getFromCassandra(connection, key, {
		column_names = { sliceColumn },
		slice_range  = { }
	})

	if not value and params.indicator.extended then
		value = getFromCassandra(connection, key, {
			column_names = { },
			slice_range  = {
				start    = sliceColumn,
				finish   = "",
				reversed = true,
				count    = 1
			}
		})
	end

	return value or 0
end

local function getCompositeValue(indicator, getValueFn)
	local function buildIndicator(input)
		return parseIndicator(input, "indicatorId")
	end
	local function getValue(input)
		if type(input) == "number" then
			return input
		end
		return getValueFn(buildIndicator(input))
	end

	local conf = indicator.composite

	if conf.operation == "percentage" then
		local total = getValue(conf.total)
		local value = getValue(conf.value)
		return total == 0 and 100 or value * 100 / total
	end

	if conf.operation == "difference" or conf.operation == "sum" then
		local operand1 = getValue(conf.operand1)
		local operand2 = getValue(conf.operand2)
		return operand1 + (conf.operation == "sum" and operand2 or -operand2)
	end

	if conf.operation == "average" then
		local count = getValue(conf.count)
		local sum = getValue(conf.sum)
		return count == 0 and 0 or sum / count
	end

	if conf.operation == "division" then
		local numerator = getValue(conf.numerator)
		local denominator = getValue(conf.denominator)
		return numerator / math.max(1, denominator)
	end

	return 0
end

local function getIndicatorValue(connection, params)
	if params.indicator.indicatorId then
		return getPlainIndicatorValue(connection, params)
	end

	if params.indicator.composite then
		return getCompositeValue(params.indicator, function(subIndicator)
			return getIndicatorValue(connection, _union(params, {
				indicator = subIndicator
			}))
		end)
	end

	return getValueForFakeIndicator(connection, params.indicator.name, params.campaignId, params.date)
end

--
--
local function query (connection, queryTable, utils, _T, idsToNames)
	LogInfo("[REPORTER.REALTIME_QUERY:query] Running Query Realtime Report.")

	--LogDebug("[REPORTER.REALTIME_QUERY:query] Executing Query Reports. queryTable: '%s'", json.encode(queryTable))

	local resultSize        = queryTable.resultSize or 40
	local lastHandle        = queryTable.lastHandle
	local campaigns         = queryTable.campaigns
	local interactionType   = queryTable.interactionType
	local initiative        = queryTable.initiative
	local indicators        = parseIndicators(queryTable.projectFields)
	local scheduleId        = queryTable.queryFields and queryTable.queryFields.scheduleId or nil
	local isSentToDatabase  = utils and scheduleId and  scheduler.is_send_type(utils, scheduleId, "database")
	local rawData           = isSentToDatabase
	local queryFields   	= queryTable.queryFields or {}
	local vcc 				= queryFields.VCC 
	--rawData = true
	LogDebug("[REPORTER.REALTIME_QUERY:query] ------> scheduleId: %s, isSentToDatabase %s, rawData %s, startDate '%s' -> endDate '%s'", scheduleId, isSentToDatabase, rawData, queryTable.startDate, queryTable.endDate)

	if type(campaigns) ~= "table" then
		LogError("[REPORTER.REALTIME_QUERY:query] The filter campaigns is required and must be a table. Campaigns is: %s", type(campaigns))
		return nil, nil, "The Campaign is required"
	end

	if type(resultSize) ~= "number" or resultSize < 1 then
		LogError("[REPORTER.REALTIME_QUERY:query] The filter resultSize is required and must be numeric an > 0. resultSize is: %s", type(campaigns))
		return nil, nil, "Invalid result size"
	end

	if initiative ~= nil and initiative ~= "Inbound" and initiative ~= "Outbound" then
		LogError("[REPORTER.REALTIME_QUERY:query] Invalid initiative. Must be nil or Inbound or Outbound")
		return nil, nil, "Invalid initiative"
	end

	local startDate, endDate, err = buildDateRange(queryTable.startDate, queryTable.endDate, lastHandle)
	if not (startDate and endDate) then
		return nil, err
	end

	local function rangeIterator(results, date, computeDay)
		if endDate:compare(date) then -- endDate:compare(date) evaluates to true only if endDate < date
			return results, date
		end

		-- not happy about it but inner function returns an array
		_foreach(computeDay(date), function(row)
			_append(results, row)
		end)

		-- may return more than asked, but not less
		if #results >= resultSize then
			return results, date
		end

		-- tail call with next day
		return rangeIterator(results, nextDay(date), computeDay)
	end

	local results, lastDate = rangeIterator({}, startDate, function(date)
		-- returns one row for each campaign
		return _map(campaigns, function(campaignId)
			-- returns the row
			return _map(indicators, function(indicator)
				local campaignAtVCC = campaignId .. "@" .. vcc
				local res = getIndicatorValue(connection, {
					indicator       = indicator,
					date            = date,
					initiative      = initiative,
					interactionType = interactionType,
					campaignId      = campaignAtVCC,
					rawData         = rawData -- por ahora no se utiliza, pero por las dudas
				})
				--local res = getIndicatorValue(connection, indicator, interactionType, campaignId, date)

				if type(res) == "number" then
					if not rawData then
						if indicator.type == "time" then
							return reporter.slices_helper.formatTime(res)
						end
						if indicator.type == "percentage" then
							return string.format("%.2f %%", res)
						end
						if math.floor(res) ~= res then
							return string.format("%.2f", res)
						end
					end
					return tostring(res)
				end
				return res
			end)
		end)
	end)

	local response = {
		headers         = _map(indicators, function(indicator) return indicator.label end),
		columnsMetadata	= indicators,
		rows            = results,
		length          = #results
	}
	local nextHandler = lastDate:toString("%Y-%m-%d 00:00:00")

	-- LogDebug("[REPORTER.REALTIME_QUERY:query] response: '%s'", json.encode(response))
	-- LogDebug("[REPORTER.REALTIME_QUERY:query] nextHandler: '%s'", nextHandler)

	--LogDebug("[REPORTER.REALTIME_QUERY:query] Runned Query Realtime Report, Total Rows: '%s', nextHandler: '%s'", response.length, nextHandler)

	return response, nextHandler
end

--=============================================================================
--[[
	Obtiene los users del VCC desde cassandra, luego invoca al AS para realizar la consulta del estado.

	Ej del json de un Usuario:
	{"lastStatusChangeTime":1591352542.623,"defaultCampaign":"","VCC":"vcc1","agentID":"onlyadmin","state":"BREAK","interactions":[],"lastLoginTime":1591352542.623,"addresses":{"MAIL":{"active":true,"resourceAddress":"","address":"aaa1111@a.com"},"IM":{"active":true,"resourceAddress":"","address":"onlyadmin@vcc1"},"CHAT":{"active":true,"resourceAddress":"","address":"onlyadmin@vcc1"}},"campaigns":{"camptel2":true,"z":true,"campvarioschannels":true,"campmail1":true,"aaa":true,"campi6tel":true},"description":"only"}
]]--
local function LoadLoggedUsers (queryTable, utils, summarizerConnection, agentConnection, authFilter)
	local queryFields   = queryTable.queryFields
	local filters       = queryTable.filters
	local startDate     = queryTable.startDate
	local endDate       = queryTable.endDate
	local lastRecord    = queryTable.lastHandle or 0
	local resultSize    = queryTable.resultSize
	local timezone      = queryTable.timezone
	local vcc           = queryFields.VCC

--	LogDebug("[REPORTER:LoadLoggedUsers] queryTable: '%s'.", json.encode(queryTable or {}))

	local function GetVCCUsers ()
		return utils.GetAllColumns {
			keyspace = agentConnection or "Agent",
			columnFamily = "VCCUsers",
			key = vcc,
			columnCount = 50000
		} or {}
	end

	local vccUsers = GetVCCUsers()

	--LogDebug("[REPORTER:LoadLoggedUsers] queryTable: vccUsers '%s'.", json.encode(vccUsers or {}))

	local users = {}
	for userIdWithoutVCC, user in pairs(vccUsers or {}) do
		table.insert(users, {Id = userIdWithoutVCC})
	end
	local agentservice = utils.GetAgentService()
	local usersStatuses = agentservice.GetAgentsStatuses(users, vcc) or {}
	--LogDebug("[REPORTER:LoadLoggedUsers] usersStatuses '%s'.", json.encode(usersStatuses or {}))

	local function MustIncludeUser (userInfo)
		if not userInfo.state or userInfo.state == "" then
			return false
		end
		if userInfo.state:upper() == "OFFLINE" then
			return false
		end
	--	LogDebug("[REPORTER:LoadLoggedUsers] Adding User '%s'.", json.encode(userInfo or {}))
		return true
	end
	local usersLoggedIn = {}

	for userId, userInfo in pairs(usersStatuses) do
		local mustInclude = MustIncludeUser(userInfo)
		if mustInclude then
			userInfo.SessionId = authFilter and authFilter:GetTokenFromUser(userInfo.agentID, vcc) or ""
			--LogDebug("[REPORTER:LoadLoggedUsers] User '%s@%s', authFilter->UserSessionId: '%s'.", userInfo.agentID, vcc, userInfo.SessionId)
			
			-- hay que agregar un registro por campana
			for campaignId, enabled in pairs(userInfo.campaigns or {}) do
				local user = utils.DeepCopy(userInfo)
				user.Campaign = campaignId
				table.insert(usersLoggedIn, user)
			end
		end
	end

	--LogDebug("[REPORTER:LoadLoggedUsers] Response usersLoggedIn: '%s'.", json.encode(usersLoggedIn))

	return true, usersLoggedIn
end

--=============================================================================
local function GetHeader (field, vcc)
	local fieldName = type(field) == "string" and field or field.label or field.dimensionData and field.dimensionData.label or field.name
	return fieldName
end

--=============================================================================
local function getColumnMetadata (field)
	local fieldName = type(field) == "string" and field or field.label or field.dimensionData and field.dimensionData.label or field.name
	if type(field) == "string" then
		return {
			name  = field,
			label = field,
			type  = "String"
		}
	end
	return {
		name        = field.name or field.dimension,
		label       = field.label or field.dimensionData and field.dimensionData.label or field.name,
		description = field.description or field.dimensionData and field.dimensionData.description,
		type        = field.type or field.dimensionData and field.dimensionData.type
	}
end

local m_usersStates = {}
function GetUserState (stateId, utils, keyspace)
	if m_usersStates[stateId] then
		return m_usersStates[stateId]
	end
	local state = utils.GetAllColumns {
		keyspace = keyspace or "Agent",
		columnFamily = "AgentStates",
		startKey = stateId or "",
		endKey = stateId or "",
	}
	m_usersStates[stateId] = state
	return state
end

--=============================================================================
local function queryCustom (summarizerConnection, queryTable, utils, agentConnection, authFilter)
	assert(queryTable)

	--LogDebug("[REPORTER.REALTIME_QUERY:queryCustom] Executing Query Custom Reports. queryTable: '%s'", json.encode(queryTable))
	-- LogDebug("[REPORTER.REALTIME_QUERY:queryCustom] Executing Query Custom Reports. summarizerConnection: '%s'", summarizerConnection)
	-- LogDebug("[REPORTER.REALTIME_QUERY:queryCustom] Executing Query Custom Reports. agentConnection: '%s'", agentConnection)

	LogInfo("[REPORTER.REALTIME_QUERY:queryCustom] Running Query Realtime Custom Report.")

	local response = {
		headers			= {},
		columnsMetadata = {},
		rows			= {},
		length			= 0,
		areAllRecords   = true -- utilizado en la exportacion en los luas del agent app report
	}
	local nextHandler = 1


	local saveContentTo     = queryTable.queryFields and queryTable.queryFields.saveContentTo or nil
	local scheduleId        = queryTable.queryFields and queryTable.queryFields.scheduleId or nil
	local folderToExport    = queryTable.folderToExport  or nil
	local filenameToExport  = queryTable.filenameToExport or nil
	local exportContentType = queryTable.exportContentType or nil
	local projectFields     = queryTable.projectFields
	local lastRecord        = queryTable.lastHandle or 0
	local timezone          = queryTable.timezone or {offset = 0} -- Default to UTC
	local isSentToDatabase  = utils and scheduleId and  scheduler.is_send_type(utils, scheduleId, "database")
	local rawData           = isSentToDatabase

	--LogDebug("[REPORTER.REALTIME_QUERY:queryCustom] ------> scheduleId: %s, isSentToDatabase %s, rawData %s", scheduleId, isSentToDatabase, rawData)

	local ok, result = LoadLoggedUsers(queryTable, utils, summarizerConnection, agentConnection, authFilter)
	if not ok then
		LogError("[REPORTER.REALTIME_QUERY:queryCustom] Error executing query over elastic search.")
		return nil, nil, result.error or ""
	end

	response.headers = {}
	for idx, field in ipairs(projectFields) do
		response.headers[idx] = GetHeader(field, vcc)
	end

	response.columnsMetadata = {}
	for idx, field in ipairs(projectFields) do
		response.columnsMetadata[idx] = getColumnMetadata(field)
	end

	response.rows = {}
	--local i = 0
	for id, info in pairs(result) do
		--i = i + 1
		--LogDebug(" *** %d ->  info: %s", i, json.encode(info))
		local row = {}
		for idx, field in ipairs(projectFields) do
			--LogDebug("   - field: %s", json.encode(field))
			local dimensionData = field.dimensionData
			local source = dimensionData.source or ""
			local value = info[source] or ""
			local dimensionType = (dimensionData.type or ""):lower()

			if dimensionType == "date" then
				local formatDate = dimensionData.format
				if rawData then
					value = LuaOrm.Date(value):toString(formatDate)
				else
					value = reporter.slices_helper.ConvertUTCDateToTimezoneDate(
						tonumber(value), timezone, formatDate, utils, true).stringDate
				end
			elseif dimensionType == "enum" then
				if not rawData then
					local domain = dimensionData.domain
					--LogDebug("111 - > %s", json.encode(domain or {}))
					if domain then
						if domain[value] then
							value = domain[value]
						else
							if (dimensionData.id or ""):lower() == "stateraw" then
								local stateDef = GetUserState(value, utils, agentConnection)
								local state = stateDef and stateDef[value]
								if state then
									value = state.Description or value
								end
							end
						end
					end
				end
			end
			row[idx] = value
			--LogDebug("     ->  row[idx]: %s", row[idx])
		end
		table.insert(response.rows, row)
	end
	response.length = #response.rows

	lastRecord = lastRecord ~= "" and lastRecord or 0 -- TODO: Revisar !!
	local nextHandler = 1 --0 -- TODO: Revisar !!

--	LogDebug("[REPORTER.REALTIME_QUERY:queryCustom] response: '%s'", json.encode(response))
--	LogDebug("[REPORTER.REALTIME_QUERY:queryCustom] nextHandler: '%s'", nextHandler)

	LogInfo("[REPORTER.REALTIME_QUERY:queryCustom] Runned Query Realtime Custom Report, Total Rows: '%s', nextHandler: '%s'", response.length, nextHandler)

	return response, nextHandler
end

--=============================================================================
return {
	query       = query,
	queryCustom = queryCustom
}
--=============================================================================
