local summary_query  = require "reporter.summary_query"
local detail_query   = require "reporter.detail_query"
local realtime_query = require "reporter.realtime_query"
local dialerbatch_query = require "reporter.dialerbatch_query"
local dialercontacts_query = require "reporter.dialercontacts_query"
local outbound_dialerbatch_query = require "reporter.outbound_dialerbatch_query"
local outbound_dialercontacts_query = require "reporter.outbound_dialercontacts_query"
local cassandra = require "lua_cassandra"
require "LuaOrm.Date"
local m_helpers = require "helpers"

local keyspaces = {
	Agent          = { connection = nil },
	Historical     = { connection = nil },
	SummarizerData = { connection = nil }
}

module(..., package.seeall)

---------------------------------------------------------------------------
-- GLobalTimer
---------------------------------------------------------------------------

--the reference is anyway dummy cause reads a local copy, but it lives on the module
--in order to avoid adding the servers everytime

function SetGlobalTimer (cluster)
	LogInfo("[REPORTER:QueryDetail] Setting global Timer: '%s'", cluster)
	require "reporter.mydate" -- Expone el objeto RPT (myDate)
	RPT.SetGlobalTimer(cluster)
end

---------------------------------------------------------------------------
-- Cassandra connection
---------------------------------------------------------------------------
function DisconnectCassandra ()
	for _,data in pairs(keyspaces) do
		if data.connection then
			data.connection:Close()
			data.connection = nil
		end
	end
end

function ConnectCassandra (IP, port)
	DisconnectCassandra()
	assert(cassandra.seed(IP, port), "Connection with Cassandra failed.")
	for keyspace,data in pairs(keyspaces) do
		data.connection = cassandra:CassandraConnection(keyspace)
	end
end

---------------------------------------------------------------------------
-- Summary reporting
---------------------------------------------------------------------------
function QuerySummary (query, utils, _T, idsToNames, redis, authFilter, _reporterClient)
	local handle = query.lastHandle or summary_query.QuerySummary(keyspaces.SummarizerData.connection, query, utils, _T, idsToNames or {})
	--LogDebug("[REPORTER:QuerySummary] handle: %s", json.encode(handle or {}))
	local result, handle = summary_query.NextPage(keyspaces.SummarizerData.connection, handle, query.resultSize, idsToNames, query, utils)
	--LogDebug("[REPORTER:QuerySummary] result: %s, handle: %s", json.encode(result or {}), json.encode(handle or {}))
	return result, handle
end

--[[--------------------------------------------------------------------------
	Detail reporting
		Estas funciones son invocadas desde la app "reports" (init.lua)
			exportReport->executeReportCommon->get_more_data->runQuery
---------------------------------------------------------------------------]]
function QueryDetail (queryTable, utils, _T, idsToNames, redis, authFilter, reporterClient)
	local ids = idsToNames or {}
	local queryFn, detailType
	local summarizerConnection = keyspaces.SummarizerData.connection

	if queryTable.steps then
		detailType = "by steps"
		queryFn = detail_query.querySteps
	elseif queryTable.callTrace then
		detailType = "by transfer"
		queryFn = detail_query.queryCallTrace
	elseif queryTable.transfer then
		detailType = "by transfer"
		queryFn = detail_query.queryTransfer
	elseif queryTable.threaded then
		detailType = "by threaded"
		queryFn = detail_query.queryThreaded
	elseif queryTable.composite then
		detailType = "by composite"
		queryFn = detail_query.queryComposite
	else
		detailType = "default"
		queryFn = detail_query.query
	end

	LogInfo("[REPORTER:QueryDetail] Execute query detail type: '%s'", detailType)

	return queryFn(summarizerConnection, queryTable, utils, _T, ids, reporterClient)
end

---------------------------------------------------------------------------
-- Realtime reporting
---------------------------------------------------------------------------
function QueryRealtime (queryTable, utils, _T, idsToNames, redis, authFilter, _reporterClient)
	--LogDebug("[REPORTER:QueryRealtime] queryTable: %s", json.encode(queryTable or {}))
	return realtime_query.query(keyspaces.Historical.connection, queryTable, utils, _T, idsToNames or {})
end

---------------------------------------------------------------------------
-- Queued RealTime reporting
---------------------------------------------------------------------------
function QueryLiveDetail (queryTable, utils, _T, idsToNames, redis, authFilter, _reporterClient)
	--LogDebug("[REPORTER:QueryLiveDetail] queryTable: %s", json.encode(queryTable or {}))
	return detail_query.queryLive(keyspaces.SummarizerData.connection, queryTable, utils, _T, idsToNames or {}, redis)
