local logger = require "fullcontact.logger"
local utf8 = require "icu.utf8"
local AddressMapper = require "fullcontact.address_mapper"
local json = require "cjson"
require "socket"
require "LuaOrm.Date"
local icuregex = require "icu.regex"
local ustring  = require "icu.ustring"
require "lua_telephonyproxyclient"
local m_helpers = require "helpers"

local EXPIRE_PROCESS_DIALER_CONTACT_IMPORT_IN_SEC = 30 -- La key del current process de importacion de contact dialer expira a los 30 segundos

local function LogInfo (...) logger.info(...) end
local function LogDebug (...) logger.debug(...) end
local function LogWarning (...) logger.warn(...) end
local function LogError (...) logger.error(...) end

local function ltrim (s) return (s or ""):gsub('^%s*','') end
local function rtrim (s) return (s or ""):gsub('%s*$','') end
local function alltrim (s) return rtrim(ltrim(s)) end
local function toUTF8 (s)
	if type(s) ~= "string" then
		return s
	end
	return icu.ustring.encode(icu.ustring(s), "UTF-8")
end

local function TableIsEmpty (t)
	return m_helpers.table:TableIsEmpty(t)
end

local function DeepCopy (t)
	return m_helpers.table:DeepCopy(t)
end

local function JSON_Decode (value)
	return m_helpers.table:JSON_Decode(value)
end

local function JSON_Encode (value)
	return m_helpers.table:JSON_Encode(value)
end

local function CapitalizeUpperCase (value)
	return m_helpers.string:CapitalizeUpperCase(value)
end

--[[
]]--
local function MakeRequestInfoForEs (info)
	return m_helpers.elasticsearch:MakeRequestInfo(info)
end

--[[
	Para las busquedas con LIKE (*algo*) contra ElasticSearch se deben Escapear los : (dos puntos) del valor.
]]--
local function SanitizeQueryValue (value)
	assert(value)
	return value:gsub(":", "\\:")
end

---------------------------------
-- Funciones de validaciones (si es fecha, si es un telefono, si es un correo electronico, etc)
--------------------------------
local m_validation = {}
local m_cassandra = nil
local m_cassandraInfo = nil
local m_redis = nil
local m_timeServer = nil
local m_cluster = nil -- "MiniClusterUbuntu"
local function SetupConnections (connections)
	m_cassandra = connections.cassandra
	m_cassandraInfo = connections.cassandraInfo
	m_cluster = connections.cluster
	m_redis = connections.redis
	m_timeServer = connections.timeServer
end

local function GetDatasource ()
	return {cassandra = m_cassandra, redis = m_redis, cassandraInfo = m_cassandraInfo}
end

local function GetConfigValues ()
	local info = {
		importation = {
			maximumNumberOfRowsInDialerImport  = 50000,   -- Tope máximo de filas que se pueden importar en una importacion de dialer contacts.
			updateStatusCounter                = 500,     -- cantidad de rows (loop) que tienen que pasar para actualizar en REDIS el estado de la importacion
			rowsPerPage                        = 500,     -- cantidad de rows por página que retorna por defecto el obtener los contactos de una importacion
			type                               = "BATCH", -- (SINGLE or BATCH) Determina el tipo de Importacion que se ha de realizar (uno a uno o de a muchos). SINGLE is DEPRECATED.
			maximumNumberFailedContactsToStore = 5000     -- Tope máximo de contactos fallidos que se almacenaran en cassandra para una importacion.
		}
	}
	-- Si los primeros N contactos "breakImportFirstFailedContactos" de la importacion son fallidos, debe abortar la misma.
	--   el valor "maximumFirstFailedContactsForBreakImport", tiene que ser el MISMO de cada batch "updateStatusCounter"
	info.importation.maximumFirstFailedContactsForBreakImport = info.importation.updateStatusCounter
	return info
end

--[[
	Sample config:
		cassandraList = {
			{host = "192.168.4.11", port = 9160}
		}
		timeConfigList = {
			{IP = "192.168.4.11", port = 8463}
		}
]]--
local m_telephonyProxyClient = nil
local function GetTelephonyProxyClient ()
	if not m_telephonyProxyClient then
		local cassandraList = {}
		table.insert(cassandraList, m_cassandraInfo)
		local timeConfigList = {}
		table.insert(timeConfigList, m_timeServer)
		-- LogDebug("[FULLCONTACT.helpers:GetTelephonyProxyClient] 1 - cluster: '%s'.", m_cluster)
		-- LogDebug("[FULLCONTACT.helpers:GetTelephonyProxyClient] 2 - timeConfigList: '%s'.", JSON_Encode(timeConfigList or {}))
		-- LogDebug("[FULLCONTACT.helpers:GetTelephonyProxyClient] 3 - timeConfigList: '%s'.", JSON_Encode(timeConfigList or {}))
		m_telephonyProxyClient = lua_telephonyproxyclient.LuaTelephonyProxyClient(m_cluster, "fullcontact")
		m_telephonyProxyClient:ConfigureCassandra(cassandraList)
		m_telephonyProxyClient:ConfigureTimeClient(timeConfigList)
	end
	return m_telephonyProxyClient
end

