local manage        = require "reporter.manage"
local utf8          = require "icu.utf8"
local resultset     = require "reporter.resultset"
local slices_helper = require "reporter.slices_helper"
local ormDate       = require "LuaOrm.Date"
local cassandra     = require "lua_cassandra"
local zip           = require "brimworks.zip"
local resources     = require "reporter.export_resources"
local base64        = require "base64"
local scheduler     = require "reporter.scheduler"
local prce          = require "rex_pcre"
local lfs           = require "lfs"
local Repository    = require "icc_repository"
local m_redisValues = require "reporter.redis_values"
local m_exportJson  = require "reporter.export_to_json"
local m_allegro     = require "reporter.allegro"
local m_request     = require "reporter.request"
local m_helpers     = require "helpers"	

require "LuaOrm.Date"

--=============================================================================
local separator = "|"
local indexCF   = "IndexesDetailData"
local dataCF    = "DetailsData"
local filterCF  = "DetailFilters"
local cacheStepSize = 1000
local queryThreads = 5

local SOURCE_ROOT_PATH_TEMPORAL_FILES = "/tmp/" -- idem al directorio temporal del archivo "scheduler.lua"
local DEFAULT_HTML_FILE_NAME          = "Index.html"
local m_utcTimezone                   = {offset = 0} -- Default to UTC
local m_formatDate                    = "%Y-%m-%d %H:%M:%S"

--=============================================================================
local function SanitizeFileName (id)
	if type(id) ~= "string" then
		return
	end
	id = string.gsub(id, ":", "_")
	id = string.gsub(id, "{", "")
	id = string.gsub(id, "}", "")
	return id
end

--=============================================================================
local function rowToTable (row, metadata)
	if #row ~= #metadata then
		error("row an metadata lengths are different")
	end
	local row_d = {}
	for index, value in pairs(row) do
		row_d[metadata[index].name] = value
	end
	return row_d
end

local function tableToRow (row_d, metadata, formatter)
	local row = {}
	for index, definition in pairs(metadata) do
		local value = row_d[definition.name]
		if formatter and formatter[index] then
			value = formatter[index](value)
		end
		table.insert(row, value)
	end
	return row
end

--=============================================================================
local function InitRepository (vcc)
	local function MakeCompatibleLogger ()
		return {
			debug = LogDebug,
			info = LogInfo,
			warn = LogWarning,
			error = LogError,
			fatal = LogFatal
		}
	end

	local function MakeCassandraConn ()
		return {
			host = os.getenv("CASSANDRA_IP"),
			port = tonumber(os.getenv("CASSANDRA_PORT"))
		}
	end

	local config = {
		cassandra = MakeCassandraConn(),
		logger    = MakeCompatibleLogger()
	}

	local allegroWebhandlerInfo = m_allegro.GetWebhandlerInfo(vcc)
	if allegroWebhandlerInfo then
		config.allegroConfig = {
			webhandlerInfo = allegroWebhandlerInfo
		}
	end

	if Repository.init(config) then
		LogInfo("[REPORTER.DETAIL_QUERY:InitRepository] Successfully started repository.")
		m_exportJson.SetRepository(Repository)
		return true
	else
		LogWarning("[REPORTER.DETAIL_QUERY:InitRepository] Unable to start repository.")
		return false
	end
end

--=============================================================================
local function GetCampaignAccounts (campaignIdWithVcc, vcc, keyspace, columns, utils)
	assert(utils)
	if not campaignIdWithVcc or not vcc then
		return
	end
	campaign_id_with_vcc = utils.AppendVcc(utils.GetId(campaignIdWithVcc), vcc)

	local params = {
		keyspace = keyspace or "Agent",
		superColumns = true,
		columnFamily = "CampaignAccounts",
		key = campaignIdWithVcc,
		columns = columns
	}

	local campaign = utils.GetAllColumns( params ) or {}

	--LogDebug("[REPORTER.DETAIL_QUERY:GetCampaignAccount] Loading Campaign '%s', vcc: '%s', Campaign Info: '%s'.", campaignIdWithVcc, vcc, json.encode(campaign))

	return campaign
end

--=============================================================================
local function GetCampaignAccount (campaignIdWithVcc, accountId, vcc, keyspace, columns, utils, proxyType)
	assert(utils)
	if not campaignIdWithVcc or not accountId or not vcc then
		return
	end

	campaignIdWithVcc = utils.AppendVcc(utils.GetId(campaignIdWithVcc), vcc)
	local accounts = GetCampaignAccounts(campaignIdWithVcc, vcc, keyspace, columns, utils)

	local account
	for _, accountInfo in pairs(accounts or {}) do
		if (accountInfo.uid or ""):lower() == accountId:lower() then
			account = accountInfo
			break
		end
	end
	--LogDebug("[REPORTER.DETAIL_QUERY:GetCampaignAccount] Loading Account: '%s', campaign '%s', vcc: '%s', accountInfo: '%s'.", accountId, campaignIdWithVcc, vcc, json.encode(account))

	return account
end

--=============================================================================
local function resolve (connection, filterName, vcc)
	if manage.existsFilter(connection, "system", filterName) then
		return filterName .. "@system"
	end
	if manage.existsFilter(connection, vcc, filterName) then
		return filterName .. "@" .. vcc
	end
	return nil, "unable to resolve filter: " .. filterName
end

--=============================================================================
local function getFilterScope (connection, filterName, vcc)
	local filter, errMessage = manage.getFilter(connection, vcc, filterName)

	if not filter then return nil, errMessage end

	return filter.scope
end

--=============================================================================
local function formatFieldsToQuery (queryFields, utils)
	--LogDebug("[REPORTER.DETAIL_QUERY:formatFieldsToQuery] Entra ----> queryFields '%s'.", json.encode(queryFields))
	local sortedNames= {}
	for name in pairs(queryFields) do table.insert(sortedNames, name) end
	table.sort(sortedNames)

	local formattedFields = {{}}

	for i, name in ipairs(sortedNames) do
		local value = queryFields[name]

		if type(value) == "table" then
		-- Cuando el valor del campo del filtro llega en un array (caso campana y agentes)
			local temporalFormattedFields = {}
			for _, filter in ipairs(formattedFields) do
				for n, v in ipairs(value) do
					v = utf8.lower(tostring(v))
					local copy = utils.DeepCopy(filter)
					table.insert(copy, name .. separator .. v)
					table.insert(temporalFormattedFields, copy)
				end
			end
			formattedFields = temporalFormattedFields
		else
		-- Cuando el valor del campo del filtro llega en un string
			value = utf8.lower(tostring(value))
			for _, filter in ipairs(formattedFields) do
				table.insert(filter, name .. separator .. value)
			end
		end

		--LogDebug("[REPORTER.DETAIL_QUERY:formatFieldsToQuery] name: '%s', formattedFields[%s] ----> '%s' = .", name, i, json.encode(formattedFields[i]))
	end

	local filter = {}
	for _, field in ipairs(formattedFields) do
		local stringFilter = table.concat(field, separator)
		--LogDebug("%d * -----> stringFilter %s", _, stringFilter)
		table.insert(filter, stringFilter)
	end

	--LogDebug("[REPORTER.DETAIL_QUERY:formatFieldsToQuery] Sale ----> filter '%s'.", json.encode(filter))
	return filter
end

--=============================================================================
local function doQuery (connection, key, firstRecord, resultSize)
	local startColumn = firstRecord or ""
	local blockSize = resultSize
	if startColumn ~= "" then
		blockSize = blockSize + 1
	end
	local range = connection:GetSliceRange(key, indexCF, "", {
		column_names = {},
		slice_range  = {
			start    = startColumn,
			finish   = "",
			reversed = false,
			count    = blockSize
		}
	})

	local block = resultset.new()
	for _, column in pairs(range) do
		if column.name ~= startColumn then
			if block:length() < resultSize then
				block:append(column.name, column.value)
			end
		end
	end
	return block
end

--=============================================================================
local function resultComparator (handleA, handleB)
	if handleA < handleB then return 1
	elseif handleA > handleB then return -1
	else return 0
	end
end

--=============================================================================
local function queryIdsFromCache(resultSize, cache, reporterClient)
	local result = reporterClient:FetchDetailUniqueIdsCache(json.encode({
		cache = cache,
		resultSize = resultSize
	}))
	if result.result ~= "OK" then
		return nil, result.reason
	end

	local cachedIds = result.ids
	local sumBlock = resultset.new()
	for i = 1, resultSize do
		if #cachedIds == 0 then break end

		local column = table.remove(cachedIds)
		sumBlock:append(column.name, column.value)
	end
	return sumBlock
end

local function queryIds (connection, filterNameAtVCC, queryFields, startDate, endDate, 
						 lastRecord, resultSize, cache, utils, reporterClient)
	if cache then
		return queryIdsFromCache(resultSize, cache, reporterClient)
	end

	--LogDebug("[REPORTER.DETAIL_QUERY:queryIds] ----> queryFields '%s'.", json.encode(queryFields))
	local slices, errMessage = slices_helper.getSlices(startDate, endDate)

	if not slices then
		return nil, errMessage
	end
	local sumBlock = resultset.new()

	local query = formatFieldsToQuery(queryFields, utils)

	--LogDebug("[REPORTER.DETAIL_QUERY:queryIds] ----> slices '%s'.", json.encode(slices))
	for _, slice in pairs(slices) do
		for _, filter in ipairs(query) do
			--LogDebug("[REPORTER.DETAIL_QUERY:queryIds] %d----> filter '%s'.", _, filter)
			--LogDebug("----------------")
			local key = filterNameAtVCC .. separator .. slice .. separator .. filter
			--LogDebug("[REPORTER.DETAIL_QUERY:queryIds] %d----> key '%s'.", _, key)
			local partialBlock, errMessage = doQuery(connection, key, lastRecord, resultSize)
			if not partialBlock then
				--LogDebug("--> errMessage: %s", json.encode(errMessage or {}))
				return nil, errMessage
			end
			--LogDebug("--> partialBlock: %s", json.encode(partialBlock or {}))
			sumBlock = sumBlock:union(partialBlock, resultComparator, resultSize)
		end
	end

	return sumBlock
end

--=============================================================================
local function queryIdsCache (connection, filterNameAtVCC, queryFields, startDate, endDate, utils, reporterClient)
	--LogDebug("[REPORTER.DETAIL_QUERY:queryIdsCache] ----> queryFields '%s'.", json.encode(queryFields))

	local slices, errMessage = slices_helper.getSlices(startDate, endDate)
	if not slices then
		return nil, errMessage
	end

	local query = formatFieldsToQuery(queryFields, utils)
	--LogDebug("[REPORTER.DETAIL_QUERY:queryIdsCache] ----> slices '%s'.", json.encode(slices))
	local result = reporterClient:StartDetailUniqueIdsCache(json.encode({
		filterNameAtVCC = filterNameAtVCC,
		slices = slices,
		query = query,
		queryThreads = queryThreads
	}))
	if result.result ~= "OK" then
		return nil, result.reason
	end

	return result.cache
end

--==============================================================================
-- auxiliary functions for project fields from multiple documents
--
-- Note that in order to support that kind of projections fields should be
-- referred as: documentType.fieldName, to correct address documentType, if
-- there is no documentType we will assume that you are projecting from the
-- filtername's document type.
--==============================================================================
--==============================================================================
-- given a fieldname it split document from fieldName if there is no
-- documenttype the default will be returned

local function splitDocumentField (field, docDefaultType)
	local fieldName = field
	if type(field) == "table" then
		fieldName = field.name
	end

	local docType, beyondLastPeriod = fieldName:match("^([^%.]+)%.(.+)$")
	if docType and beyondLastPeriod then
		return docType, beyondLastPeriod
	else
		return docDefaultType, fieldName
	end
end

--==============================================================================
-- split fields by documentType, fields should be in canonical form
local function splitByType (projectFields, docDefaultType)
	local splittedFields = {}
	local normalizedFields = {}
	for _, field in ipairs(projectFields) do
		local docType, fieldName, errMessage = splitDocumentField(field, docDefaultType)
		if not docType then
			return nil, nil, errMessage
		end

		if not splittedFields[docType] then
			splittedFields[docType] = {}
		end
		table.insert(splittedFields[docType], fieldName)
		table.insert(normalizedFields, docType .. "." .. fieldName)
	end
	return splittedFields, normalizedFields
end

--=============================================================================
local function getFormatters (projectFields, timezone, _T, idsToNames, utils)
	local formatters = {}
	local trueLabel = _T and _T("Yes") or "Yes"
	local falseLabel = _T and _T("No") or "No"
	local emptyLabel = _T and _T("N/A") or "N/A"

	for _, field in ipairs(projectFields) do
		local f = function(data) return data and data or emptyLabel end
		if type(field) == "table" then
			local fieldType = field.type or field.dimensionData and field.dimensionData.type
			if fieldType == "Date" then
				local t = timezone or m_utcTimezone -- Default to UTC
				f = function(utcDate, rawData)
					if not utcDate then
						return emptyLabel
					end
					if rawData then
						t = m_utcTimezone
					end
					local formatDate = (field.format ~= "") and field.format or m_formatDate
					return slices_helper.ConvertUTCDateToTimezoneDate(
						utcDate, timezone, formatDate, utils, true).stringDate
				end
			elseif fieldType == "Time" then
				local multiplier = field.multiplier or 1
				f = function(data, rawData)
					if not data and data ~= 0 then
						return emptyLabel
					end
					data = tonumber(data) or 0
					if rawData then
						return data
					end
					return slices_helper.formatTime(data * multiplier)
				end
			elseif fieldType == "Decimal" then
				local multiplier = field.multiplier or 1
				f = function(data, rawData)
					if not data and data ~= 0 then
						return emptyLabel
					end
					data = tonumber(data) or 0
					if rawData then
						return data
					end
					return data * multiplier
				end
			elseif fieldType == "Percentage" then
				local multiplier = field.multiplier or 1
				f = function(data, rawData)
					if not data and data ~= 0 then
						return emptyLabel
					end
					data = tonumber(data) or 0
					if rawData then
						return data
					end
					return ("%.2f%%"):format(data * (multiplier or 1))
				end
			elseif fieldType == "boolean" then
				f = function(data, rawData)
					if not rawData then
						return (data == "True" or data == true) and trueLabel or falseLabel
					end
					return (data == "True" or data == true) and true or false
				end
			elseif fieldType == "enum" then
				local domain = field.domain or field.dimensionData and field.dimensionData.domain
				if field.dimension == "State" then
					f = function(data, rawData)
						if not data and data ~= 0 then
							return emptyLabel
						end
						if rawData then
							return data
						end
						data = domain[data] or data
						return data:gsub("State: ", "")
					end
				elseif field.dimension == "Ender" then
					local agentLabel = _T("Agent") .. ":"
					f = function(data, rawData)
						if not data and data ~= 0 then
							return emptyLabel
						end
						if rawData then
							return data
						end
						return (domain[data] or data):gsub("Agent:", agentLabel)
					end
				else
					f = function(data, rawData)
						if not data and data ~= 0 then
							return emptyLabel
						end
						if rawData then
							return data
						end
						return domain[data] or data
					end
				end
			elseif fieldType == "string" then
				local fieldIdsToNames = idsToNames[field.dimension]
				if fieldIdsToNames then
					f = function(data, rawData)
						if not data and data ~= 0 then
							return emptyLabel
						end
						if rawData then
							return data
						end
						return fieldIdsToNames[data] or data
					end
				end
			end
		end
		table.insert(formatters, f)
	end

	return formatters
end

--=============================================================================
local function EscapeHTML (s)
	return s and s:gsub('[<>&="\' ]', function (c)
		return "&#"..string.byte(c)..";"
	end):gsub("[\r\n]", "<br>")
end

--=============================================================================
local function WriteHTML (tableHTML, htmlToAdd) table.insert(tableHTML, htmlToAdd) end

