local utf8 = require "icu.utf8"
local cassandra = require "lua_cassandra"
local WrapRedis = require "wrapredis"
local Date = require "LuaOrm.Date"
local uuid = require "uuid"
local pl = {
	tablex = require "pl.tablex"
}
local m_fullContactModule = nil
local logger = {}
package.preload["fullcontact.logger"] = function()
	return logger
end
local model = require "fullcontact.model"
local helpers = require "fullcontact.helpers"
local m_configValues = helpers.GetConfigValues()
local m_blackList = require "fullcontact.blacklist"
local m_migrations = require "fullcontact.migrations"

-- Tope máximo de filas que se pueden importar en una importacion de contacts dialer.
local MAXIMUM_NUMBER_OF_ROWS_IN_DIALER_IMPORT = m_configValues.importation.maximumNumberOfRowsInDialerImport
-- tope maximo de contactos fallidos que se almacenaran por cada importacion.
local MAXIMUM_NUMBER_OF_FAILED_CONTACTS_TO_STORE = m_configValues.importation.maximumNumberFailedContactsToStore
-- Tipo de importacion (SINGLE O BATCH) --> SINGLE is DEPRECATED.
local IMPORTATION_TYPE = m_configValues.importation.type
-- Si los primeros N contactos "maximumFirstFailedContactsForBreakImport" de la importacion son fallidos, debe abortar la misma.
local MAXIMUM_FIRST_FAILED_CONTACTS_FOR_BREAK_IMPORT = m_configValues.importation.maximumFirstFailedContactsForBreakImport

local function init (options)
	assert(type(options) == "table", "options must be a table")
	assert(type(options.logger) == "table", "options.logger must be a table")

	for k,v in pairs(options.logger) do
		logger[k] = v
	end
	setmetatable(logger, getmetatable(options.logger))

	-- logger.debug("[FULLCONTACT:init] - Initializing FullContact from '%s'", options.owner)
	-- logger.debug("[FULLCONTACT:init] 1 - options.cassandra '%s'", helpers.JSON_Encode(options.cassandra or {}))
	-- logger.debug("[FULLCONTACT:init] 2 - options.elasticsearch '%s'", helpers.JSON_Encode(options.elasticsearch or {}))
	-- logger.debug("[FULLCONTACT:init] 3 - options.elasticsearchDialer '%s'", helpers.JSON_Encode(options.elasticsearchDialer or {}))
	-- logger.debug("[FULLCONTACT:init] 4 - options.redis '%s'", helpers.JSON_Encode(options.redis or {}))
	-- logger.debug("[FULLCONTACT:init] 5 - options.redis '%s'", helpers.JSON_Encode(options.redis or {}))
	-- logger.debug("[FULLCONTACT:init] 6 - options.timeServers '%s'", helpers.JSON_Encode(options.timeServers or {}))
	-- logger.debug("[FULLCONTACT:init] 7 - options.cluster '%s'", options.cluster)

	-- cassandra
	assert(options.cassandra, "options.cassandra is missing")
	assert(options.cassandra.host, "missing options.cassandra.host field")
	assert(options.cassandra.port, "missing options.cassandra.port field")

	-- elastic search (de los contactos que se generan por la config de las cuentas de las campañas)
	assert(options.elasticsearch, "options.elasticsearch is missing")
	assert(options.elasticsearch.host, "missing options.elasticsearch.host field")
	assert(options.elasticsearch.port, "missing options.elasticsearch.port field")
	options.elasticsearch.numberOfShards = options.elasticsearch.numberOfShards or tonumber(os.getenv("NUMBER_OF_SHARDS"))
	options.elasticsearch.numberOfReplicas = options.elasticsearch.numberOfReplicas or tonumber(os.getenv("NUMBER_OF_REPLICAS"))
	assert(options.elasticsearch.numberOfShards, "missing options.elasticsearch.numberOfShards field")
	assert(options.elasticsearch.numberOfReplicas, "missing options.elasticsearch.numberOfReplicas field")

	-- elastic search dialer (para el oe config)
	if options.elasticsearchDialer then
		assert(options.elasticsearchDialer.host, "missing options.elasticsearchDialer.host field")
		assert(options.elasticsearchDialer.port, "missing options.elasticsearchDialer.port field")
		options.elasticsearchDialer.numberOfShards = options.elasticsearchDialer.numberOfShards or tonumber(os.getenv("NUMBER_OF_SHARDS"))
		options.elasticsearchDialer.numberOfReplicas = options.elasticsearchDialer.numberOfReplicas or tonumber(os.getenv("NUMBER_OF_REPLICAS"))
		assert(options.elasticsearchDialer.numberOfShards, "missing options.elasticsearchDialer.numberOfShards field")
		assert(options.elasticsearchDialer.numberOfReplicas, "missing options.elasticsearchDialer.numberOfReplicas field")
	end
	-- utilizado por la rocka "lua_telephonyproxyclient" para la validacion de teléfonos
	local timeServer
	if options.timeServers and #options.timeServers > 0 then
		timeServer = options.timeServers[1]
		assert(timeServer.IP, "missing timeServer.IP field")
		assert(timeServer.port, "missing timeServer.port field")
	end
	-- redis
	local wrapRedis
	if options.redis then
		assert(options.redis.host, "missing options.redis.host field")
		assert(options.redis.port, "missing options.redis.port field")
		wrapRedis = WrapRedis(options.redis.host, options.redis.port, options.redis.pwd)
		if not wrapRedis then
			error("Unable to initialize fullcontact app, connection to redis failed")
		end
	end
	-- logger.debug("wrapRedis: %s", wrapRedis)

	cassandra.seed(options.cassandra.host, options.cassandra.port)
	model.SetDatasource({
		cassandra           = cassandra,
		cassandraInfo       = options.cassandra, -- host y port
		cluster             = options.cluster,
		elasticsearch       = options.elasticsearch,
		elasticsearchDialer = options.elasticsearchDialer,
		redis               = wrapRedis,
		timeServer          = timeServer -- IP y port
	})
	-- blacklist
	m_blackList:Init(m_fullContactModule)
	-- migrations
	m_migrations:Init(m_fullContactModule)
	-- ok
	return true
end

---
-- Trusted APIs (to be used, for instance, with the InteractionRouter)
-- This means that when an id is supplied, it must be in the form ident@vcc,
-- having a valid vcc
--
local trusted = {}

---
-- asserts the given string is in the form ident@vcc
-- returns the vcc or fail
--
local function assert_vcc (s, msg)
	local id, vcc = helpers.SeparateVCC(s)
	return assert(vcc, msg)
end

function trusted.Get (contact_id, options)
	assert(type(contact_id) == "string", "contact_id must be a string")
	assert_vcc(contact_id, "contact_id must be in the form ident@vcc")
	options = options or {}
	--logger.debug("[FULLCONTACT:trusted.Get] - contact_id '%s'", contact_id)
	return model.GetContact(contact_id, options)
end

---
--
local function GetContactByAddress (address_type, address, vcc, options)
	assert(type(address_type) == "string", "address_type must be a string")
	assert(type(vcc) == "string", "vcc must be a string")
	if type(address) == "string" then
		local addressInfo = helpers.BuildAddressInstance(address_type, address, vcc)
		if addressInfo then
			address = addressInfo
		else
			return
		end
	end
	options = options or {}
	return model.GetContactByAddress(address_type, address, vcc, options)
end

---
--
local function GetContactByExternalId (external_id, vcc, options)
	assert(external_id, "external_id must be provided")
	assert(type(vcc) == "string", "vcc must be a string")
	options = options or {}
	return model.GetContactByExternalId(external_id, vcc, options)
end

---
--
local function GetContactByHandle (handle_type, handle, vcc, options)
	assert(type(handle_type) == "string", "type must be provided and must be a string")
	assert(handle, "'handle' must be provided")
	assert(type(vcc) == "string", "vcc must be a string")

	options = options or {}

	return model.GetContactByHandle(handle_type, handle, vcc, options)
end

---
--
local function GetContact (contact_id, vcc, options)
	assert(type(contact_id) == "string", "id must be a string")
	assert(type(vcc) == "string", "vcc must be a string")

	options = options or {}

	contact_id = helpers.AddVCC(contact_id, vcc)
	return trusted.Get(contact_id, options)
end

---
--
local function SearchContacts (params)
	assert(type(params) == "table", "parmas must be a table")
	assert(type(params.filter) == "table", "params.filter must be a table")
	return model.Search(params)
end