-- Receives a string.
-- If the string is of the form 'id@VCC' (with VCC containing no @), returns (id, VCC).
-- Else, returns the string.
local function SeparateVCC (s)
	local a, b = s:match("^(.+)@([^@]+)$")
	--logger.debug("[FULLCONTACT.helpers:SeparateVCC] value: '%s', id: '%s', vcc: '%s'.", s, a, b)
	return (a or s), b
end

-- Receives a string and a VCC. If the string is of the form 'id@VCC', the '@VCC' part is removed.
-- Returns the modified string.
local function StripVCC (s, VCC)
	local strippedID, strippedVCC = SeparateVCC(s)
	local valueToReturn = strippedVCC == VCC and strippedID or s
	--logger.debug("[FULLCONTACT.helpers:StripVCC] strippedID: '%s', strippedVCC: '%s', valueToReturn: '%s'.", strippedID, strippedVCC, valueToReturn)
	return valueToReturn
end

-- Receives a string and a VCC. If the string is NOT of the form 'id@VCC', the '@VCC' part is added.
-- Returns the modified string.
local function AddVCC (s, VCC)
	local valueToReturn = StripVCC(s, VCC) .. "@" ..VCC
	--logger.debug("[FULLCONTACT.helpers:AddVCC] s: '%s', VCC: '%s', valueToReturn: '%s'.", s, VCC, valueToReturn)
	return valueToReturn
end

local function ArrayToHash (array, mapAttrName, transformKeyFn, transformValueFn)
	return m_helpers.table:ArrayToHash(array, mapAttrName, transformKeyFn, transformValueFn)
end

--[[
	recursively compare two tables.
	returns true if they are equal
	if different returns false.
]]
local function StructuralCompare (v1, v2, CanValidateFn, current_attr)
	return m_helpers.table:StructuralCompare(v1, v2, CanValidateFn, current_attr)
end

--[[
]]--
local function MergeTables (source, target)
	return m_helpers.table:Merge(source, target)
end

--[[
]]--
local function MergeArrayWithoutRepeats (sourceArray, targetArray, mapAttrName)
	assert(sourceArray)
	assert(targetArray)
	assert(mapAttrName)
	local sourceMap = ArrayToHash(sourceArray, mapAttrName, function(s)
		return s[mapAttrName]:lower()
	end)
	local result = {}
	for i, source in ipairs(sourceArray) do
		table.insert(result, source)
	end
	for i, target in ipairs(targetArray) do
		local attrValue = target[mapAttrName]:lower()
		if not sourceMap[attrValue] then
			table.insert(result, target)
		end
	end
	--LogDebug("[FULLCONTACT.helpers:MergeArrayWithoutRepeats] ---> mapAttrName: '%s' result: '%s'.", mapAttrName, json.encode(result))
	return result
end

---
-- Builds an address id, to be used mostly when storing an address in Cassandra
--
local function BuildAddressId (addressType, address, vcc, options)
	local formattedAddress, err, isOk
	options = options or {}
	addressType = addressType:upper()
	err = {}
	isOk = true
	--logger.debug(" ENTRA-> BuildAddressId - addressType: '%s'", addressType)
	--for k,v in pairs(address) do
		--logger.debug( ("     BuildAddressId- address.%s = '%s'"):format(tostring(k), tostring(v)) )
	--end

	if addressType == "PHONE" then
		local country = address.Country
		local area = address.Area
		local phoneNumber
		if not address.Validation
			or (
				address.Validation.Success == false
				and address.Validation.Error
				and address.Validation.Error.errType == "INVALID_PHONE_NUMBER"
				and address.Validation.Error.phoneData
				and address.Validation.Error.phoneData.number ~= address.Number
			)
		then
			-- 1. si el telefono aun no ha sido validado se valida el mismo
			----LogDebug("[FULLCONTACT.helpers:BuildAddressId] 1-> YES Validate Phone. address: '%s'", JSON_Encode(address))
			local success, response = m_validation:IsPhone(address.Number, country, area, vcc, nil, nil, options)
			-- response: {"number":"24000007","dialLocal":"24000007","dialGlobal":"59824000007","dialSameCountry":"24000007","countryCode":"598","areaCode":""}
			if success then
				phoneNumber = response.number -- response.dialLocal
				address.Country = response.countryCode or ""
				address.Area = response.areaCode or ""
				isOk = true
			else
				err = response
				--err.address = address
				phoneNumber = address.Number
				--LogWarning("[FULLCONTACT.helpers:BuildAddressId] Unable to build AddressId. Phone validation err: '%s'", JSON_Encode(err))
				isOk = false
			end
		else
			-- 2. si el telefono ya fue validado previamente NO lo valida y retorna el addressid, tambien debo asignar el numero de telefono
			local function SetPhoneNumber (attrName)
				phoneNumber = address[attrName] -- estos valores fueron creados en address_mapper.lua
			end
			--[[
				Estos atributos fueron creados en address_mapper.lua
					tienen que estar en este orden, ya que en un celular 099925896 el valor debe ser sin el 099 (simplenumber = 925896)
					en caso de que no llegue el atributo SimpleNumber, debe de ir por LocalNumber y de ultimo el Number que siempre viene.
			]]--
			local phoneNumberAttrs = {"SimpleNumber", "LocalNumber", "Number"}
			for _, attrName in ipairs(phoneNumberAttrs) do
				SetPhoneNumber(attrName)
				if phoneNumber and phoneNumber ~= "" then break end
			end
			--LogDebug("[FULLCONTACT.helpers:BuildAddressId] 2-> NO Validate Phone. The phone has already been validated. phoneNumber: '%s', address: '%s'", phoneNumber, JSON_Encode(address))
		end
		formattedAddress = ("PHONE_%s_%s_%s_%s"):format(
			(address.Country   or ""),
			(address.Area      or ""),
			(phoneNumber       or ""),
			(address.Extension or "")
		)
	elseif addressType == "MAIL" then
		formattedAddress = ("EMAIL_%s"):format( utf8.lower(address.address) )
	elseif addressType == "FACEBOOK" then
		formattedAddress = ("FACEBOOK_%s"):format(address.UID or address.User or "")
	elseif addressType == "INSTAGRAM" then
		formattedAddress = ("INSTAGRAM_%s"):format(address.UID or address.User or "")
	elseif addressType == "YOUTUBE" then
		formattedAddress = ("YOUTUBE_%s"):format(address.UID or address.User or "")
	elseif addressType == "APPSTORE" then
		formattedAddress = ("APPSTORE_%s"):format(address.UID or address.User or "")
	elseif addressType == "GOOGLEPLAY" then
		formattedAddress = ("GOOGLEPLAY_%s"):format(address.UID or address.User or "")
	elseif addressType == "IM" then
		formattedAddress = ("IM_%s_%s"):format(utf8.lower(address.protocol), utf8.lower(address.buddy))
	elseif addressType == "TWITTER" then
		formattedAddress = ("TWITTER_%s"):format(utf8.lower(address.User))
	else
		error("Unknown address type " .. tostring(addressType))
	end

	--LogDebug(" SALE-> BuildAddressId - isOk: %s, addressType: '%s', formattedAddress: '%s', err: '%s'", isOk, addressType, formattedAddress, JSON_Encode(err))
	if isOk then
		return true, formattedAddress
	end
	return false, formattedAddress, err