--=============================================================================
-- Get and format content for CALL Interactions
local function GetHtmlCallContent (self, interactionId, options)
	LogInfo("[REPORTER.DETAIL_QUERY:GetHtmlCallContent] id: '%s', vcc: '%s', hasAllegroIntegration: '%s', options: '%s'.", interactionId, options.vcc, options.hasAllegroIntegration, options)
	local utils = options.utils
	local queryFields = options.queryFields
	local content

	if options.hasAllegroIntegration then
		--LogDebug("[REPORTER.DETAIL_QUERY:GetHtmlCallContent] 1- Retrieving Call ALLEGRO Interaction id: '%s' Parts.", interactionId)
		--[[
			Obtengo las partes de la interaccion.
			Realizo un request contra Allegro para obtener las partes de la interaccion.
		]]--
		local success, encodedContent, httpCodeErr = m_request.GetCallConversationPartsOfVcc(interactionId, options.vcc)
		if not success then
			LogWarning("[REPORTER.DETAIL_QUERY:GetHtmlCallContent] Unable to retrieve allegro interaction '%s' all parts. HTTP code: '%s', Error '%s'.", interactionId, httpCodeErr, encodedContent)
			return {}
		end
		content = json.decode(encodedContent)
		--LogDebug("1 - allegro encodedContent %s", encodedContent)
		-- LogDebug("2 - allegro content %s", content)
		-- LogDebug("3 - allegro content.status %s", content.status)
		-- LogDebug("4 - allegro content.records %s", #content.records)
		if not content or not content.status then
			LogWarning("[REPORTER.DETAIL_QUERY:GetHtmlCallContent] Unable to retrieve allegro interaction '%s' all parts. Error: '%s'.", interactionId, encodedContent)
			return {}
		end
	else
		--LogDebug("[REPORTER.DETAIL_QUERY:GetHtmlCallContent] 2- Retrieving Call i6 (NATIVE) Interaction id: '%s' Parts.", interactionId)
		local encodedInteractionData = utils.GetColumnValue({
			keyspace     = options.keyspaceInteractions,
			columnFamily = "Interactions",
			key          = interactionId,
			column       = "interactionData"
		})
		--LogDebug("[REPORTER.DETAIL_QUERY:GetHtmlCallContent] ---> Call i6 interaction '%s'. encodedInteractionData '%s'.", interactionId, encodedInteractionData)
		if not encodedInteractionData then
			LogWarning("[REPORTER.DETAIL_QUERY:GetHtmlCallContent] Unable to retrieve Call i6 interaction '%s'. interactionData not found.", interactionId)
			return {}
		end
		local interactionData = utils.JSON_Decode(encodedInteractionData)
		if not interactionData then
			LogWarning("[REPORTER.DETAIL_QUERY:GetHtmlCallContent] Unable to retrieve i6 Call interaction. Invalid interactionData.", interactionId)
			return {}
		end
		content = interactionData
	end

	local totalParts = #(content.records or {})
	LogInfo("[REPORTER.DETAIL_QUERY:GetHtmlCallContent] Interaction '%s' contains '%s' parts.", interactionId, totalParts)

	if totalParts < 1 then
		LogWarning("[REPORTER.DETAIL_QUERY:GetHtmlCallContent] Unable to retrieve Content of Call interaction. Interactions without Parts.", interactionId)
		return {}
	end

	--[[
		Funcion que obtiene el nombre del Agente.
		Lo obtiene de diferentes atributos ya que en el caso de Allegro llega en "AgentId" y en Nativo en "agent", y en el caso de este ultimo llega con @vcc.
	]]--
	local function GetAgentId (record)
		local agentId = record.AgentId or record.agent
		if not agentId then
			return false, "ANY_AGENT"
		end
		return true, (utils.GetId(agentId) or agentId)
	end

	--[[
		Funcion que valida si un record debe incluirse.
		Ej: si es un reporte por agente, y el filtro inlcuye al agente, tiene que retornan solo las grabaciones (records) de ese agente.
	]]--
	local function MustInclude (record)
		if not queryFields then
			return true, "OK - NO QUERY FIELDS"
		end
		local succesReason
		if type(queryFields.Agent) == "string" then
			-- si el fitro incluye un agente, valida que las grabaciones sean solo de este agente.
			local filterAgent = utils.GetId(queryFields.Agent):lower()
			local hasAgent, recordAgent = GetAgentId(record)
			if not hasAgent then
				return false, "AGENT IS REQUIRED IN RECORD"
			end
			recordAgent = recordAgent:lower()
			if filterAgent ~= recordAgent then
				return false, string.format("AGENT FILTER NO MATCH - %s DISTINCT %s", filterAgent, recordAgent)
			end
			succesReason = "OK - AGENT FILTER MATCH"
		else
			succesReason = "OK - FILTER HAS NOT AGENT QUERY"
		end
		return true, succesReason
	end

	local result = {}
	for _, record in ipairs(content.records) do
		local success, conversation
		local section = record.Section and tostring(record.Section)
		local part = record.Part and tostring(record.Part)
		local mustInclude, includeReason = MustInclude(record)

		if mustInclude then
			LogDebug("[REPORTER.DETAIL_QUERY:GetHtmlCallContent] ==> OK - INCLUDE RECORD. include record. Reason: %s. Call interaction '%s', section: '%s', part '%s'.", includeReason, interactionId, section, part)
			if options.hasAllegroIntegration then
				success, conversation = Repository.allegro.retrieve_file(options.vcc, interactionId, section, part, false)
			else
				section = section or part
				local interactionId_name = interactionId
				if record.Mode == "screen_recording" then
					interactionId_name = interactionId_name .. "_" .. record.Mode 
				elseif record.Mode == "all" or record.Mode == "video_only" then
					interactionId_name = interactionId_name .. "_video"
				end
				--LogDebug("===> interactionId_name: %s - section: %s - part: %s - options.keyspaceRepository: %s", interactionId_name, section,part,options.keyspaceRepository)
				success, conversation = Repository.retrieve_file(interactionId_name, section, part, false, options.keyspaceRepository)
			end
			--LogDebug("[REPORTER.DETAIL_QUERY:GetHtmlCallContent] ----> Interaction '%s' success '%s' conversation.", interactionId, success, conversation)
			if success then
				if record.Mode == "all" or record.Mode == "video_only" or record.Mode == "screen_recording" then
					formatFile = record.VideoFormat or "mp4"
				else	
					formatFile = record.Format or "wav"
				end
				local _1, agentId = GetAgentId(record)
				local conversationFileId = ("%s_%s_%s_%s.%s"):format(agentId, interactionId, section, part, formatFile):upper()
				--LogDebug("----> conversationFileId: '%s'", conversationFileId)
				result[conversationFileId] = conversation
			else
				LogWarning("[REPORTER.DETAIL_QUERY:GetHtmlCallContent] Unable to retrieve Call interaction '%s', section: '%s', part '%s'.", interactionId, section, part)
			end
		else
			LogDebug("[REPORTER.DETAIL_QUERY:GetHtmlCallContent] --> NOT INCLUDE RECORD. Reason: %s. Call interaction '%s', section: '%s', part '%s'.", includeReason, interactionId, section, part)
		end
	end

	return result
end

--=============================================================================
-- Get and format content for VOICEMAIL Interactions
local function GetHtmlVoicemailContent (self, interactionId, options)
	LogInfo("[REPORTER.DETAIL_QUERY:GetHtmlVoicemailContent] id: '%s', vcc: '%s', hasAllegroIntegration: '%s', options: '%s'.", interactionId, options.vcc, options.hasAllegroIntegration, options)
	local utils = options.utils
	local queryFields = options.queryFields
	local content

	if options.hasAllegroIntegration then
		content = {
			voicemailRecord = {
				part    = 1,
				section = -1
			}
		}
	else
		LogDebug("[REPORTER.DETAIL_QUERY:GetHtmlVoicemailContent] 2- Retrieving Call i6 (NATIVE) Interaction id: '%s' Parts.", interactionId)
		local encodedInteractionData = utils.GetColumnValue({
			keyspace     = options.keyspaceInteractions,
			columnFamily = "Interactions",
			key          = interactionId,
			column       = "interactionData"
		})
		LogDebug("[REPORTER.DETAIL_QUERY:GetHtmlVoicemailContent] 2.1 ---> Call i6 interaction '%s'. encodedInteractionData '%s'.", interactionId, encodedInteractionData)
		if not encodedInteractionData then
			LogWarning("[REPORTER.DETAIL_QUERY:GetHtmlVoicemailContent] 2.2 Unable to retrieve Call i6 interaction '%s'. interactionData not found.", interactionId)
			return {}
		end
		local interactionData = utils.JSON_Decode(encodedInteractionData)
		if not interactionData then
			LogWarning("[REPORTER.DETAIL_QUERY:GetHtmlVoicemailContent] 2.3 Unable to retrieve i6 Call interaction. Invalid interactionData.", interactionId)
			return {}
		end
		content = interactionData
	end

	--local totalParts = #(content.voicemailRecord or {})

	local totalParts = 0
	if content.voicemailRecord and content.voicemailRecord.part then
		totalParts = 1
	end

	LogInfo("[REPORTER.DETAIL_QUERY:GetHtmlVoicemailContent] Interaction '%s' contains '%s' parts.", interactionId, totalParts)

	if totalParts < 1 then
		LogWarning("[REPORTER.DETAIL_QUERY:GetHtmlVoicemailContent] Unable to retrieve Content of Call interaction. Interactions without Parts.", interactionId)
		return {}
	end

	--[[
		Funcion que obtiene el nombre del Agente.
		Lo obtiene de diferentes atributos ya que en el caso de Allegro llega en "AgentId" y en Nativo en "agent", y en el caso de este ultimo llega con @vcc.
		En Voicemail, el agente esta en enderAgent
	]]--
	local function GetAgentId (content)
		local agentId = content.AgentId or content.agent or content.enderAgent
		if not agentId then
			return false, "ANY_AGENT"
		end
		return true, (utils.GetId(agentId) or agentId)
	end

	--[[
		Funcion que valida si un record debe incluirse.
		Ej: si es un reporte por agente, y el filtro inlcuye al agente, tiene que retornan solo las grabaciones (records) de ese agente.
	]]--
	local function MustInclude (record)
		if not queryFields then
			return true, "OK - NO QUERY FIELDS"
		end
		local succesReason
		if type(queryFields.Agent) == "string" and not options.hasAllegroIntegration then
			-- si el fitro incluye un agente, valida que las grabaciones sean solo de este agente.
			local filterAgent = utils.GetId(queryFields.Agent):lower()
			local hasAgent, recordAgent = GetAgentId(record)
			if not hasAgent then
				return false, "AGENT IS REQUIRED IN RECORD"
			end
			recordAgent = recordAgent:lower()
			if filterAgent ~= recordAgent then
				return false, string.format("AGENT FILTER NO MATCH - %s DISTINCT %s", filterAgent, recordAgent)
			end
			succesReason = "OK - AGENT FILTER MATCH"
		else
			succesReason = "OK - FILTER HAS NOT AGENT QUERY"
		end
		return true, succesReason
	end

	local result = {}
	--for _, record in ipairs(content.voicemailRecord) do
	if content.voicemailRecord and content.voicemailRecord.part then
		local record = content.voicemailRecord
		local success, conversation
		local section = record.section and tostring(record.section)
		local part = record.part and tostring(record.part)
		local mustInclude, includeReason = MustInclude(record)

		if mustInclude then
			LogDebug("[REPORTER.DETAIL_QUERY:GetHtmlVoicemailContent] ==> OK - INCLUDE RECORD. include record. Reason: %s. Voicemail interaction '%s', section: '%s', part '%s'.", includeReason, interactionId, section, part)
			if options.hasAllegroIntegration then
				local response

				success, response = Repository.allegro.retrieve_file(options.vcc, interactionId, section, part, false, true)
				-- #18610 Right now only 2 fromats are available for voicemails. WAV and MP3 so only check for MP3 content-type
				if success then
					if response.headers and response.headers["content-type"] == "audio/mpeg" then
						record.Format = "mp3"
					else
						record.Format = "wav"
					end
					conversation = response.file
				end
			else
				section = section or part
				local interactionId_name = interactionId
				if record.Mode == "screen_recording" then
					interactionId_name = interactionId_name .. "_" .. record.Mode
				elseif record.Mode == "all" or record.Mode == "video_only" then
					interactionId_name = interactionId_name .. "_video"
				end

				interactionId_name = string.gsub(interactionId_name, 'VOICEMAIL_', '')

				LogDebug("===> MOD interactionId_name: %s - section: %s - part: %s - options.keyspaceRepository: %s", interactionId_name, section,part,options.keyspaceRepository)
				success, conversation = Repository.retrieve_file(interactionId_name, section, part, false, options.keyspaceRepository)

			end

			if success
			then
				local fileFormat = "wav"

				if record.Mode == "all" or record.Mode == "video_only" or record.Mode == "screen_recording" then
					fileFormat = record.VideoFormat or "mp4"
				elseif record.Format then
					fileFormat = record.Format
				elseif record.id and options.keyspaceRepository then
					-- Retrieve file format from repository
					fileFormat = utils.GetColumnValue({
						keyspace     = options.keyspaceRepository,
						columnFamily = "Files",
						key          = record.id,
						column       = "Format"
					})
					if not fileFormat or fileFormat == '' then
						fileFormat = "wav"
					end
				end
				local conversationFileId = ("%s_%s_%s.%s"):format(interactionId, section, part, fileFormat):upper()
				LogDebug("----> conversationFileId: '%s'", conversationFileId)
				result[conversationFileId] = conversation
			else
				LogWarning("[REPORTER.DETAIL_QUERY:GetHtmlVoicemailContent] Unable to retrieve Voicemail interaction '%s', section: '%s', part '%s'.", interactionId, section, part)
			end
		else
			  LogDebug("[REPORTER.DETAIL_QUERY:GetHtmlVoicemailContent] --> NOT INCLUDE RECORD. Reason: %s. Call interaction '%s', section: '%s', part '%s'.", includeReason, interactionId, section, part)
		end
	end

	return result
end

--=============================================================================
-- Get and format content for Webchat Interactions
local function GetHtmlWebchatContent (self, id, options)
	LogDebug("[REPORTER.DETAIL_QUERY:GetHtmlWebchatContent] interactionId: '%s', options: '%s'.", id, options)
	--TODO: poner ip y port del repositorio posta e intentar con varios de ellos si uno falla
	local success, conversation = Repository.retrieve_file(id, 0, 0, false, options.keyspaceRepository)
	if not success then
		return
	end

	local response, messages = pcall(json.decode, conversation)

	if type(messages) == "table" and messages.messages then
		local participants = {}
		local availables = {"participantOne", "participantTwo"}
		local function getParticipantClass(p)
			if not participants[p] then
				participants[p] = availables[1] or "participantOther"
				table.remove(availables, 1)
			end
			return participants[p]
		end

		local tableHTML = {}
		WriteHTML(tableHTML, [[
<!DOCTYPE html>
<html>
<head>
	<meta charset="utf-8">
	<link rel="stylesheet" type="text/css" href="../resources/webchat.css"></link>
</head>
<body>
<table border="0" cellspacing="0" cellpadding="0">
]])

		for _, message in ipairs(messages.messages) do
			if message.msgtype == "text" then
				local timeZoneDate = slices_helper.ConvertUTCDateToTimezoneDate(
					message.msgtime, options.timezone, m_formatDate, options.utils, true).stringDate
				local name = message.display_name or ""
				name = name..":" 
				local text = message.text

				WriteHTML(tableHTML, '<tr class="'..getParticipantClass(name)..'">')
				WriteHTML(tableHTML, "<td>"..EscapeHTML(timeZoneDate).."</td>")
				WriteHTML(tableHTML, "<td><span>"..EscapeHTML(name).."</span><span>"..EscapeHTML(text).."</span></td>")
				WriteHTML(tableHTML, "</tr>")
			end
		end

		WriteHTML(tableHTML, [[
</table>
</body>
</html>
		]])

		return table.concat(tableHTML, "\r\n")
	end
end

--=============================================================================
-- Get and format content for Sms Interactions
local function GetHtmlSmsContent (self, id, options)
	LogInfo("[REPORTER.DETAIL_QUERY:GetHtmlSmsContent] interactionId: '%s', vcc: '%s', options: '%s'.", id, options.vcc, options)
	local utils = options.utils
	local interactionEvents = options.keyspaceInteractions:GetSliceRange(id,
		"Interactions",
		"", {
			column_names = {"events"},
			slice_range = {}
		}
	)
	local events
	if interactionEvents then
		for _, v in ipairs(interactionEvents) do
			events = v.value
		end
	end

	if not events then
		return
	end

	events = json.decode(events)

	local participants = {}
	local availables = {"participantOne", "participantTwo"}
	local function getParticipantClass(p)
		if not participants[p] then
			participants[p] = availables[1] or "participantOther"
			table.remove(availables, 1)
		end
		return participants[p]
	end

	local tableHTML = {}
	WriteHTML(tableHTML, [[
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<link rel="stylesheet" type="text/css" href="../resources/webchat.css"></link>
</head>
<body>
<table border="0" cellspacing="0" cellpadding="0">
]])

	for _, event in ipairs(events) do
		local message = event.message or {}
		local timeZoneDate = slices_helper.ConvertUTCDateToTimezoneDate(
			message.timestamp, options.timezone, m_formatDate, utils, true).stringDate
		local name = ""
		if event.creator == "agent" then
			name = (utils.GetId(event.agent) or "") .. " ("..event.creator.."):"
		elseif event.creator == "bot" then
			name = "bot"
		elseif event.creator == "contact" then
			name = (event.address and event.address.Number or "") .. " ("..event.creator.."):"
		end
		local text = message.text or ""

		WriteHTML(tableHTML, '<tr class="'..getParticipantClass(event.creator)..'">')
		WriteHTML(tableHTML, "<td>"..EscapeHTML(timeZoneDate).."</td>")
		WriteHTML(tableHTML, "<td><span>"..EscapeHTML(name).."</span><span>"..EscapeHTML(text).."</span></td>")
		WriteHTML(tableHTML, "</tr>")
	end

	WriteHTML(tableHTML, [[
</table>
</body>
</html>
	]])

	return table.concat(tableHTML, "\r\n")
end

--=============================================================================
-- Get and format content for Rcs Interactions
local function GetHtmlRcsContent (self, id, options)
	LogInfo("[REPORTER.DETAIL_QUERY:GetHtmlRcsContent] interactionId: '%s', vcc: '%s', options: '%s'.", id, options.vcc, options)
	local utils = options.utils
	local interactionEvents = options.keyspaceInteractions:GetSliceRange(id,
		"Interactions",
		"", {
			column_names = {"events"},
			slice_range = {}
		}
	)
	local events
	if interactionEvents then
		for _, v in ipairs(interactionEvents) do
			events = v.value
		end
	end

	if not events then
		return
	end

	events = json.decode(events)

	local participants = {}
	local availables = {"participantOne", "participantTwo"}
	local function getParticipantClass(p)
		if not participants[p] then
			participants[p] = availables[1] or "participantOther"
			table.remove(availables, 1)
		end
		return participants[p]
	end

	local tableHTML = {}
	WriteHTML(tableHTML, [[
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<link rel="stylesheet" type="text/css" href="../resources/webchat.css"></link>
</head>
<body>
<table border="0" cellspacing="0" cellpadding="0">
]])

	for _, event in ipairs(events) do
		local message = event.message or {}
		local timeZoneDate = slices_helper.ConvertUTCDateToTimezoneDate(
			message.timestamp, options.timezone, m_formatDate, utils, true).stringDate
		local name = ""
		if event.creator == "agent" then
			name = (utils.GetId(event.agent) or "") .. " ("..event.creator.."):"
		elseif event.creator == "bot" then
			name = "bot"
		elseif event.creator == "contact" then
			name = (event.address and event.address.Number or "") .. " ("..event.creator.."):"
		end
		local text = message.text or ""

		WriteHTML(tableHTML, '<tr class="'..getParticipantClass(event.creator)..'">')
		WriteHTML(tableHTML, "<td>"..EscapeHTML(timeZoneDate).."</td>")
		WriteHTML(tableHTML, "<td><span>"..EscapeHTML(name).."</span><span>"..EscapeHTML(text).."</span></td>")
		WriteHTML(tableHTML, "</tr>")
	end

	WriteHTML(tableHTML, [[
</table>
</body>
</html>
	]])

	return table.concat(tableHTML, "\r\n")
end

--=============================================================================
-- Get and format content for Whatsapp Interactions
local function GetHtmlWhatsappContent (self, id, options)
	LogInfo("[REPORTER.DETAIL_QUERY:GetHtmlWhatsappContent] interactionId: '%s', vcc: '%s', options: '%s'.", id, options.vcc, options)
	local utils = options.utils
	local interactionEvents = options.keyspaceInteractions:GetSliceRange(id,
		"Interactions",
		"", {
			column_names = {"events"},
			slice_range = {}
		}
	)

	local events
	if interactionEvents then
		for _, v in ipairs(interactionEvents) do
			events = v.value
		end
	end

	if not events then
		return
	end

	events = json.decode(events)

	local participants = {}
	local availables = {"participantOne", "participantTwo"}
	local function getParticipantClass(p)
		if not participants[p] then
			participants[p] = availables[1] or "participantOther"
			table.remove(availables, 1)
		end
		return participants[p]
	end

	local tableHTML = {}
	WriteHTML(tableHTML, [[
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<link rel="stylesheet" type="text/css" href="../resources/webchat.css"></link>
</head>
<body>
<table border="0" cellspacing="0" cellpadding="0">
]])

	for _, event in ipairs(events) do
		local message = event.message or {}
		local timeZoneDate = slices_helper.ConvertUTCDateToTimezoneDate(
			message.timestamp, options.timezone, m_formatDate, utils, true).stringDate
		local name = ""
		if event.creator == "agent" then
			name = (utils.GetId(event.agent) or "") .. " ("..event.creator.."):"
		elseif event.creator == "bot" then
			name = "bot"
		elseif event.creator == "contact" then
			name = (event.address and event.address.Number or "") .. " ("..event.creator.."):"
		end
		local text = message.text or ""

		WriteHTML(tableHTML, '<tr class="'..getParticipantClass(event.creator)..'">')
		WriteHTML(tableHTML, "<td>"..EscapeHTML(timeZoneDate).."</td>")
		WriteHTML(tableHTML, "<td><span>"..EscapeHTML(name).."</span><span>"..EscapeHTML(text).."</span></td>")
		WriteHTML(tableHTML, "</tr>")
	end

	WriteHTML(tableHTML, [[
</table>
</body>
</html>
	]])

	return table.concat(tableHTML, "\r\n")
end

--=============================================================================
local function UrlEncode (str)
	str = string.gsub(str, "\n", "\r\n")
	str = string.gsub(str, "([^%w%-%_%.%~])", function (c)
		return string.format ("%%%02X", string.byte(c))
	end)
	return str
end

--=============================================================================
-- Get and format content for Email Interactions
local function GetHtmlEmailContent (self, id, options)
	LogInfo("[REPORTER.DETAIL_QUERY:GetHtmlEmailContent] interactionId: '%s', vcc: '%s', options: '%s'.", id, options.vcc, options)
	local interactionEvents = options.keyspaceInteractions:GetSliceRange(id,
		"Interactions",
		"", {
			column_names = {"events"},
			slice_range = {}
		}
	)
	local events
	if interactionEvents then
		for _, v in ipairs(interactionEvents) do
			events = v.value
		end
	end
	if not events then
		return nil
	end
	events = json.decode(events)
	local results = nil
	for _, event in ipairs(events) do
		if event.creator ~= "automatic" then
			local result, conversation = Repository.retrieve_file(event.id, -1, 0, false, options.keyspaceRepository)
			if result then
				results = results or {}
				table.insert(results,conversation)
				--results[event.id..".eml"] = conversation
			end
		end
	end
	return results
end

--=============================================================================
local function BuildEventsHierarchy (events)
	-- Genero la lista de eventos con la fecha de creacion para luego ordenar
	local interactionsEvents = {}
	local knownIds = {}
	local forSorted = {}
	local idx = 0
	for _,event in ipairs(events) do
		local message = event.message
		if message then
			local messageId = message.id or message.message_id or ""
			if not knownIds[messageId] then
				local eventData = {}
				local stringDateMessage = message.created_time
				local year, month, day = prce.match(stringDateMessage, [[^((?:19|20)\d\d)[- /.](0[1-9]|1[012])[- /.](0[1-9]|[12][0-9]|3[01])\s*]])
				local hour, min, sec, millisec = prce.match(stringDateMessage, [[\s*(0[0-9]|1[0-9]|2[0-3]):([0-6][0-9]):([0-6][0-9])(?:\+(\d\d\d\d))?$]])
				local arrayDate = { year = year, month = month, day = day, hour = hour, min = min, sec = sec, isdst = true}
				local messageTimestamp = LuaOrm.Date(arrayDate)._time

				local name = ""
				if event.creator == "agent" and event.agent then
					name = event.agent
				elseif message.from.name then
					name = message.from.name
				end

				eventData.name = name
				eventData.creator = event.creator
				eventData.text = message.text or message.message or message.description
				eventData.childrens = {}
				eventData.creationDate = message.created_time
				eventData.timestamp = messageTimestamp
				eventData.id = messageId
				eventData.attachment = message.attachment
				if message.parent_id then
					eventData.parent_id = message.parent_id
				end
				interactionsEvents[messageId] = eventData
				knownIds[messageId] = true
				forSorted[messageId] = messageTimestamp
			end
		end
	end

	if not forSorted then
		return nil
	end

	-- Ordeno la lista de eventos por fecha de creacion
	local sortedEvents = {}
	local idxEvents = 0
	while true do
		local bestEvent, bestCreationDate, idx
		for id, creationDate in pairs(forSorted) do
			if not bestCreationDate or creationDate < bestCreationDate then
				bestEvent = id
				bestCreationDate = creationDate
			end
		end
		if not bestEvent then
			break
		end
		sortedEvents[idxEvents] = bestEvent
		idxEvents = idxEvents + 1
		forSorted[bestEvent] = nil
	end

	-- Armo el arbol gerarquico para luego imprimir
	local eventsHierarchy={}
	local nodesIndices = {}
	local idx = 2
	--El primer evento segun fecha tiene que ser el primer evento de la interaccion
	eventsHierarchy[1]=interactionsEvents[sortedEvents[0]]
	nodesIndices[sortedEvents[0]] = 1
	for i=1,#sortedEvents do
		local messageId = sortedEvents[i]
		local event = interactionsEvents[messageId]
		if event.parent_id then
			parentId = nodesIndices[event.parent_id]
			local parent = eventsHierarchy[parentId]
			if parent then
				table.insert(parent.childrens,event)
			else
				eventsHierarchy[idx] = event
				nodesIndices[event.id] = idx
				idx = idx + 1
			end
		else
			eventsHierarchy[idx] = event
			nodesIndices[event.id] = idx
			idx = idx + 1
		end
	end
	return eventsHierarchy
end

--=============================================================================
-- Get and format content for facebook Interactions
local function GetHtmlFacebookContent (self, id, options)
	LogInfo("[REPORTER.DETAIL_QUERY:GetHtmlFacebookContent] interactionId: '%s', vcc: '%s', options: '%s'.", id, options.vcc, options)
	local utils = options.utils
	local interactionEvents = options.keyspaceInteractions:GetSliceRange(id,
		"Interactions",
		"", {
			column_names = {"events"},
			slice_range = {}
		}
	)
	local events
	if interactionEvents then
		for _, v in ipairs(interactionEvents) do
			events = v.value
		end
	end

	if not events then
		return
	end

	events = json.decode(events)
	local sortedEvents = BuildEventsHierarchy(events)
	if sortedEvents then

		local participants = {}
		local availables = {"participantOne", "participantTwo"}
		local function getParticipantClass(p)
			if not participants[p] then
				participants[p] = availables[1] or "participantOther"
				table.remove(availables, 1)
			end
			return participants[p]
		end

		local tableHTML = {}
		WriteHTML(tableHTML, [[
<!DOCTYPE html>
<html>
<head>
	<meta charset="utf-8">
	<link rel="stylesheet" type="text/css" href="../resources/webchat.css"></link>
</head>
<body>
<table border="0" cellspacing="0" cellpadding="0">
]])

	--[[
		Funcion la cual añade el markup necesario para que se puedan visualizar las imágenes.
			Ej:
				Recibe el attachment correspondiente a una imagen:
					{
						"mime_type": "image/jpeg",
						"image_data": {
							"preview_url": "https://scontent.xx.fbcdn.net/v/t34.0-0/p280x280/17793204_10208898729126401_546316151_n.jpg?oh=6d1da5d466f80c3db669c2125e983a47&oe=58E47D15",
							"image_type": 1,
							"render_as_sticker": false,
							"url": "https://scontent.xx.fbcdn.net/v/t34.0-12/17793204_10208898729126401_546316151_n.jpg?oh=496c917b41de1bc6777836dec6d099c3&oe=58E56456",
							..
							..
						},
						..
						..
					}

				El valor que va retornar en "message" es el siguiente:
					<img src='https://scontent.xx.fbcdn.net/v/t34.0-12/17793204_10208898729126401_546316151_n.jpg?oh=496c917b41de1bc6777836dec6d099c3&oe=58E56456'>
]
	]]
	local function AddMediaHTML (event, message)

		if not event or not event.attachment then
			return message
		end
		message = message or ""

	--	LogDebug("Facebook AddMediaHTML --> event.attachment: %s", json.encode(event.attachment))

		if utils.IsArray(event.attachment) then
		-- private messages
			--LogDebug("Facebook AddMediaHTML - Voy por attachments del Private Message.")
			for _, attachment in ipairs(event.attachment) do

				--LogDebug("  attachment--> %s", json.encode(attachment))
				local mediaHTML
				if attachment.image_data and attachment.image_data.url then
					mediaHTML = "<img title='image' src='%s'>"
					mediaHTML = mediaHTML:format(attachment.image_data.url)

				elseif attachment.video_data and attachment.video_data.url then
					mediaHTML = "<a title='video' href='%s' target='_blank'><img src='%s'></a>"
					mediaHTML = mediaHTML:format(attachment.video_data.url, attachment.video_data.preview_url)

				elseif attachment.file_url then
					mediaHTML = "<a title='file' href='%s' target='_blank'>%s</a>"
					mediaHTML = mediaHTML:format(attachment.file_url, attachment.name or "Link")

				else
					LogWarning("[REPORTER.DETAIL_QUERY:GetHtmlFacebookContent] Unable to Add Media HTML to Private Message. Attachment: '%s'", json.encode(attachment))
				end

				if mediaHTML then
					message = message .. "<BR>" .. mediaHTML
				end
			end
		else
		-- comment
			--LogDebug("Facebook AddMediaHTML - Voy por attachments del Comment.")
			local content = {}
			if event.attachment.media and event.attachment.media.image and event.attachment.media.image.src then
				content.picture = event.attachment.media.image.src
			end

			if event.attachment.type == "photo" then
				content.type = "photo"
			elseif event.attachment.type == "sticker" then
				content.type = "sticker"
			else
				content.type = "video/link"
				content.link = event.attachment.url
			end

			if content.picture then
				local mediaHTML
				if content.link then
					mediaHTML = ("<a title='%s' href='%s' target='_blank'><img src='%s'></a>"):format(content.type, content.link, content.picture)
				else
					mediaHTML = ("<img title='%s' src='%s'>"):format(content.type, content.picture)
				end

				if mediaHTML then
					message = message .. "<BR>" .. mediaHTML
				end
			else
				LogWarning("[REPORTER:GetHtmlFacebookContent] Unable to Add Media HTML to Comment. Attachment: '%s'", json.encode(attachment))
			end
		end

		return message
	end

		for _, event in ipairs(sortedEvents) do
			local timeZoneDate = slices_helper.ConvertUTCDateToTimezoneDate(
				event.timestamp, options.timezone, m_formatDate, utils, true).stringDate

			local name = event.name.." ("..event.creator.."):" or ""
			local message = EscapeHTML(event.text or "")
			-- Agrego el markup correspondiente a Media (imágenes, gif animados).
			message = AddMediaHTML(event, message)

			WriteHTML(tableHTML, '<tr class="' .. getParticipantClass(event.creator) .. '">')
			WriteHTML(tableHTML, "<td>" .. EscapeHTML(timeZoneDate) .. "</td>")
			WriteHTML(tableHTML, "<td><span>" .. EscapeHTML(name) .. "</span><div>" .. message .. "</div></td>")
			WriteHTML(tableHTML, "</tr>")

			if event.childrens then
				for _,child in ipairs(event.childrens) do
					local timeZoneDate = slices_helper.ConvertUTCDateToTimezoneDate(
						child.timestamp, options.timezone, m_formatDate, utils, true).stringDate

					local name = child.name.."("..child.creator.."):" or ""
					local message = EscapeHTML(child.text or "")
					-- Agrego el markup correspondiente a Media (imágenes, gif animados).
					message = AddMediaHTML(child, message)

					WriteHTML(tableHTML, '<tr class="'..getParticipantClass(child.creator)..'">')
					WriteHTML(tableHTML, "<td>" .. EscapeHTML(timeZoneDate) .. "</td>")
					WriteHTML(tableHTML, "<td><span>     |___ </span><span>" .. EscapeHTML(name) .. "</span><div>" .. message .. "</div></td>")
					WriteHTML(tableHTML, "</tr>")
				end
			end
		end

		WriteHTML(tableHTML, [[
</table>
</body>
</html>
		]])

		return table.concat(tableHTML, "\r\n")
	end