---
-- returns success (true / false) plus the newly added contact
local function AddContact (id, vcc, contact_data, options)
	assert(type(id) == "string", "id must be a string")
	assert(type(vcc) == "string", "vcc must be a string")
	assert(type(contact_data) == "table", "contact data must be a table")
	assert(type(contact_data.campaigns) == "table", "contact data has no campaigns")

	--logger.debug("[FULLCONTACT:AddContact] ------> id '%s', vcc: '%s', , contact_data: '%s'", id, vcc, helpers.JSON_Encode(contact_data))

	local contact = pl.tablex.deepcopy(contact_data)
	contact.Id = helpers.AddVCC(id, vcc)
	contact.VCC = vcc
	contact.LastUpdate = LuaOrm.Date():toString()
	contact.CreatedAt = contact.LastUpdate
	contact.handles = contact.handles or {}
	contact.FromInteraction = contact.FromInteraction -- optional, id of the interaction that created this contact

	if contact.externalId == "" then
		return false, { code = "E_INVALID_ARG", msg = "External id can't be blank", contact_id = contact.Id }
	end

	options = options or {}
	if options.mustReplaceWithValidatedAddresses == nil then
		options.mustReplaceWithValidatedAddresses = false
	end

	-- hack to make sure this property used by the agent is never stored (issue #4340)
	contact.isNew = nil

	return model.Save(contact, options)
end

--[[
	Agrega contactos, solo con direcciones validas. Las direcciones inválidas las excluye y no las agrega.
		Ejemplo utilizado desde el ABM de contactos (contact mng) desde la ventana de los clientes (controller /apps/agent/contacts/).
]]--
local function AddContactOnlyValidAddresses (id, vcc, contact_data, options)
	logger.info("[FULLCONTACT:AddContactOnlyValidAddresses] ------> id '%s', vcc: '%s'", id, vcc)
	--logger.debug("[FULLCONTACT:AddContactOnlyValidAddresses] ------> id '%s', vcc: '%s', contact_data: '%s'", id, vcc, json.encode(contact_data))
	options = options or {}
	options.mustReplaceWithValidatedAddresses = true
	return AddContact(id, vcc, contact_data, options)
end

---
--
local function DeleteContact (contact_id, vcc, options)
	assert(type(contact_id) == "string", "contact_id must be a string")
	assert(type(vcc) == "string", "vcc must be a string")

	options = options or {}
	if type(options.logical) ~= "boolean" then
		options.logical = true -- defaults to logical deletes
	end

	contact_id = helpers.AddVCC(contact_id, vcc)

	return model.DeleteContact(contact_id, options)
end

---
--
local function MergeContacts (id, vcc, contact_data, obsolete_contact_id)
	assert(type(obsolete_contact_id) == "string", "obsolete_contact_id must be a string")
	assert(type(id) == "string", "id must be a string")
	assert(type(vcc) == "string", "vcc must be a string")
	assert(type(contact_data) == "table", "contact data must be a table")

	obsolete_contact_id = helpers.AddVCC(obsolete_contact_id, vcc)

	local contact = pl.tablex.deepcopy(contact_data)
	contact.Id = helpers.AddVCC(id, vcc)
	contact.VCC = vcc
	contact.LastUpdate = LuaOrm.Date():toString()
	contact.handles = contact.handles or {}

	if contact.externalId == "" then
		return nil, { code = "E_INVALID_ARG", msg = "External id can't be blank", contact_id = contact.Id }
	end

	return model.MergeContacts(contact, obsolete_contact_id)
end

---
--
local function AddToFavorites (username, vcc, contact_id)
	assert(type(username) == "string", "username must be a string")
	assert(type(vcc) == "string", "vcc must be a string")
	assert(type(contact_id) == "string", "contact_id must be a string")

	local user_id = utf8.lower(helpers.AddVCC(username, vcc))
	contact_id = helpers.AddVCC(contact_id, vcc)

	return model.AddToFavorites(user_id, contact_id)
end

---
--
local function RemoveFromFavorites (username, vcc, contact_id)
	assert(type(username) == "string", "username must be a string")
	assert(type(vcc) == "string", "vcc must be a string")
	assert(type(contact_id) == "string", "contact_id must be a string")

	local user_id = utf8.lower(helpers.AddVCC(username, vcc))
	contact_id = helpers.AddVCC(contact_id, vcc)

	return model.RemoveFromFavorites(user_id, contact_id)
end

---
--
local function GetUserFavorites (params)
	assert(type(params) == "table", "params must be a table")
	assert(type(params.user) == "string", "params.user must be a string")
	assert(type(params.vcc) == "string", "params.vcc must be a string")

	return model.GetUserFavorites(params)
end

---
--
local function GetAvatar (contact_id, vcc)
	assert(type(contact_id) == "string", "contact_id must be a string")
	assert(type(vcc) == "string", "vcc must be a string")

	contact_id = helpers.AddVCC(contact_id, vcc)

	local contact = model.GetContact(contact_id, vcc)
	if not contact then return contact, err end

	if contact and contact.Avatar then
		return contact.Avatar
	else
		return nil, "Avatar not found"
	end
end

---
--
local function SaveAvatar (contact_id, vcc, avatar)
	assert(type(contact_id) == "string", "contact_id must be a string")
	assert(type(avatar) == "table", "avatar must be a table")
	assert(type(avatar.image) == "string", "avatar data must have a string field 'image'")
	assert(type(avatar.size) == "number", "avatar data must have a number field 'size'")

	contact_id = helpers.AddVCC(contact_id, vcc)

	return model.SaveAvatar(contact_id, avatar)
end

---
--
local function ChangeContactState (contact_id, vcc, new_state)
	assert(type(contact_id) == "string", "contact_id must be a string")
	assert(type(vcc) == "string", "vcc must be a string")
	assert(type(new_state) == "string", "new_state must be a string")

	contact_id = helpers.AddVCC(contact_id, vcc)

	return model.ChangeState(contact_id, new_state)
end


--[[
********************************************************************************************************
Funciones (AddContactToES, RemoveContactFromES, ListAllContactsFromCassandra) que quedan expuestas en el API,
   para que puedan ser utilizar desde los migrations (Ej: "20180524T235959_7726_AddContactsSearchEngineTOElasticSearch.sh")
********************************************************************************************************
]]--

--------------------------
-- Funcion que crea el Indice en el Motor de búsqueda Elastic Search
--------------------------
local function CreateESIndex (vcc, elasticType)
	return model.CreateESIndex(vcc, elasticType)
end

--------------------------
-- Funcion que elimina el Indice del Motor de búsqueda Elastic Search
--------------------------
local function DropESIndex (vcc, elasticType)
	return model.DropESIndex(vcc, elasticType)
end

--------------------------
-- Funcion que crea el Template en el Motor de búsqueda Elastic Search
--------------------------
local function CreateESTemplate (elasticType)
	return model.CreateESTemplate(elasticType)
end

--------------------------
-- Funcion que retorna el nombre del Indice utilizado en el Motor de búsqueda Elastic Search
--------------------------
local function GetESIndexName (vcc, elasticType)
	return model.GetESIndexName(vcc, elasticType)
end

--------------------------
-- Agrega un contacto al Motor de búsqueda Elastic Search
--------------------------
local function AddContactToES (contact, elasticType)
	assert(type(contact) == "table", "contact must be a table")
	return model.AddContactToES(contact, elasticType)
end

--------------------------
-- Remueve un contacto del Motor de búsqueda Elastic Search
--------------------------
local function RemoveContactFromES (contact_id, vcc)
	assert(type(contact_id) == "string", "contact_id must be a string")
	assert(type(vcc) == "string", "vcc must be a string")
	return model.RemoveContactFromES(contact_id, vcc)
end

--------------------------
-- Lista todos los contactos existentes en Cassandra
--------------------------
local function ListAllContactsFromCassandra (vcc, options)
	return model.ListAllContactsFromCassandra(vcc, options)
end

--------------------------
-- Lista un grupo de contactos existentes en Cassandra
--------------------------
local function ListContactsChunkFromCassandra (options, cassandraConnection)
	return model.ListContactsChunkFromCassandra(options, cassandraConnection)
end

------------------------------------------
-- ADDRESS BOOK
------------------------------------------

local m_addressesBook = {}

---
--
m_addressesBook.Get = function(addressId)
	assert(type(addressId) == "string", "parmas must be a string")
	return model.AddressesBook.Get(addressId)
end

---
--
m_addressesBook.List = function(params)
	assert(type(params) == "table", "parmas must be a table")
	return model.AddressesBook.List(params)
end

m_addressesBook.ListByChannel = function(params)
	assert(type(params) == "table", "parmas must be a table")
	return model.AddressesBook.ListByChannel(params)
end

---
--
m_addressesBook.Save = function(params)
	assert(type(params) == "table", "parmas must be a table")
	return model.AddressesBook.Save(params)
end

---
--
m_addressesBook.Remove = function(addressId)
	assert(type(addressId) == "string", "parmas must be a string")
	return model.AddressesBook.Remove(addressId)
end

------------------------------------------
-- CONTACTS DIALER
------------------------------------------

local m_dialer = {}

-- INDICES .........................................................
--

m_dialer.CreateESTemplate = function(self)
	return CreateESTemplate("CONTACTS_DIALER")
end

m_dialer.CreateESIndex = function(self, vcc)
	return CreateESIndex(vcc, "CONTACTS_DIALER")
end

m_dialer.DropESIndex = function(self, vcc)
	return DropESIndex(vcc, "CONTACTS_DIALER")
end

m_dialer.GetESIndexName = function(self, vcc)
	return GetESIndexName(vcc, "CONTACTS_DIALER")
end

-- CONTACTOS .........................................................
--

m_dialer.SanitizeContactToSave = function(self, id, vcc, contactData, now)
	assert(type(id) == "string", "id must be a string")
	assert(type(vcc) == "string", "vcc must be a string")
	assert(type(contactData) == "table", "contact data must be a table")
	local contact = pl.tablex.deepcopy(contactData)
	contact.Id = helpers.AddVCC(id, vcc)
	contact.VCC = vcc
	contact.CreatedAt = now
	contact.LastUpdate = now
	contact.isNew = nil
	return contact
end

m_dialer.SaveContact = function(self, id, vcc, contactData, options)
	--logger.info("[FULLCONTACT.m_dialer:SaveContact] Adding Dialer Contact - id: %s, vcc: %s contactData: %s", id, vcc, contactData)
	-- logger.debug("[FULLCONTACT.m_dialer:SaveContact] Adding Dialer Contact - id: %s, vcc: %s contactData: %s", id, vcc, helpers.JSON_Encode(contactData))
	assert(type(id) == "string", "id must be a string")
	assert(type(vcc) == "string", "vcc must be a string")
	assert(type(contactData) == "table", "contact data must be a table")
	local now = LuaOrm.Date():toString()
	local contact = self:SanitizeContactToSave(id, vcc, contactData, now)
	options = options or {}
	return model.Dialer:SaveContact(contact, options)
end

m_dialer.SaveContactsBatch = function(self, vcc, contactsData, importInfo)
	assert(type(vcc) == "string", "vcc must be a string")
	assert(type(contactsData) == "table", "contact data must be a table")

	--logger.info("[FULLCONTACT.m_dialer:SaveContactsBatch] Adding Dialer - %s Contacts BATCH - vcc: %s.", #contactsData, vcc)
	-- logger.debug("[FULLCONTACT.m_dialer:SaveContactsBatch] Adding Dialer Contacts BATCH - vcc: %s contactsData: %s", vcc, helpers.JSON_Encode(contactsData))

	if #contactsData == 0 then
		return false, {}
	end

	local now = LuaOrm.Date():toString()
	local contacts = {}
	for _, contactData in ipairs(contactsData) do
		local contact = self:SanitizeContactToSave(contactData.Id, vcc, contactData, now)
		table.insert(contacts, contact)
	end

	local options = {
		DuplicateCheck  = importInfo.DuplicateCheck,
		DuplicateSolver = importInfo.DuplicateSolver
	}

	return model.Dialer:SaveContactsBatch(vcc, contacts, options)
end

m_dialer.MakeAddresses = function(self, rawAddresses, vcc, options)
	local addresses = {}
	local invalidAddresses = {}
	for _, rawAddress in pairs(rawAddresses or {}) do
		--logger.debug(" 1 -----> m_dialer.MakeAddresses: rawAddress '%s'", helpers.JSON_Encode(rawAddress))
		local addressInstance = helpers.BuildAddressInstance(rawAddress.type, rawAddress, vcc, options)
		if addressInstance then
			if addressInstance.Validation.Success then
				--logger.debug(" 2 -----> m_dialer.MakeAddresses: VALID addressInstance '%s'", helpers.JSON_Encode(addressInstance))
				table.insert(addresses, addressInstance)
			else
				--logger.debug(" 3 -----> m_dialer.MakeAddresses: InVALID addressInstance '%s'", helpers.JSON_Encode(addressInstance))
				table.insert(invalidAddresses, addressInstance)
			end
		else
			--logger.debug(" 4 -----> m_dialer.MakeAddresses: InVALID addressInstance '%s'", helpers.JSON_Encode(addressInstance))
			table.insert(invalidAddresses, rawAddress)
		end
	end
	return addresses, invalidAddresses
end

m_dialer.MakeHomeAddresses = function(self, sourceList)
	local targetList = {}
	for _, source in pairs(sourceList or {}) do
		table.insert(targetList, {
			Value          = utf8.lower(helpers.toUTF8(source.value)),
			AddressSubType = source.addressSubType,
			AddressArea    = source.addressArea,
			AddressCountry = source.addressCountry
		})
	end
	return targetList
end

m_dialer.MakeNameValues = function(self, nameValues)
	local fullNameValues = {}
	for _, nameValue in pairs(nameValues or {}) do
		--logger.debug(" ---> MakeNameValues name: '%s' = value: '%s'", nameValue.name, nameValue.value)
		table.insert(fullNameValues, {
			Name  = nameValue.name,
			Value = utf8.lower(helpers.toUTF8(nameValue.value))
		})
	end
	return fullNameValues
end

m_dialer.MakeSkills = function(self, skills)
	--logger.debug("[FULLCONTACT:dialer.MakeSkills] 1-ENTRA----------------> skills: '%s'", helpers.JSON_Encode(skills))
	local defaulMode = "REQUIRED"
	local fullSkills = {}
	for _, skill in pairs(skills or {}) do
		--[[
			el skill mode (SORT, PREFERRED o REQUIRED) se asigna segun:
				1 - si el valor configurado en el formato es SORT, al contacto siempre se le va a almacenar SORT.
				2 - si el valor configurado en el formato es PREFERRED o REQUIRED, en el contacto se va a almacenar el valor del formato siempre
					y cuando el valor de SkillReqValue (SkillReqColumn) no sea 0 (cero)
		]]--
		local skillMode = tostring(skill.Mode or defaulMode)
		if skillMode == "PREFERRED" or skillMode == "REQUIRED" then
			local skillReqValue = tonumber(skill.SkillReqValue)
			if skillReqValue == 0 then
				skillMode = "SORT"
			end
		end
		table.insert(fullSkills, {
			SkillId        = skill.SkillId,
			Value          = tonumber(utf8.lower(helpers.toUTF8(skill.value))),
			Mode           = skillMode
		})
	end
	--logger.debug("[FULLCONTACT:dialer.MakeSkills] 2-SALE----------------> fullSkills: '%s'", helpers.JSON_Encode(fullSkills))
	return fullSkills
end

m_dialer.CanAddContact = function(self, contactId, vcc, importFormat, csvContactData, blacklists)
	assert(type(contactId) == "string", "vcc must be a string")
	assert(type(vcc) == "string", "vcc must be a string")
	assert(type(importFormat) == "table", "importFormat must be a table")
	assert(type(importFormat.mappingFields) == "table", "importFormat.mappingFields must be a table")
	assert(type(csvContactData) == "table", "csvContactData must be a table")

	--logger.debug("[FULLCONTACT:dialer.CanAddContact] ----------> Validating can add Contact: '%s', vcc: '%s'", contactId, vcc)
	--logger.debug("[FULLCONTACT:dialer.CanAddContact] 1-----------------> importFormat.mappingFields: '%s'", helpers.JSON_Encode(importFormat.mappingFields))
	--logger.debug("[FULLCONTACT:dialer.CanAddContact] 2-----------------> csvContactData: '%s'", helpers.JSON_Encode(csvContactData))
	--logger.debug("[FULLCONTACT:dialer.CanAddContact] 3-----------------> blacklists: '%s'", helpers.JSON_Encode(blacklists))

	--[[
		-- Lista de categorias evaluables por las cuales se puede cancelar o marcar como alertas al contacto
	]]
	local totalsByCategories = {
		-- "PHONEDATA" (7)
		["7"] = {
			categoryId   = "7",
			categoryName = "PHONEDATA",
			total        = 0,
			errors       = {},
			isRequired   = true
		},
		-- "EMAIL" (9)
		["9"] = {
			categoryId   = "9",
			categoryName = "EMAIL",
			total        = 0,
			errors       = {},
			isRequired   = false
		},
		-- "NAME VALUE" (11)
		["11"] = {
			categoryId   = "11",
			categoryName = "NAME VALUE",
			total        = 0,
			errors       = {},
			isRequired   = false
		},
		-- "SCHEDULED CALL" (12)
		["12"] = {
			categoryId   = "12",
			categoryName = "SCHEDULED CALL",
			total        = 0,
			errors       = {},
			isRequired   = false
		},
		-- "SKILL" (14)
		["14"] = {
			categoryId   = "14",
			categoryName = "SKILL",
			total        = 0,
			errors       = {},
			isRequired   = false
		}
	}
	--[[
		Funcion que acumula errores o alertas al contacto
	]]--
	local errorType = nil
	local function MakeErrType (err, csvFieldName, categoryType)
		if not errorType then
			errorType = err
		else
			errorType = errorType .. " - AND " .. err
		end
		local currentErr = csvFieldName and string.format(" (Header Name: %s)", csvFieldName) or ""
		errorType = errorType .. currentErr
		table.insert(totalsByCategories[categoryType].errors, {errType = currentErr})
	end

	--[[
		Funcion que valida una linea del CSV
	]]--
	local function ValidateLineCsv2Table (csvFieldName, csvFieldValue, fieldInfo)
		--logger.debug("[FULLCONTACT:dialer.CanAddContact] ValidateLineCsv2Table *****************> fieldInfo: '%s'", helpers.JSON_Encode(fieldInfo))
		local categoryType = tostring(fieldInfo.Type):upper()
		if totalsByCategories[categoryType] then
			totalsByCategories[categoryType].total = totalsByCategories[categoryType].total + 1
		end
		-- "PHONEDATA" (7)
		if categoryType == "7" then
			local phoneNumber = helpers.alltrim(csvFieldValue)
			--logger.debug("[FULLCONTACT:dialer.CanAddContact] a -----------------> validating PHONEDATA phoneNumber: '%s'", phoneNumber)
			if #phoneNumber > 0 then
				local isValidPhone, response = helpers.validation:IsPhone(phoneNumber, fieldInfo.PhoneCountry, fieldInfo.PhoneArea, vcc)
				if not isValidPhone then
					local errType = response.result and response.result .. (response.reason and ": INVALID PHONE - " .. response.reason or "") or "INVALID PHONE"
					MakeErrType(errType, csvFieldName, categoryType)
				else
					-- BlackList validation
					local addressInstance = helpers.BuildAddressInstance("PHONE", {
						Number = phoneNumber,
						Country = response.countryCode,
						Area = response.areaCode
					}, vcc)
					if addressInstance then
						local inBlacklisted, err = GetBlackListApi():ValidateAddressInBlackLists(addressInstance.SanitizedId, blacklists)
						if inBlacklisted then
							local errType = "PHONE IN BLACKLISTS - " .. err.reason
							MakeErrType(errType, csvFieldName, categoryType)
						end
					else
						local errType = "INVALID PHONE"
						MakeErrType(errType, csvFieldName, categoryType)
					end
				end
			else
				-- es un telefono vacio, debo de restar para que el calculo no lo contabilice como error ni warning.
				totalsByCategories[categoryType].total = totalsByCategories[categoryType].total - 1
				--logger.debug("[FULLCONTACT:dialer.CanAddContact] b -----------------> WITHOUT Phone value --> PHONEDATA csvFieldName: '%s', csvFieldValue: '%s'", csvFieldName, phoneNumber)
			end

		-- "EMAIL" (9)
		elseif categoryType == "9" then
			--logger.debug("[FULLCONTACT:dialer.CanAddContact] b -----------------> validating EMAIL csvFieldName: '%s', csvFieldValue: '%s'", csvFieldName, csvFieldValue)
			local isValidEmail = helpers.validation:IsEmail(csvFieldValue)
			if not isValidEmail then
				MakeErrType("INVALID EMAIL", csvFieldName, categoryType)
			end

		-- "NAME VALUE" (11)
		elseif categoryType == "11" then
			--logger.debug("[FULLCONTACT:dialer.CanAddContact] c -----------------> validating NAME VALUE csvFieldName: '%s', csvFieldValue: '%s'", csvFieldName, csvFieldValue)
			-- NAME VALUE SEARCHABLE
			if fieldInfo.IsSearchable then
				-- NAME VALUE SEARCHABLE DATA TYPE
				local searchableDataType = (fieldInfo.SearchableDataType or ""):upper()
				if searchableDataType == "DATE" then
					local isValidDate = helpers.validation:IsDate(csvFieldValue)
					if not isValidDate then
						MakeErrType("INVALID NAME VALUE DATE - SUGGESTED: YYYY-MM-DD HH:MM", csvFieldName, categoryType)
					end

				elseif searchableDataType == "NUMERIC" then
					local isValidNumber = tonumber(csvFieldValue)
					if not isValidNumber then
						MakeErrType("INVALID NAME VALUE NUMBER - IS NOT A NUMBER", csvFieldName, categoryType)
					end
				end
			end

		-- "SCHEDULED CALL" (12)
		elseif categoryType == "12" then
			--logger.debug("[FULLCONTACT:dialer.CanAddContact] d -----------------> validating SCHEDULED CALL csvFieldName: '%s', csvFieldValue: '%s'", csvFieldName, csvFieldValue)
			local isValidDate = helpers.validation:IsDate(csvFieldValue)
			if not isValidDate then
				MakeErrType("INVALID SCHEDULED CALL DATE - SUGGESTED: YYYY-MM-DD HH:MM", csvFieldName, categoryType)
			end

		-- "SKILL" (14)
		elseif categoryType == "14" then
			--logger.debug("[FULLCONTACT:dialer.CanAddContact] e -----------------> validating SKILL csvFieldName: '%s', csvFieldValue: '%s'", csvFieldName, csvFieldValue)
			local isValidNumber = tonumber(csvFieldValue)
			if not isValidNumber then
				MakeErrType("INVALID SKILL VALUE - IS NOT A NUMBER", csvFieldName, categoryType)
			end
		end
	end

	--[[
		1)

		Validamos todas las lineas del CSV.
			Pueden venir lineas simples o multiples.

			- (1 a 1)      Las lineas simples son utilizadas cuando se ha mapeado una columna del CSV a una columna del formato de importacion .
			- (1 a varios) Las lineas multiples son utilizadas cuando se ha mapeado una misma columna del CSV a varias columnas del formato de importacion.
	]]--
	for csvFieldName, csvFieldValue in pairs(csvContactData) do
		local fieldInfo = importFormat.mappingFields[helpers.alltrim(csvFieldName):upper()]
		if not fieldInfo then
			-- TODO: aca no deberia llegar nunca
		else
			--logger.debug("[FULLCONTACT:dialer.CanAddContact] *****************> fieldInfo: '%s'", helpers.JSON_Encode(fieldInfo))
			if fieldInfo.isMulti then
			-- lineas multipes.
				--LogDebug("YES multi ->ValidateLineCsv2Table")
				for _, multiFieldInfo in ipairs(fieldInfo.list) do
					ValidateLineCsv2Table(csvFieldName, csvFieldValue, multiFieldInfo)
				end
			else
			-- lineas simples
				--LogDebug("NO multi ->ValidateLineCsv2Table")
				ValidateLineCsv2Table(csvFieldName, csvFieldValue, fieldInfo)
			end
		end
	end

	--logger.debug("[FULLCONTACT:dialer.CanAddContact] ----------> Validation result. Contact: '%s', vcc: '%s', totalsByCategories: %s", contactId, vcc, json.encode(totalsByCategories))

	--[[
		2)

		Aca valido si el contacto puede o no agregarse.
			Para poder ser agregado tiene que tener al menos un tipo de categoria valida.
			Ej: si tiene 2 telefonos 1 es valido y el otro es invalido. El contacto puede ser agregado con el telefono valido.
	]]--
	local success = true
	for categoryType, data in pairs(totalsByCategories) do
		if data.isRequired and data.total == 0 then
			success = false
			MakeErrType(data.categoryName .. " IS REQUIRED", nil, categoryType)
			logger.debug("[FULLCONTACT:dialer.CanAddContact] -------IS REQUIRED (CategoryType)-----> Contact: '%s', vcc: '%s', categoryType: %s.", contactId, vcc, categoryType, withErrorInCategory, data.total, #data.errors)
		else
			local withErrorInCategory = data.total > 0 and data.total == #data.errors
			--logger.debug("[FULLCONTACT:dialer.CanAddContact] ------------> Contact: '%s', vcc: '%s', categoryType: %s, withErrorInCategory: %s, total in category: %s, errors in category: %s.", contactId, vcc, categoryType, withErrorInCategory, data.total, #data.errors)
			if withErrorInCategory then
				success = false
			end
		end
	end
	-- 3) Aca fallo y el contacto NO se puede agregar
	if not success then
		local err = {reason = errorType, type = errorType}
		--logger.warn("[FULLCONTACT:dialer.CanAddContact] ==> Unable to Add Contact '%s' vcc '%s'. Err: %s", contactId, vcc, err.type)
		return false, err
	end
	-- 4) ok
	-- valida si hay errores parciales, pero el contacto se agrega de todas formas. Ejemplo, tiene 2 telefonos, 1 valido y 1 invalido.
	local partialErrors
	if errorType then
		-- 5) errores parciales (alertas)
		errorType = "ALERT--> " .. errorType
		partialErrors = {reason = errorType, type = errorType}
	end

	return true, partialErrors