end

---
-- Allows to build addresses using a saner "type", "value" approach.
--
local function BuildAddressInstance (address_type, address_value, vcc, options)
	assert(type(address_type) == "string", "address_type must be a string")
	address_type = address_type:lower()
	options = options or {}
	--LogDebug("[FULLCONTACT.helpers:BuildAddressInstance] 1 - ENTRA address_type: '%s', address_value: '%s'.", address_type, JSON_Encode(address_value))

	local function PhoneTypeFn (address)
		local addressTable = address
		if type(addressTable) ~= "table" then
			addressTable = {
				Type    = "Phone",
				Number  = address,
				Country = options.originCountryCode,
				Area    = options.originAreaCode
			}
		end
		return AddressMapper.phone(addressTable, vcc, options)
	end

	local types = {
		cellular = PhoneTypeFn,
		home     = PhoneTypeFn,
		office   = PhoneTypeFn,
		phone    = PhoneTypeFn,

		mail = function(address)
			if type(address) == "table" then
				return (AddressMapper.mail(address, vcc))
			end
			return { Type = "Mail", address = address, displayable = address }
		end,

		facebook = function(address)
			if type(address) == "table" then
				return AddressMapper.facebook(address, vcc)
			end
			return { Type = "Facebook", UID = address, displayable = address }
		end,

		instagram = function(address)
			if type(address) == "table" then
				return AddressMapper.instagram(address, vcc)
			end
			return { Type = "Instagram", UID = address, displayable = address }
		end,

		youtube = function(address)
			if type(address) == "table" then
				return AddressMapper.youtube(address, vcc)
			end
			return { Type = "Youtube", UID = address, displayable = address }
		end,

		appstore = function(address)
			if type(address) == "table" then
				return AddressMapper.appstore(address, vcc)
			end
			return { Type = "Appstore", UID = address, displayable = address }
		end,

		googleplay = function(address)
			if type(address) == "table" then
				return AddressMapper.googleplay(address, vcc)
			end
			return { Type = "Googleplay", UID = address, displayable = address }
		end,

		twitter = function(address)
			if type(address) == "table" then
				return AddressMapper.twitter(address, vcc)
			end
			return { Type = "Twitter", User = address, displayable = address }
		end
	}
	-- aliases
	types.email = types.mail

	local mapper = types[address_type]
	if mapper then
		local addressInstance = mapper(address_value)
		--LogDebug("[FULLCONTACT.helpers:BuildAddressInstance] 2 - SALE address_type: '%s', addressInstance: '%s'.", address_type, JSON_Encode(addressInstance))
		return addressInstance
	end
	error("Unknown address type " .. address_type)
end

-- local function GetAllColumns (options)
-- 	return m_helpers.cassandra.GetAllColumns(options)
-- end