end

--=============================================================================
-- Get and format content for instagram Interactions
local function GetHtmlInstagramContent (self, id, options)
	LogInfo("[REPORTER.DETAIL_QUERY:GetHtmlInstagramContent] interactionId: '%s', vcc: '%s', options: '%s'.", id, options.vcc, options)
	local utils = options.utils
	local interactionEvents = options.keyspaceInteractions:GetSliceRange(id,
		"Interactions",
		"", {
			column_names = {"events"},
			slice_range = {}
		}
	)
	local events
	if interactionEvents then
		for _, v in ipairs(interactionEvents) do
			events = v.value
		end
	end

	if not events then
		return
	end


	events = json.decode(events)
	local sortedEvents = BuildEventsHierarchy(events)
	if sortedEvents then

		local participants = {}
		local availables = {"participantOne", "participantTwo"}
		local function getParticipantClass(p)
			if not participants[p] then
				participants[p] = availables[1] or "participantOther"
				table.remove(availables, 1)
			end
			return participants[p]
		end

		local tableHTML = {}
		WriteHTML(tableHTML, [[
<!DOCTYPE html>
<html>
<head>
	<meta charset="utf-8">
	<link rel="stylesheet" type="text/css" href="../resources/webchat.css"></link>
</head>
<body>
<table border="0" cellspacing="0" cellpadding="0">
]])

		for _, event in ipairs(sortedEvents) do
			local timeZoneDate = slices_helper.ConvertUTCDateToTimezoneDate(
				event.timestamp, options.timezone, m_formatDate, utils, true).stringDate

			local name = event.name.." ("..event.creator.."):" or ""
			local message = EscapeHTML(event.text or "")

			WriteHTML(tableHTML, '<tr class="' .. getParticipantClass(event.creator) .. '">')
				WriteHTML(tableHTML, "<td>" .. EscapeHTML(timeZoneDate) .. "</td>")
				WriteHTML(tableHTML, "<td><span>" .. EscapeHTML(name) .. "</span><div>" .. message .. "</div></td>")
			WriteHTML(tableHTML, "</tr>")

			if event.childrens then
				for _,child in ipairs(event.childrens) do
					local timeZoneDate = slices_helper.ConvertUTCDateToTimezoneDate(
						event.timestamp, options.timezone, m_formatDate, utils, true).stringDate

					local name = child.name.."("..child.creator.."):" or ""
					local message = EscapeHTML(child.text or "")

					WriteHTML(tableHTML, '<tr class="'..getParticipantClass(child.creator)..'">')
						WriteHTML(tableHTML, "<td>" .. EscapeHTML(timeZoneDate) .. "</td>")
						WriteHTML(tableHTML, "<td><span>     |___ </span><span>" .. EscapeHTML(name) .. "</span><div>" .. message .. "</div></td>")
					WriteHTML(tableHTML, "</tr>")
				end
			end
		end

		WriteHTML(tableHTML, [[
</table>
</body>
</html>
		]])

		return table.concat(tableHTML, "\r\n")
	end

end

--=============================================================================
-- Get and format content for youtube Interactions
local function GetHtmlYoutubeContent (self, id, options)
	LogInfo("[REPORTER.DETAIL_QUERY:GetHtmlYoutubeContent] interactionId: '%s', vcc: '%s', options: '%s'.", id, options.vcc, options)
	local utils = options.utils
	local interactionEvents = options.keyspaceInteractions:GetSliceRange(id,
		"Interactions",
		"", {
			column_names = {"events"},
			slice_range = {}
		}
	)
	local events
	if interactionEvents then
		for _, v in ipairs(interactionEvents) do
			events = v.value
		end
	end

	if not events then
		return
	end

	events = json.decode(events)

	local participants = {}
	local availables = {"participantOne", "participantTwo"}
	local function getParticipantClass(p)
		if not participants[p] then
			participants[p] = availables[1] or "participantOther"
			table.remove(availables, 1)
		end
		return participants[p]
	end

	local tableHTML = {}
	WriteHTML(tableHTML, [[
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<link rel="stylesheet" type="text/css" href="../resources/webchat.css"></link>
</head>
<body>
<table border="0" cellspacing="0" cellpadding="0">
]])

	for _, event in ipairs(events) do
		local message = event.message or {}
		local timeZoneDate = slices_helper.ConvertUTCDateToTimezoneDate(
			message.timestamp, options.timezone, m_formatDate, utils, true).stringDate

		local name = ""
		if event.creator == "agent" then
			name = (utils.GetId(event.agent) or "") .. " ("..event.creator.."):"
		elseif event.creator == "bot" then
			name = "bot"
		elseif event.creator == "contact" then
			name = (event.address and event.address.Number or "") .. " ("..event.creator.."):"
		end
		local text = message.text or ""

		WriteHTML(tableHTML, '<tr class="'..getParticipantClass(event.creator)..'">')
			WriteHTML(tableHTML, "<td>"..EscapeHTML(timeZoneDate).."</td>")
			WriteHTML(tableHTML, "<td><span>"..EscapeHTML(name).."</span><span>"..EscapeHTML(text).."</span></td>")
		WriteHTML(tableHTML, "</tr>")
	end

	WriteHTML(tableHTML, [[
</table>
</body>
</html>
	]])

	return table.concat(tableHTML, "\r\n")
end

--=============================================================================
-- Get and format content for appstore Interactions
local function GetHtmlAppstoreContent (self, id, options)
	LogInfo("[REPORTER.DETAIL_QUERY:GetHtmlAppstoreContent] interactionId: '%s', vcc: '%s', options: '%s'.", id, options.vcc, options)
	local utils = options.utils
	local interactionEvents = options.keyspaceInteractions:GetSliceRange(id,
		"Interactions",
		"", {
			column_names = {"events"},
			slice_range = {}
		}
	)

	local events
	if interactionEvents then
		for _, v in ipairs(interactionEvents) do
			events = v.value
		end
	end

	if not events then
		return
	end

	events = json.decode(events)

	local participants = {}
	local availables = {"participantOne", "participantTwo"}
	local function getParticipantClass(p)
		if not participants[p] then
			participants[p] = availables[1] or "participantOther"
			table.remove(availables, 1)
		end
		return participants[p]
	end

	local tableHTML = {}
	WriteHTML(tableHTML, [[
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<link rel="stylesheet" type="text/css" href="../resources/webchat.css"></link>
</head>
<body>
<table border="0" cellspacing="0" cellpadding="0">
]])

	for _, event in ipairs(events) do
		local message = event.message or {}
		local timeZoneDate = slices_helper.ConvertUTCDateToTimezoneDate(
			message.timestamp, options.timezone, m_formatDate, utils, true).stringDate

		local name = ""
		if event.creator == "agent" then
			name = (utils.GetId(event.agent) or "") .. " ("..event.creator.."):"
		elseif event.creator == "bot" then
			name = "bot"
		elseif event.creator == "contact" then
			name = (event.address and event.address.Number or "") .. " ("..event.creator.."):"
		end
		local text = message.text or ""

		WriteHTML(tableHTML, '<tr class="'..getParticipantClass(event.creator)..'">')
			WriteHTML(tableHTML, "<td>"..EscapeHTML(timeZoneDate).."</td>")
			WriteHTML(tableHTML, "<td><span>"..EscapeHTML(name).."</span><span>"..EscapeHTML(text).."</span></td>")
		WriteHTML(tableHTML, "</tr>")
	end

	WriteHTML(tableHTML, [[
</table>
</body>
</html>
	]])

	return table.concat(tableHTML, "\r\n")
end

--=============================================================================
-- Get and format content for googleplay Interactions
local function GetHtmlGoogleplayContent (self, id, options)
	LogInfo("[REPORTER.DETAIL_QUERY:GetHtmlGoogleplayContent] interactionId: '%s', vcc: '%s', options: '%s'.", id, options.vcc, options)
	local utils = options.utils
	local interactionEvents = options.keyspaceInteractions:GetSliceRange(id,
		"Interactions",
		"", {
			column_names = {"events"},
			slice_range = {}
		}
	)
	local events
	if interactionEvents then
		for _, v in ipairs(interactionEvents) do
			events = v.value
		end
	end

	if not events then
		return
	end

	events = json.decode(events)

	local participants = {}
	local availables = {"participantOne", "participantTwo"}
	local function getParticipantClass(p)
		if not participants[p] then
			participants[p] = availables[1] or "participantOther"
			table.remove(availables, 1)
		end
		return participants[p]
	end

	local tableHTML = {}
	WriteHTML(tableHTML, [[
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<link rel="stylesheet" type="text/css" href="../resources/webchat.css"></link>
</head>
<body>
<table border="0" cellspacing="0" cellpadding="0">
]])

	for _, event in ipairs(events) do
		local message = event.message or {}
		local timeZoneDate = slices_helper.ConvertUTCDateToTimezoneDate(
			message.timestamp, options.timezone, m_formatDate, utils, true).stringDate

		local name = ""
		if event.creator == "agent" then
			name = (utils.GetId(event.agent) or "") .. " ("..event.creator.."):"
		elseif event.creator == "bot" then
			name = "bot"
		elseif event.creator == "contact" then
			name = (event.address and event.address.Number or "") .. " ("..event.creator.."):"
		end
		local text = message.text or ""

		WriteHTML(tableHTML, '<tr class="'..getParticipantClass(event.creator)..'">')
			WriteHTML(tableHTML, "<td>"..EscapeHTML(timeZoneDate).."</td>")
			WriteHTML(tableHTML, "<td><span>"..EscapeHTML(name).."</span><span>"..EscapeHTML(text).."</span></td>")
		WriteHTML(tableHTML, "</tr>")
	end

	WriteHTML(tableHTML, [[
</table>
</body>
</html>
	]])

	return table.concat(tableHTML, "\r\n")
end

--=============================================================================
-- Get and format content for twitter Interactions
local function GetHtmlTwitterContent (self, id, options)
	LogInfo("[REPORTER.DETAIL_QUERY:GetHtmlTwitterContent] interactionId: '%s', vcc: '%s', options: '%s'.", id, options.vcc, options)
	local interactionEvents = options.keyspaceInteractions:GetSliceRange(id,
										   "Interactions",
										   "", {
												column_names = {"events"},
												slice_range = {}
												}
											)
	local events
	if interactionEvents then
		for _, v in ipairs(interactionEvents) do
			events = v.value
		end
	end

	if not events then
		return
	end

	events = json.decode(events)
	local participants = {}
	local availables = {"participantOne", "participantTwo"}
	local function getParticipantClass(p)
		if not participants[p] then
			participants[p] = availables[1] or "participantOther"
			table.remove(availables, 1)
		end
		return participants[p]
	end

	local tableHTML = {}

	WriteHTML(tableHTML, [[
<!DOCTYPE html>
<html>
<head>
	<meta charset="utf-8">
	<link rel="stylesheet" type="text/css" href="../resources/webchat.css"></link>
</head>
<body>
<table border="0" cellspacing="0" cellpadding="0">
]])

	--[[
		Funcion encargada de reemplazar las URLs de imágenes de evento por markup para que se puedan visualizar las imágenes.
			Ej:
				Recibe el evento correspondiente a una imagen:
					[
						{
							"type": "photo",
							"expanded_url": "https://twitter.com/messages/media/848885440417587207",
							"display_url": "pic.twitter.com/lynlSo0PHT",
							"media_url": "https://ton.twitter.com/1.1/ton/data/dm/848885440417587207/848885433182420993/1SJyRq9Z.jpg",
							"url": "https://t.co/lynlSo0PHT",
							"media_url_https": "https://ton.twitter.com/1.1/ton/data/dm/848885440417587207/848885433182420993/1SJyRq9Z.jpg"
							..
						}
					]
				y el mensaje contiene lo siguiente:
					Hola https://t.co/lynlSo0PHT

				El valor que va retornar en "message" es el siguiente:
				Hola <a title='image' href='https://twitter.com/messages/media/848885440417587207' target='_blank'><img src='https://t.co/lynlSo0PHT'></a>
]
	]]--
	local function AddMediaHTML (event, message)
		if not event or not event.entities or not event.entities.media then
			return message
		end
		local mediaHTMLToInsertLast = ""
		for _, media in pairs(event.entities.media) do
			local mediaURL = media.media_url_https or media.media_url
			if mediaURL then
				local mediaType = (media.type or ""):lower()
				if mediaType == "photo" then
				--images
					local href = media.expanded_url or media.url
					local mediaHTML = "<BR>" .. ("<a title='%s' href='%s' target='_blank'><img src='%s'></a>"):format("image", href, mediaURL)
					if media.url and message:find(media.url) then
						message = message:gsub(media.url, mediaHTML)
					else
						mediaHTMLToInsertLast = mediaHTMLToInsertLast .. mediaHTML
					end
				elseif mediaType == "animated_gif" then
				--gif animados
					local displayURL = media.display_url or media.media_url or media.media_url_https
					local href = media.expanded_url or media.url
					local mediaHTML = "<BR>" .. ("<a title='%s' href='%s' target='_blank'>%s</a>"):format("animated gif", href, displayURL)
					if media.url and message:find(media.url) then
						message = message:gsub(media.url, mediaHTML)
					else
						mediaHTMLToInsertLast = mediaHTMLToInsertLast .. mediaHTML
					end
				end
			end
		end
		if mediaHTMLToInsertLast:len() > 0 then
			-- Si no se han reemplzado el link con el markup de la imagen, inserto el markup debajo de todo el mensaje.
			message = message .. mediaHTMLToInsertLast
		end
		return message
	end

	local function IsDirectMessage (event)
		return (event.direct_message or not event.source) and true or false
	end

	for _, event in ipairs(events) do
		local timeZoneDate = slices_helper.ConvertUTCDateToTimezoneDate(
			event.timestamp, options.timezone, m_formatDate, utils, true).stringDate
		local name = ""
		if event.creator == "agent" and event.agent then
			name = event.agent
		else
			local user = event.user or event.sender or nil
			if user then
				name = user.screen_name
			end
		end
		name = name.."("..event.creator..")"
		if IsDirectMessage(event) then
			name = "(DirectMessage) "..name
		elseif event.retweeted or event.retweeted_status then
			name = "(ReTweet) "..name
		else
			name = "(Mention) "..name
		end
		name = name..":"
		local message = EscapeHTML(event.text)
		-- Agrego el markup correspondiente a Media (imágenes, gif animados).
		message = AddMediaHTML(event, message)

		WriteHTML(tableHTML, '<tr class="'..getParticipantClass(event.creator)..'">')
			WriteHTML(tableHTML, "<td>"..EscapeHTML(timeZoneDate).."</td>")
			WriteHTML(tableHTML, "<td><span>" .. EscapeHTML(name) .. "</span><div>" .. message .. "</div></td>")
		WriteHTML(tableHTML, "</tr>")
	end

	WriteHTML(tableHTML, [[
</table>
</body>
</html>
	]])

	return table.concat(tableHTML, "\r\n")