end

m_dialer.AssignContactErrorToCSV = function(self, csvContactData, errorType)
	assert(type(csvContactData) == "table", "csvContactData must be a table")
	assert(type(errorType) == "string", "errorType must be a string")
	--LogDebug("[AssignContactErrorToCSV] errorType: %s", errorType)
	if csvContactData.INVALIDATION_TYPE then
		csvContactData.INVALIDATION_TYPE = csvContactData.INVALIDATION_TYPE .. ". AND - "
	else
		csvContactData.INVALIDATION_TYPE = ""
	end
	csvContactData.INVALIDATION_TYPE = csvContactData.INVALIDATION_TYPE .. tostring(errorType)
end

m_dialer.MakeContactInfo = function(self, contactData, importInfo, options)
	assert(type(contactData) == "table", "contact data must be a table")
	assert(type(importInfo) == "table", "importInfo must be a table")
	--logger.debug("----------> MakeContactInfo 1 --> Contact: Contactid:  '%s', contactData: '%s'", contactData.ID, helpers.JSON_Encode(contactData))
	options = options or {}
	local addresses, invalidAddresses = m_dialer:MakeAddresses(contactData.addresses, importInfo.VCC, options)
	--logger.debug("----------> MakeContactInfo 2 --> Contact: Contactid:  '%s', importInfo: '%s'", contactData.ID, helpers.JSON_Encode(importInfo))
	local contact = {
		ImportId                = utf8.lower(importInfo.ID or ""),
		VCC                     = utf8.lower(importInfo.VCC or ""),
		Id                      = contactData.ID,
		Name                    = contactData.NAME,
		ExternalId              = utf8.lower(contactData.EXTERNALID or ""),
		Category                = utf8.lower(contactData.CATEGORY or ""),
		AccountOfficer          = utf8.lower(contactData.ACCOUNTOFFICER or ""),
		AccountGroup            = utf8.lower(contactData.ACCOUNTGROUP or ""),
		IsClient                = importInfo.AreClientsImport or false,
		LastImportName          = utf8.lower(contactData.LastImportName or ""),
		Campaign                = utf8.lower(contactData.CAMPAIGN or ""),
		ContactScheduleDate     = contactData.scheduledCall,
		State                   = "FREE",
		-- addresses (telefono, emails, etc)
		Addresses               = addresses,
		HomeAddresses           = m_dialer:MakeHomeAddresses(contactData.homeAddresses),
		-- name values
		NameValuesSimple        = m_dialer:MakeNameValues(contactData.nameValuesSimple),
		NameValuesSearchDate    = m_dialer:MakeNameValues(contactData.nameValuesSearchDate),
		NameValuesSearchNumeric = m_dialer:MakeNameValues(contactData.nameValuesSearchNumeric),
		NameValuesSearchText    = m_dialer:MakeNameValues(contactData.nameValuesSearchText),
		-- skills
		Skills                  = m_dialer:MakeSkills(contactData.skills),
		-- migration
		MigrationType           = options.Type or "NONE",
		MigrationSourceContact  = json.encode(options.SourceContact or {})
	}
	--logger.debug("----------> MakeContactInfo 3--> Contact: Contactid:  '%s', Result Contact: '%s'", contactData.ID, helpers.JSON_Encode(contact))
	-- logger.debug("----------> MakeContactInfo 4--> Contact: Contactid:  '%s', addresses: '%s'", contactData.ID, helpers.JSON_Encode(addresses))
	-- logger.debug("----------> MakeContactInfo 5--> Contact: Contactid:  '%s', invalidAddresses: '%s'", contactData.ID, helpers.JSON_Encode(invalidAddresses))
	return contact, addresses, invalidAddresses