end

---------------------------------------------------------------------------
-- Dialer Batch reporting (allegro)
---------------------------------------------------------------------------
function QueryDialerBatch (queryTable, utils, _T, idsToNames, redis, authFilter, _reporterClient)
	--LogDebug("[REPORTER:QueryDialerBatch] queryTable: %s", json.encode(queryTable or {}))
	return dialerbatch_query.query(keyspaces.Historical.connection, queryTable, QuerySummary, utils, _T, idsToNames or {})
end

---------------------------------------------------------------------------
-- Dialer Contacts reporting (allegro)
---------------------------------------------------------------------------
function QueryDialerContacts (queryTable, utils, _T, idsToNames, redis, authFilter, _reporterClient)
	--LogDebug("[REPORTER:QueryDialerContacts] queryTable: %s", json.encode(queryTable or {}))
	return dialercontacts_query.query(keyspaces.Historical.connection, queryTable, utils, _T, idsToNames or {})
end

---------------------------------------------------------------------------
-- Outbound Dialer Batch reporting
---------------------------------------------------------------------------
function QueryOutboundDialerBatch (queryTable, utils, _T, idsToNames, redis, authFilter, _reporterClient)
	--LogDebug("[REPORTER:QueryOutboundDialerBatch] queryTable: %s", json.encode(queryTable or {}))
	return outbound_dialerbatch_query.query(keyspaces.Historical.connection, queryTable, QuerySummary, utils, _T, idsToNames or {})
end

---------------------------------------------------------------------------
-- Outbound Dialer Contacts reporting
---------------------------------------------------------------------------
function QueryOutboundDialerContacts (queryTable, utils, _T, idsToNames, redis, authFilter, _reporterClient)
	--LogDebug("[REPORTER:QueryOutboundDialerContacts] queryTable: %s", json.encode(queryTable or {}))
	return outbound_dialercontacts_query.query(keyspaces.Historical.connection, queryTable, utils, _T, idsToNames or {})
end

---------------------------------------------------------------------------
-- Queued RealTime Custom reporting
---------------------------------------------------------------------------
function QueryRealtimeCustom (queryTable, utils, _T, idsToNames, redis, authFilter, _reporterClient)
	--LogDebug("[REPORTER:QueryRealtimeCustom] queryTable: %s", json.encode(queryTable or {}))
	return realtime_query.queryCustom(keyspaces.SummarizerData.connection, queryTable, utils, keyspaces.Agent.connection, authFilter)
end

---------------------------------------------------------------------------
-- Cache - El mismo se realiza en REDIS.
---------------------------------------------------------------------------
local m_cache = {
	enabled = true, -- si dejamos este valor en false, NO se realizara cache de los request
	maxTimeInRunningStatusSec = 30,
	incrementTimeExpiredTimeSec = 0 -- atributo utilizado por si tenemos que incrementar tiempo (siempre) al calculo de expiracion.
}

--[[
	La key expira segun el tiempo restante del slice.

	Para realizar el cache, se fija en que Slice se encuentra, cuanto tiempo ha transcurrido desde que el mismo comenzo y el tiempo de Cacheo/Expiracion es el tiempo restante hasta el siguiente Slice.
	Ej:
		Es la hora 06:55:55 AM y se procede a realizar el Cache del Slice "15Min", entonces el Slice en ejecucion se encuentra en el rango 06:45:00 a las 06:59:59.
		El tiempo transcurrido son 10 minutos 55 segundos, y el tiempo del Cache va a ser de 4 minutos 5 segundos (245 segundos).
]]--
m_cache.expiredTimeBySlice = {
	["15min"] = {seconds = 900  , id = "15min"  }, 
	["30min"] = {seconds = 1800 , id = "30min"  }, 
	["hour"]  = {seconds = 3600 , id = "hour"   }, 
	["day"]   = {seconds = 86400, id = "day"    },
	default   = {seconds = 600  , id = "default"}
}

m_cache.IsEnabled = function(self)
	--LogDebug("[REPORTER:QuerySummaryFromCache] m_cache.IsEnabled -----> enabled: '%s'.", self.enabled)
	return self.enabled
end