end

--=============================================================================
local function uploadToRepository (id, filename, path)
	local f = io.open(path, "rb")
	local content = ""
	if f then
		content = f:read("*a")
		f:close()
	else
		LogWarning("The file '%s' not exists.", path )
		return false
	end

	-- Subo el archivo al Repo directamente utilizando el icc_repository
	local source = ltn12.source.string(content)
	local timestamp = os.time(os.date("!*t"))
	local sink_context = {}
	local newFile = {
		PartialId         = id,
		Section           = "-1",
		Format            = "zip",
		ContentType       = "application/zip",
		FileName          = filename,
		ServerPath        = "",
		CreateIfNotexists = 1,
		FileSize          = #content or 0,
		TimeStamp         = timestamp
	}

	local function when_completed (ok, err)
		if ok then
			LogInfo("File Upload to Repository.")
		else
			LogWarning("File IS NOT Upload to Repo. err: "..err)
		end
	end

	local sink = Repository.make_store_sink(newFile, false, sink_context, when_completed)

	local ret, err = ltn12.pump.all( source, sink )
	if not ret then
		return false, err
	end

	--Elimino el archivo de tmp
	LogInfo("Deleting temporary file: %s", path )
	os.remove(path)
	return true
end

--=============================================================================
local function GetHtmlIndex (resultRows, contentType, Transform)
	local tableHTML = {}

	WriteHTML(tableHTML, [[
<!DOCTYPE html>
<html>
<head>
	<meta charset="utf-8">
	<title>Content Index</title>
	<link rel="stylesheet" type="text/css" href="resources/datatables.css">
	<script src="resources/jquery.js"></script>
	<script src="resources/datatables.js"></script>
</head>
<body>

<div id="content">
	<div class="loadingWaitingContainer" >
		<div class="loadingWaitingMessage">Please wait until we load data.</div>
		<div class="loadingWaitingAnimation"></div>
		<div class="loadingWaitingOverlay"></div>
	</div>
</div>
]])

	--TODO: poner siguiente texto descriptivo de lo que se está exportando
	--<h1>Inbound Chats by Campaign.<span>Summary of all chat interactions in the specified time period.</span></h1>

	WriteHTML(tableHTML, '<script>')

	local t1 = {}

	for _, row in ipairs(resultRows.rows) do
		local t2 = {}
		for i, cell in ipairs(row) do
			local _, val = Transform(resultRows.headers[i], cell)
			if val then
				table.insert(t2, "'"..val.."'")
			end
		end
		table.insert(t1, "["..table.concat(t2, ",").."]")
	end

	WriteHTML(tableHTML, 'var dataSet = ['..table.concat(t1, ",\r\n")..'];')
	WriteHTML(tableHTML, [[
$(document).ready(function() {
	$('#content').html( '<table cellpadding="0" cellspacing="0" border="0" class="display" id="table"></table>' );
	$('#table').dataTable({
		"iDisplayLength": 100,
		"deferRender": true,
		"data": dataSet,
		"columns": [
	]]
)

	for _, head in ipairs(resultRows.headers) do
		local h = Transform(head, nil)
		if h then
			WriteHTML(tableHTML, '{ "title": "'..h..'" },')
		end
	end

	--[[
		Los Headers correspondientes al Content (HTML JSON)
	]]
	contentType = contentType or "HTML"
	if contentType == "HTML" or contentType == "HTML_AND_JSON" then
		WriteHTML(tableHTML, '{ "title": "HTML Content" },') -- La columna HTML Content (header)
	end

	if contentType == "JSON" or contentType == "HTML_AND_JSON" then
		WriteHTML(tableHTML, '{ "title": "JSON Content" },') -- La columna JSON Content (header)
	end
	-----

	WriteHTML(tableHTML, [[
		]
		});
		});
		</script>
		</body>
		</html>
		]]
	)

	return table.concat(tableHTML, "\r\n")
end

--=============================================================================
local function GetHeader (field)
	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 function addIfNotExists (fields, field)
	for _, f in ipairs(fields) do
		if f == field then
			return fields
		end
	end
	table.insert(fields, field)
	return fields
end

--=============================================================================
local function SaveDataOnFile (path, nameFile, data)
	local dirAux=io.open(path, "r")
	if dirAux == nil then
		os.execute ("mkdir -p "..path)
	else
		dirAux:close()
	end
	nameFile = SanitizeFileName(nameFile)
	local fullPath = path..nameFile
	local f = io.open(fullPath, "a+")
	if f then
		f:write(data or "")
		f:close()
	end
end

--=============================================================================
local function GetVCCExportSpaceUtilized (utils, vcc)
	local SUMMARIZER_KS = "SummarizerData"
	local EXPORTED_CONTENT_CF = "ReportSchedulesExportedContent"
	local records = utils.GetAllColumns {
		keyspace = SUMMARIZER_KS,
		columnFamily = EXPORTED_CONTENT_CF,
		key = vcc,
	} or {}

	local totalSize = 0
	for _, record in pairs(records) do
		totalSize = totalSize + record.size
	end

	return totalSize
end

--=============================================================================
local function GetVCCExportSpaceQuota (utils, vcc)
	if m_helpers.utils.HasLicenseSystem() then
		local options = {}
		if inconcert and inconcert.environment and inconcert.environment.apps and inconcert.environment.apps.agent and inconcert.environment.apps.agent.agentservice and inconcert.environment.apps.agent.cluster then
			options.host = inconcert.environment.apps.agent.agentservice.host
			options.port = inconcert.environment.apps.agent.agentservice.port
			options.cluster = inconcert.environment.apps.agent.cluster
		end	
		local vccInfo = m_helpers.agentservice.GetVccLicense(options, vcc)
		return 1024 * 1024 * (vccInfo.ExportSpaceQuota or 0)
	else
		local AGENT_KS = "Agent"
		local VCCS_CF = "VCCs"
		local vcc = utils.GetAllColumns {
				keyspace = AGENT_KS,
				columnFamily = VCCS_CF,
				key = vcc
		} or {}

		--space quota is in MB and we need in Bytes
		return 1024 * 1024 * (tonumber(vcc.ExportSpaceQuota) or 0)
	end	
end

local isOkRepository = nil
--=============================================================================
local function LoadDetails (connection, ids, projectFields, docDefaultType, timezone, vcc, saveContentTo, schedule,
							utils, folderToExport, fileNameToExport, _T, idsToNames, resultES, exportContentType, formatters, rawData,
							MustIncludeRowFn, queryFields, reporterClient)

	MustIncludeRowFn = MustIncludeRowFn or function() return true end

--LogDebug("[REPORTER.DETAIL_QUERY:LoadDetails] 1  - ids: '%s'", json.encode(ids or {}))
--LogDebug("[REPORTER.DETAIL_QUERY:LoadDetails] 2  - projectFields: '%s'", json.encode(projectFields or {}))
--LogDebug("[REPORTER.DETAIL_QUERY:LoadDetails] 3  - idsToNames: '%s'", json.encode(idsToNames or {}))
	local splittedByTypeHeaders, normalizedFields, errMessage = splitByType(projectFields, docDefaultType)
--LogDebug("[REPORTER.DETAIL_QUERY:LoadDetails] 4  - splittedByTypeHeaders: '%s'", json.encode(splittedByTypeHeaders or {}))
--LogDebug("[REPORTER.DETAIL_QUERY:LoadDetails] 5  - normalizedFields: '%s'", json.encode(normalizedFields or {}))
--LogDebug("[REPORTER.DETAIL_QUERY:LoadDetails] 6  - formatters: '%s'", formatters)
--LogDebug("[REPORTER.DETAIL_QUERY:LoadDetails] 7  - rawData: '%s'", rawData)
--LogDebug("[REPORTER.DETAIL_QUERY:LoadDetails] 8  - vcc: '%s'", vcc)
--LogDebug("[REPORTER.DETAIL_QUERY:LoadDetails] 9  - exportContentType: '%s'", exportContentType)
--LogDebug("[REPORTER.DETAIL_QUERY:LoadDetails] 10 - resultES: '%s'", json.encode(resultES or {}))
--LogDebug("[REPORTER.DETAIL_QUERY:LoadDetails] 11 - queryFields: '%s'", json.encode(queryFields or {}))

	if not splittedByTypeHeaders then
		return nil, errMessage
	end

	formatters = formatters or getFormatters(projectFields, timezone, _T, idsToNames, utils)
	local fileName

	local dataPath = nil
	if saveContentTo then
		if not folderToExport then
			fileName = DEFAULT_HTML_FILE_NAME
			folderToExport = SOURCE_ROOT_PATH_TEMPORAL_FILES .. saveContentTo .. "/"
		end
		local resourcePath = folderToExport.."/resources/"
		SaveDataOnFile(resourcePath,"jquery.js",resources.JQUERY_MIN_JS)
		SaveDataOnFile(resourcePath,"datatables.js",resources.DATATABLES_MIN_JS)
		SaveDataOnFile(resourcePath,"datatables.css",resources.DATATABLES_CSS)
		SaveDataOnFile(resourcePath,"webchat.css",resources.WEBCHAT_CSS)
		local imagePath = folderToExport.."/images/"
		SaveDataOnFile(imagePath,"waitingAnimation.gif", base64.decode(resources.WAITING_ANIMATION_GIF_B64))
		dataPath = folderToExport.."data/"
		local dirAux=io.open(dataPath, "r")
		if dirAux == nil then
			os.execute ("mkdir -p "..dataPath)
		end
	end

	--[[
		Función encargada de obtener el contenido en ambos formatos (html y json)
	]]--
	local function GetHtmlAndJson (self, id, info)
		local result = {
			moreThanOne = true,
			data = {
				HTML = {
					contentType = "HTML",
					content = type(self.HTML) == "function" and self:HTML(id, info)
				},
				JSON = {
					contentType = "JSON",
					content = type(self.JSON) == "function" and self:JSON(id, info)
				}
			}
		}
		return result
	end

	local GetContentFnByType = {
		APPSTORE      = {HTML = GetHtmlAppstoreContent   , JSON = m_exportJson.GetAppstoreContent   , HTML_AND_JSON = GetHtmlAndJson},
		CALL          = {HTML = GetHtmlCallContent       , JSON = nil                               , HTML_AND_JSON = GetHtmlAndJson},
		VOICEMAIL     = {HTML = GetHtmlVoicemailContent  , JSON = nil                               , HTML_AND_JSON = GetHtmlAndJson},
		CHAT          = {HTML = GetHtmlWebchatContent    , JSON = m_exportJson.GetWebchatContent    , HTML_AND_JSON = GetHtmlAndJson},
		FACEBOOK      = {HTML = GetHtmlFacebookContent   , JSON = m_exportJson.GetFacebookContent   , HTML_AND_JSON = GetHtmlAndJson},
		GOOGLEPLAY    = {HTML = GetHtmlGoogleplayContent , JSON = m_exportJson.GetGoogleplayContent , HTML_AND_JSON = GetHtmlAndJson},
		INSTAGRAM     = {HTML = GetHtmlInstagramContent  , JSON = m_exportJson.GetInstagramContent  , HTML_AND_JSON = GetHtmlAndJson},
		INTERNAL_CHAT = {HTML = GetHtmlWebchatContent    , JSON = m_exportJson.GetWebchatContent    , HTML_AND_JSON = GetHtmlAndJson},
		MAIL          = {HTML = GetHtmlEmailContent      , JSON = m_exportJson.GetEMailContent      , HTML_AND_JSON = GetHtmlAndJson},
		SMS           = {HTML = GetHtmlSmsContent        , JSON = m_exportJson.GetSmsContent        , HTML_AND_JSON = GetHtmlAndJson},
		RCS           = {HTML = GetHtmlRcsContent        , JSON = m_exportJson.GetRcsContent        , HTML_AND_JSON = GetHtmlAndJson},
		TWITTER       = {HTML = GetHtmlTwitterContent    , JSON = m_exportJson.GetTwitterContent    , HTML_AND_JSON = GetHtmlAndJson},
		WHATSAPP      = {HTML = GetHtmlWhatsappContent   , JSON = m_exportJson.GetWhatsappContent   , HTML_AND_JSON = GetHtmlAndJson},
		YOUTUBE	      = {HTML = GetHtmlYoutubeContent    , JSON = m_exportJson.GetYoutubeContent    , HTML_AND_JSON = GetHtmlAndJson}
	}

	local emptyLabel = _T and _T("N/A") or "N/A"
	local interval_processed_rows = 200
	local next_interval = 0
	local processed_rows = 0
	local resultRows = {
		headers = projectFields,
		rows = {},
		length = #ids
	}

	local keyspaceAgent = cassandra:CassandraConnection("Agent")
	local cachedCampaignsAccounts = {}

	local function GetAccountLanguage (campaign, account, vcc, proxyType)
		local accountLanguage
		if not cachedCampaignsAccounts[account] then
			local accountInfo = GetCampaignAccount(campaign, account, vcc, keyspaceAgent, nil, utils, proxyType)
			--LogDebug("[REPORTER.DETAIL_QUERY:LoadDetails] GetAccountLanguage - campaign '%s', account: '%s', vcc: '%s', accountInfo: '%s'.", campaign, account, vcc, json.encode(accountInfo))
			if accountInfo then
				cachedCampaignsAccounts[account] = accountInfo
				accountLanguage = accountInfo.language
			end
		else
			accountLanguage = cachedCampaignsAccounts[account].language
		end
		--LogDebug("[REPORTER.DETAIL_QUERY:LoadDetails] GetAccountLanguage - campaign '%s', account: '%s', vcc: '%s', accountLanguage: '%s'.", campaign, account, vcc, accountLanguage)
		return accountLanguage
	end

	-- Flag utilizado para saber si ha iniciado sesión el "system user" para realizar request contra la app "allegro" para obtener las partes de las interacciones de CALL
	-- Sirve para que solo inicie sesión una única vez en el batch.
	local allegroLoggedIn = false

	if isOkRepository == nil then
		isOkRepository = InitRepository(vcc)
	end

	local keyspaceInteractions = cassandra:CassandraConnection("Interactions")
	local keyspaceRepository = cassandra:CassandraConnection("Repository")
	local hasAllegroIntegration = nil

	local cassandraDetails = {}
	if not resultES then
		local result = reporterClient:FetchDetailIndicators(json.encode({
			ids = ids,
			splittedByTypeHeaders = splittedByTypeHeaders,
			queryThreads = queryThreads
		}))
		if result.result ~= "OK" then
			return nil, result.reason
		end

		cassandraDetails = result.details
	end

	for i, id in pairs(ids) do
		if hasAllegroIntegration == nil then
			hasAllegroIntegration = utils.HasAllegroIntegration(id.vcc)
		end
		--LogDebug("[REPORTER.DETAIL_QUERY:LoadDetails] %d -, --------> interaction: '%s' hasAllegroIntegration: '%s'.", i, json.encode(id), hasAllegroIntegration)

		if processed_rows == next_interval then
			-- el estado del schedule se actualiza cada "interval_processed_rows" rows procesadas
			scheduler.update_state( utils, schedule, "running" )
			next_interval = next_interval + interval_processed_rows
		end
		processed_rows = processed_rows + 1

		local interactionValues = {}
		local temporalRow = {}
		-- one query per docType
		for docType, fields in pairs(splittedByTypeHeaders) do
			if resultES then  -- Es un reporte Live y ya tengo los datos
				--LogDebug("[REPORTER.DETAIL_QUERY:LoadDetails] A- ElasticSearch - processing 'splittedByTypeHeaders' interaction %s", id.id)
				for _, field in pairs(fields) do
					if resultES[id.id][field] then
						local key = docType .. "." ..field
						temporalRow[key] = resultES[id.id][field][1]
						interactionValues[field] = temporalRow[key]
						--LogDebug("[REPORTER.DETAIL_QUERY:LoadDetails]   A.1- Cassandra - processing 'splittedByTypeHeaders' key '%s', %s='%s'.", key, field, temporalRow[key])
					end
				end
			else
				local key = id.vcc .. "@" .. docType .. ":" .. id.id
				--LogDebug("[REPORTER.DETAIL_QUERY:LoadDetails] B- Cassandra - processing 'splittedByTypeHeaders' key '%s'.", key)
				local range = cassandraDetails[key] or {}
				for _, column in pairs(range) do
					local rowKey = docType .. "." ..column.name
					temporalRow[rowKey] = column.value
					interactionValues[column.name] = temporalRow[rowKey]
					--LogDebug("[REPORTER.DETAIL_QUERY:LoadDetails]   B.1- Cassandra - processing 'splittedByTypeHeaders' key '%s', rowKey: '%s', %s='%s'.", key, rowKey, column.name, temporalRow[rowKey])
				end
			end
		end

		--LogDebug("[REPORTER.DETAIL_QUERY:LoadDetails] interaction %s, temporalRow: '%s'", id.id, json.encode(temporalRow))
		--LogDebug("[REPORTER.DETAIL_QUERY:LoadDetails] interaction %s, values: '%s'", id.id, json.encode(interactionValues))

		--[[
			Invoca al Callback para determinar si debe o no proyectar la Row.
			Sirve para el Caso de filtrado de campana segun la visibildad de las campanas de un supervisor.
		]]--
		local mustIncludeRow = MustIncludeRowFn(interactionValues)
		--LogDebug("[REPORTER.DETAIL_QUERY:LoadDetails] --> interaction %s, mustIncludeRow: '%s'", id.id, mustIncludeRow)

		if mustIncludeRow then
			-- original project field order for rows
			local row = {}
			for i, field in ipairs(normalizedFields) do
				local data = temporalRow[field]
				local formattedData = formatters[i](data, rawData)
				--LogDebug(" LoadDetails ---> data: %s, formattedData: %s", data, formattedData)
				table.insert(row, formattedData)
			end

			--try to obtain the type of the interaction (if this report is for iteractions)

			-- InteractionAttentionLevel InteractionRule InteractionInternalChat InteractionInternalCall Association AgentStateLapse InteractionCount InteractionVoiceBlaster Agent
			local exportInteractionId = id.id
			local scopesInteractionsType = {
				["Interaction.Type"]      = {defualtValue = nil, MakeExportInteractionId = nil},
				["InteractionAgent.Type"] = {defualtValue = nil, MakeExportInteractionId = function(interactionId)
					-- Funcion que sanitiza el Id de la interaction.
					if type(interactionId) ~= "string" then
						--LogDebug("[REPORTER.DETAIL_QUERY:LoadDetails]  a-(MakeExportInteractionId) interactionId is not a string")
						return
					end
					local reversedInteractionId = interactionId:reverse()
					local inverseSeparatorPos = reversedInteractionId:find("-") or 0
					local separatorPos = interactionId:len() - inverseSeparatorPos
					local exportInteractionId = interactionId:sub(1, separatorPos) -- se queda con el GUID de la interaction.
					--LogDebug("[REPORTER.DETAIL_QUERY:LoadDetails]  b-(MakeExportInteractionId) interactionId: '%s' exportInteractionId: '%s'.", interactionId, exportInteractionId)
					return exportInteractionId
				end},
				["InteractionInternalChat.Type"] = {defualtValue = "INTERNAL_CHAT", MakeExportInteractionId = nil},
			}
			local Type, scopeInteractionType
			for attrName, info in pairs(scopesInteractionsType) do
				if temporalRow[attrName] then
					Type = info.defualtValue or temporalRow[attrName]
					scopeInteractionType = attrName
					exportInteractionId = info.MakeExportInteractionId and info.MakeExportInteractionId(id.id) or id.id
					break
				end
			end
			--LogDebug("[REPORTER.DETAIL_QUERY:LoadDetails] -interaction %s, Type: '%s', scopeInteractionType: '%s'", exportInteractionId, Type, scopeInteractionType)
			--LogDebug("[REPORTER.DETAIL_QUERY:LoadDetails]    formatted row: '%s'",  json.encode(row))

			if folderToExport then
				if Type then
					local allowedExportContentType = {HTML = true, JSON = true, HTML_AND_JSON = true}
					if not allowedExportContentType[exportContentType or ""] then
						exportContentType = "HTML"
					end
					local contentDef = GetContentFnByType[Type]
					--LogDebug("[REPORTER.DETAIL_QUERY:LoadDetails] contentDef: '%s' for interaction %s, channel: '%s', exportContentType: '%s'.", contentDef, exportInteractionId, Type, exportContentType)
					local GetContent = contentDef and contentDef[exportContentType]
					--LogDebug("[REPORTER.DETAIL_QUERY:LoadDetails] GetContent: '%s'.", GetContent)

					--[[
						Si es una interacción de tipo CALL tengo que realizar un login con el system user, para que pueda realizar los request
						contra la app "allegro" para obtener las partes de las interacciones.
					]]--
					if Type == "CALL" and not allegroLoggedIn then
						local success = m_request.DoLogin()
						if success then
							-- flageo para que solo inicie sesión una única vez.
							allegroLoggedIn = true
						end
					end

					local content = GetContent and GetContent(contentDef, exportInteractionId, {
							vcc = id.vcc,
							hasAllegroIntegration = hasAllegroIntegration,
							timezone = timezone,
							utils = utils,
							interaction = temporalRow,
							BuildEventsHierarchyFn = BuildEventsHierarchy,
							language = GetAccountLanguage(interactionValues.Campaign, interactionValues.Account, id.vcc, interactionValues.Type),
							keyspaceInteractions = keyspaceInteractions,
							keyspaceRepository = keyspaceRepository,
							queryFields = queryFields
						}
					)

					--LogDebug("[REPORTER.DETAIL_QUERY:LoadDetails] 3.-> interactionId %s, content: '%s'.", exportInteractionId, content)

					collectgarbage()
					collectgarbage()
					collectgarbage()

					if content then
						--[[
							Función encargada de almacenar el contenido e insertarlo en la tabla para luego armar el HTML. 
						]]--
						local function SaveContent (myContent, contentType)
							local idToNameFile = SanitizeFileName(exportInteractionId)

							--LogDebug("[REPORTER.DETAIL_QUERY:LoadDetails] Adding content from interaction %s. File Name:'%s', contentType: '%s'.", exportInteractionId, idToNameFile, contentType)

							if type(myContent) == "string" then
								local conversationFileName = idToNameFile .. (contentType == "JSON" and ".json" or ".html")
								--LogDebug("[REPORTER.DETAIL_QUERY:LoadDetails] a conversationFileName: '%s'", conversationFileName)
								SaveDataOnFile(dataPath, conversationFileName, myContent)
								table.insert(row, "data/" .. conversationFileName)

							elseif type(myContent) == "table" then
								--LogDebug("[REPORTER.DETAIL_QUERY:LoadDetails] b")
								local dataPathInt = dataPath.."/"..idToNameFile.."/"
								local cont = 1
								for name, sub_content in pairs(myContent) do
									--Como nombre de archivo pongo el id de Interaccion mas un consecutivo
									local contentFileName
									if Type == "MAIL" then
										contentFileName = string.format ("%s_%d.eml", idToNameFile, cont)
									elseif Type == "CALL" or Type == "VOICEMAIL" then
										contentFileName = name
									else
										contentFileName = string.format ("%s_%d", idToNameFile, cont)
									end
									SaveDataOnFile(dataPathInt, contentFileName, sub_content) 
									cont = cont + 1
								end
								table.insert(row, "data/"..idToNameFile)
							else
								LogWarning("[REPORTER.DETAIL_QUERY:LoadDetails] Unsupported contentType: '%s'.", contentType)
							end
						end

						if type(content) == "table" and content.moreThanOne then
							--LogDebug("[REPORTER.DETAIL_QUERY:LoadDetails] Saving a MORE Than One content (HTML y JSON).")
							for _, contentInfo in pairs(content.data) do
								SaveContent(contentInfo.content, contentInfo.contentType)
							end
						else
							--LogDebug("[REPORTER.DETAIL_QUERY:LoadDetails] Saving a SINGLE content (HTML o JSON).")
							SaveContent(content, exportContentType)
						end
					else
						--LogDebug("[REPORTER.DETAIL_QUERY:LoadDetails] NOT Content for interaction %s", exportInteractionId)
						table.insert(row, emptyLabel)
					end
					--LogDebug("[REPORTER.DETAIL_QUERY:LoadDetails] continue Type '%s' for interaction %s", Type, exportInteractionId)
					table.insert(row, emptyLabel)
				else
					--LogDebug("[REPORTER.DETAIL_QUERY:LoadDetails] NOT Type '%s' for interaction %s", Type, exportInteractionId)
					table.insert(row, emptyLabel)
				end
			else
				--LogDebug("[REPORTER.DETAIL_QUERY:LoadDetails] NOT folderToExport '%s' for interaction %s", folderToExport, exportInteractionId)
			end
			table.insert(resultRows.rows, row)
		end
	end

	-- Si tenía una sesión del system user abierta, la cierro después del batch de interacciones.
	if allegroLoggedIn then
		m_request.Logout()
	end

	keyspaceAgent:Close()
	keyspaceInteractions:Close()
	keyspaceRepository:Close()

	LogInfo("[REPORTER:LoadDetails] Temporary export folder: '%s'.", folderToExport)

	if folderToExport then
		local headers = {}

		-- LogDebug("[REPORTER.DETAIL_QUERY:LoadDetails]---> resultRows '%s'", json.encode(resultRows))

		local index = GetHtmlIndex(resultRows,
			exportContentType,
			function (header, value)
				--LogDebug("-->header:'%s', value:'%s'", header, value)
				if not header then
					--this is the link to the file
					if value == emptyLabel then
						return nil, emptyLabel
					else
						return nil, '<a href="'..value..'" target="_blank">View conversation</a>'
					end
				end
				if not headers[header] then
					headers[header] = EscapeHTML(GetHeader(header))
				end
				return headers[header], EscapeHTML(value)
			end
		)

		if not fileName then
			fileName = fileNameToExport and fileNameToExport .. ".html" or DEFAULT_HTML_FILE_NAME
		end

		LogInfo("[REPORTER:LoadDetails] Temporary export file name: '%s'.", fileName)

		SaveDataOnFile(folderToExport, fileName, index)
	end

	return resultRows, nil, isOkRepository