end

m_dialer.PrepareContactForBatch = function(self, vcc, contactData, importInfo, importFormat, csvContactData, blacklists)
	assert(type(vcc) == "string", "vcc must be a string")
	assert(type(contactData) == "table", "contact data must be a table")
	assert(type(importInfo) == "table", "importInfo must be a table")
	assert(type(importInfo.ID) == "string", "importInfo.ID info must be a string")
	assert(type(importInfo.VCC) == "string", "importInfo.VCC info must be a string")
	assert(type(importFormat) == "table", "importFormat must be a table")
	assert(type(csvContactData) == "table", "csvContactData must be a table")

	-- logger.debug("-------------------------------------------------\PrepareContactForBatch-------------------------------------------------")
	-- logger.debug("PREPARING CONTACT FOR BATCH DIALER contactId: '%s', contactData.Name: '%s', importId: '%s', importVCC: '%s'.", contactData.ID, contactData.NAME, importInfo.ID, importInfo.VCC)
	-- logger.debug("----------> PREPARING New DIALER Contact: Contactid:  '%s', Contact: '%s'", contactData.ID, helpers.JSON_Encode(contact))

	local mustAdd, err = self:CanAddContact(contactData.ID, vcc, importFormat, csvContactData, blacklists)
	if not mustAdd then
		-- no paso la validacion, no lo puedo agregar
		self:AssignContactErrorToCSV(csvContactData, err.type)
		return false, contactData.ID, err
	end
	local partialErrors = err
	--logger.debug("----------> partialErrors: '%s'", helpers.JSON_Encode(partialErrors))
	if partialErrors then
		-- existen algunas validaciones invalidas, al contacto lo puedo agregar, pero debo dejar los warnings en el archivo de download.
		self:AssignContactErrorToCSV(csvContactData, partialErrors.type)
	end
	-- paso la validacion, genero la info del contacto para agregarlo.
	--
	local contact = self:MakeContactInfo(contactData, importInfo)
	return true, contactData.ID, contact, partialErrors
end

m_dialer.AddContact = function(self, vcc, contactData, importInfo, importFormat, csvContactData)
	assert(type(vcc) == "string", "vcc must be a string")
	assert(type(contactData) == "table", "contact data must be a table")
	assert(type(importInfo) == "table", "importInfo must be a table")
	assert(type(importInfo.ID) == "string", "importInfo.ID info must be a string")
	assert(type(importInfo.VCC) == "string", "importInfo.VCC info must be a string")
	assert(type(importFormat) == "table", "importFormat must be a table")
	assert(type(csvContactData) == "table", "csvContactData must be a table")

	-- logger.debug("-------------------------------------------------\ADD DIALER CONTACT-------------------------------------------------")
	-- logger.debug("Adding DIALER contactId: '%s', contactData.Name: '%s', importId: '%s', importVCC: '%s'.", contactData.ID, contactData.NAME, importInfo.ID, importInfo.VCC)
	-- logger.debug("----------> Adding New DIALER Contact: Contactid:  '%s', Contact: '%s'", contactData.ID, helpers.JSON_Encode(contact))

	local mustAdd, err = self:CanAddContact(contactData.ID, vcc, importFormat, csvContactData)
	if not mustAdd then
		-- no paso la validacion, no lo puedo agregar
		self:AssignContactErrorToCSV(csvContactData, err.type)
		return false, contactData.ID, err
	end
	local partialErrors = err
	--logger.debug("----------> partialErrors: '%s'", helpers.JSON_Encode(partialErrors))
	if partialErrors then
		-- existen algunas validaciones invalidas, al contacto lo puedo agregar, pero debo dejar los warnings en el archivo de download.
		self:AssignContactErrorToCSV(csvContactData, partialErrors.type)
	end
	-- paso la validacion, genero la info del contacto para agregarlo.
	--
	local contact = self:MakeContactInfo(contactData, importInfo)
	--logger.debug("----------> Adding New DIALER Contact: Contactid:  '%s', Contact: '%s'", contactData.ID, helpers.JSON_Encode(contact))

	local options = {
		DuplicateCheck  = importInfo.DuplicateCheck,
		DuplicateSolver = importInfo.DuplicateSolver
	}
	local ok, newContactId, result = m_dialer:SaveContact(contactData.ID, vcc, contact, options)
	--logger.debug("--> Result New DIALER Contact: Contactid:  '%s', ok: '%s', newContactId: '%s', result: '%s'", contactData.ID, ok, newContactId, result)
	if not ok then
		-- error
		local err = result
		self:AssignContactErrorToCSV(csvContactData, err.type)
		return false, newContactId, err
	end
	-- ok
	local contactAlreadyExist = result
	return true, newContactId, contactAlreadyExist, partialErrors
end

m_dialer.ValidateContactLimitsInBatch = function(self, importInfo)
	assert(type(importInfo) == "table")
	-- 1- Valido que la cantidad de contactos del CSV no supere el numero maximo soportado por el sistema
	if importInfo.totalContacts and importInfo.totalContacts > MAXIMUM_NUMBER_OF_ROWS_IN_DIALER_IMPORT then
		-- Error overflow
		local msg = string.format("The import must not exceed %d contacts. The current import contains %d contacts.", MAXIMUM_NUMBER_OF_ROWS_IN_DIALER_IMPORT, importInfo.totalContacts)
		local err = {
			result = "IMPORT EXCEEDS THE LIMIT OF CONTACTS SUPPORTED",
			code = -104,
			reason = msg,
			maximumNumberRowsInImport = MAXIMUM_NUMBER_OF_ROWS_IN_DIALER_IMPORT,
			currentRows = importInfo.totalContacts
		}
		logger.error("[FULLCONTACT:ValidateContactLimitsInBatch] Contact Import Error. %s", helpers.JSON_Encode(err))
		-- Is Error
		return false, err
	end
	-- Ok
	return true
end

m_dialer.CanStartImport = function(self, importInfo)
	assert(type(importInfo) == "table")
	assert(type(importInfo.VCC) == "string")
	-- 1- Valido que NO exista ningun otro proceso de batch en curso (importacion, alta, baja)
	local isAnyProcessBatch = helpers.dialer.IsAnyProcessBatchInProgressInVCC(importInfo.VCC)
	if isAnyProcessBatch then
		-- Error overflow
		local msg = "Unable to Import. There another process batch in progress"
		local err = {
			result = "THERE ANOTHER PROCESS BATCH IN PROGRESS",
			code = -107,
			reason = msg
		}
		logger.error("[FULLCONTACT:CanStartImport] Dialer Contact Import Error. %s", helpers.JSON_Encode(err))
		-- Is Error
		return false, err
	end
	-- Ok
	return true
end