--[[
	Esta funcion calcula el tiempo de expiracion de la clave en Redis.
	El tiempo de expiracion, se calcula segun el tiempo restante que le quede al slice, desde la hora actual hasta la hora de fin del slice en curso.
	En el caso del slice x dia, se debe de calcular la fecha y hora actual en el timezone del usuario contra la fecha y hora de fin del slice en UTC.
]]--
m_cache.GetTimeExpiredKey = function(self, query)
	assert(type(query) == "table")
	assert(type(query.sliceSize) == "string")

	local sliceSize = query.sliceSize:lower()
	local nowUTCDate = LuaOrm.Date()
	local nowUTCStringDate = nowUTCDate:toString("%Y-%m-%d %H:%M:%S")
	local nowTimezoneDate = m_helpers.date:ConvertUTCDateToDate(nowUTCStringDate, nil, nil, true, query.timezone)
	local nowTimezoneStringDate = nowTimezoneDate:toString("%Y-%m-%d %H:%M:%S")
	local now
	if sliceSize == "day" then
		now = nowTimezoneDate
	else
		now = nowUTCDate
	end
	local function CalculateFirstStartSlice ()
		local firstStartSliceDate
		if sliceSize == "day" then
			firstStartSliceDate = LuaOrm.Date {year = nowUTCDate:year(), month = nowUTCDate:month(), day = nowUTCDate:day(), hour = 0, min = 0, sec = 0}
		else
			firstStartSliceDate = LuaOrm.Date {year = nowUTCDate:year(), month = nowUTCDate:month(), day = nowUTCDate:day(), hour = nowUTCDate:hour(), min = 0, sec = 0}
		end
		return firstStartSliceDate
	end

	local expiredConfig = self.expiredTimeBySlice[sliceSize] or self.expiredTimeBySlice.default
	local secondsToIncrement = expiredConfig.seconds
	local startSliceDate, elapsedTimeSec, expiredTimeSec

	-- loopeo obteniendo la fecha start del slice actual y el tiempo de expiracion.
	while true do
		if not startSliceDate then
			startSliceDate = CalculateFirstStartSlice()
			now = now:add(nil, nil, nil, nil, nil, secondsToIncrement)
		else
			startSliceDate = startSliceDate:add(nil, nil, nil, nil, nil, secondsToIncrement)
		end
		--LogDebug("[REPORTER:QuerySummaryFromCache] m_cache.GetTimeExpiredKey -----> sliceSize: '%s' now: '%s'.", sliceSize, now)
		--LogDebug("[REPORTER:QuerySummaryFromCache] m_cache.GetTimeExpiredKey -----> sliceSize: '%s' startSliceDate: '%s'.", sliceSize, startSliceDate)
		elapsedTimeSec = now:time() - startSliceDate:time()
		--LogDebug("[REPORTER:QuerySummaryFromCache] m_cache.GetTimeExpiredKey -----> sliceSize: '%s' elapsedTimeSec: '%s'.", sliceSize, elapsedTimeSec)
		if elapsedTimeSec <= secondsToIncrement then
			expiredTimeSec = expiredConfig.seconds - elapsedTimeSec
			break
		end
	end

	local endSliceDate = startSliceDate:add(nil, nil, nil, nil, nil, secondsToIncrement)
	local sliceInfo = {
		timezoneId     = query.timezone.id,
		timezoneOffset = query.timezone.offset,
		startDate      = startSliceDate:toString(),
		startTimeStamp = startSliceDate:time(),
		endDate        = endSliceDate:toString(),
		endTimeStamp   = endSliceDate:time()
	}
	sliceInfo.durationSec = sliceInfo.endTimeStamp - sliceInfo.startTimeStamp

	--LogDebug("[REPORTER:QuerySummaryFromCache] m_cache.GetTimeExpiredKey *******-----> nowUTCDate: '%s', timezoneId: '%s', timezoneOffset: '%s', nowTimezoneDate: '%s' - sliceSize: '%s' expiredTimeSec: '%s' elapsedTimeSec: '%s' expiredConfig: '%s' sliceInfo: '%s'.",
	--	nowUTCStringDate, sliceInfo.timezoneId, sliceInfo.timezoneOffset, nowTimezoneStringDate, query.sliceSize, expiredTimeSec, elapsedTimeSec, json.encode(expiredConfig), json.encode(sliceInfo))

	-- si fuese necesario de incrementar siempre N segundos al timempo expiracion.
	expiredTimeSec = expiredTimeSec + (self.incrementTimeExpiredTimeSec + 0)
	return expiredTimeSec, sliceInfo
end