end

--=============================================================================
local function LoadDetailsForSendToFTP (connection, ids, projectFields, docDefaultType, timezone, vcc, saveContentTo, schedule,
											utils, folderToExport, fileNameToExport, _T, idsToNames, resultES, exportContentType,
											formatters, rawData, MustIncludeRowFn, queryFields, reporterClient)
	LogInfo("[REPORTER.DETAIL_QUERY:LoadDetailsForSendToFTP] Making detail for send to FTP. Schedule id: '%s', saveContentTo: '%s', fileNameToExport: '%s', folderToExport: '%s', exportContentType: '%s'.", schedule and schedule.Id, saveContentTo, fileNameToExport, folderToExport, exportContentType)

	return LoadDetails(connection, ids, projectFields, docDefaultType, timezone, vcc, saveContentTo,
						schedule, utils, folderToExport, fileNameToExport, _T, idsToNames, resultES,
						exportContentType, formatters, rawData, MustIncludeRowFn, queryFields, reporterClient)
end

--=============================================================================
local function LoadDetailsForSendToSFTP (connection, ids, projectFields, docDefaultType, timezone, vcc, saveContentTo, schedule,
											utils, folderToExport, fileNameToExport, _T, idsToNames, resultES, exportContentType,
											formatters, rawData, MustIncludeRowFn, queryFields, reporterClient)
		LogInfo("[REPORTER.DETAIL_QUERY:LoadDetailsForSendToSFTP] Making detail for send to SFTP. Schedule id: '%s', saveContentTo: '%s', fileNameToExport: '%s', folderToExport: '%s', exportContentType: '%s'.", schedule and schedule.Id, saveContentTo, fileNameToExport, folderToExport, exportContentType)

		return LoadDetails(connection, ids, projectFields, docDefaultType, timezone, vcc, saveContentTo,
						schedule, utils, folderToExport, fileNameToExport, _T, idsToNames, resultES,
						exportContentType, formatters, rawData, MustIncludeRowFn, queryFields, reporterClient)
end

--=============================================================================
local function LoadDetailsForSendToDataBase (connection, ids, projectFields, docDefaultType, timezone, vcc, saveContentTo, schedule,
		utils, folderToExport, fileNameToExport, _T, idsToNames, resultES, exportContentType, formatters, rawData, MustIncludeRowFn,
		queryFields, reporterClient)
	LogInfo("[REPORTER.DETAIL_QUERY:LoadDetailsForSendToDataBase] Making detail for send to DATABASE. Schedule id: '%s', saveContentTo: '%s', fileNameToExport: '%s', folderToExport: '%s', exportContentType: '%s'.", schedule and schedule.Id, saveContentTo, fileNameToExport, folderToExport, exportContentType)

	return LoadDetails(connection, ids, projectFields, docDefaultType, timezone, vcc, saveContentTo,
						schedule, utils, folderToExport, fileNameToExport, _T, idsToNames, resultES,
						exportContentType, formatters, rawData, MustIncludeRowFn, queryFields, reporterClient)
end

--=============================================================================
-- Genero archivo ZIP con lo exportado y lo sube al repositorio.
--
local function BuildExportedZipFile (schedule, saveContentTo, utils)
	if not saveContentTo then
		return
	end

	local zipFileName = SOURCE_ROOT_PATH_TEMPORAL_FILES .. saveContentTo .. ".zip"

	LogInfo("[REPORTER:BuildExportedZipFile] Generating ZIP file '%s'.", zipFileName)

	local vccSchedule = schedule and schedule.Vcc or ""

	os.execute("cd " .. SOURCE_ROOT_PATH_TEMPORAL_FILES .. saveContentTo .. "; zip -m -r -9 " .. zipFileName .. " *")
	os.remove(SOURCE_ROOT_PATH_TEMPORAL_FILES ..saveContentTo)
	if vccSchedule ~= "" then
		local exportSize = lfs.attributes(zipFileName, "size")
		local spaceUtilized = GetVCCExportSpaceUtilized(utils, vccSchedule)
		local vccQuota = GetVCCExportSpaceQuota(utils, vccSchedule)
		if exportSize and spaceUtilized and vccQuota then
			LogInfo("[REPORTER:BuildExportedZipFile] exportSize:%s, spaceUtilized:%s, vccQuota:%s",exportSize,spaceUtilized,vccQuota)
			if spaceUtilized + exportSize > vccQuota then
				os.remove(zipFileName)
				LogWarning("[REPORTER:BuildExportedZipFile] VCC '%s' has exceeded its quota for exported content", vccSchedule)
				return false, "VCC " .. vccSchedule .. " has exceeded its quota for exported content"
			end
		end
	end

	LogInfo("[REPORTER:BuildExportedZipFile] Uploading file %s content to Repository.", zipFileName)

	local ok, errorMessages = uploadToRepository(saveContentTo, "Content.zip", zipFileName)
	collectgarbage()
	collectgarbage()
	collectgarbage()
	collectgarbage()
	collectgarbage()

	if not ok then
		return false, errorMessages
	end

	return true
end

--=============================================================================
--
local function LoadDetailsForSendToMAIL (connection, ids, projectFields, docDefaultType, timezone, vcc, saveContentTo, schedule,
											utils, folderToExport, fileNameToExport, _T, idsToNames, resultES, exportContentType,
											formatters, rawData, MustIncludeRowFn, queryFields, reporterClient)
	LogInfo("[REPORTER.DETAIL_QUERY:LoadDetailsForSendToMAIL] Making detail for send to MAIL. Schedule id: '%s', saveContentTo: '%s', fileNameToExport: '%s', folderToExport: '%s', exportContentType: '%s'.", schedule and schedule.Id, saveContentTo, fileNameToExport, folderToExport, exportContentType)

	local detail, err, isOkRepository = LoadDetails(connection, ids, projectFields, docDefaultType, timezone, vcc, saveContentTo,
		schedule, utils, folderToExport, fileNameToExport, _T, idsToNames, resultES, exportContentType, formatters, rawData,
		MustIncludeRowFn, queryFields, reporterClient)

	if not err and isOkRepository and saveContentTo and not folderToExport then
		local success, zipErr = BuildExportedZipFile(schedule, saveContentTo, utils)
		if not success then
			err = zipErr
		end
	end

	return detail, err
end

--=============================================================================
local function GetDetails (connection, ids, projectFields, docDefaultType, timezone, vcc, saveContentTo, scheduleId, utils,
							folderToExport, filenameToExport, _T, idsToNames, resultES, exportContentType, formatters, rawData,
							MustIncludeRowFn, queryFields, reporterClient)

	MustIncludeRowFn = MustIncludeRowFn or function() return true end

	local schedule, sendType

	--LogDebug("[REPORTER.DETAIL_QUERY:GetDetails] 1 - scheduleId: '%s'. exportContentType: '%s'.", scheduleId, exportContentType)

	if utils and scheduleId then
		local schExist
		schExist, schedule = scheduler.get_schedule(utils, scheduleId)
		if schExist then
			sendType = schedule.SendType
		else
			LogWarning("[REPORTER.DETAIL_QUERY:GetDetails] Error get report details. Schedule '%s' does not exist.", scheduleId)
		end
	end

	local function GetExportContentType ()
		local defaultExportContentType = "HTML"
		if exportContentType == nil then
			--LogDebug("[REPORTER.DETAIL_QUERY:GetDetails] 1 - exportContentType: '%s'. Return nil.", exportContentType)
			return defaultExportContentType
		end
		if type(exportContentType) ~= "string" then
			--LogDebug("[REPORTER.DETAIL_QUERY:GetDetails] 2 - exportContentType: '%s'. Return default :'%s'.", exportContentType, defaultExportContentType)
			return defaultExportContentType
		end
		local supportedExporteContentType = {
			HTML          = true,
			JSON          = true,
			HTML_AND_JSON = true
		}
		local currentExportContentType = exportContentType:upper()
		if supportedExporteContentType[currentExportContentType] then
			--LogDebug("[REPORTER.DETAIL_QUERY:GetDetails] 3 - exportContentType: '%s'. Return supported :'%s'.", exportContentType, currentExportContentType)
			return currentExportContentType
		end
		--LogDebug("[REPORTER.DETAIL_QUERY:GetDetails] 4 - exportContentType: '%s' Unsupported. Return default :'%s'.", exportContentType, defaultExportContentType)
		return defaultExportContentType
	end

	local expContentType = GetExportContentType()
	sendType = sendType or "mail"

	if sendType == "mail" then
		return LoadDetailsForSendToMAIL(connection, ids, projectFields, docDefaultType, timezone, vcc, saveContentTo, schedule,
										utils, folderToExport, filenameToExport, _T, idsToNames, resultES, exportContentType,
										formatters, rawData, MustIncludeRowFn, queryFields, reporterClient)
	elseif sendType == "ftp" then
		return LoadDetailsForSendToFTP(connection, ids, projectFields, docDefaultType, timezone, vcc, saveContentTo, schedule,
										utils, folderToExport, filenameToExport, _T, idsToNames, resultES, exportContentType,
										formatters, rawData, MustIncludeRowFn, queryFields, reporterClient)
	elseif sendType == "sftp" then
		return LoadDetailsForSendToSFTP(connection, ids, projectFields, docDefaultType, timezone, vcc, saveContentTo, schedule,
										utils, folderToExport, filenameToExport, _T, idsToNames, resultES, exportContentType,
										formatters, rawData, MustIncludeRowFn, queryFields, reporterClient)
	elseif sendType == "database" then
		return LoadDetailsForSendToDataBase(connection, ids, projectFields, docDefaultType, timezone, vcc, saveContentTo, schedule,
										utils, folderToExport, filenameToExport, _T, idsToNames, resultES, exportContentType,
										formatters, rawData, MustIncludeRowFn, queryFields, reporterClient)
	end

	assert(sendType == "mail" or sendType == "ftp" or sendType == "sftp" or sendType == "database" , "[REPORTER:GetDetails] Unrecognized send type '" .. tostring(sendType) .. "'")
end

--=============================================================================
local function checkQueryTable (connection, queryTable)
	if type(queryTable.vcc) ~= "string" then
		return nil, "vcc must be a string"
	end

	if not queryTable.projectFields  or #queryTable.projectFields == 0 then
		return nil, "invalid aquery: must project at least one field"
	end

	for _, field in ipairs(queryTable.projectFields) do
		if type(field) == "table" then
			if type(field.name)   ~= "string"
			then
				return nil, "invalid query project field, if a table it must have a name"
			end
		elseif type(field) ~= "string" then
			return nil, "invalid query project field name must be string not " .. type(field)
		end
	end

	if not queryTable.queryFields or type(queryTable.queryFields) ~= "table" then
		return nil, "invalid query: queryFields is missing or not a table"
	end

	if not queryTable.filterName or type(queryTable.filterName) ~= "string" then
		return nil, "invalid query: filterName ".. (queryTable.filterName and " must be string" or " is missing")
	end

	if not queryTable.startDate or not queryTable.endDate then
		return nil, "invalid query: date range is mandatory"
	end

	if type(queryTable.startDate) ~= "string" or type(queryTable.endDate)   ~= "string" then
		return nil, "invalid query: date must be string: YYYY-MM-DD"
	end

	if not queryTable.resultSize or queryTable.resultSize <= 0 then
		return nil, "resultSize must be greater than 0"
	end

	local filter, errMessage = manage.getFilter(connection, queryTable.vcc, queryTable.filterName)
	if not filter then return nil, errMessage end

	local applyingFilters = {}
	if filter.mandatoryFields then
		for _, fieldName in ipairs(filter.mandatoryFields) do
			if not queryTable.queryFields[fieldName] then
				return nil, "missing mandatory field: " .. fieldName
			end
			applyingFilters[fieldName] = queryTable.queryFields[fieldName]
		end
	end
	if filter.optionalFields then
		for _, fieldName in ipairs(filter.optionalFields) do
			applyingFilters[fieldName] = queryTable.queryFields[fieldName]
		end
	end
	queryTable.queryFields = applyingFilters
	return true
end

--=============================================================================
local function getHeaderWithFormat (connection, field, vcc, timezone, _T, idsToNames, utils)
	local fieldName = type(field) == "string" and field or field.label or field.dimensionData and field.dimensionData.label
	local formatter = getFormatters({field}, timezone, _T, idsToNames, utils)[1]
	return fieldName, formatter
end