--[[
	Generic function to retrieve all columns in a keyspace and columnFamily

	ATENCION:
	---------
	ESTA FUNCION ES UNA COPIA EXACTA DE LA QUE EXISTE EN EL REPO "AGENT" EN "shared.apps.utils.LUA"
	Hay que manterla igual en ambos lados, es decir si la modificamos acá hay que actualizarla en "shared.apps.utils.LUA" y viceversa
--]]
local function GetAllColumns (options)
	assert(options, "You must supply options")

	local function XOR (A, B)
		return (A and not B) or (B and not A)
	end

	assert(XOR(options.key, options.startKey or options.endKey), "You must supply key or <startKey, endKey> but not both")

	--LogDebug(" 1 ENTRA GetAllColumns " .. tostring(options.columnFamily))

	local has_close_keyspace = true
	local keyspace
	if type( options.keyspace ) == "string" then
		keyspace = m_cassandra:CassandraConnection( options.keyspace )
	else
		keyspace = options.keyspace
		has_close_keyspace = false
	end

	local func, specific_columns
	if options.superColumns then
		func = keyspace.GetSuperRangeSlices
		specific_columns = {}
	else
		func = keyspace.GetRangeSlices
		specific_columns = options.columns or {}
	end

	local get_rows_chunk = function( start_key, end_key, load_chunk_rows )
		local ok, rows = pcall(func,
			keyspace,
			options.columnFamily, --column family
			"", --supercolumn
			start_key, --startkey
			end_key, --endkey
			{ --predicate
				column_names = specific_columns,
				slice_range = {
					start = options.startColumn or "",
					finish = options.endColumn or "",
					reversed = options.reversed or false,
					count = options.columnCount or 10000
				}
			},
			load_chunk_rows --total keys
		)
		if ok then
			return rows
		end
	end

	local get_value = function( column, super_column, key )
		if type(options.transform_value) == "function" then
			return options.transform_value( column, super_column, key )
		else
			return options.valueAsNumber and column.value_as_number or JSON_Decode(column.value)
		end
	end

	local all_result = {}
	local chunk_result = {}
	local previsous_chunk_result = {}
	local start_chunk_key = options.key or options.startKey or ""
	local end_chunk_key = options.key or options.endKey or ""
	local previous_chunk_key
	local top_rows = options.rowCount
	local total_rows = 0
	local continue = true
	local chunks_loop = 0

	-- Funcion encargada de determinar si la funcion tiene que retornar todos los registros de los chunks, o si tiene que invocar al callback
	-- con los rows que se han obtenido en el chunk
	local function HasRetriveChunks ()
		return options.callbacks and options.callbacks.chunk_iter
	end

	local rows_per_chunk = options.rows_per_chunk or 30000
	local sleep_between_chunk_sec = options.sleep_between_chunk_sec or nil
	--LogDebug("rows_per_chunk: " .. tostring(rows_per_chunk) .. ", sleep_between_chunk_sec: " .. tostring(sleep_between_chunk_sec))

	while continue do
		chunks_loop = chunks_loop + 1
		if type(sleep_between_chunk_sec) == "number" and chunks_loop > 1 then
			--LogInfo("[GetAllColumns] Wait moment please, there is a " .. tostring(sleep_between_chunk_sec) .. " seconds pause between chunk. Chunk " .. tostring(chunks_loop) .. " of " .. tostring(rows_per_chunk) .. " rows.")
			socket.sleep(sleep_between_chunk_sec)
		end

		local load_chunk_rows = rows_per_chunk
		-- El primer chunk siempre retorna la totalidad especificada en el parametro recibido "rows_per_chunk",
		--  pero a partir del segundo chunk tengo que incrementar en 1 el valor recibido como parametro (solo una vez),
		--  para que los siguientes, chunks retornen la misma cantidad.
		-- Esto es asi ya que el punto de partida para obtener informacion es el ultimo row del chunk anterior, y si no hacemos esto,
		--   en el siguiente chunk va a venir repetido el ultimo row del chunk anterior.
		if chunks_loop > 1 then
			load_chunk_rows = rows_per_chunk + 1
		end
		previous_chunk_key = start_chunk_key

		local rows = get_rows_chunk( start_chunk_key, end_chunk_key, load_chunk_rows )

		--LogDebug("Chunk Number: " .. tostring(chunks_loop) .. ", Column Family" .. tostring(options.columnFamily))
		if not rows then
			break
		end

		local total_rows_in_chunk = 0

		for key, columns in rows:Iterator() do
			-- Si el row fue agregado en el chunk anterior, no lo tiene que agregar.
			if not previsous_chunk_result[key] then
				for _, column in pairs(columns) do
					all_result[key] = all_result[key] or {}
					if not options.superColumns then
						all_result[key][column.name] = get_value(column, nil, key)
					else
						local super_column = column
						all_result[key][super_column.name] = {}
						for _, col_of_super_column in pairs(super_column.columns) do
							all_result[key][super_column.name][col_of_super_column.name] = get_value(col_of_super_column, super_column, key)
						end
					end
				end
				chunk_result[key] = DeepCopy(all_result[key])
			end

			total_rows = total_rows + 1
			total_rows_in_chunk = total_rows_in_chunk + 1
			--LogDebug(tostring(total_rows) .. "' - CHUNK_KEY: '" .. tostring(start_chunk_key) .. "' - chunk row: " .. tostring(total_rows_in_chunk))

			start_chunk_key = key -- actualizo el chunk con el que se ha procesado, para cuando se vuela a pedir los siguientes chunks.

			local is_top_rows = total_rows == top_rows
			if is_top_rows then
				--LogDebug("BREAK - is_top_rows: " .. tostring(is_top_rows))
				if HasRetriveChunks() then
				-- Invoco al callback con los rows del chunk y luego limpio la tabla, para continuar con los chunks siguientes.
					--LogDebug("1-->RETORNO al callback del chunk: " .. tostring(total_rows_in_chunk))
					options.callbacks.chunk_iter(chunk_result, chunks_loop)
				end
				previsous_chunk_result = nil
				previsous_chunk_result = DeepCopy(chunk_result)
				chunk_result = {}
				continue = false
				break
			end
		end

		if continue then
			if HasRetriveChunks() then
			-- Invoco al callback con los rows del chunk y luego limpio la tabla, para continuar con los chunks siguientes.
				--LogDebug("2-->RETORNO al callback del chunk: " .. tostring(total_rows_in_chunk))
				options.callbacks.chunk_iter(chunk_result, chunks_loop)
			end
			previsous_chunk_result = nil
			previsous_chunk_result = DeepCopy(chunk_result)
			chunk_result = {}

			if total_rows_in_chunk < rows_per_chunk then
				--LogDebug("BREAK - total_rows_in_chunk: " .. tostring(total_rows_in_chunk) .. " rows_per_chunk: " .. tostring(rows_per_chunk))
				continue = false
			else
				local is_equal_chunks = previous_chunk_key == start_chunk_key

				if is_equal_chunks then
					--LogDebug("BREAK - is_equal_chunks: " .. tostring(is_equal_chunks) .. " start_chunk_key: " .. start_chunk_key)
					continue = false
				end
			end
		end
	end

	if has_close_keyspace then
		keyspace:Close()
	end

	if HasRetriveChunks() then
	-- Como la funcion que retorna los rows de cada chunk se encuentra en el callback, me voy y no retorno nada mas.
		return
	end

	-- Como no se fueron retornando los row por cada callback, retorno todo de una.

	if options.key then
		all_result = all_result[options.key]
	end

	--LogDebug("all_result: " .. JSON_Encode(all_result))
	--LogDebug(" 2 SALE GetAllColumns " .. tostring(options.columnFamily))

	return all_result