--[[
	Esta funcion crea la Key de Redis, si el Slice es de tipo "day" le suma a la key el TimeZoneMinutes.
]]--
m_cache.MakeKey = function(self, query)
	assert(type(query) == "table")
	assert(type(query.vcc) == "string")
	assert(type(query.cut) == "string")
	assert(type(query.sliceSize) == "string")
	assert(type(query.filter) == "table")
	assert(type(query.timezone) == "table")
	local template = "CacheSummaryReporter:Vcc:%s:Cut:%s:SliceSize:%s:Campaign:%s:Account:%s:AttLevel:%s:Type:%s"
	local f = query.filter
	local function Val (v)
		return tostring(v or ""):lower()
	end
	local sliceSize = query.sliceSize:lower()
	local key = string.format(template, query.vcc:lower(), query.cut:lower(), sliceSize,
		Val(f.Campaign),
		Val(f.Account),
		Val(f.AttentionLevel),
		Val(f.Type)
	)
	if sliceSize == "day" then
		local offset = query.timezone.offset
		local minutes = query.timezone.minutes
		if offset < 0 then
			minutes = minutes * -1
		end
		local nowUTCStringDate = LuaOrm.Date():toString()
		local isDst = m_helpers.date:IsEnabledDateBetweenDaylightSavingTime(nowUTCStringDate, query.timezone)
		if isDst then
			offset = offset + 1
		end
		minutes = (offset * 60) + minutes
		key = string.format("%s:TimeZoneMinutes:%s", key, Val(minutes))
	end
	--LogDebug("[REPORTER:QuerySummaryFromCache] m_cache.MakeKey -----> key: '%s'.", key)
	return key
end

m_cache.Get = function(self, query, redis)
	assert(redis)
	--self:Delete(query, redis)
	local key = self:MakeKey(query)
	local encodedCachedInfo = redis:get(key)
	local cachedInfo
	if encodedCachedInfo then
		cachedInfo = json.decode(encodedCachedInfo)
		--LogDebug("[REPORTER:QuerySummaryFromCache] m_cache.Get -----> key: '%s', HAS CACHED INFO.", key)
	else
		--LogDebug("[REPORTER:QuerySummaryFromCache] m_cache.Get -----> key: '%s', NO Cached Info.", key)
	end
	--LogDebug("[REPORTER:QuerySummaryFromCache] m_cache.Get -----> key: '%s', cachedInfo: '%s'.", key, encodedCachedInfo)
	return cachedInfo, key
end

m_cache.Set = function(self, query, info, redis)
	assert(redis)
	assert(info)
	local key = self:MakeKey(query)
	local expireInSec, sliceInfo = self:GetTimeExpiredKey(query)
	info.expireInSec = expireInSec
	info.sliceInfo = sliceInfo
	--LogDebug("[REPORTER:QuerySummaryFromCache] m_cache.Set -----> key: '%s' expire in %s seconds., info to cache: '%s'.", key, expireInSec, info)
	redis:set(key, json.encode(info))
	redis:expire(key, expireInSec)
end

m_cache.Start = function(self, query, redis)
	assert(redis)
	--self:Delete(query, redis)
	local cachedInfo, key = self:Get(query, redis)
	if cachedInfo then
		local elapsedTimeSec = (os.time() - cachedInfo.startTimestamp)
		-- local remainingTimeSec = cachedInfo.expireInSec - elapsedTimeSec
		-- LogDebug("[REPORTER:QuerySummaryFromCache] m_cache.Start 1-----> key: '%s', cache status: '%s'. Elapsed Time %s seconds. Remaining time %s seconds cache to expire.", key, cachedInfo.status, elapsedTimeSec, remainingTimeSec)
		if cachedInfo.status == "READY" then
			return true, cachedInfo
		elseif cachedInfo.status == "RUNNING" then
			if self.maxTimeInRunningStatusSec then
				if elapsedTimeSec >= self.maxTimeInRunningStatusSec then
					--LogDebug("[REPORTER:QuerySummaryFromCache] m_cache.Start 1.1 key: '%s', cache status: '%s'. Force delete key. Elapsed Time %s seconds. maxTimeInRunningStatusSec: %s.", key, cachedInfo.status, elapsedTimeSec, self.maxTimeInRunningStatusSec)
					self:Delete(query, redis)
				end
			end
			return false, cachedInfo
		else
			LogWarning("[REPORTER:QuerySummaryFromCache] m_cache.Start 2-----> key: '%s', unrecognized cache status: '%s'.", key, cachedInfo.status)
			return false, cachedInfo
		end
	end
	--LogDebug("[REPORTER:QuerySummaryFromCache] m_cache.Start 3-----> key: '%s', NO cache.", key)
	local info = {
		key = key,
		status = "RUNNING",
		-- UTC time
		startTimestamp = os.time(),
		startDate = os.date()
	}
	self:Set(query, info, redis)
	return true, info