--=============================================================================
local function query (connection, queryTable, utils, _T, idsToNames, reporterClient)
	--LogDebug("[REPORTER.DETAIL_QUERY:query] Executing Query Reports. queryTable: '%s'", json.encode(queryTable))
	LogInfo("[REPORTER.DETAIL_QUERY:query] Executing Query Reports. queryTable.queryFields: '%s'", json.encode(queryTable.queryFields))
	--[[
		Genero un Map de las campanas que han llegado en el filtro.
		Las mismas serán utlizadas en la función/callback "MustIncludeRowFn"
		para saber si debe o no incluir la row.
	]]--
	local campaignsFilter
	local totalCampignsFilter = 0
	if queryTable.queryFields then
		if type(queryTable.queryFields.Campaign) == "string" then
			if queryTable.queryFields.Campaign:len() == 0 then
				queryTable.queryFields.Campaign = nil
			end
		else
			campaignsFilter = utils.DeepCopy(queryTable.queryFields.Campaign or {})
			totalCampignsFilter = #campaignsFilter
			--LogDebug("1 -- > campaignsFilter: %s", json.encode(campaignsFilter))
			if totalCampignsFilter > 0 then
				campaignsFilter = utils.ArrayToHash(campaignsFilter, nil, function(item, key)
						return utils.GetId(key:lower())
				end)
				--LogDebug("2 -- > campaignsFilter: %s", json.encode(campaignsFilter))
			end
		end
	end
	--------------------------
	local saveContentTo     = queryTable.queryFields and queryTable.queryFields.saveContentTo or nil
	local scheduleId        = queryTable.queryFields and queryTable.queryFields.scheduleId or nil
	local isUTC             = queryTable.queryFields and queryTable.queryFields.isUTC
	local withCache     	= queryTable.queryFields and queryTable.queryFields.withCache
	local folderToExport    = queryTable.folderToExport or nil
	local filenameToExport  = queryTable.filenameToExport or nil
	local exportContentType = queryTable.exportContentType or nil
	local isSentToDatabase  = utils and scheduleId and scheduler.is_send_type(utils, scheduleId, "database")
	local rawData           = isSentToDatabase
	--rawData = true
	LogInfo("[REPORTER.DETAIL_QUERY:query] scheduleId: %s, isSentToDatabase %s, rawData %s", scheduleId, isSentToDatabase, rawData)
	if isSentToDatabase then
		-- Si es un RUN de tipo database, tiene que consultar datos infinitos, por esto agregamos un numero
		queryTable.resultSize = 99999999
	end

	local valid, errMessage = checkQueryTable(connection, queryTable)
	if not valid then
		return nil, nil, errMessage
	end

	local queryFields   = queryTable.queryFields
	local startDate     = queryTable.startDate
	local endDate       = queryTable.endDate
	local projectFields = queryTable.projectFields
	local lastHandle    = queryTable.lastHandle or {}
	local lastRecord	= lastHandle.field
	local cache			= lastHandle.cache
	local resultSize    = queryTable.resultSize
	local vcc           = queryFields and queryFields.VCC or queryTable.vcc

	local filterNameAtVCC, errMessage = resolve(connection, queryTable.filterName, vcc)
	if not filterNameAtVCC then
		return nil, nil, errMessage
	end

	local docType, errMessage  = getFilterScope(connection, queryTable.filterName, vcc)
	if not docType then return nil, nil, errMessage end

	local fromTimezone, endTimezone = slices_helper.GetTimezonesFromAndTo(queryTable.timezone, isUTC, utils)
	local startDateUTC = slices_helper.ConvertTimezoneDateToUTCDate(startDate, fromTimezone, nil, utils).stringDate
	local endDateUTC   = slices_helper.ConvertTimezoneDateToUTCDate(endDate,   endTimezone, nil, utils).stringDate

	--LogDebug("[REPORTER.DETAIL_QUERY:query] isUTC: '%s' startDate '%s' -> endDate '%s' ------ startDateUTC '%s' -> endDateUTC '%s\ntimezone: '%s'", isUTC, startDate, endDate, startDateUTC, endDateUTC, json.encode(queryTable.timezone or {}))

	if withCache and cache then
		local result = reporterClient:ExistsDetailUniqueIdsCache(json.encode({ cache = cache }))
		if result.result ~= "OK" then
			LogDebug("[REPORTER.DETAIL_QUERY:query] cache %s not found in this node", cache)
			return nil, nil, "other_node"
		end
	elseif withCache then
		cache, errMessage =
			queryIdsCache(connection, filterNameAtVCC, queryFields, startDateUTC, endDateUTC, utils, reporterClient)
		if not cache then
			return nil, nil, errMessage
		end
	end

	local result, errMessage =
		queryIds(connection, filterNameAtVCC, queryFields, startDateUTC, endDateUTC, lastRecord,
			resultSize, cache, utils, reporterClient)
	if not result then
		return nil, nil, errMessage
	end
	local ids = {}

	while not result:atEnd() do
		local value = result:getCurrentValue()
		--LogDebug("[REPORTER.DETAIL_QUERY:query]  -------> value '%s'", value)
		local vcc, id = value:match("^([^@]+)@[^:]+:(.+)$")
		if vcc and id then
			table.insert(ids, { vcc = vcc, id = id })
		end
		result:moveToNext()
	end
	--LogDebug("[REPORTER.DETAIL_QUERY:query] -------> total ids %s", #ids)

	-- Ugly hack: getting values from other docTypes is disabled by the moment.
	-- All projectFields are prefixed with the docType.
	local tweakedProjectFields = {}
	for idx, field in ipairs(projectFields) do
		if type(field) == "string" then
			tweakedProjectFields[idx] = docType .. "." .. field
		else
			tweakedProjectFields[idx] = {}
			for k, v in pairs(field) do
				tweakedProjectFields[idx][k] = v
			end
			tweakedProjectFields[idx].name = docType .. "." .. field.name
		end
	end

	--[[
		Funcion enviada como Callback a la funcion "LoadDetails" para que la invoque por cada Row, para determinar si debe o no proyectar la row.
	]]--
	local function MustIncludeRowFn (conversation)
		--LogDebug("[REPORTER.DETAIL_QUERY:query.MustIncludeRowFn] ............conversation.ID %s, conversation.Campaign: %s", conversation.ID, conversation.Campaign)
		if totalCampignsFilter == 0 then
			--LogDebug("[REPORTER.DETAIL_QUERY:query.MustIncludeRowFn] ............1 SI Agrega conversation.ID %s", conversation.ID)
			return true
		end
		if not conversation.Campaign then
			--LogDebug("[REPORTER.DETAIL_QUERY:query.MustIncludeRowFn] ............2 SI Agrega conversation.ID %s", conversation.ID)
			return true
		end
		local campaign = utils.GetId(conversation.Campaign:lower())
		if campaignsFilter[campaign] then
			--LogDebug("[REPORTER.DETAIL_QUERY:query.MustIncludeRowFn] ............3 SI Agrega conversation.ID %s", conversation.ID)
			return true
		end
		--LogDebug("[REPORTER.DETAIL_QUERY:query.MustIncludeRowFn] ............4 NOOOOOOO Agrega conversation.ID %s", conversation.ID)
		return false
	end

	local details, errMessage = GetDetails(connection, ids, tweakedProjectFields, docType, fromTimezone, vcc, saveContentTo, scheduleId,
											utils, folderToExport, filenameToExport, _T, idsToNames, nil, exportContentType, nil, rawData,
											MustIncludeRowFn, queryFields, reporterClient)
	if not details then
		return nil, nil, errMessage
	end
	-- Ugly hack part 2: headers are sent just as the part after the last period.
	details.headers = {}
	for idx, field in ipairs(projectFields) do
		details.headers[idx] = GetHeader(field)
	end

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

	-- And the Ugly hack part 3: if we have content, add an extra column to the headers
	if saveContentTo then
		table.insert(details.headers, "Exported content")
	end
	--LogDebug("[REPORTER.DETAIL_QUERY:query] Response ---> details: %s", json.encode(details))
	return details, { field = result:getLastHandle(), cache = cache }
end

--=============================================================================
local function queryComposite (connection, queryTable, utils, _T, idsToNames, reporterClient)
	LogDebug("[REPORTER.DETAIL_QUERY:queryComposite] Executing Query Compositer. queryTable: '%s' idsToNames %s", json.encode(queryTable), json.encode(idsToNames))
	local queryFields   = queryTable.queryFields
	local projectFields = queryTable.projectFields
	local vcc           = queryTable.queryFields and queryTable.queryFields.VCC or queryTable.vcc
	local scheduleId    = nil
	local isUTC             = queryTable.queryFields and queryTable.queryFields.isUTC
	local fromTimezone, endTimezone = slices_helper.GetTimezonesFromAndTo(queryTable.timezone, isUTC, utils)
	local exportContentType = queryTable.exportContentType or nil
	local saveContentTo     = nil
	local folderToExport    = nil
	local filenameToExport  = nil
	local isSentToDatabase  = utils and scheduleId and scheduler.is_send_type(utils, scheduleId, "database")
	local rawData           = isSentToDatabase
		
	local docType, errMessage  = getFilterScope(connection, queryTable.filterName, vcc)
	if not docType then return nil, nil, errMessage end
		
	local ids = {queryTable.interactionId}
	LogDebug("queryComposite -------> total ids %s", #ids)
	
	-- Ugly hack: getting values from other docTypes is disabled by the moment.
	-- All projectFields are prefixed with the docType.
	local tweakedProjectFields = {}
	for idx, field in ipairs(projectFields) do
		if type(field) == "string" then
			tweakedProjectFields[idx] = docType .. "." .. field
		else
			tweakedProjectFields[idx] = {}
			for k, v in pairs(field) do
				tweakedProjectFields[idx][k] = v
			end
			tweakedProjectFields[idx].name   = docType .. "." .. field.name
		end
	end

	--[[
		Funcion enviada como Callback a la funcion "LoadDetails" para que la invoque por cada Row, para determinar si debe o no proyectar la row.
	]]--
	local function MustIncludeRowFn (conversation)
		LogDebug("[REPORTER.DETAIL_QUERY:queryComposite.MustIncludeRowFn] ............conversation.ID %s, conversation.Campaign: %s", conversation.ID, conversation.Campaign)
		return true
	end

	local details, errMessage = GetDetails(connection, ids, tweakedProjectFields, docType, fromTimezone, vcc, saveContentTo,
										   scheduleId, utils, folderToExport, filenameToExport, _T, idsToNames, nil,
										   exportContentType, nil, rawData, MustIncludeRowFn, nil, reporterClient)
	if not details then
		return nil, nil, errMessage
	end

	details.columnsMetadata = {}
	for idx, field in ipairs(projectFields) do
		details.columnsMetadata[idx] = getColumnMetadata(field)
	end
	local detailsToTable = #details.rows > 0 and rowToTable(details.rows[1], details.columnsMetadata) or {}
	LogDebug("queryComposite ---> details: %s", json.encode(detailsToTable))
	return detailsToTable
end

--=============================================================================
local function queryThreaded (connection, queryTable, utils, _T, idsToNames, reporterClient)
	--LogDebug("[REPORTER.DETAIL_QUERY:queryThreaded] Executing Query Threaded Reports. queryTable: '%s'", json.encode(queryTable))

	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
--rawData = true
	LogDebug("[REPORTER.DETAIL_QUERY:queryThreaded] scheduleId: %s, isSentToDatabase %s, rawData %s", scheduleId, isSentToDatabase, rawData)
	if isSentToDatabase then
		-- Si es un RUN de tipo database, tiene que consultar datos infinitos, por esto agregamos un numero
		queryTable.resultSize = 99999999
	end
	-- validate filter scope is "Interaction"
	-- validate interaction type is MAIL
	-- get interaction detail
	-- build threading information for each detail

	local function indexOf(tbl, elem)
		for idx,v in pairs(tbl or {}) do
			if v == elem then return idx  end
		end
		return 0
	end

	local function forEach(tbl, fn)
		for k, v in pairs(tbl or {}) do
			fn(v, k)
		end
	end

	local filter = manage.getFilter(connection, "system", queryTable.filterName)
	if not filter then
		filter = manage.getFilter(connection, queryTable.vcc, queryTable.filterName)
		if not filter then
			return nil, nil, "Unable to resolve filter: ".. queryTable.filterName
		end
	end

	if filter.scope ~= "Interaction" then
		return nil, nil, "Invalid filter: Scope must be 'Interaction'"
	end

	if indexOf(filter.mandatoryFields, "Type") == 0 then
		return nil, nil, "Invalid filter: Interaction Type must be mandatory"
	end

	if queryTable.queryFields.Type == "WEBCONTACT" then
		local associationReport = manage.getReport(connection, "system", "ReportDetailAssociation")

		associationReport.timezone = queryTable.timezone
		associationReport.startDate = queryTable.startDate
		associationReport.endDate = queryTable.endDate
		associationReport.resultSize = queryTable.resultSize
		associationReport.lastHandle = ""

		table.insert(queryTable.projectFields, "ID")
		local res, handle, err = query(connection, queryTable, utils, _T, idsToNames, reporterClient)
		table.remove(queryTable.projectFields)

		if not res then
			return res, handle, err
		end

		local idx = indexOf(res.headers, "Id")
		if idx ~= 0 then
			table.remove(res.headers, idx)
		end

		res.threadingInfo = {
			headers			= {},
			columnsMetadata = {},
			records			= {}
		}

		local formatters = {}
		forEach(queryTable.threadedFields, function(field)
			local header, formatter = getHeaderWithFormat(connection, field, queryTable.vcc, queryTable.timezone, _T, idsToNames, utils)
			table.insert(res.threadingInfo.headers, header)
			table.insert(formatters, formatter)
			table.insert(res.threadingInfo.columnsMetadata, getColumnMetadata(field))
		end)

		forEach(res.rows, function(row)
			local processedRows = {}
			associationReport.queryFields = {
				ParentID = row[idx]
			}
			table.remove(row, idx) --we don't need this anymore
			local nestedRes, nestedHandle, err = query(connection, associationReport, utils, _T, idsToNames, reporterClient)
			table.insert(res.threadingInfo.records, nestedRes and nestedRes.rows or {})
		end)

		return res, handle
	end

	if queryTable.queryFields.Type ~= "MAIL"
		and queryTable.queryFields.Type ~= "TWITTER"
		and queryTable.queryFields.Type ~= "FACEBOOK"
		and queryTable.queryFields.Type ~= "INSTAGRAM"
		and queryTable.queryFields.Type ~= "YOUTUBE"
		and queryTable.queryFields.Type ~= "APPSTORE"
		and queryTable.queryFields.Type ~= "GOOGLEPLAY"
		and queryTable.queryFields.Type ~= "RCS"
		and queryTable.queryFields.Type ~= "SMS"
		and queryTable.queryFields.Type ~= "WHATSAPP"
	then
		return nil, nil, "Threading reports only valid for interactions having Type 'MAIL', 'SMS', 'RCS', 'WHATSAPP', 'TWITTER', 'FACEBOOK', 'INSTAGRAM', 'YOUTUBE', 'APPSTORE' or 'GOOGLEPLAY'"
	end


	-- make sure threading information comes with the result
	table.insert(queryTable.projectFields, "ThreadingInfo")
	local res, handle, err = query(connection, queryTable, utils, _T, idsToNames, reporterClient)
	table.remove(queryTable.projectFields)

	if not res then
		return res, handle, err
	end

	local idx = indexOf(res.headers, "ThreadingInfo")
	if idx == 0 then
		return nil, nil, "Failure resolving threading information"
	end
	table.remove(res.headers, idx)

	res.threadingInfo = {
		headers         = {},
		columnsMetadata = {},
		records         = {}
	}

	local formatters = {}
	forEach(queryTable.threadedFields, function(field)
		local header, formatter = getHeaderWithFormat(connection, field, queryTable.vcc, queryTable.timezone, _T, idsToNames, utils)
		table.insert(res.threadingInfo.headers, header)
		table.insert(formatters, formatter)
		table.insert(res.threadingInfo.columnsMetadata, getColumnMetadata(field))
	end)

	forEach(res.rows, function(row)
		local processedRows = {}
		local ok, rawInfo = pcall(json.decode, row[idx])

		if ok then
			forEach(rawInfo, function(nestedRow)
				local processedRow = {}
				for fIndex,field in ipairs(queryTable.threadedFields) do 
					local data = nestedRow[field.name]
					local formattedData = formatters[fIndex](data, rawData)
					--LogDebug(" queryThreaded ---> data: %s, formattedData: %s", data, formattedData)
					table.insert(processedRow, formattedData)
				end
				table.insert(processedRows, processedRow)
			end)
		end

		table.insert(res.threadingInfo.records, processedRows)
		table.remove(row, idx)
	end)

	return res, handle
end

--=============================================================================
local function querySteps (connection, queryTable, utils, _T, idsToNames, reporterClient)
	--LogDebug("[REPORTER.DETAIL_QUERY:querySteps] Executing Query Steps Reports. queryTable: '%s'", json.encode(queryTable))

	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
--rawData = true
	LogDebug("[REPORTER.DETAIL_QUERY:querySteps] scheduleId: %s, isSentToDatabase %s, rawData %s", scheduleId, isSentToDatabase, rawData)
	if isSentToDatabase then
		-- Si es un RUN de tipo database, tiene que consultar datos infinitos, por esto agregamos un numero
		queryTable.resultSize = 99999999
	end

	local function indexOf(tbl, elem)
		for idx,v in pairs(tbl or {}) do
			if v == elem then return idx  end
		end
		return 0
	end

	local function forEach(tbl, fn)
		for k, v in pairs(tbl or {}) do
			fn(v, k)
		end
	end

	local filter = manage.getFilter(connection, "system", queryTable.filterName)
	if not filter then
		filter = manage.getFilter(connection, queryTable.vcc, queryTable.filterName)
		if not filter then
			return nil, nil, "Unable to resolve filter: ".. queryTable.filterName
		end
	end

	if filter.scope ~= "Interaction" then
		return nil, nil, "Invalid filter: Scope must be 'Interaction'"
	end

	if indexOf(filter.mandatoryFields, "Type") == 0 then
		return nil, nil, "Invalid filter: Interaction Type must be mandatory"
	end

	-- make sure steps information comes with the result
	table.insert(queryTable.projectFields, "StepInfo")
	local res, handle, err = query(connection, queryTable, utils, _T, idsToNames, reporterClient)
	table.remove(queryTable.projectFields)
	if not res then
		return res, handle, err
	end
	
	local idx = indexOf(res.headers, "StepInfo")
	if idx == 0 then
		return nil, nil, "Failure resolving steps information"
	end
	table.remove(res.headers, idx)

	res.threadingInfo = {
		headers = {},
		columnsMetadata = {},
		records = {}
	}
	local formatters = {}
	forEach(queryTable.threadedFields, function(field)
		local header, formatter = getHeaderWithFormat(connection, field, queryTable.vcc, queryTable.timezone, _T, idsToNames, utils)
		table.insert(res.threadingInfo.headers, header)
		table.insert(res.threadingInfo.columnsMetadata, getColumnMetadata(field))
		table.insert(formatters, formatter)
	end)

	forEach(res.rows, function(row)
		local processedRows = {}
		local ok, rawInfo = pcall(json.decode, row[idx])

		if ok then
			forEach(rawInfo, function(nestedRow)
				local detailRow = nestedRow and nestedRow.detail or {}
				local processedRow = {}
				for fIndex, field in ipairs(queryTable.threadedFields) do
					local data = detailRow[field.name]
					local formattedData = formatters[fIndex](data, rawData)
					--LogDebug(" querySteps ---> data: %s, formattedData: %s", data, formattedData)
					table.insert(processedRow, formattedData)
				end
				table.insert(processedRows, processedRow)
			end)
		end
		table.insert(res.threadingInfo.records, processedRows)
		table.remove(row, idx)
	end)

	return res, handle
end

--=============================================================================
local function queryTransfer (connection, queryTable, utils, _T, idsToNames, reporterClient)
--	LogDebug("[REPORTER.DETAIL_QUERY:queryTransfer] Executing Query Transfer Reports. queryTable: '%s'", json.encode(queryTable))

	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
--rawData = true
	LogDebug("[REPORTER.DETAIL_QUERY:queryTransfer] scheduleId: %s, isSentToDatabase %s, rawData %s", scheduleId, isSentToDatabase, rawData)
	if isSentToDatabase then
		-- Si es un RUN de tipo database, tiene que consultar datos infinitos, por esto agregamos un numero
		queryTable.resultSize = 99999999
	end

	local function indexOf(tbl, elem)
		for idx,v in pairs(tbl or {}) do
			if v == elem then return idx  end
		end
		return 0
	end

	local function forEach(tbl, fn)
		for k, v in pairs(tbl or {}) do
			fn(v, k)
		end
	end

	local filter = manage.getFilter(connection, "system", queryTable.filterName)
	if not filter then
		filter = manage.getFilter(connection, queryTable.vcc, queryTable.filterName)
		if not filter then
			return nil, nil, "Unable to resolve filter: ".. queryTable.filterName
		end
	end

	if filter.scope ~= "Interaction" then
		return nil, nil, "Invalid filter: Scope must be 'Interaction'"
	end

	if indexOf(filter.mandatoryFields, "Type") == 0 then
		return nil, nil, "Invalid filter: Interaction Type must be mandatory"
	end

	local docType, errMessage = getFilterScope(connection, queryTable.filterName, queryTable.vcc)
	if not docType then 
		return nil, nil, errMessage 
	end
	
	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

	-- make sure transfer pointers comes with the result
	table.insert(queryTable.projectFields, "OriginalInteraction")	
	table.insert(queryTable.projectFields, "TransferDestinationInteraction")
	local res, handle, err = query(connection, queryTable, utils, _T, idsToNames, reporterClient)
	table.remove(queryTable.projectFields)
	table.remove(queryTable.projectFields)
	if not res then
		return res, handle, err
	end

	res.threadingInfo = {
		headers = {},
		columnsMetadata = {},
		records = {}
	}
	
	local tweakedFields = {}
	local formatters = {}
	
	table.insert(queryTable.threadedFields, 1, "TransferDestinationInteraction")
	table.insert(queryTable.threadedFields, 1, "OriginalInteraction")
	forEach(queryTable.threadedFields, function(field)
		local tweakedField = {}
		if type(field) == "string" then
			tweakedField.name = docType .. "." .. field
		else
			for k, v in pairs(field) do
				tweakedField[k] = v
			end
			tweakedField.name = docType .. "." .. field.name
		end

		local header, formatter = getHeaderWithFormat(connection, field, queryTable.vcc, queryTable.timezone, _T, idsToNames, utils)

		table.insert(res.threadingInfo.headers, header)
		table.insert(res.threadingInfo.columnsMetadata, getColumnMetadata(field))
		table.insert(tweakedFields, tweakedField)
		table.insert(formatters, formatter)
	end)
	table.remove(res.threadingInfo.headers, 1)
	table.remove(res.threadingInfo.headers, 1)
	table.remove(res.threadingInfo.columnsMetadata, 1)
	table.remove(res.threadingInfo.columnsMetadata, 1)
	table.remove(queryTable.threadedFields, 1)
	table.remove(queryTable.threadedFields, 1)

	local transferDirection = {
		type = "string",
		name = "transferDirection",
		label = _T("Direction"),
		description = _T("Interaction direction (origin, current or destination)")
	}
	table.insert(res.threadingInfo.columnsMetadata, 1, transferDirection)
	table.insert(res.threadingInfo.headers, 1, transferDirection.label)

	local interactionRows_d = {}
	local function getInteractionRow(interactionId)
		if interactionRows_d[interactionId] then
			return interactionRows_d[interactionId]
		end
		local ids = { { vcc = queryTable.queryFields.VCC, id = interactionId } }
		local details, errMessage = GetDetails(connection, ids, tweakedFields, docType, queryTable.timezone,
			queryTable.vcc, saveContentTo, scheduleId, utils, folderToExport, filenameToExport, _T, idsToNames, nil,
			exportContentType, formatters, nil, nil, nil, reporterClient)
		if not details then
			return nil, errMessage
		end
		if #details.rows ~= 1 then
			return nil, "row not found"
		end
		local row = details.rows[1]
		interactionRows_d[interactionId] = row
		return row
	end

	local function addBeforeInteractions(processedRows, interactionId)
		if not interactionId or interactionId == "" or interactionId == "N/A" then
			return
		end
		local row, errMessage = getInteractionRow(interactionId)
		if not row or row[3] == "N/A" then
			LogError("addBeforeInteractions %s - error getting interaction: %s", interactionId, errMessage)
			return
		end
		
		local processedRow = {}
		table.insert(processedRow, _T("Origin"))
		for i = 3, #row do
			table.insert(processedRow, row[i])
		end
		table.insert(processedRows, 1, processedRow)
		
		local beforeInteractionId = row[1]
		addBeforeInteractions(processedRows, beforeInteractionId)
	end

	local function addNextInteractions(processedRows, interactionId)
		if not interactionId or interactionId == "" or interactionId == "N/A" then
			return
		end
		local row, errMessage = getInteractionRow(interactionId)
		if not row or row[3] == "N/A" then
			LogError("addNextInteractions %s - error getting interaction: %s", interactionId, errMessage)
			return
		end
		
		local processedRow = {}
		table.insert(processedRow, _T("Destination"))
		for i = 3, #row do
			table.insert(processedRow, row[i])
		end
		table.insert(processedRows, processedRow)
		
		local nextInteractionId = row[2]
		addNextInteractions(processedRows, nextInteractionId)
	end

	local function addCurrentInteraction(processedRows, row_d)
		row_d.transferDirection = _T("Current")
		local row = tableToRow(row_d, res.threadingInfo.columnsMetadata)
		table.insert(processedRows, row)
	end

	forEach(res.rows, function(row)
		local processedRows = {}

		local row_d = rowToTable(row, res.columnsMetadata)
		
		--clean transfer pointers
		table.remove(row)
		table.remove(row)

		addBeforeInteractions(processedRows, row_d.OriginalInteraction)
		addCurrentInteraction(processedRows, row_d)
		addNextInteractions(processedRows, row_d.TransferDestinationInteraction)
		
		table.insert(res.threadingInfo.records, processedRows)
	end)
	
	--clean transfer pointers
	table.remove(res.headers)
	table.remove(res.headers)
	table.remove(res.columnsMetadata)
	table.remove(res.columnsMetadata)

	--avoid to hide filter parameters like campaign
	for _, metadata in pairs(res.threadingInfo.columnsMetadata) do
		if queryTable.queryFields[metadata.name] then
			metadata.name = "Transfer" .. metadata.name
		end
	end

	return res, handle
end

--=============================================================================
local function GetElasticSearchConfig (utils, info)
	assert(utils)
	local requestInfo = utils.GetElasticSearchInstance():MakeRequestInfo({
		query   = info.query,
		fromAPI = info.fromAPI
	})
	if not requestInfo or not requestInfo.host or not requestInfo.port then
		LogWarning("[REPORTER:GetElasticSearchConfig] Invalid ES Config: %s", json.encode(requestInfo or {}))
		return false
	end
	return requestInfo
end

--=============================================================================
function count_elastic (vcc, query, utils)
	local elasticsearchConfig = GetElasticSearchConfig(utils, {
		query = query
	})
	if not elasticsearchConfig then
		return false
	end

	LogInfo("[REPORTER:count_elastic] Query for elasticsearch: (vcc: %s) %s", vcc, elasticsearchConfig.encodedData)

	local http = require "socket.http"
	local ltn12 = require "ltn12"
	local response_body = {}
	local index_name = ("vcc-%s"):format(vcc)

	local ok, response_code, response_headers, response_status_line = http.request(
	{
		host = elasticsearchConfig.host,
		port = elasticsearchConfig.port,
		url = ("/%s/_count"):format(index_name),
		headers = elasticsearchConfig.headers,
		method = "POST",
		source = elasticsearchConfig.source,
		sink = ltn12.sink.table(response_body),
		redirect = false
	})
	if not ok then
		LogError("[REPORTER:count_elastic] %s\n%s\n%s\n%s", response_code, json.encode(response_headers), response_status_line,
			json.encode(response_body))
		return false, { error = response_status_line, status = response_code }
	end

	--LogWarning("=-=-=-=-= %s", json.encode(response_headers, true))

	local raw_response = table.concat(response_body)
	if not response_headers["content-type"]:match("^application/json") then
		LogError("[REPORTER:count_elastic]  Unknown response from elasticsearch. headers: %s\nresponse: %s", json.encode(response_headers), json.encode(raw_response))
		return false, { error = "inconcert unknown", status = 500 }
	end
	local response = json.decode(raw_response)
	if response.error then
		LogError("[REPORTER:count_elastic] Request failed with the following error: '%s'", json.encode(response.error))
		return false, response
	end

	return true, response
end

--=============================================================================
function search_elastic (vcc, query, pitID, utils)
	local elasticsearchConfig = GetElasticSearchConfig(utils, {
		query = query
	})
	if not elasticsearchConfig then
		return false
	end
	LogInfo("[REPORTER:search_elastic]------> Query for elasticsearch: (vcc: %s) %s", vcc, elasticsearchConfig.encodedData)

	local http = require "socket.http"
	local ltn12 = require "ltn12"
	local response_body = {}
	local index_name = ("vcc-%s"):format(vcc)
	local url = "/_search"
	if not pitID then
		url = ("/%s/_search"):format(index_name)
	end
	local ok, response_code, response_headers, response_status_line = http.request(
	{
		host = elasticsearchConfig.host,
		port = elasticsearchConfig.port,
		url = url,
		headers = elasticsearchConfig.headers,
		method = "POST",
		source = elasticsearchConfig.source,
		sink = ltn12.sink.table(response_body),
		redirect = false
	})
	if not ok then
		LogError("[REPORTER:search_elastic] %s\n%s\n%s\n%s", response_code, json.encode(response_headers), response_status_line,
			json.encode(response_body))
		return false, { error = response_status_line, status = response_code }
	end

	--LogWarning("=-=-=-=-= %s", json.encode(response_headers, true))

	local raw_response = table.concat(response_body)
	if not response_headers["content-type"]:match("^application/json") then
		LogError("[REPORTER:search_elastic]  Unknown response from elasticsearch. headers: %s\nresponse: %s", json.encode(response_headers), json.encode(raw_response))
		return false, { error = "inconcert unknown", status = 500 }
	end
	local response = json.decode(raw_response)
	if response.error then
		LogError("[REPORTER:search_elastic] Request failed with the following error: '%s'", json.encode(response.error))
		return false, response
	end

	return true, response
end

--=============================================================================
function pit_elastic (vcc, utils)
	local elasticsearchConfig = GetElasticSearchConfig(utils)
	if not elasticsearchConfig then
		return false
	end
	elasticsearchConfig["Content-Type"] = nil

	LogInfo("[REPORTER:pit_elastic] Get PIT id from elasticsearch. (vcc: %s)", vcc)

	local http = require "socket.http"
	local ltn12 = require "ltn12"
	local response_body = {}
	local index_name = ("vcc-%s"):format(vcc)

	local ok, response_code, response_headers, response_status_line = http.request(
	{
		host = elasticsearchConfig.host,
		port = elasticsearchConfig.port,
		url = ("/%s/_pit?keep_alive=3m"):format(index_name),
		headers = elasticsearchConfig.headers,
		method = "POST",
		sink = ltn12.sink.table(response_body),
		redirect = false
	})
	if not ok then
		LogError("[REPORTER:pit_elastic] %s\n%s\n%s\n%s", response_code, json.encode(response_headers), response_status_line,
			json.encode(response_body))
		return false, { error = response_status_line, status = response_code }
	end

	--LogWarning("=-=-=-=-= %s", json.encode(response_headers, true))

	local raw_response = table.concat(response_body)
	if not response_headers["content-type"]:match("^application/json") then
		LogError("[REPORTER:pit_elastic]  Unknown response from elasticsearch. headers: %s\nresponse: %s", json.encode(response_headers), json.encode(raw_response))
		return false, { error = "inconcert unknown", status = 500 }
	end
	local response = json.decode(raw_response)
	if response.error then
		LogError("[REPORTER:pit_elastic] Request failed with the following error: '%s'", json.encode(response.error))
		return false, response
	end

	return true, response.id
end

--=============================================================================
local function makeQueryCountToES (queryTable, utils)
	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 isUTC         = queryFields.isUTC
	local fromTimezone, endTimezone = slices_helper.GetTimezonesFromAndTo(queryTable.timezone, isUTC, utils)

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

	local query = {
		query = {
			bool = {
				filter = { }
			}
		}
	}
	
	-- Creo el filtro a ejecutar contra ES
	--
	-- a- Filtro Channels
	local channels = {}
	if filters and filters.Type then
		channels = {terms = {["type"] = {filters.Type}}}
	else
		channels = {terms = {["type"] = {"CALL","CHAT","SMS","WHATSAPP","FACEBOOK","INSTAGRAM","YOUTUBE","APPSTORE","GOOGLEPLAY","RCS","MAIL","TWITTER","WEBCONTACT","VOICEMAIL"}}}
	end
	-- b- Filtro Campaign
	local queryCampaigns
	if queryFields.Campaign then
		local campaigns = {}
		if type(queryFields.Campaign) == "table" then
			--LogDebug("11---> queryFields.Campaign: %s", json.encode(queryFields.Campaign))
		-- Este es el caso en cual llega el Valor del Campo del filtro en una tabla
			local addedCampaigns = {}
			for _, campaign in ipairs(queryFields.Campaign) do
				local campaignIdAtVCC = utils.AppendVcc(campaign, queryFields.VCC):lower()
				if not addedCampaigns[campaignIdAtVCC] then
					addedCampaigns[campaignIdAtVCC] = true
					table.insert(campaigns, campaignIdAtVCC)
				end
			end
		else
			--LogDebug("22---> queryFields.Campaign: %s", queryFields.Campaign)
			if queryFields.Campaign:len() > 0 then
				--LogDebug("22.1---> queryFields.Campaign: %s", queryFields.Campaign)
			-- Este es el caso en cual llega el Valor del Campo del filtro en un string (deprecated pero queda x compatiblidad)
				local campaignIdAtVCC = utils.AppendVcc(queryFields.Campaign, queryFields.VCC):lower()
				table.insert(campaigns, campaignIdAtVCC)
			end
		end

		if #campaigns > 0 then
			queryCampaigns = {terms = {["campaign"] = campaigns}}
			--LogDebug("33---> queryCampaigns: %s", json.encode(queryCampaigns))
		end
	end
	-- c- Filtro Estado de la Interacción
	local states = {}
	local interactionState = (queryFields.InteractionState or "ALL"):upper()
	if interactionState == "ALL" then
		states = {terms = {["state"] = {"TAKEN", "TRANSFER TAKEN", "LOCKED", "QUEUED", "WAITING", "OPEN", "WRAPUP"}}}
	else
		if interactionState == "TAKEN" then
			states = {terms = {["state"] = {"TAKEN", "TRANSFER TAKEN", "LOCKED"}}}
		else
			states = {terms = {["state"] = {interactionState}}}
		end
	end

	-- d- Filtro Período de la Interacción (desde / hasta)
	local dateRange
	if startDate and endDate then
		local startDateUTC = slices_helper.ConvertTimezoneDateToUTCDate(startDate, fromTimezone, m_formatDate, utils).stringDate
		local endDateUTC   = slices_helper.ConvertTimezoneDateToUTCDate(endDate, endTimezone, m_formatDate, utils).stringDate
		--LogDebug("[REPORTER.DETAIL_QUERY:makeQueryCountToES] isUTC: '%s' startDate '%s' -> endDate '%s' ------ startDateUTC '%s' -> endDateUTC '%s'", isUTC, startDate, endDate, startDateUTC, endDateUTC)
		dateRange = {range = {start_timestamp = {gte = startDateUTC, format = "yyyy-MM-dd HH:mm:ss", lte = endDateUTC}}}
	end

	-- El filtro que se ha creado, lo asigno para su ejecución contra ES
	table.insert(query.query.bool.filter, channels)
	table.insert(query.query.bool.filter, states)

	if queryCampaigns then
		table.insert(query.query.bool.filter, queryCampaigns)
	end
	if dateRange then
		table.insert(query.query.bool.filter, dateRange)
	end
	local ok, response = count_elastic(queryFields.VCC, query, utils)
	LogInfo("[REPORTER.DETAIL_QUERY:makeQueryCountToES] response: %s", json.encode(response))
	
	if not ok or not response.count then
		return false, -1
	end

	return true, response.count
end

--=============================================================================
local function makeQueryToES (queryTable, utils)
	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 isUTC         = queryFields.isUTC
	local fromTimezone, endTimezone = slices_helper.GetTimezonesFromAndTo(queryTable.timezone, isUTC, utils)
	local pitID         = queryTable.pitID

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

	local query
	if pitID then
		query = {
			query = {
				bool = {
					filter = {}
				}
			},
			size = resultSize,
			sort = {}
		}
		query.sort["_shard_doc"] = {}
		query.sort["_shard_doc"].order = "asc"
		query.pit = {}
		query.pit.id = pitID
		query.pit.keep_alive = "3m"
		if lastRecord ~= 0 and lastRecord ~="" then	
			query.search_after = lastRecord
		end
		
	else
		query = {
			query = {
				bool = {
					filter = { }
				}
			}
		}
		query.from = lastRecord ~= "" and lastRecord or 0
		query.size = resultSize
		
	end
	-- Lista de campos a proyectar en la búsqueda contra ES
	query.fields = {
			"campaign",
			"account",
			"attention_level",
			"start_timestamp",
			"state_timestamp",
			"contact_name",
			"contact_address",
			"agent",
			"VCC",
			"initiative",
			"number_of_events",
			"first_response_date",
			"transferred_date",
			"transferred_type",
			"transferred_to",
			"exported_content"
		}
	-- Creo el filtro a ejecutar contra ES
	--
	-- a- Filtro Channels
	local channels = {}
	if filters and filters.Type then
		channels = {terms = {["type"] = {filters.Type}}}
	else
		channels = {terms = {["type"] = {"CALL","CHAT","SMS","WHATSAPP","FACEBOOK","INSTAGRAM","YOUTUBE","APPSTORE","GOOGLEPLAY","RCS","MAIL","TWITTER","WEBCONTACT","VOICEMAIL"}}}
	end
	-- b- Filtro Campaign
	local queryCampaigns
	if queryFields.Campaign then
		local campaigns = {}
		if type(queryFields.Campaign) == "table" then
			--LogDebug("11---> queryFields.Campaign: %s", json.encode(queryFields.Campaign))
		-- Este es el caso en cual llega el Valor del Campo del filtro en una tabla
			local addedCampaigns = {}
			for _, campaign in ipairs(queryFields.Campaign) do
				local campaignIdAtVCC = utils.AppendVcc(campaign, queryFields.VCC):lower()
				if not addedCampaigns[campaignIdAtVCC] then
					addedCampaigns[campaignIdAtVCC] = true
					table.insert(campaigns, campaignIdAtVCC)
				end
			end
		else
			--LogDebug("22---> queryFields.Campaign: %s", queryFields.Campaign)
			if queryFields.Campaign:len() > 0 then
				--LogDebug("22.1---> queryFields.Campaign: %s", queryFields.Campaign)
			-- Este es el caso en cual llega el Valor del Campo del filtro en un string (deprecated pero queda x compatiblidad)
				local campaignIdAtVCC = utils.AppendVcc(queryFields.Campaign, queryFields.VCC):lower()
				table.insert(campaigns, campaignIdAtVCC)
			end
		end

		if #campaigns > 0 then
			queryCampaigns = {terms = {["campaign"] = campaigns}}
			--LogDebug("33---> queryCampaigns: %s", json.encode(queryCampaigns))
		end
	end
	-- c- Filtro Estado de la Interacción
	local states = {}
	local interactionState = (queryFields.InteractionState or "ALL"):upper()
	if interactionState == "ALL" then
		states = {terms = {["state"] = {"TAKEN", "TRANSFER TAKEN", "LOCKED", "QUEUED", "WAITING", "OPEN", "WRAPUP"}}}
	else
		if interactionState == "TAKEN" then
			states = {terms = {["state"] = {"TAKEN", "TRANSFER TAKEN", "LOCKED"}}}
		else
			states = {terms = {["state"] = {interactionState}}}
		end
	end

	-- d- Filtro Período de la Interacción (desde / hasta)
	local dateRange
	if startDate and endDate then
		local startDateUTC = slices_helper.ConvertTimezoneDateToUTCDate(startDate, fromTimezone, m_formatDate, utils).stringDate
		local endDateUTC   = slices_helper.ConvertTimezoneDateToUTCDate(endDate, endTimezone, m_formatDate, utils).stringDate
		--LogDebug("[REPORTER.DETAIL_QUERY:makeQueryToES] isUTC: '%s' startDate '%s' -> endDate '%s' ------ startDateUTC '%s' -> endDateUTC '%s'", isUTC, startDate, endDate, startDateUTC, endDateUTC)
		dateRange = {range = {start_timestamp = {gte = startDateUTC, format = "yyyy-MM-dd HH:mm:ss", lte = endDateUTC}}}
	end

	-- El filtro que se ha creado, lo asigno para su ejecución contra ES
	table.insert(query.query.bool.filter, channels)
	table.insert(query.query.bool.filter, states)

	if queryCampaigns then
		table.insert(query.query.bool.filter, queryCampaigns)
	end
	if dateRange then
		table.insert(query.query.bool.filter, dateRange)
	end
	--LogDebug("[REPORTER:makeQueryToES]=> query: %s",json.encode(query))
	local ok, response = search_elastic(queryFields.VCC, query, pitID, utils)
	if not ok then
		return false, {}
	end

	LogInfo("[REPORTER.DETAIL_QUERY:makeQueryToES] Query took %s ms and returned %s hits out of %s", response.took, #response.hits.hits, response.hits.total.value)
	local result = {}
	local lastId
	if response.hits and response.hits.total.value > 0 then
		for _, inter in pairs(response.hits.hits) do
			result[inter._id] = inter.fields
			if pitID and inter.sort then
				lastId = inter.sort
			end
		end
	end
	--LogDebug("[REPORTER:makeQueryToES]---> lastId: %s - result: '%s'.", lastId, json.encode(result))
	return true, result, lastId
end

--[[
	Función encargada de obtener desde REDIS valores para las diferentes Entidades (por el momento sólo Interaction, pero se podría expandir).
	Para determinar si una columna se debe cargar desde REDIS, se determina a partir del atributo "source" de la definición de cada columna.

	El atributo "source", viene con el siguiente formato: "REDIS.interaction.FirstPendingDate".
	En el cual los valores:
		- REDIS:           Indica que el valor va a ser obtenido desde REDIS.
		- interaction:     Nos indica la entidad de la cual se va a obtener dicha información Ej "interaction"
		- FirstPendingDate: Es el nombre de la función la cual va retornar el valor.


	Ej. Para obtener la primer fecha en la cual la interacción ha estado en QUEUED o WAITING, el atributo estaría armado de la siguiente forma:
		source = "REDIS.interaction.FirstPendingDate"

	Para la utilización del source "REDIS", debemos implementar funciones en el archivo .LUA "redis_values.lua" para cada entidad.
	Ej:
		interaction.FirstPendingDate = function(conversation)
			..
			return "ACA RETORNA EL VALOR"
		end
]]--
local function LoadLiveInfoFromRedis (queryTable, resultES, redis)
	assert(queryTable)
	assert(resultES)

	--LogInfo("[REPORTER:LoadLiveInfoFromRedis] Loading live query result from REDIS: %s", redis)

	if not redis then
		return
	end

	--LogDebug("[REPORTER:LoadLiveInfoFromRedis] queryTable: %s", json.encode(queryTable))
	--LogDebug("[REPORTER:LoadLiveInfoFromRedis] resultES: %s", json.encode(resultES))

	-- 1 - Primero recorro la lista de campos que se van a proyectar y valido si tiene el atributo "source"
	--
	local projectFields = queryTable.projectFields or {}
	local redisProjectFields = {} -- En esta tabla temporal se van a almacenar la definición de los campos REDIS que posteriormente se van a obtener para ser proyectados.

	for _, field in ipairs(projectFields) do
		--LogDebug("   field.dimensionData.source %s", field.dimensionData.source)
		if type(field) == "table" and type(field.dimensionData) == "table" and type(field.dimensionData.source) == "string" then
			-- Parseo el source para obtener el type "REDIS"
			local fieldId = field.dimension or field.name
			local separator = field.dimensionData.source:find(".", nil, true)

			if separator and separator > -1 then
				field.__source = field.dimensionData.source:sub(1, separator -1)
				if field.__source:upper() == "REDIS" then
					-- Parseo el source para obtener la "ENTIDAD"
					local entity =  field.dimensionData.source:sub(separator +1)
	--				LogDebug("entity: '%s'", entity)
					separator = entity:find(".", nil, true)
					if separator and separator > -1 then
					-- Obtengo la "ENTIDAD" y el nombre de la "FUNCIÓN" de esta.
						field.__sourceEntity = entity:sub(1, separator -1)
						field.__sourceValueFn = entity:sub(separator +1)
	--					LogDebug("   source '%s', sourceEntity: '%s', sourceValueFn: '%s'", field.__source, field.__sourceEntity, field.__sourceValueFn)
					-- Ahora agrego a la tabla temporal la definición de campos REDIS que debemos consultar y proyectar.
						redisProjectFields[fieldId] = field
					else
						LogDebug("[REPORTER:LoadLiveInfoFromRedis] -Unable to include field '%s' from REDIS. Invalid Source: '%s'.", fieldId, field.dimensionData.source)
					end
				else
					LogDebug("[REPORTER:LoadLiveInfoFromRedis] Source: %s", field.__source)
				end
			else
				--LogDebug("[REPORTER:LoadLiveInfoFromRedis] Unable to include field '%s' from REDIS. Invalid Source: '%s'.", fieldId, field.dimensionData.source)
			end
		end
	end

--	LogDebug("[REPORTER:LoadLiveInfoFromRedis] REDIS Project fields: %s", json.encode(redisProjectFields))

	-- En esta tabla se cachean las interacciones de REDIS así no las vamos a pedir todas las veces en el mismo loop.
	local cachedRedisInteractions = {}

	-- 2 - Ahora recorremos la lista de campos a proyectar desde REDIS y vamos invocando a la función correspondiente para obtener el valor y agregarlo al juego de valores a proyectar
	for fieldId, fieldInfo in pairs(redisProjectFields) do
		--LogDebug(" fieldId: '%s'", fieldId)
		for interactionId, values in pairs(resultES) do
			--LogDebug("   interactionId: '%s'", interactionId)
			-- Me fijo que el campo ya no se encuentre proyectando (NO debería pasar, pero hombre prevenido vale x 2)
			if not values[fieldId] then
				-- Obtengo la Entity Interaction
				local sourceEntity = m_redisValues[fieldInfo.__sourceEntity]
				if sourceEntity then
					-- Obtengo la Interacción de REDIS
					local interaction = cachedRedisInteractions[interactionId] or sourceEntity.Get(interactionId, redis)
					if interaction then
						cachedRedisInteractions[interactionId] = interaction
						-- Obtengo la función de la Entity Interaction que nos retornará el valor a proyectar en el campo
						local valueFn = sourceEntity[fieldInfo.__sourceValueFn]
						if type(valueFn) == "function" then
							-- Invoca a la función de la Entity Interaction para obtener el valor
							local redisValue = valueFn(interaction, redis)
							--LogDebug("        Adding from REDIS. Interaction '%s', name:'%s' value: '%s'.", interactionId, fieldId, redisValue)
							-- Ahora con el valor que hemos obtenido desde REDIS, lo inserto en la lista de campos a proyectar.
							local info = {}
							table.insert(info, redisValue)
							resultES[interactionId][fieldId] = info
						else
							LogWarning("[REPORTER:LoadLiveInfoFromRedis] Unable to include field '%s' from REDIS to Interaction '%s'. Function '%s.%s' does not defined.", fieldId, interactionId, fieldInfo.__sourceEntity, fieldInfo.__sourceValueFn)
						end
					else
						LogWarning("[REPORTER:LoadLiveInfoFromRedis] Unable to include field '%s' from REDIS. Interaction '%s' does not exist.", fieldId, interactionId)
					end
				else
					LogWarning("[REPORTER:LoadLiveInfoFromRedis] Unable to include field '%s' from REDIS to Interaction '%s'. Entity Source: '%s' does not defined.", fieldId, interactionId, fieldInfo.__sourceEntity)
				end
			else
				LogWarning("[REPORTER:LoadLiveInfoFromRedis] Unable to include field '%s' from REDIS. This field value already exist in Elastic Search. Interaction: '%s'.", fieldId, interactionId)
			end
		end
	end

	--LogDebug("[REPORTER:LoadLiveInfoFromRedis] Information to Project: %s", json.encode(resultES))
end

--=============================================================================
local function queryLive (connection, queryTable, utils, _T, idsToNames, redis)
	assert(queryTable)

	--LogDebug("[REPORTER.DETAIL_QUERY:queryLive] Executing Query Live Reports. queryTable: '%s'", json.encode(queryTable))
	LogDebug("[REPORTER.DETAIL_QUERY:queryLive] Executing Query Live Reports. queryTable.queryFields: '%s'", json.encode(queryTable.queryFields))

	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
	local isSentToDatabase  = utils and scheduleId and scheduler.is_send_type(utils, scheduleId, "database")
	local rawData           = isSentToDatabase
--rawData = true
	LogDebug("[REPORTER.DETAIL_QUERY:queryLive] scheduleId: %s, isSentToDatabase %s, rawData %s", scheduleId, isSentToDatabase, rawData)

	local vcc = queryTable.queryFields.VCC or queryTable.vcc

	local docType, errMessage  = getFilterScope(connection, queryTable.filterName, vcc)
	if not docType then return nil, nil, errMessage end

	if not queryTable.pitID then
		if queryTable.exportType ~= "" or isSentToDatabase then
			--Si estoy exportando tengo que llegar al final y para eso tengo que usar PIT...
			local ok, countRecords = makeQueryCountToES(queryTable, utils)
			if ok and countRecords > 9000 then
				--Debo armar el PIT
				local ok, pitID = pit_elastic(vcc, utils)
				if ok then
					queryTable.pitID = pitID
				end
			end
		end
	end
	
	local ok, resultES, lastID
	if isSentToDatabase then
		resultES = {}
		-- Si es a BD obtengo todos los datos aca antes de retornar.
		loops = 1
		while true do
			local resultEStmp
			ok, resultEStmp, lastID = makeQueryToES(queryTable, utils)
			if not ok then
				LogError("[REPORTER.DETAIL_QUERY:queryLive] Error executing query over elastic search.")
				return nil, nil, resultES.error or ""
			end
			if lastID then
				for rowId, row in pairs(resultEStmp) do
					resultES[rowId] = row
				end
				queryTable.lastHandle = lastID
			else
				break
			end
			loops = loops + 1
			if loops > 150 then
				LogError("[REPORTER.DETAIL_QUERY:queryLive] Too many loops. Cancelling.")
				break
			end
		end
	else
		ok, resultES, lastID = makeQueryToES(queryTable, utils)
		if not ok then
			LogError("[REPORTER.DETAIL_QUERY:queryLive] Error executing query over elastic search.")
			return nil, nil, resultES.error or ""
		end
		queryTable.lastHandle = lastID
	end

	-- Obtengo valores de la interacción desde REDIS y los agrego en los resultados.
	LoadLiveInfoFromRedis(queryTable, resultES, redis)

	local ids = {}
	local countRecords = 0
	for interactionId, values in pairs(resultES) do
		local resultVcc = values.VCC[1]
		local id = interactionId
		countRecords = countRecords + 1
		if resultVcc and id then
			table.insert(ids, { vcc = resultVcc, id = id })
		end
	end

	--LogDebug("[REPORTER.DETAIL_QUERY:queryLive] ids: %s", json.encode(ids))

	-- Ugly hack: getting values from other docTypes is disabled by the moment.
	-- All projectFields are prefixed with the docType.
	local tweakedProjectFields = {}
	for idx, field in ipairs(projectFields) do
		if type(field) == "string" then
			tweakedProjectFields[idx] = docType .. "." .. field
		else
			tweakedProjectFields[idx] = {}
			for k, v in pairs(field) do
				tweakedProjectFields[idx][k] = v
			end
			tweakedProjectFields[idx].name   = docType .. "." .. field.name
		end
	end

	--LogDebug("[REPORTER.DETAIL_QUERY:queryLive] tweakedProjectFields: %s", json.encode(tweakedProjectFields))

	local details, errMessage = GetDetails(connection, ids, tweakedProjectFields, docType, timezone, vcc, saveContentTo,
											scheduleId, utils, folderToExport, filenameToExport, _T, idsToNames , resultES,
											exportContentType, nil, rawData)
	if not details then
		return nil, nil, errMessage
	end
	-- Ugly hack part 2: headers are sent just as the part after the last period.
	details.headers = {}
	for idx, field in ipairs(projectFields) do
		details.headers[idx] = GetHeader(field)
	end

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

	-- And the Ugly hack part 3: if we have content, add an extra column to the headers
	if saveContentTo then
		table.insert(details.headers, "Exported content")
	end
	lastRecord = lastRecord ~= "" and lastRecord or 0
	local nextHandler 
	if queryTable.pitID then	
		nextHandler = queryTable.lastHandle 
	else
		nextHandler = lastRecord and lastRecord + countRecords or countRecords
	end 

	LogDebug("[REPORTER.DETAIL_QUERY:queryLive] details: '%s'", json.encode(details))
	LogDebug("[REPORTER.DETAIL_QUERY:queryLive] nextHandler: '%s'", json.encode(nextHandler))

	return details, nextHandler
end

--=============================================================================
-- Devuelve las llamadas y, para cada una, los voicemail o appointments asociados
local function queryCallTrace (connection, queryTable, utils, _T, idsToNames, reporterClient) 
--	LogDebug("[REPORTER.DETAIL_QUERY:queryCallTrace] Executing Query Transfer Reports. queryTable: '%s'", json.encode(queryTable))

	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
--rawData = true
	LogDebug("[REPORTER.DETAIL_QUERY:queryCallTrace] scheduleId: %s, isSentToDatabase %s, rawData %s", scheduleId, isSentToDatabase, rawData)
	if isSentToDatabase then
		-- Si es un RUN de tipo database, tiene que consultar datos infinitos, por esto agregamos un numero
		queryTable.resultSize = 99999999
	end

	local function indexOf(tbl, elem)
		for idx,v in pairs(tbl or {}) do
			if v == elem then return idx  end
		end
		return 0
	end

	local function forEach(tbl, fn)
		for k, v in pairs(tbl or {}) do
			fn(v, k)
		end
	end

	local filter = manage.getFilter(connection, "system", queryTable.filterName)
	if not filter then
		filter = manage.getFilter(connection, queryTable.vcc, queryTable.filterName)
		if not filter then
			return nil, nil, "Unable to resolve filter: ".. queryTable.filterName
		end
	end

	if filter.scope ~= "Interaction" then
		return nil, nil, "Invalid filter: Scope must be 'Interaction'"
	end

	if indexOf(filter.mandatoryFields, "Type") == 0 then
		return nil, nil, "Invalid filter: Interaction Type must be mandatory"
	end
	local docType, errMessage = getFilterScope(connection, queryTable.filterName, queryTable.vcc)
	if not docType then 
		return nil, nil, errMessage 
	end
	
	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

	-- make sure transfer pointers comes with the result
	table.insert(queryTable.projectFields, "OriginalInteraction")	
	table.insert(queryTable.projectFields, "HasVoiceMail")
	table.insert(queryTable.projectFields, "HasAppointment")
	table.insert(queryTable.projectFields, "IsAppointment")
	table.insert(queryTable.projectFields, "AppointmentID")
	table.insert(queryTable.projectFields, "Initiative")
	table.insert(queryTable.projectFields, "VoicemailID")
	table.insert(queryTable.projectFields, "HasVirtualhold")
	table.insert(queryTable.projectFields, "IsVirtualhold")
	table.insert(queryTable.projectFields, "VirtualholdID")
	local res, handle, err = query(connection, queryTable, utils, _T, idsToNames, reporterClient)	
	table.remove(queryTable.projectFields)	--VirtualholdID	
	table.remove(queryTable.projectFields)	--IsVirtualhold	
	table.remove(queryTable.projectFields)	--HasVirtualhold	
	table.remove(queryTable.projectFields)	--VoicemailID	
	table.remove(queryTable.projectFields)	--Initiative
	table.remove(queryTable.projectFields)	--AppointmentID
	table.remove(queryTable.projectFields)	--IsAppointment
	table.remove(queryTable.projectFields)	--HasAppointment
	table.remove(queryTable.projectFields)	--HasVoiceMail
	table.remove(queryTable.projectFields)	--OriginalInteraction
	if not res then
		return res, handle, err
	end

	res.threadingInfo = {
		headers = {},
		columnsMetadata = {},
		records = {}
	}
	
	local tweakedFields = {}
	local formatters = {}
	
	table.insert(queryTable.threadedFields, 1, "AppointmentID")
	table.insert(queryTable.threadedFields, 1, "OriginalInteraction")
	table.insert(queryTable.threadedFields, 1, "VirtualholdID")
	forEach(queryTable.threadedFields, function(field)
		local tweakedField = {}
		if type(field) == "string" then
			tweakedField.name = docType .. "." .. field
		else
			for k, v in pairs(field) do
				tweakedField[k] = v
			end
			tweakedField.name = docType .. "." .. field.name
		end

		local header, formatter = getHeaderWithFormat(connection, field, queryTable.vcc, queryTable.timezone, _T, idsToNames, utils)

		table.insert(res.threadingInfo.headers, header)
		table.insert(res.threadingInfo.columnsMetadata, getColumnMetadata(field))
		table.insert(tweakedFields, tweakedField)
		table.insert(formatters, formatter)
	end)
	table.remove(res.threadingInfo.headers, 1)			--VirtualholdID
	table.remove(res.threadingInfo.headers, 1)			--OriginalInteraction
	table.remove(res.threadingInfo.headers, 1)			--AppointmentID
	table.remove(res.threadingInfo.columnsMetadata, 1)	--VirtualholdID
	table.remove(res.threadingInfo.columnsMetadata, 1)	--OriginalInteraction
	table.remove(res.threadingInfo.columnsMetadata, 1)	--AppointmentID
	table.remove(queryTable.threadedFields, 1)			--VirtualholdID
	table.remove(queryTable.threadedFields, 1)			--OriginalInteraction
	table.remove(queryTable.threadedFields, 1)			--AppointmentID

	local callDirection = {
		type = "string",
		name = "callDirection",
		label = _T("Type"),
		description = _T("Interaction direction (Outbound, Inbound or Voicemail)")
	}
	table.insert(res.threadingInfo.columnsMetadata, 1, callDirection)
	table.insert(res.threadingInfo.headers, 1, callDirection.label)

	local interactionRows_d = {}
	local function getInteractionRow(interactionId)
		if interactionRows_d[interactionId] then
			return interactionRows_d[interactionId]
		end
		local ids = { { vcc = queryTable.queryFields.VCC, id = interactionId } }
		local details, errMessage = GetDetails(connection, ids, tweakedFields, docType, queryTable.timezone,
			queryTable.vcc, saveContentTo, scheduleId, utils, folderToExport, filenameToExport, _T, idsToNames, nil,
			exportContentType, formatters, nil, nil, nil, reporterClient)
		if not details then
			return nil, errMessage
		end
		if #details.rows ~= 1 then
			return nil, "row not found"
		end
		local row = details.rows[1]
		interactionRows_d[interactionId] = row
		return row
	end

	local function addNextInteractions(processedRows, interactionId, rowType)
		rowType = rowType or "Outbound"
		if not interactionId or interactionId == "" or interactionId == "N/A" then
			return
		end
		local row, errMessage = getInteractionRow(interactionId)
		if not row or row[4] == "N/A" then
			LogWarning("addNextInteractions %s - missing interaction: %s", interactionId, errMessage)
			return
		end
		
		local processedRow = {}
		table.insert(processedRow, _T(rowType))
		for i = 4, #row do
			table.insert(processedRow, row[i])
		end
		table.insert(processedRows, processedRow)
		
		local nextInteractionId = row[2]
		addNextInteractions(processedRows, nextInteractionId)
	end

	local function addCurrentInteraction(processedRows, row_d)
		local initiative = row_d.Initiative:sub(1,1):upper()..row_d.Initiative:sub(2):lower()
		row_d.callDirection = _T(initiative)
		local row = tableToRow(row_d, res.threadingInfo.columnsMetadata)
		table.insert(processedRows, row)
	end

	forEach(res.rows, function(row)
		local processedRows = {}
		local row_d = rowToTable(row, res.columnsMetadata)

		--clean transfer pointers
		table.remove(row)	--VirtualholdID
		table.remove(row)	--IsVirtualhold
		table.remove(row)	--HasVirtualhold
		table.remove(row)	--VoicemailID
		table.remove(row)	--Initiative
		table.remove(row)	--AppointmentID
		table.remove(row)	--IsAppointment
		table.remove(row)	--HasAppointment
		table.remove(row)	--HasVoiceMail
		table.remove(row)	--OriginalInteraction
		local voicemailId = nil
		if row_d.HasVoiceMail == "True" then
			voicemailId = row_d.VoicemailID ~= "N/A" and row_d.VoicemailID or "VOICEMAIL_" .. row_d.ID
		end

		addCurrentInteraction(processedRows, row_d)
		addNextInteractions(processedRows, voicemailId, "Voicemail")
		addNextInteractions(processedRows, row_d.AppointmentID, "Appointment")	
		addNextInteractions(processedRows, row_d.VirtualholdID, "Virtual Hold")
		table.insert(res.threadingInfo.records, processedRows)
	end)
	
	--clean transfer pointers
	table.remove(res.headers)	--VirtualholdID
	table.remove(res.headers)	--IsVirtualhold
	table.remove(res.headers)	--HasVirtualhold
	table.remove(res.headers)	--VoicemailID
	table.remove(res.headers)	--Initiative
	table.remove(res.headers)	--AppointmentID
	table.remove(res.headers)	--IsAppointment
	table.remove(res.headers)	--HasAppointment
	table.remove(res.headers)	--HasVoiceMail
	table.remove(res.headers)	--OriginalInteraction
	table.remove(res.columnsMetadata)	--VirtualholdID
	table.remove(res.columnsMetadata)	--IsVirtualhold
	table.remove(res.columnsMetadata)	--HasVirtualhold
	table.remove(res.columnsMetadata)	--VoicemailID
	table.remove(res.columnsMetadata)	--Initiative
	table.remove(res.columnsMetadata)	--AppointmentID
	table.remove(res.columnsMetadata)	--HasAppointment
	table.remove(res.columnsMetadata)	--HasVoiceMail	
	table.remove(res.columnsMetadata)	--OriginalInteraction

	--avoid to hide filter parameters like campaign
	for _, metadata in pairs(res.threadingInfo.columnsMetadata) do
		if queryTable.queryFields[metadata.name] then
			metadata.name = "Transfer" .. metadata.name
		end
	end
	
	return res, handle
end

--=============================================================================
return {
	query         = query,
	queryThreaded = queryThreaded,
	querySteps    = querySteps,
	queryLive     = queryLive,
	queryTransfer = queryTransfer,
	queryCallTrace = queryCallTrace,
	queryComposite = queryComposite
}