m_dialer.AddContactsBatch = function(self, importInfo, contactsData, parsedRawCSV, importFormat)
	assert(type(importInfo) == "table", "importInfo must be a table")
	assert(type(contactsData) == "table", "contactsData must be a table")
	assert(type(parsedRawCSV) == "table", "parsedRawCSV must be a table")
	assert(type(importFormat) == "table", "importFormat must be a table")
	assert(IMPORTATION_TYPE == "BATCH", "unrecognized IMPORTATION_TYPE" .. tostring(IMPORTATION_TYPE))

	local totalContacts = #contactsData
	logger.info("[FULLCONTACT:AddContactsBatch] ADD DIALER CONTACTS BATCH - IMPORTATION TYPE: '%s'. totalContacts: '%s'", IMPORTATION_TYPE, totalContacts)
	--logger.debug("[FULLCONTACT:AddContactsBatch] ====> importInfo: %s", helpers.JSON_Encode(importInfo))

	-- start
	local statusInfo = {
		status = "RUNNING",
		action = "START PROCESS",
		added = 0,
		updated = 0,
		errors = 0,
		deleted = 0,
		warnings = 0,
		total = totalContacts
	}

	local failedContactsList = {}
	local keyspace = model.GetDatasource().cassandra:CassandraConnection("OutboundEngine")
	model.Dialer:SaveContactImport(importInfo, statusInfo, nil, failedContactsList, keyspace)

	local vcc = importInfo.VCC
	local newContacts = 0
	local updatedContacts = 0
	local contactErrors = 0
	local updateStatusCounter = m_configValues.importation.updateStatusCounter
	local counter = 0
	local timeStamp = os.time()
	--[[
		Funcion que inserta en la tabla de failed contacts, validando que NO supere el limite de failed contacts soportados por importacion.
	]]--
	local function InsertFailedContact (csvContactData)
		if not csvContactData then
			return
		end
		if #failedContactsList >= MAXIMUM_NUMBER_OF_FAILED_CONTACTS_TO_STORE then
			--logger.debug("[FULLCONTACT:AddContactsBatch] .. InsertFailedContact --> Unable to store more failed contacts in importation: '%s'. MAXIMUM_NUMBER_OF_FAILED_CONTACTS_TO_STORE: %s", importInfo.ID, MAXIMUM_NUMBER_OF_FAILED_CONTACTS_TO_STORE)
			return
		end
		csvContactData.timeStamp = timeStamp
		table.insert(failedContactsList, csvContactData)
	end

	local contactsBatch = {}
	local csvContactsDataBatch = {}
	local duplicatedContactsDataBatch = {} -- tabla utilizada para poder realizar reload de los contactos en el controller de la app "outbound_engine."
	local batchNumber = 0

	local function SaveContactsBatch ()
		batchNumber = batchNumber + 1

		--logger.debug("[FULLCONTACT:AddContactsBatch] 1--> SaveContactsBatch - batchNumber: %s', contactsBatch: '%s'.", batchNumber, #contactsBatch)
		--logger.debug("[FULLCONTACT:AddContactsBatch] 2--> SaveContactsBatch - batchNumber: %s', contactsBatch: '%s'.", batchNumber, json.encode(contactsBatch))
		--logger.debug("[FULLCONTACT:AddContactsBatch] 3--> SaveContactsBatch - failedContactsList: '%s'.", json.encode(failedContactsList))

		--1. envio a guardar el batch de N contactos
		--
		local success, result, detail = m_dialer:SaveContactsBatch(vcc, contactsBatch, importInfo)
		--logger.debug("[FULLCONTACT:AddContactsBatch]  -- SaveContactsBatch - success '%s' result: '%s' detail: '%s'", success, json.encode(result or {}), json.encode(detail or {}))
		if success then
			-- 2. si es ok, actualiza los contadores (created, updated y errors + los invalid para descargar el CSV)
			--
			--logger.debug("[FULLCONTACT:AddContactsBatch]  success --> result: '%s'.", json.encode(result))
			--
			local previousContactHasDeleted = {isDeleted = false, index = -1}
			for i, item in ipairs(result.items or {}) do
				--logger.debug("[FULLCONTACT:AddContactsBatch]     %d success --> item: '%s'.", i, json.encode(item))
				if item.delete then
					if (item.delete.result or ""):lower() == "deleted" then
						--logger.debug("[FULLCONTACT:AddContactsBatch]          0.5 - delete (deleted) --> item: '%s'.", i, json.encode(item))
						previousContactHasDeleted.isDeleted = true
						previousContactHasDeleted.index = i
					--else
						--logger.debug("[FULLCONTACT:AddContactsBatch]          0.6 - delete (NO ACTION) --> item: '%s'.", i, json.encode(item))
					end
				elseif item.create then
					if (item.create.result or ""):lower() == "created" then
						if previousContactHasDeleted.isDeleted and previousContactHasDeleted.index == (i -1) then
							-- como contra ES en la actualizacion de un contacto, primero se elimina y se vuelve a crear, tenemos que realizar esta pisada para los contadores.
							--logger.debug("[FULLCONTACT:AddContactsBatch]          1 - create (updated) --> item: '%s'.", i, json.encode(item))
							statusInfo.updated = statusInfo.updated + 1
							previousContactHasDeleted.isDeleted = false
							previousContactHasDeleted.index = -1
						else
							--logger.debug("[FULLCONTACT:AddContactsBatch]          2 - create (added) --> item: '%s'.", i, json.encode(item))
							statusInfo.added = statusInfo.added + 1
						end
					else
						--logger.debug("[FULLCONTACT:AddContactsBatch]          3 - create (error) --> item: '%s'.", i, json.encode(item))
						statusInfo.errors = statusInfo.errors + 1
						InsertFailedContact(csvContactsDataBatch[i])
					end
				elseif item.update then
					if (item.update.result or ""):lower() == "updated" then
						--logger.debug("[FULLCONTACT:AddContactsBatch]          4 - update (updated) --> item: '%s'.", i, json.encode(item))
						statusInfo.updated = statusInfo.updated + 1
					else
						--logger.debug("[FULLCONTACT:AddContactsBatch]          5 - update (error) --> item: '%s'.", i, json.encode(item))
						statusInfo.errors = statusInfo.errors + 1
						InsertFailedContact(csvContactsDataBatch[i])
					end
				--else
					--logger.debug("[FULLCONTACT:AddContactsBatch]          6 - (NO ACTION) --> item: '%s'.", i, json.encode(item))
				end
			end
			-- caso KEEP OLD
			if detail then
				statusInfo.updated = statusInfo.updated + (detail.updated or 0)
				if type(detail.duplicatedContactsInfo) == "table" then
					table.insert(duplicatedContactsDataBatch, detail.duplicatedContactsInfo)
				end
			end
		else
			-- 3. si es error, actualiza los contadores (errors + los invalid para descargar el CSV)
			--
			statusInfo.errors = statusInfo.errors + #contactsBatch
			for _, csvFailedContact in ipairs(csvContactsDataBatch) do
				InsertFailedContact(csvFailedContact)
			end
		end
		-- 4. actualiza el status de la importacion (redis, cassandra)
		contactsBatch = {}
		csvContactsDataBatch = {}
		model.Dialer:SaveContactImport(importInfo, statusInfo, nil, failedContactsList, keyspace)
		--
		return
	end

	-- invoco a la funcion que valida si la blacklists que ha posteado el usuario, existe en la base de datos.
	local success, blacklists, unrecognizedBlacklists = GetBlackListApi():ValidateBlacklists(vcc, importInfo.Blacklists)

	local currentContactIndex = 0
	local function AddContactToImportBatch (contact, csvContactData)
		local ok, contactId, contactInfo, partialErrors = m_dialer:PrepareContactForBatch(vcc, contact, importInfo, importFormat, csvContactData, blacklists)
		--logger.debug("--> ok %s, contactId: %s of batchNumber: %s", ok, contactId, (batchNumber + 1))
		if ok then
			table.insert(contactsBatch, contactInfo)
			table.insert(csvContactsDataBatch, csvContactData) -- utilizado para el download de failed csv
			if partialErrors then
				InsertFailedContact(csvContactData)
				statusInfo.warnings = statusInfo.warnings + 1
			end
		else
			InsertFailedContact(csvContactData)
			statusInfo.errors = statusInfo.errors + 1
		end
		--logger.debug("[FULLCONTACT:AddContactsBatch] 4--> AddContactToImportBatch - batchNumber: %s', statusInfo.errors: '%s'.", (batchNumber + 1), statusInfo.errors)
		if (batchNumber + 1) == 1 and #contactsBatch == 0 and statusInfo.errors >= MAXIMUM_FIRST_FAILED_CONTACTS_FOR_BREAK_IMPORT then
			-- Si los primero N contactos de la importacion han fallado, debe cancelar la misma.
			--
			logger.warn("[FULLCONTACT:AddContactsBatch]--> BREAK Import '%s'. The first %s contacts have been unsuccessful. BatchNumber: %s", importInfo.ID, MAXIMUM_FIRST_FAILED_CONTACTS_FOR_BREAK_IMPORT, (batchNumber + 1))
			return {status = "BREAK IMPORT", reason = "THE FIRST CONTACTS HAVE BEEN UNSUCCESSFUL", MAXIMUM_FIRST_FAILED_CONTACTS_FOR_BREAK_IMPORT = MAXIMUM_FIRST_FAILED_CONTACTS_FOR_BREAK_IMPORT}
		end
		if #contactsBatch == updateStatusCounter then
			statusInfo.action = "UPDATED CONTACTS: " .. tostring(currentContactIndex)
			-- cuando llega al limite del bulk (ej: 500) envia a guardar.
			SaveContactsBatch()
		end
	end

	local function Start ()
		for i, contact in ipairs(contactsData) do
			currentContactIndex = i
			local csvContactData = parsedRawCSV[i]
			contact.ID = utf8.lower(contact.ID or uuid.new())
			if IMPORTATION_TYPE == "BATCH" then
				local result = AddContactToImportBatch(contact, csvContactData)
				--logger.debug("[FULLCONTACT:AddContactsBatch] --> result: %s", helpers.JSON_Encode(result))
				if result and result.status == "BREAK IMPORT" then
					return result
				end
			else
				assert(false, "DEPRECATED " .. tostring(IMPORTATION_TYPE))
			end
		end
	end

	local result = Start()
	--logger.debug("[FULLCONTACT:AddContactsBatch] --> RESULT: %s", json.encode(result))

	statusInfo.status = "COMPLETE"
	statusInfo.action = "ENDING"
	local success

	if not result or result.status ~= "BREAK IMPORT" then
		success = true
		if IMPORTATION_TYPE ~= "BATCH" then
			assert(false, "DEPRECATED " .. tostring(IMPORTATION_TYPE))
		end
		SaveContactsBatch()
		logger.info("[FULLCONTACT:AddContactsBatch] RESULT: Total DIALER Contacts: %s - newContacts: %s - updatedContacts: %s - contactErrors: %s - contactWarnings: %s", statusInfo.total, statusInfo.added, statusInfo.updated, statusInfo.errors, statusInfo.warnings or "0")
	else
		success = false
		statusInfo.code = -106
		statusInfo.result = result
		model.Dialer:SaveContactImport(importInfo, statusInfo, nil, failedContactsList, keyspace)
		logger.info("[FULLCONTACT:AddContactsBatch] ABORTING IMPORT. statusInfo.result: %s ", json.encode(statusInfo.result or {}))
	end

	keyspace:Close()
	statusInfo.duplicatedContactsDataBatch = duplicatedContactsDataBatch
	return success, statusInfo
end

m_dialer.SearchContacts = function(self, params)
	assert(type(params) == "table", "parmas must be a table")
	assert(type(params.filter) == "table", "params.filter must be a table")
	return model.Dialer:SearchContacts(params)
end

m_dialer.GetContact = function(self, contact_id, vcc, options)
	assert(type(contact_id) == "string", "id must be a string")
	assert(type(vcc) == "string", "vcc must be a string")
	options = options or {}
	contact_id = helpers.AddVCC(contact_id, vcc)
	--logger.debug("[FULLCONATCT.dialer:GetContact] ----> contact_id: %s", contact_id)
	return model.Dialer:GetContact(contact_id, options)
end

m_dialer.GetContactByExternalId = function(self, externalId, vcc, options)
	assert(type(externalId) == "string", "externalId must be a string")
	assert(type(vcc) == "string", "vcc must be a string")
	options = options or {}
	--logger.debug("[FULLCONATCT.dialer:GetContactByExternalId] ----> externalId: %s", externalId)
	return model.Dialer:GetContactByExternalId(externalId, vcc, options)
end

--[[

Los diferentes tipos de categorias para los campos
[
  {
    "Name": "ACCOUNTGROUP",
    "Category": "FIELDTYPES",
    "Value": "6",
    "DisplayCategory": "FIELDTYPES",
    "Domain": "CONTACTIMPORT",
    "DisplayName": "Grupo de cuenta"
  },
  {
    "Name": "ACCOUNTOFFICER",
    "Category": "FIELDTYPES",
    "Value": "5",
    "DisplayCategory": "FIELDTYPES",
    "Domain": "CONTACTIMPORT",
    "DisplayName": "Oficial de cuenta"
  },
  {
    "Name": "ADDRESS",
    "Category": "FIELDTYPES",
    "Value": "8",
    "DisplayCategory": "FIELDTYPES",
    "Domain": "CONTACTIMPORT",
    "DisplayName": "Dirección"
  },
  {
    "Name": "CAMPAIGN",
    "Category": "FIELDTYPES",
    "Value": "4",
    "DisplayCategory": "FIELDTYPES",
    "Domain": "CONTACTIMPORT",
    "DisplayName": "Campaña"
  },
  {
    "Name": "CATEGORY",
    "Category": "FIELDTYPES",
    "Value": "3",
    "DisplayCategory": "FIELDTYPES",
    "Domain": "CONTACTIMPORT",
    "DisplayName": "Categoría"
  },
  {
    "Name": "EMAIL",
    "Category": "FIELDTYPES",
    "Value": "9",
    "DisplayCategory": "FIELDTYPES",
    "Domain": "CONTACTIMPORT",
    "DisplayName": "Correo electrónico"
  },
  {
    "Name": "ID",
    "Category": "FIELDTYPES",
    "Value": "0",
    "DisplayCategory": "FIELDTYPES",
    "Domain": "CONTACTIMPORT",
    "DisplayName": "Identificador"
  },
  {
    "Name": "NAME",
    "Category": "FIELDTYPES",
    "Value": "2",
    "DisplayCategory": "FIELDTYPES",
    "Domain": "CONTACTIMPORT",
    "DisplayName": "Nombre"
  },
  {
    "Name": "NAMEVALUE",
    "Category": "FIELDTYPES",
    "Value": "11",
    "DisplayCategory": "FIELDTYPES",
    "Domain": "CONTACTIMPORT",
    "DisplayName": "Nombre-Valor"
  },
  {
    "Name": "PHONEDATA",
    "Category": "FIELDTYPES",
    "Value": "7",
    "DisplayCategory": "FIELDTYPES",
    "Domain": "CONTACTIMPORT",
    "DisplayName": "Datos del teléfono"
  },
  {
    "Name": "PREDEFINEDDATA",
    "Category": "FIELDTYPES",
    "Value": "10",
    "DisplayCategory": "FIELDTYPES",
    "Domain": "CONTACTIMPORT",
    "DisplayName": "Datos predefinidos"
  },
  {
    "Name": "SCHEDULED CALL",
    "Category": "FIELDTYPES",
    "Value": "12",
    "DisplayCategory": "FIELDTYPES",
    "Domain": "CONTACTIMPORT",
    "DisplayName": "Llamada programada"
  },
  {
    "Name": "SKILL",
    "Category": "FIELDTYPES",
    "Value": "14",
    "DisplayCategory": "FIELDTYPES",
    "Domain": "CONTACTIMPORT",
    "DisplayName": "Habilidad"
  }
]
]]--
m_dialer.StartImport = function(self, importInfo, csvContent, onImported)
	assert(type(importInfo) == "table", "vcc must be a table")
	assert(type(importInfo.VCC) == "string", "vcc must be a string")
	assert(type(csvContent) == "string", "csvContent data must be a string")
	assert(type(importInfo.FormatId) == "string", "FormatId data must be a string")
	onImported = onImported or function() return true end

	local msg = string.format("[FULLCONTACT:dialer.StartImport] + STARTING IMPORTATION TYPE '%s' - ADD DIALER CONTACTS BATCH FROM CSV", IMPORTATION_TYPE)
	local processId = helpers.elapsedTime:Start(msg)

	helpers.memory:ShowMemory()

	local vcc = importInfo.VCC
	local canAdd, err = m_dialer.CanStartImport(m_dialer, importInfo)
	if not canAdd then
		return false, err
	end

	--logger.debug("")
	--logger.debug("0 - importInfo: " .. helpers.JSON_Encode(importInfo))

	--logger.debug("")
	--logger.debug("1 - csv content: " .. csvContent)

	-- start parsing in validating
	local alreadyExistImport = self:GetImportation(importInfo.ID, vcc)
	logger.info("[FULLCONTACT:dialer.StartImport]   -> ImportId '%s@%s', already exist: '%s'.", importInfo.ID, vcc, alreadyExistImport)
	if importInfo.isNew and alreadyExistImport then
		helpers.memory:CollectAllMemory()
		local reason = string.format("UNABLE TO CREATE IMPORTATION '%s' - ALREADY EXIST.", importInfo.ID)
		local err = {reason = reason, type = "UNABLE TO CREATE IMPORTATION - ALREADY EXIST"}
		logger.error("[FULLCONTACT:dialer.StartImport] Error Starting Importation. Err: %s", err.reason)
		return false, err
	end

	local csvHeader = helpers.csv:ParseCSVHeader(csvContent)
	if not csvHeader then
		helpers.memory:CollectAllMemory()
		local reason = "ERROR PARSING CSV HEADER"
		local err = {reason = reason, type = reason}
		logger.error("[FULLCONTACT:dialer.StartImport] Error Starting Importation. Err: %s", err.type)
		return false, err
	end

	--logger.debug("")
	--logger.debug("2 -csvHeader: " .. helpers.JSON_Encode(csvHeader))

	local importFormatId = importInfo.FormatId
	local success, importFormat = self:GetImportFormat(importFormatId, vcc)
	if not success then
		helpers.memory:CollectAllMemory()
		local reason = string.format("IMPORT FORMAT DOES NOT EXIST %s", importFormatId)
		local err = {reason = reason, type = "IMPORT FORMAT DOES NOT EXIST"}
		logger.error("[FULLCONTACT:dialer.StartImport] Error Starting Importation. Err: %s", err.reason)
		return false, err
	end
	if type(importFormat.ContactImportFormatFields) ~= "table" or #importFormat.ContactImportFormatFields == 0 then
		helpers.memory:CollectAllMemory()
		local reason = string.format("INVALID IMPORT FORMAT %s. MISSING FIELDS.", importFormatId)
		local err = {reason = reason, type = "INVALID IMPORT FORMAT - MISSING FIELDS"}
		logger.error("[FULLCONTACT:dialer.StartImport] Error Starting Importation. Err: %s", err.reason)
		return false, err
	end

	--logger.debug("")
	--logger.debug("2.1   - (array) importFormat.      : " .. helpers.JSON_Encode(importFormat.ContactImportFormatFields))

	--[[
		Funcion que transforma la key del CSV a mayusculas.
	]]--
	local function TransformKeyFn(item, key)
		if type(key) == "string" then
			key = key:upper()
		end
		return key
	end
	local mappingFields = helpers.ArrayToHash(importFormat.ContactImportFormatFields, "Name", TransformKeyFn)
	importFormat.mappingFields = mappingFields

	--logger.debug("3   - (hash) mappingFields      : " .. helpers.JSON_Encode(mappingFields))

	logger.info("[FULLCONTACT:dialer.StartImport]    + START Parsing & Validating CSV.")
	local ROWS_PER_BULK = 500
	local logCsvRow = ROWS_PER_BULK

	local statusInfo = {
		status = "RUNNING",
		action = "PARSING & VALIDATING CSV",
		parsed = 0,
		added = 0,
		updated = 0,
		errors = 0,
		deleted = 0,
		total = 0
	}
	local failedContactsList = {}
	model.Dialer:SaveContactImport(importInfo, statusInfo, nil, failedContactsList)
	--

	--[[ -------------------------------------------------------------------------------------------
		Funcion invocada para convertir una linea del CSV a un item de la tabla.
	------------------------------------------------------------------------------------------- ]]--
	local function ProcessLineCsv2Table (csvFieldName, csvFieldValue, rawRow, fieldInfo, contact)
		local categoryType = tostring(fieldInfo.Type):upper()
		--logger.debug("1 -    [ProcessLineCsv2Table]  categoryType: '%s' categoryTypeName: '%s' fieldInfo: " .. categoryType, fieldInfo.TypeName, helpers.JSON_Encode(fieldInfo))

		-- "PHONEDATA" (7), "EMAIL" (9)
		if categoryType == "7" or categoryType == "9" then
			if #csvFieldValue == 0 then
				return nil
			end
			local addressInfo = {
				address        = csvFieldValue,
				addressSubType = fieldInfo.ContactAddressType
			}
			if categoryType == "7" then
			-- "PHONEDATA" (7)
				--logger.debug("[FULLCONTACT:dialer.StartImport] - Creo PHONEDATA")
				addressInfo.type = "PHONE"
				local phoneNumber = csvFieldValue
				local isValidPhone, response = helpers.validation:IsPhone(phoneNumber, fieldInfo.PhoneCountry, fieldInfo.PhoneArea, vcc)
				if isValidPhone then
					-- la validacion que retorna "lua_telephonyproxyclient":
						-->  response:  {"number":"1234","dialLocal":"1234","dialGlobal":"5981234","dialSameCountry":"1234","countryCode":"598","areaCode":""}
					-- los datos como los espera "address_mapper.lua"
					addressInfo.Number = response.dialGlobal
					addressInfo.LocalNumber = response.dialLocal
					addressInfo.Country = response.Country --or fieldInfo.PhoneCountry
					addressInfo.Area = response.Area -- or fieldInfo.PhoneArea
					addressInfo.SimpleNumber = response.number
				end
			else
			-- "EMAIL" (9)
				--logger.debug("[FULLCONTACT:dialer.StartImport] - Creo EMAIL")
				addressInfo.type = "MAIL"
			end
			-- Agregamos los channels soportados por el Address (CALL, SMS, ETC)
			--   En principio unicamente seria soportado por PHONE, pero lo dejamos aca por si hay que implementarlo para los demas canales.
			--logger.debug("[FULLCONTACT:dialer.StartImport] ----> fieldInfo.Channels: %s", json.encode(fieldInfo.Channels or {}))
			addressInfo.Channels = fieldInfo.Channels or nil
			-- Creamos el array de addresses
			contact.addresses = contact.addresses or {}
			table.insert(contact.addresses, addressInfo)
			return nil

		-- "ADDRESS" (DIRECCION)
		elseif categoryType == "8" then
			--logger.debug("[FULLCONTACT:dialer.StartImport] - Creo ADDRESS")
			contact.homeAddresses = contact.homeAddresses or {}
			table.insert(contact.homeAddresses, {
				value          = csvFieldValue,
				addressSubType = fieldInfo.ContactAddressType,
				addressArea    = fieldInfo.AddressArea,
				addressCountry = fieldInfo.AddressCountry
			})
			return nil

		--NAME VALUE
		elseif categoryType == "11" then
			--logger.debug("[FULLCONTACT:dialer.StartImport] - Creo NAMEVALUE")
			local nameValueInfo = {
				name  = utf8.lower(helpers.alltrim(csvFieldName)),
				value = csvFieldValue
			}
			-- NAME VALUE SEARCHABLE
			if fieldInfo.IsSearchable then
				-- NAME VALUE SEARCHABLE DATA TYPE
				local searchableDataType = (fieldInfo.SearchableDataType or ""):upper()

				if searchableDataType == "DATE" then
					local date = nameValueInfo.value
					local isValidDate, fullDate = helpers.validation:IsDate(date)
					if isValidDate then
						nameValueInfo.value = fullDate
					else
						nameValueInfo.value = date
					end
					contact.nameValuesSearchDate = contact.nameValuesSearchDate or {}
					table.insert(contact.nameValuesSearchDate, nameValueInfo)

				elseif searchableDataType == "NUMERIC" then
					contact.nameValuesSearchNumeric = contact.nameValuesSearchNumeric or {}
					table.insert(contact.nameValuesSearchNumeric, nameValueInfo)

				elseif searchableDataType == "TEXT" then
					contact.nameValuesSearchText = contact.nameValuesSearchText or {}
					table.insert(contact.nameValuesSearchText, nameValueInfo)

				else
					logger.warn("[FULLCONATCT.dialer:StartImport] Unable to import CSV Value: '%s'. Unrecognized searchable data type: '%s' in format: '%s'.", helpers.JSON_Encode(nameValueInfo), searchableDataType, importFormatId)
				end

				return nil
			else
				-- NAME VALUE NORMAL
				contact.nameValuesSimple = contact.nameValuesSimple or {}
				table.insert(contact.nameValuesSimple, nameValueInfo)
				return nil
			end

		-- "SCHEDULED CALL"
		elseif categoryType == "12" then
			--logger.debug("[FULLCONTACT:dialer.StartImport] - Creo SCHEDULED CALL")
			local date = csvFieldValue or ""
			local isValidDate, fullDate = helpers.validation:IsDate(date)
			if isValidDate then
				contact.scheduledCall = fullDate
			else
				contact.scheduledCall = date
			end
			return nil

		-- "SKILL"
		elseif categoryType == "14" then
			--logger.debug("[FULLCONTACT:dialer.StartImport] - Creo SKILL")
			contact.skills = contact.skills or {}
			table.insert(contact.skills, {
				value          = csvFieldValue,
				SkillId        = fieldInfo.SkillType,
				SkillReqColumn = fieldInfo.SkillReqColumn,
				SkillReqValue  = rawRow[fieldInfo.SkillReqColumn],
				Mode           = (fieldInfo.Mode or "SORT"):upper() -- SORT PREFERRED REQUIRED
			})
			return nil
		end

		--logger.debug("[FULLCONTACT:dialer.StartImport] - simple category type '%s' fieldInfo.TypeName: %s'", categoryType, fieldInfo.TypeName)

		-- "ID" (0), "EXTERNAL ID" (5)
		contact[fieldInfo.TypeName] = csvFieldValue

		return nil
	end

	--[[ -------------------------------------------------------------------------------------------
		Comienza a convertir el CSV a Tabla de LUA.
	------------------------------------------------------------------------------------------- ]]--
	local parsedCSV, parsedRawCSV = helpers.csv:Csv2Table(csvContent, function(csvFieldName, csvFieldValue, contact, rowIndex, columnIndex, rawRow)
		csvFieldName = helpers.toUTF8(csvFieldName)
		csvFieldValue = helpers.toUTF8(csvFieldValue)

		local fieldInfo = mappingFields[helpers.alltrim(utf8.upper(csvFieldName))]
		csvFieldValue = helpers.alltrim(csvFieldValue)

		if rowIndex == logCsvRow then
			logger.info("[FULLCONTACT:dialer.StartImport]         -> " .. tostring(rowIndex) .. " rows validated of CSV.")
			logCsvRow = logCsvRow + ROWS_PER_BULK
			--
			local currentProcessBatch = m_dialer.GetVCCStatusProcessBatch(importInfo.VCC)
			if currentProcessBatch then
				currentProcessBatch.parsed = (currentProcessBatch.parsed or 0) + ROWS_PER_BULK
				helpers.dialer.UpdateVCCStatusProcessBatch(currentProcessBatch)
			end
		end

		--logger.debug("1 - line: " .. tostring(rowIndex) .. " -  [Csv2Table] csvFieldName: ".. csvFieldName .. ", csvFieldValue: " .. tostring(csvFieldValue) .. ", fieldInfo: " .. tostring(fieldInfo))
		--logger.debug("1.1 - temporal contact: " .. helpers.JSON_Encode(contact))
		--logger.debug("1.2 - rawRow: " .. helpers.JSON_Encode(rawRow))
		--logger.debug("-------------------------------------")

		if not fieldInfo then
			-- Si la columna del CSV, NO es una columna valida
			return
		end
		--[[
			Pueden venir lineas simples o mulipes.

			- (1 a 1)      Las lineas simples son utilizadas cuando se ha mapeado una columna del CSV a una columna del formato de importacion .
			- (1 a varios) Las lineas multiples son utilizadas cuando se ha mapeado una misma columna del CSV a varios columnas del formato de importacion.
		]]--
		if fieldInfo.isMulti then
			--LogDebug("YES multi")
			for _, multiFieldInfo in ipairs(fieldInfo.list) do
				ProcessLineCsv2Table(csvFieldName, csvFieldValue, rawRow, multiFieldInfo, contact)
			end
		else
			--LogDebug("NO multi")
			return ProcessLineCsv2Table(csvFieldName, csvFieldValue, rawRow, fieldInfo, contact)
		end
	end)

	logger.info("[FULLCONTACT:dialer.StartImport]    - END Parsed & Validated CSV. Total Rows: " .. tostring(#parsedCSV))

	--logger.debug("4 ------------> despues helpers.csv:Csv2Table - parsedCSV: %s, parsedRawCSV: %s", parsedCSV, parsedRawCSV)
	--logger.debug("4.01 ---------> despues helpers.csv:Csv2Table - parsedCSV: %s", helpers.JSON_Encode(parsedCSV))
	--logger.debug("4.02 ---------> despues helpers.csv:Csv2Table - parsedRawCSV: %s", helpers.JSON_Encode(parsedRawCSV))

	--[[ -------------------------------------------------------------------------------------------
		asfadfasdf
	------------------------------------------------------------------------------------------- ]]--
	-- Los contactos parseados del CSV en una tabla
	local contacts = parsedCSV
	importInfo.totalContacts = #contacts
	local canAdd, err = m_dialer.ValidateContactLimitsInBatch(m_dialer, importInfo)
	if not canAdd then
		if not alreadyExistImport then
			--logger.debug("4.1 ------------> Removing aborted New Importation: %s@s", importInfo.ID, vcc)
			local currentProcessBatch = m_dialer.GetVCCStatusProcessBatch(importInfo.VCC)
			if currentProcessBatch then
				currentProcessBatch.Status = "COMPLETED"
				helpers.dialer.UpdateVCCStatusProcessBatch(currentProcessBatch)
			end
			m_dialer:RemoveImport(importInfo.ID, vcc)
		end
		return false, err
	end

	--logger.debug("5 ------------> PARSED Contacts: " .. helpers.JSON_Encode(contacts or {}))
	local success, response = m_dialer.AddContactsBatch(m_dialer, importInfo, contacts, parsedRawCSV, importFormat)
	--logger.debug("6 ------------> processId: " .. processId)

	if success then
		-- creo los contactos a recargar contra el OEClient
		m_dialer.MakeContactsToReload(m_dialer, response.duplicatedContactsDataBatch, function(success, contactsByProcesses)
			if not success then
				return
			end
			-- invoco al callback los contactos que ya existente en la BD que se encuentren asociados a procesos.
			--  con ellos el controller va a invocar al OEClient para que recargue los mismos.
			onImported(contactsByProcesses)
		end)
	end

	local msg = "[FULLCONTACT:dialer.StartImport] - END IMPORTATION - ADD DIALER CONTACTS BATCH FROM CSV"
	helpers.elapsedTime:End(processId, msg)
	helpers.memory:CollectAllMemory()

	return success, response
end

--[[
	Funcion que recibe en batch los contactos duplicados que ya existian en la Bd en la importacion.
	Con ellos los va a agrupar por procesos para retornarlos en el callback hacia la app "outbound_engine",
		luego esta app va a invocar al OEClient para que recargue estos contactos.
]]--
m_dialer.MakeContactsToReload = function(self, duplicatedContactsDataBatch, onMaked)
	logger.info("[FULLCONTACT:dialer.MakeContactsToReload] - Making Contacts To Reload. Batchs: '%s'.", #duplicatedContactsDataBatch)
	--logger.debug("[FULLCONTACT:dialer.MakeContactsToReload] ---> Making Contacts To Reload '%s' onMaked: %s'", helpers.JSON_Encode(duplicatedContactsDataBatch), onMaked)
	if type(duplicatedContactsDataBatch) ~= "table" or #duplicatedContactsDataBatch == 0 then
		return false
	end
	local totalProcess = 0
	local totalContactsByProcesses = {}
	local contactsByProcesses = {}
	for _, contactsBatch in ipairs(duplicatedContactsDataBatch) do
		for _, contactInfo in ipairs(contactsBatch) do
			local contact = contactInfo.duplicatedContact or {}
			if contact.OutboundProcessId then
				local processId = contact.OutboundProcessId
				contactsByProcesses[processId] = contactsByProcesses[processId] or {}
				table.insert(contactsByProcesses[processId], {
					id = contact.Id
				})
				-- totales (solo para los logs)
				if not totalContactsByProcesses[processId] then
					totalProcess = totalProcess + 1
					totalContactsByProcesses[processId] = 0
				end
				totalContactsByProcesses[processId] = totalContactsByProcesses[processId] + 1
			end
		end
	end
	logger.info("[FULLCONTACT:dialer.MakeContactsToReload] - Maked contacts to reload. totalProcess: %s, totalContactsByProcesses: %s.", totalProcess, helpers.JSON_Encode(totalContactsByProcesses))
	--logger.debug("[FULLCONTACT:dialer.MakeContactsToReload] --> Maked contactsByProcesses: %s'", helpers.JSON_Encode(contactsByProcesses))
	onMaked(true, contactsByProcesses)
end

--[[
]]--
m_dialer.ValidatePhone = function(self, number, country, area, vcc, withTimezone, withTrace)
	return helpers.validation:IsPhone(number, country, area, vcc, withTimezone, withTrace)
end

--[[
]]--
m_dialer.IsGlobalPhone = function(self, number, country, area)
	return helpers.validation:IsGlobalPhone(number, country, area)
end

--[[
]]--
m_dialer.AddContactToES = function(self, contact)
	local elasticType = "CONTACTS_DIALER"
	return AddContactToES(contact, elasticType)
end

m_dialer.GetVCCStatusProcessBatch = function(vcc)
	assert(type(vcc) == "string", "vcc must be a string")
	-- obtengo el current process de REDIS
	local encodedCurrentProcessBatch = helpers.dialer.GetVCCStatusProcessBatch(vcc)
	if not encodedCurrentProcessBatch then
		return
	end
	local currentProcessBatch = helpers.JSON_Decode(encodedCurrentProcessBatch)
	return currentProcessBatch, encodedCurrentProcessBatch
end

-- FORMATOS DE IMPORTACION .........................................................
--

--[[
]]--
m_dialer.GetImportFormat = function(self, importFormatId, vcc)
	assert(type(importFormatId) == "string", "importFormatId must be a string")
	assert(type(vcc) == "string", "vcc must be a string")
	importFormatId = helpers.AddVCC(importFormatId, vcc)
	local success, importFormatInfo = model.Dialer.GetImportFormat(importFormatId)
	if not success then
		return false
	end
	return true, importFormatInfo
end

--[[
	Definicion de total de NameValuesSearcheabes permitidos por tipo (date, numeric, text) que se podran almacenar en ES.
]]--
m_dialer.GetAllowedNameValuesSearchByType = function(toMigrateAllegro)
	-- tabla utilizada con los topes máximos de namevalue searchables
	if toMigrateAllegro then
		-- allegro
		return {
			DATE   = {maxAllowed = 5, total = 0},
			NUMBER = {maxAllowed = 5, total = 0},
			STRING = {maxAllowed = 5, total = 0}
		}
	end
	-- nativo
	return {
		DATE    = {maxAllowed = 5, total = 0},
		NUMERIC = {maxAllowed = 5, total = 0},
		TEXT    = {maxAllowed = 5, total = 0}
	}
end

--[[
]]--
m_dialer.SaveImportFormat = function(self, importFormatInfo, ksOE)
	assert(type(importFormatInfo) == "table", "importFormatInfo must be a table")
	--logger.debug("[FULLCONATCT.m_dialer:SaveImportFormat] --> Saving importFormatInfo: %s.", helpers.JSON_Encode(importFormatInfo or {}))
	local success, data = m_dialer.Data:LoadImportFieldsTypes({returnType = "INDEXED LIST"})
	if success and data.indexedList then
		-- tabla utilizada con los topes máximos de namevalue searchables
		local nameValuesSearchByType = m_dialer.GetAllowedNameValuesSearchByType(false)
		--[[
			Funcion que valida si el Campo del formato puede o no ser incluido.
		]]--
		local function CanSaveImportFormat (importFormat)
			local importType = tostring(importFormat.Type)
			-- "NAME VALUE" (11)
			if importType == "11" then
				local searchableDataType = tostring(importFormat.SearchableDataType):upper()
				local info = nameValuesSearchByType[searchableDataType]
				if info then
					-- NAME VALUE SEARCHEABLE
					info.total = info.total + 1
					if info.total > info.maxAllowed then
						-- error max allow
						local reason = string.format("Only %s searchable name-value fields of type %s are allowed.", info.maxAllowed, searchableDataType)
						local err = {reason = reason, type = "NAME VALUE SEARCHABLE - EXCEEDED MAX ALLOWED", info = {
							searchableDataType = searchableDataType,
							maxAllowed = info.maxAllowed
						}}
						logger.error("[FULLCONTACT:dialer.SaveImportFormat] Unable to Save Format '%s' vcc '%s'. Err: %s", importFormatInfo.Id, importFormatInfo.VCC, reason)
						return false, err
					end
				end
			end
			-- ok
			return true
		end
		-- se le agregan atributos server side.
		for _, importFormat in ipairs(importFormatInfo.ContactImportFormatFields or {}) do
			local canSave, err = CanSaveImportFormat(importFormat)
			if not canSave then
				-- Si falla no permite guardar Ningun campo del formato
				return false, err
			end
			local importType = tostring(importFormat.Type)
			if importFormat.Name then
				importFormat.Name = helpers.alltrim(importFormat.Name)
			end
			if data.indexedList[importType] then
				importFormat.TypeName = data.indexedList[importType].Name
			else
				logger.warn("[FULLCONATCT.m_dialer:SaveImportFormat] Import Type '%s' does not exist in indexed list.", importType)
			end
			if importType == "7" then -- PHONEDATA
				importFormat.Channels = importFormat.Channels or {"CALL"} -- Valor por defecto (utilizando en las migraciones de allegro)
			end
		end
	end
	-- paso la validacion, puede almacenar
	local success, importFormatId, err = model.Dialer.SaveImportFormat(importFormatInfo, ksOE)
	if not success then
		return false, err
	end
	return true, importFormatId
end

--[[
]]--
m_dialer.RemoveImportFormat = function(self, importFormatId, vcc)
	assert(type(importFormatId) == "string", "importFormatId must be a string")
	assert(type(vcc) == "string", "vcc must be a string")
	importFormatId = helpers.AddVCC(importFormatId, vcc):lower()
	local success, err = model.Dialer.RemoveImportFormat(importFormatId)
	if not success then
		return false, err
	end
	return true
end

--[[
]]--
m_dialer.ListImportFormat = function(self, params)
	assert(type(params) == "table", "parmas must be a table")
	return model.Dialer.ListImportFormat(params)
end

-- IMPORTACIONES .........................................................
--

--[[
]]--
m_dialer.ListImportations = function(self, params)
	assert(type(params) == "table", "parmas must be a table")
	return model.Dialer.ListImportations(params)
end

m_dialer.GetImportation = function(self, importId, vcc)
	assert(type(importId) == "string", "importId must be a string")
	assert(type(vcc) == "string", "vcc must be a string")
	importId = helpers.AddVCC(importId, vcc)
	local success, importInfo = model.Dialer.GetImportation(importId)
	if not success then
		return false
	end
	return true, importInfo
end

--[[
]]--
m_dialer.RemoveImport = function(self, importId, vcc, userId)
	assert(type(importId) == "string", "importId must be a string")
	assert(type(vcc) == "string", "vcc must be a string")
	importId = helpers.AddVCC(importId, vcc):lower()
	helpers.memory:ShowMemory()

	local msg = string.format("[FULLCONTACT:dialer.RemoveImport] + STARTING REMOVING IMPORTATION '%s'", importId)
	local processId = helpers.elapsedTime:Start(msg)

	local success, err = model.Dialer.RemoveImport(importId, userId)

	local msg = string.format("[FULLCONTACT:dialer.RemoveImport] - END REMOVING IMPORTATION '%s'", importId)
	helpers.elapsedTime:End(processId, msg)

	helpers.memory:CollectAllMemory()
	if not success then
		return false, err
	end
	return true
end

--[[
]]--
m_dialer.DepurateImport = function(self, importId, vcc, userId)
	assert(type(importId) == "string", "importId must be a string")
	assert(type(vcc) == "string", "vcc must be a string")
	importId = helpers.AddVCC(importId, vcc):lower()
	helpers.memory:ShowMemory()
	local success, result = model.Dialer.DepurateImport(importId, userId)
	helpers.memory:CollectAllMemory()
	if not success then
		local err = result
		return false, err
	end
	return true, result
end

m_dialer.GetImportFailedContacts = function(self, importId, vcc, updateId)
	assert(type(importId) == "string", "importId must be a string")
	assert(type(vcc) == "string", "vcc must be a string")
	assert(type(updateId) == "string", "updateId must be a string")
	importId = helpers.AddVCC(importId, vcc)
	helpers.memory:ShowMemory()
	local success, importInfo = model.Dialer.GetImportation(importId)
	if not success then
		local err = {
			result = "IMPORT DOES NOT EXIST",
			importId = importId,
			type = "1"
		}
		return false, err
	end
	if #(importInfo.Detail or {}) == 0 then
		local err = {
			result = "IMPORT NOT HAS UPDATES",
			importId = importId,
			type = "2"
		}
		return false, err
	end
	updateId = updateId:lower()
	local currentUpdate
	for _, detail in ipairs(importInfo.Detail) do
		if (detail.DetailId or ""):lower() == updateId then
			currentUpdate = detail
			break
		end
	end
	if not currentUpdate then
		local err = {
			result = "IMPORT UPDATE DOES NOT EXIST",
			importId = importId,
			updateId = updateId,
			type = "3"
		}
		return false, err
	end
	-- obtengo los failed contacts de una actualización específica de la importación.
	local success, failedContactsList = model.Dialer.GetImportFailedContacts(importId, updateId)
	if not success or helpers.TableIsEmpty(failedContactsList) then
		local err = {
			result = "IMPORT UPDATE NOT HAS FAILED CONTACTS",
			importId = importId,
			updateId = updateId,
			type = "4"
		}
		return false, err
	end
	-- ok
	local csvFailedContacts = helpers.csv:Table2Csv(failedContactsList)
	helpers.memory:CollectAllMemory()
	local info = {
		updateInfo         = currentUpdate,
		failedContactsList = failedContactsList,
		csvFailedContacts  = csvFailedContacts,
		csvSize            = #csvFailedContacts
	}
	return true, info
end

-------------------------------------------------------
-- DIALER REALTIME
-------------------------------------------------------

--[[
]]--
m_dialer.Realtime = {}

--[[
]]--
m_dialer.Realtime.IsAnyProcessBatchInProgressInVCC = function (self, vcc)
	local isRunning = helpers.dialer.IsAnyProcessBatchInProgressInVCC(vcc)
	logger.info("[FULLCONTACT:IsAnyProcessBatchInProgressInVCC] Is Any Process Batch In Progress In VCC: %s = %s", vcc, isRunning)
	return {isRunning = isRunning}
end
-------

-------------------------------------------------------
-- DIALER DATA
-------------------------------------------------------
--[[
]]--
m_dialer.Data = {}

m_dialer.Data.m_cachedJsonFiles = {}

m_dialer.Data.Modules = {
	addressDestinations  = require "fullcontact.data.addressDestinations",
	contactDestinations  = require "fullcontact.data.contactDestinations",
	customColumns        = require "fullcontact.data.customColumns",
	destinations         = require "fullcontact.data.destinations",
	fieldsTypes          = require "fullcontact.data.fieldsTypes",
	mailDestinations     = require "fullcontact.data.mailDestinations",
	reserverdWords       = require "fullcontact.data.reserverdWords",
	responsesByChannel   = require "fullcontact.data.responsesByChannel",
	schedulesDateFormats = require "fullcontact.data.schedulesDateFormats",
	timezones            = require "fullcontact.data.timezones"
}

m_dialer.Data.LoadJsonDataFile = function (self, fileName)
	assert(type(fileName) == "string")
	assert(m_dialer.Data.Modules[fileName])
	--logger.debug("------------> 1 fileName: %s", fileName)
	if m_dialer.Data.m_cachedJsonFiles[fileName] then
		return true, m_dialer.Data.m_cachedJsonFiles[fileName]
	end
	local m = m_dialer.Data.Modules[fileName]
	local jsonData = m.Load()
	--logger.debug("------------> 2 jsonData: %s", jsonData)
	m_dialer.Data.m_cachedJsonFiles[fileName] = {
		jsonData = jsonData,
		data     = helpers.JSON_Decode(jsonData)
	}
	return true, m_dialer.Data.m_cachedJsonFiles[fileName]
end

--[[
]]--
m_dialer.Data.Load = function(self, fileName)
	local success, err = m_dialer.Data:LoadJsonDataFile(fileName)
	if not success then
		return false, err
	end
	local data = err
	return true, data
end

--[[
]]--
m_dialer.Data.LoadReservedWords = function(self)
	local fileName = "reserverdWords"
	return m_dialer.Data:Load(fileName)
end

--[[
]]--
m_dialer.Data.LoadScheduledDateFormats = function(self)
	local fileName = "schedulesDateFormats"
	return m_dialer.Data:Load(fileName)
end

--[[
]]--
m_dialer.Data.LoadContactMailDestinations = function(self)
	local fileName = "mailDestinations"
	return m_dialer.Data:Load(fileName)
end

--[[
]]--
m_dialer.Data.LoadImportFieldsTypes = function(self, options)
	options = options or {}
	local fileName = "fieldsTypes"
	local success, data = m_dialer.Data:Load(fileName)
	if not success then
		local err = data
		return false, err
	end
	if options.returnType ~= "INDEXED LIST" then
		return true, data
	end
	-- asdf
	local indexedList = {}
	for _, row in ipairs(data.data) do
		indexedList[tostring(row.Value)] = row
	end
	data.indexedList = indexedList
	--logger.debug("[FULLCONTACT.m_dialer.LoadImportFieldsTypes]----------------------> data.indexedList: %s", json.encode(data.indexedList))
	return true, data
end

--[[
]]--
m_dialer.Data.LoadCustomColumns = function(self)
	local fileName = "customColumns"
	return m_dialer.Data:Load(fileName)
end

--[[
]]--
m_dialer.Data.LoadContactDestinations = function(self)
	local fileName = "contactDestinations"
	return m_dialer.Data:Load(fileName)
end

--[[
]]--
m_dialer.Data.LoadContactAddressDestinations = function(self)
	local fileName = "addressDestinations"
	return m_dialer.Data:Load(fileName)
end

--[[
]]--
m_dialer.Data.LoadResponsesByChannel = function(self)
	local fileName = "responsesByChannel"
	return m_dialer.Data:Load(fileName)
end

--[[
]]--
m_dialer.Data.LoadDestinations = function(self)
	local fileName = "destinations"
	return m_dialer.Data:Load(fileName)
end

--[[
]]--
m_dialer.Data.LoadTimeZones = function(self)
	local fileName = "timezones"
	return m_dialer.Data:Load(fileName)
end

---
-- Dialer All API Methods

function GetDialerApi (self)
	return m_dialer
end

------------------------------------------
-- BLACKLIST API
------------------------------------------

function GetBlackListApi (self)
	return {
		-- indices
		CreateESTemplate               = m_blackList.CreateESTemplate,
		CreateESIndex                  = m_blackList.CreateESIndex,
		DropESIndex                    = m_blackList.DropESIndex,
		GetESIndexName                 = m_blackList.GetESIndexName,
		-- importaciones
		StartImport                    = m_blackList.StartImport,
		RemoveImport                   = m_blackList.RemoveImport,
		GetImportation                 = m_blackList.GetImportation,
		ListImportations               = m_blackList.ListImportations,
		ListVccAndSystemImportations   = m_blackList.ListVccAndSystemImportations,
		GetImportFailedAddresses       = m_blackList.GetImportFailedAddresses,
		-- addresses
		GetAddress                     = m_blackList.GetAddress,
		SearchAddresses                = m_blackList.SearchAddresses,
		-- csv
		GetCsvAllowedColumns           = m_blackList.GetCsvAllowedColumns,
		-- relations
		AddRelatedCampaignsAccounts    = m_blackList.AddRelatedCampaignsAccounts,
		RemoveRelatedCampaignsAccounts = m_blackList.RemoveRelatedCampaignsAccounts,
		-- validation
		ValidateBlacklists             = m_blackList.ValidateBlacklists,
		ValidateAddressInBlackLists    = m_blackList.ValidateAddressInBlackLists,
		-- scope
		MakeScopeInfo                  = m_blackList.MakeScopeInfo,
		-- global / system
		Global = {
			StartImport                = m_blackList.Global.StartImport,
			RemoveImport               = m_blackList.Global.RemoveImport,
			ListImportations           = m_blackList.Global.ListImportations,
			SearchAddresses            = m_blackList.Global.SearchAddresses
		}
	}
end

------------------------------------------
-- ALLEGRO MIGRATIONS API
------------------------------------------

function GetAllegroMigrationsApi (apiName)
	assert(type(apiName) == "string")
	local apiDef = {
		-- contactos
		Contacts = {
			Migrate  = m_migrations.Allegro.Contacts.Migrate,
			Rollback = m_migrations.Allegro.Contacts.Rollback
		},
		-- importaciones
		Imports = {
			Migrate  = m_migrations.Allegro.Imports.Migrate,
			Rollback = m_migrations.Allegro.Imports.Rollback
		},
		-- formatos de importacion
		ImportFormats = {
			Migrate  = m_migrations.Allegro.ImportFormats.Migrate,
			Rollback = m_migrations.Allegro.ImportFormats.Rollback
		}
	}
	local api = apiDef[apiName]
	if not api then
		assert(false, string.format("Allegro API Migration does not exist: %s", apiName))
	end
	return api
end

------------------------------------------
--
-- Exported functions
--
m_fullContactModule = {
	init     = init,
	trusted  = trusted,
	GetModel = function () return model end,
	------------------------------------
	-- Funciones de contactos
	--
	Add                          = AddContact,
	AddContactOnlyValidAddresses = AddContactOnlyValidAddresses,
	Merge                        = MergeContacts,
	Delete                       = DeleteContact,
	Get                          = GetContact,
	GetByAddress                 = GetContactByAddress,
	GetByExternalId              = GetContactByExternalId,
	GetByHandle                  = GetContactByHandle,
	Search                       = SearchContacts,
	BuildAddress                 = helpers.BuildAddressInstance,
	BuildAddressId               = helpers.BuildAddressId,
	GetAvatar                    = GetAvatar,
	SaveAvatar                   = SaveAvatar,
	AddToFavorites               = AddToFavorites,
	RemoveFromFavorites          = RemoveFromFavorites,
	GetUserFavorites             = GetUserFavorites,
	ChangeState                  = ChangeContactState,

	--------------------------------------
	-- Funciones para ser utilizadas desde los migrations o reindex_elasticsearch
	--
	CreateESIndex                  = CreateESIndex,
	DropESIndex                    = DropESIndex,
	CreateESTemplate               = CreateESTemplate,
	GetESIndexName                 = GetESIndexName,
	AddContactToES                 = AddContactToES,
	RemoveContactFromES            = RemoveContactFromES,
	ListAllContactsFromCassandra   = ListAllContactsFromCassandra,
	ListContactsChunkFromCassandra = ListContactsChunkFromCassandra,

	------------------------------------
	-- Funciones de Address Book
	--
	AddressesBook = {
		Get           = m_addressesBook.Get,
		List          = m_addressesBook.List,
		ListByChannel = m_addressesBook.ListByChannel,
		Save          = m_addressesBook.Save,
		Remove        = m_addressesBook.Remove
	},

	------------------------------------
	-- Funciones Dialer Contacts
	--
	Dialer = {
		-- indices
		CreateESTemplate        = m_dialer.CreateESTemplate,
		CreateESIndex           = m_dialer.CreateESIndex,
		DropESIndex             = m_dialer.DropESIndex,
		GetESIndexName          = m_dialer.GetESIndexName,
		-- contactos
		GetContact              = m_dialer.GetContact,
		GetContactByExternalId  = m_dialer.GetContactByExternalId,
		SearchContacts          = m_dialer.SearchContacts,
		AddContactToES          = m_dialer.AddContactToES,
		-- importaciones
		StartImport             = m_dialer.StartImport,
		RemoveImport            = m_dialer.RemoveImport,
		DepurateImport          = m_dialer.DepurateImport,
		GetImportation          = m_dialer.GetImportation,
		ListImportations        = m_dialer.ListImportations,
		GetImportFailedContacts = m_dialer.GetImportFailedContacts,
		-- importaciones blacklists
		UpdateAssociatedBlacklists = m_dialer.UpdateAssociatedBlacklists,
		-- import formats
		GetImportFormat         = m_dialer.GetImportFormat,
		SaveImportFormat        = m_dialer.SaveImportFormat,
		RemoveImportFormat      = m_dialer.RemoveImportFormat,
		ListImportFormat        = m_dialer.ListImportFormat,
		-- PHONE validations
		IsGlobalPhone         	= m_dialer.IsGlobalPhone,
		ValidatePhone         	= m_dialer.ValidatePhone,
		-- realtime (para saber los procesos de importacion en ejecucion)
		Realtime                = {
			IsAnyProcessBatchInProgressInVCC = m_dialer.Realtime.IsAnyProcessBatchInProgressInVCC
		},
		-- data / system dictionary de Allegro
		Data                    = {
			LoadReservedWords              = m_dialer.Data.LoadReservedWords,
			LoadScheduledDateFormats       = m_dialer.Data.LoadScheduledDateFormats,
			LoadContactMailDestinations    = m_dialer.Data.LoadContactMailDestinations,
			LoadImportFieldsTypes          = m_dialer.Data.LoadImportFieldsTypes,
			LoadCustomColumns              = m_dialer.Data.LoadCustomColumns,
			LoadContactDestinations        = m_dialer.Data.LoadContactDestinations,
			LoadContactAddressDestinations = m_dialer.Data.LoadContactAddressDestinations,
			LoadResponsesByChannel         = m_dialer.Data.LoadResponsesByChannel,
			LoadDestinations               = m_dialer.Data.LoadDestinations,
			LoadTimeZones                  = m_dialer.Data.LoadTimeZones
		},
		-- Dialer All API Methods
		GetDialerApi = GetDialerApi,
		-- BlackList API
		GetBlackListApi = GetBlackListApi,
		-- Migrations (Allegro)
		GetAllegroMigrationsApi = GetAllegroMigrationsApi -- Allegro.MigratContactsBatch & Allegro.RollbackContactsBatch
	}
}

return m_fullContactModule