end

m_cache.Delete = function(self, query, redis)
	assert(redis)
	local key = self:MakeKey(query)
	redis:del(key)
end

---------------------------------------------------------------------------
-- Summary reporting FROM CACHE
---------------------------------------------------------------------------

function QuerySummaryFromCache (query, utils, _T, idsToNames, redis, authFilter)
	assert(type(query.vcc) == "string")
	assert(type(query.cut) == "string")
	assert(type(query.sliceSize) == "string")

	local handle = query.lastHandle
	idsToNames = idsToNames or {}
	local isEnabledCache = m_cache:IsEnabled()

	--LogDebug("--------------------------------------------------------------------------------------------------")
	--LogDebug("[REPORTER:QuerySummaryFromCache] (START) - FROM_CACHE.Enabled: '%s' -> query.lastHandle: %s, redis: %s, query.vcc: %s, query.cut: %s, query.sliceSize: %s, filter: %s", isEnabledCache, query.lastHandle, redis, query.vcc, query.cut, query.sliceSize, json.encode(query.filter or {}))
	--LogDebug("----------> timezone: %s", json.encode(query.timezone or {}))
	
	local function Execute (trans)
		local myRedis = trans or redis
		local cachedInfo
		if not handle then
			if not isEnabledCache then
				-- Sin Cache (cache Disabled)
				m_cache:Delete(query, myRedis)
			else
				-- Con Cache (cache Enabled)
				local success
				success, cachedInfo = m_cache:Start(query, myRedis)
				if success then
					if cachedInfo.status == "READY" then
						--LogDebug("[REPORTER:QuerySummaryFromCache] (END) 2 - =======> RETURN FROM CACHE --> result: %s, handle: %s", cachedInfo.result, cachedInfo.handle)
						--LogDebug("[REPORTER:QuerySummaryFromCache] 2 - =======> RETURN FROM CACHE --> result: %s,\nhandle: %s", json.encode(cachedInfo.result or {}, true), json.encode(cachedInfo.handle or {}, true))
						return cachedInfo.result, cachedInfo.handle
					end
				else
					cachedInfo.errType = "UNABLE TO RETRIEVE CACHE INFO - ANOTHER REPORTS IS RUNNING"
					LogInfo("[REPORTER:QuerySummaryFromCache] (END) 2.1 - =======> RETURN FROM CACHE --> Unable to return from cache. Another is running, wll retry. info: %s", (cachedInfo))
					-- LogDebug("[REPORTER:QuerySummaryFromCache] 2.1 - =======> RETURN FROM CACHE --> Unable to return from cache. Another is running, wll retry. info: %s", json.encode(cachedInfo))
					return false, nil, cachedInfo
				end
			end
			-- Ejecuto para obtener el Handle
			handle = summary_query.QuerySummary(keyspaces.SummarizerData.connection, query, utils, _T, idsToNames)
		end
		--LogDebug("[REPORTER:QuerySummaryFromCache] - handle: %s", handle)
		--LogDebug("[REPORTER:QuerySummaryFromCache] - handle: %s", json.encode(handle or {}, true))

		-- Ejecuto para obtener los datos de la pagina (rows) y el next Handle
		local result, handle = summary_query.NextPage(keyspaces.SummarizerData.connection, handle, query.resultSize, idsToNames, query, utils)
		if isEnabledCache and cachedInfo then
			cachedInfo.status = "READY"
			cachedInfo.result = result
			cachedInfo.handle = handle
			m_cache:Set(query, cachedInfo, myRedis)
		end
		--LogDebug("[REPORTER:QuerySummaryFromCache] (END) 3 - -------------> RETURN FROM DATABASE --> result: %s, handle: %s", result, handle)
		--LogDebug("[REPORTER:QuerySummaryFromCache] 4 - -------------> RETURN FROM DATABASE --> result: %s,\nhandle: %s", json.encode(result or {}, true), json.encode(handle or {}, true))
		--LogDebug("")
		return result, handle, cachedInfo
	end

	local result, handle, cachedInfo
	if not isEnabledCache then
		-- aca no ejecuta contra redis y elimina la key de redis si existia una clave para esta campana+etc.
		result, handle, cachedInfo = Execute(redis)
	else
		local key = m_cache:MakeKey(query)
		local options = {watch = {key}, cas = true, retry = 3}
		pcall(redis.transaction, redis, options, function(trans)
			result, handle, cachedInfo = Execute(trans)
			return true
		end)
	end
	--LogDebug("--------------------------------------------------------------------------------------------------")
	return result, handle, cachedInfo
end