end

--[[
--]]
function SaveRows (options)
	m_helpers.cassandra.SaveRows(options)
end

function ReadFile (fullFileName, returnType)
	--LogDebug("[FULLCONTACT:ReadFile] Read file '%s' return type: '%s'.", fullFileName, returnType)
--	fullFileName = webHandler and webHandler.path.normalize(fullFileName) or fullFileName

	local f = io.open(fullFileName)
	if not f then
		LogWarning("[FULLCONTACT:ReadFile] Unable to open file: %s", fullFileName)
		return false, {errType = "UNABLE TO OPEN FILE", fullFileName = fullFileName}
	end

	local f = io.open(fullFileName, "rb")
	local content

	if (returnType or ""):lower() == "table" then
		content = {}
		local k = 1
		for line in f:lines() do
			content[k] = line
			k = k + 1
		end
		--LogDebug(json.encode(content))
	else
		content = f:read("*a")
		--LogDebug("[FULLCONTACT:ReadFile] content: %s", content)
	end

	f:close()
	return true, content
end

---------------------------------
-- Funciones de manejo de CSV
--------------------------------
local m_csvMng = {}

m_csvMng.ParseCSVHeader = function (self, csv)
	return m_helpers.csv.ParseCSVHeader(csv)
end

m_csvMng.ParseCSVLine = function (self, csv)
	return m_helpers.csv.ParseCSVLine(csv)
end

m_csvMng.Csv2Table = function (self, csv, ProcessColumn, OnCompletedLine)
	return m_helpers.csv.Csv2Table(csv, ProcessColumn, OnCompletedLine)
end

m_csvMng.Table2Csv = function (self, tableRows, allowedColumns, excludeColumns, separator)
	return m_helpers.csv.Table2Csv(tableRows, allowedColumns, excludeColumns, separator)
end

-----
----
--  PROCESS FUNCTIONS
--
local function MakeVCCStatusProcessBatchId (vcc)
	assert(type(vcc) == "string")
	local key = string.format(
		"ProcessBatch:VCC:%s:DialerContactImport:Status",
		vcc
	)
	--LogDebug("[FULLCONTACT:MakeVCCStatusProcessBatchId]------------> key: '%s'", key)
	return key
end

local function GetVCCStatusProcessBatch (vcc)
	local datasource = GetDatasource()
	assert(datasource.redis)
	local key = MakeVCCStatusProcessBatchId(vcc)
	return datasource.redis:get(key)
end

local function UpdateVCCStatusProcessBatch (processBatch)
	assert(type(processBatch) == "table")
	--LogDebug("....... processBatch: %s", json.encode(processBatch))
	assert(type(processBatch.VCC) == "string")
	local datasource = GetDatasource()
	assert(datasource.redis)
	local key = MakeVCCStatusProcessBatchId(processBatch.VCC)
	if processBatch.Status == "RUNNING" or processBatch.Status == "DELETING" then
		datasource.redis:set(key, JSON_Encode(processBatch))
		datasource.redis:expire(key, EXPIRE_PROCESS_DIALER_CONTACT_IMPORT_IN_SEC)
	else
		datasource.redis:del(key)
		if GetVCCStatusProcessBatch(processBatch.VCC) then
			LogWarning("[FULLCONTACT:UpdateVCCStatusProcessBatch] Unable to delete process batch status '%s'", key)
			return false
		end
	end
	return key
end

local function IsAnyProcessBatchInProgressInVCC (vcc)
	assert(type(vcc) == "string")
	local currentEncodedProcessBatch = GetVCCStatusProcessBatch(vcc)
	if not currentEncodedProcessBatch then
		return false
	end
	--LogDebug("[FULLCONTACT:IsAnyProcessBatchInProgressInVCC]------------> currentProcessBatch: '%s'", currentEncodedProcessBatch)
	return true, JSON_Decode(currentEncodedProcessBatch)
end

local function DeleteVCCProcessBatch (batchProcessId, vcc)
	assert(type(batchProcessId) == "string")
	assert(type(vcc) == "string")
	-- La busco en REDIS
	local encodedProcessBatchStatus = GetVCCStatusProcessBatch(vcc)
	--LogDebug("[FULLCONTACT:DeleteVCCProcessBatch] ---> BD batchProcessId: '%s' vcc: '%s', running batch status: '%s'", batchProcessId, vcc, encodedProcessBatchStatus)
	local ok, currentProcessBatch = pcall(json.decode, encodedProcessBatchStatus)
	if not ok then
		return false
	end
	if not batchProcessId or (currentProcessBatch.Id or ""):lower() == batchProcessId:lower() then
		--LogInfo("[FULLCONTACT:DeleteVCCProcessBatch] Deleting Process batch Id '%s' vcc: '%s'.", batchProcessId, vcc)
		currentProcessBatch.Status = "ENDED"
		UpdateVCCStatusProcessBatch(currentProcessBatch)
		return true
	end
	--LogInfo("[FULLCONTACT:DeleteVCCProcessBatch] Unable to delete. Process %s vcc: '%s' does not exist.", batchProcessId, vcc)
	return false
end

---------------------------------
-- Funciones de memoria (collectgarbage)
--------------------------------
local m_memory = {}

m_memory.ShowMemory = function(self, msg)
	local kb = math.floor(collectgarbage("count"))
	LogDebug("[FULLCONTACT.helpers:ShowMemory] Memory used: '%s' Kb %s.", kb, msg)
end

m_memory.CollectAllMemory = function(self)
	LogDebug("[FULLCONTACT.helpers:CollectAllMemory] Starting collect all memory.")
	local n = 0
	repeat
		local count1 = math.floor(collectgarbage("count"))
		collectgarbage()
		local count2 = math.floor(collectgarbage("count"))
		n = n + 1
	until count1 == count2
	self:ShowMemory("CollectAllMemory took " .. tostring(n) .. " collect passes")
end

---------------------------------
-- Funciones de validaciones (si es fecha, si es un telefono, si es un correo electronico, etc)
--------------------------------
--[[
	Valida que sea una fecha correcta
]]--
m_validation.IsDate = function(self, stringDate)
	assert(stringDate)
	local dateLength = #stringDate
	-- 1 - la fecha tiene que tener un minimo de 10 caracteres, es decir (2022-04-14)
	if dateLength < 10 then
		--LogDebug("[FULLCONTACT.helpers:IsDate]  1 - ERROR Invalid stringDate: '%s'.", stringDate)
		return false
	end
	-- 2 - si la fecha tiene de 16 caracteres, es decir (2022-04-14 22:19) le agrega los segundos
	if dateLength == 16 then
		-- si tiene 16 le agrega los segundos.
		stringDate = stringDate .. ":00"
	end
	-- 3 - ahora valida que la fecha se correcta, validando contra el año de la fecha recibido por parámetro contra el resulado de LuaOrm.Date
	local ormDate = LuaOrm.Date(stringDate)
	local year = tonumber(ormDate:year())
	local stringYear = string.sub(stringDate, 1, 4)
	if tonumber(stringYear) ~= year then
		--LogDebug("[FULLCONTACT.helpers:IsDate]  2 - ERROR Invalid stringDate: '%s' stringYear: '%s' year: '%s'.", stringDate, stringYear, year)
		return false
	end
	-- ok
	local transformedDate = ormDate:toString("%Y-%m-%d %H:%M:%S.000") -- retorna con milisegundos.
	--LogDebug("[FULLCONTACT.helpers:IsDate] OK if it is a valid date. stringDate: '%s', transformedDate: '%s'.", stringDate, transformedDate)
	return true, transformedDate
end

--[[
	Valida que sea un correo electrónico correcto contra expresiones regulares.
]]--
m_validation.IsEmail = function(self, email)
	assert(type(email) == "string")
	email = ustring(email)
	for icumatch in icuregex.gmatch(m_validation.emailsRegexp.validate, email) do
		--LogDebug("[FULLCONTACT.helpers:IsEmail] --> OK if it is a valid EMAIL. email: '%s', icumatch: '%s'.", email, icumatch)
		return true
	end
	--LogDebug("[FULLCONTACT.helpers:IsEmail] ERROR Invalid EMAIL. email: '%s'.", email)
	return false
end

--[[
	Tabla de expresiones regulares de correo electrónico
]]--
m_validation.emailsRegexp = {
	parse    = icuregex.compile([[^\s*("<[^<>]*>"\s+|[^<>]*\s+)?(<[^<>\s]*>|[^<>\s]*)$]]),
	validate = icuregex.compile([[^((([a-z]|\d|[\\!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z]|\d|[\\!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*)|((\x22)((((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(([\x01-\x08\x0b\x0c\x0e-\x1f\x7f]|\x21|[\x23-\x5b]|[\x5d-\x7e]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(\\([\x01-\x09\x0b\x0c\x0d-\x7f]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))))*(((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(\x22)))@((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?$]], "i")
}

--[[

]]--
m_validation.SanitizePhoneNumber = function(self, n)
	assert(n)
	n = tostring(n)
	-- valido si en el numero el primer caracter es un + (signo de mas) y si es asi lo elimino de la vaildacion.
	local firstCharacter = n:sub(1, 1)
	local firstTwoCharacters = n:sub(1, 2)
	if firstCharacter == "+" or firstTwoCharacters == "00" then
		return n
	end
	-- tengo que agregarle el + (signo de mas) para la validacion en "lua_telephonyproxyclient"
	return "+" .. n
end

--[[

]]--
m_validation.IsGlobalPhone = function(self, number, country, area)
	assert(number)
	country = country or ""
	local isGlobalNumber = false
	local firstCharacter = number:sub(1, 1)
	if firstCharacter == "+" then
		isGlobalNumber = true
	else
		if country == "" or country == "0" then
			isGlobalNumber = true
		else
			local countryArea = country .. (area or "")
			--LogDebug("[FULLCONTACT.helpers:IsGlobalPhone]  -> countryArea: '%s'", countryArea)
			local countryLength = #countryArea
			local phoneCountryArea = number:sub(1, countryLength)
			--LogDebug("[FULLCONTACT.helpers:IsGlobalPhone]  -> phoneCountryArea: '%s'", phoneCountryArea)
			isGlobalNumber = countryArea == phoneCountryArea
		end
	end
	--LogDebug("[FULLCONTACT.helpers:IsGlobalPhone]  -> isGlobalNumber: '%s' number: '%s' country: '%s' area: '%s'", isGlobalNumber, number, country, area)
	return isGlobalNumber
end

--[[
	Valida que sea un Teléfono válido.
	Realiza una validación contra la rocka "lua_telephonyproxyclient" invocando al método "ValidatePhone"

	lua_telephonyproxyclient response:
		{"number":"24000007","dialLocal":"24000007","dialGlobal":"59824000007","dialSameCountry":"24000007","countryCode":"598","areaCode":""}
]]--
m_validation.IsPhone = function(self, number, country, area, vcc, withTimezone, withTrace, options)
	assert(number)
	options = options or {}
	-- LogDebug("")
	-- LogDebug("--------------------")
	-- LogDebug("[FULLCONTACT.helpers:IsPhone] 1--> Validating PHONE number: '%s', country: '%s', area: '%s'. vcc: '%s', withTimezone: '%s', withTrace: '%s', options: %s", number, country, area, vcc, withTimezone, withTrace, json.encode(options))

	-- 1. genero la data como la espera la función "ValidatePhone" del "telephony proxy client"
	number = alltrim(tostring(number))
	local country = alltrim(tostring(country or ""))
	local area = alltrim(tostring(area or ""))
	local isGlobalNumber = m_validation:IsGlobalPhone(number, country, area)
	local phoneData = {
		number = number,
		originCountryCode = country,
		originAreaCode = area,
		withTimezone = withTimezone or false,
		withTrace = withTrace or false,
		VCC = vcc
	}
	-- 2. no realizo la validacion si es que me llega por parametro.
	if options.skipValidatePhone and options.addressDetail then
		--[[
			Opcion utilizada cuando migramos un vcc Allegro a Nativo, como desde Allegro los telefonos ya son validos,
				no es necesario validar el mismo contra el telproxyclient.
			Igualmente es necesario retornar a quien me ha invocado el mismo formato que retorna el telproxyclient, para que todo funcione correctamente.
		--]]
		local dialLocal = options.addressDetail.DialLocal
		local phoneWithoutValidation = {
			number = dialLocal, --24001189,
			dialLocal = dialLocal, --24001189,
			dialGlobal = number, --59824001189,
			dialSameCountry = dialLocal, --24001189,
			countryCode = country, --598,
			areaCode = area, --""
		}
		-- LogDebug("[FULLCONTACT.helpers:IsPhone] 1.1 ====> SUCCESS SKIP VALIDATION PHONE number: '%s'. isGlobalNumber: %s, phoneWithoutValidation: %s.", number, isGlobalNumber, JSON_Encode(phoneWithoutValidation))
		-- LogDebug("--------------------")
		-- LogDebug("")
		return true, phoneWithoutValidation, isGlobalNumber
	end

	-- 3. obtengo la instancia del "telephony proxy client"
	--LogDebug("[FULLCONTACT.helpers:IsPhone] 2--> GetTelephonyProxyClient()")
	local telephonyProxy = GetTelephonyProxyClient()

	-- 4. invoco a la función para realizar el teléfono en un pcall para cubrirnos de errores que puedan suceder dentro del propio "telephony proxy client"
	--LogDebug("[FULLCONTACT.helpers:IsPhone] --> Execute: telephonyProxy:ValidatePhone(%s).", JSON_Encode(phoneData))
	local success, response = pcall(telephonyProxy.ValidatePhone, telephonyProxy, phoneData)
	if not success or not response then
		-- ERROR
		LogWarning("[FULLCONTACT.helpers:IsPhone] 3--> Error in function 'ValidatePhone' of TelephonyProxyClient.")
		return false, {errType = "INVALID_PHONE_NUMBER", phoneData = phoneData}
	end
	-- 5. valido los resultados
	local result = (response.result or ""):upper()
	if result ~= "OK" then
		-- ERROR - VALIDATION
		response.errType = "INVALID_PHONE_NUMBER"
		response.phoneData = phoneData
		--LogWarning("[FULLCONTACT.helpers:IsPhone] 4----> ERROR validating PHONE number: '%s'. response: %s", number, JSON_Encode(response))
		--LogDebug("--------------------")
		--LogDebug("")
		return false, response
	end
	-- LogDebug("[FULLCONTACT.helpers:IsPhone] 5 ====> SUCCESS validating PHONE number: '%s'. isGlobalNumber: %s, response: %s.", number, isGlobalNumber, JSON_Encode(response))
	-- LogDebug("--------------------")
	-- LogDebug("")
	if not response.data then
		-- ERROR - INCOMPLETE RESPONSE
		response.errType = "INVALID_PHONE_NUMBER"
		response.phoneData = phoneData
		LogWarning("[FULLCONTACT.helpers:IsPhone] 6 - Incomplete response info of TelephonyProxyClient. PHONE number: '%s'. response: %s.", number, JSON_Encode(response))
		return false, response
	end
	-- OK
	return true, response.data, isGlobalNumber
end

---------------------------------
-- Funciones tiempo transcurrido
--------------------------------
local m_elapsedTime = {}

m_elapsedTime.Start = function(self, msg)
	return m_helpers.utils.ElapsedTime.Start(msg)
end

m_elapsedTime.End = function(self, processId, msg)
	return m_helpers.utils.ElapsedTime.End(processId, msg)
end

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

return {
	SeparateVCC          = SeparateVCC,
	StripVCC             = StripVCC,
	AddVCC               = AddVCC,
	BuildAddressId       = BuildAddressId,
	BuildAddressInstance = BuildAddressInstance,
	GetAllColumns        = GetAllColumns,
	SaveRows             = SaveRows,
	SetupConnections     = SetupConnections,
	GetDatasource        = GetDatasource,
	SanitizeQueryValue   = SanitizeQueryValue,
	-- elasticsearch
	MakeRequestInfoForEs = MakeRequestInfoForEs,
	-- tables
	TableIsEmpty         = TableIsEmpty,
	ArrayToHash          = ArrayToHash,
	StructuralCompare    = StructuralCompare,
	MergeTables          = MergeTables,
	MergeArrayWithoutRepeats = MergeArrayWithoutRepeats,
	DeepCopy             = DeepCopy,
	-- json
	JSON_Encode          = JSON_Encode,
	JSON_Decode          = JSON_Decode,
	-- string
	alltrim              = alltrim,
	toUTF8               = toUTF8,
	CapitalizeUpperCase  = CapitalizeUpperCase,
	-- file
	ReadFile             = ReadFile,
	-- csv
	csv                  = {
		Csv2Table      = m_csvMng.Csv2Table,
		ParseCSVHeader = m_csvMng.ParseCSVHeader,
		ParseCSVLine   = m_csvMng.ParseCSVLine,
		Table2Csv      = m_csvMng.Table2Csv
	},
	-- dialer
	dialer = {
		IsAnyProcessBatchInProgressInVCC = IsAnyProcessBatchInProgressInVCC,
		UpdateVCCStatusProcessBatch      = UpdateVCCStatusProcessBatch,
		DeleteVCCProcessBatch            = DeleteVCCProcessBatch,
		GetVCCStatusProcessBatch         = GetVCCStatusProcessBatch
	},
	-- memory (collectgarbage)
	memory = {
		CollectAllMemory = m_memory.CollectAllMemory,
		ShowMemory       = m_memory.ShowMemory
	},
	-- validations
	validation = {
		IsDate        = m_validation.IsDate,
		IsEmail       = m_validation.IsEmail,
		IsPhone       = m_validation.IsPhone,
		IsGlobalPhone = m_validation.IsGlobalPhone
	},
	-- others
	GetConfigValues  = GetConfigValues,
	-- others elapsedtime
	elapsedTime = {
		Start = m_elapsedTime.Start,
		End   = m_elapsedTime.End
	}
}
