local http = require "socket.http"
local ltn12 = require "ltn12"
local logger = require "fullcontact.logger"
local json = require "cjson"
local utf8 = require "icu.utf8"
local helpers = require "fullcontact.helpers"
local AddressMapper = require "fullcontact.address_mapper"

local m_dialer = {}
local m_blackList = {}

local m_elasiticInfo = {
	contacts = {
		TYPE          = "CONTACTS",
		INDEX_NAME    = "vcc_contacts",
		TEMPLATE_NAME = "vcc_contacts-*",
		TEMPLATE_URL  = "/_template/inconcert_tpl_vcc_contacts",
		connection    = nil,
		MakeTemplate  = nil,
		FieldsKeys = {
			addressNumber = "addresses.SanitizedId",
			addressType   = "addresses.Type",
			path          = "addresses"
		}
	},
	contactsDialer = {
		TYPE          = "CONTACTS_DIALER",
		INDEX_NAME    = "vcc_contacts_dialer",
		TEMPLATE_NAME = "vcc_contacts_dialer-*",
		TEMPLATE_URL  = "/_template/inconcert_tpl_vcc_contacts_dialer",
		connection    = nil,
		MakeTemplate  = nil,
		FieldsKeys = {
			addressNumber = "Addresses.Number",
			addressType   = "Addresses.Type",
			path          = "Addresses"
		}
	},
	blacklistDialer = {
		TYPE          = "BLACKLIST_DIALER",
		INDEX_NAME    = "blacklist_dialer",
		TEMPLATE_NAME = "blacklist_dialer",
		TEMPLATE_URL  = "/_template/inconcert_tpl_blacklist_dialer",
		connection    = nil,
		MakeTemplate  = nil,
		FieldsKeys = {}
	}
}

local function SetConnection (connection)
	assert(type(connection) == "table", "connection is missing")
	assert(type(connection.elasticsearch) == "table", "ELASTIC SEARCH connection is missing")
	assert(connection.elasticsearch.host, "missing ELASTIC SEARCH connection.host field")
	assert(connection.elasticsearch.port, "missing ELASTIC SEARCH connection.port field")
	if connection.elasticsearchDialer then
		assert(connection.elasticsearchDialer.host, "missing ELASTIC SEARCH DIALER connection.host field")
		assert(connection.elasticsearchDialer.port, "missing ELASTIC SEARCH DIALER connection.port field")
	end
	--logger.debug("[FULLCONTACT:ES:SetConnection] - Setting ELASTIC SEARCH connection: '%s'.", json.encode(connection))
	m_elasiticInfo.contacts.connection = connection.elasticsearch
	m_elasiticInfo.contactsDialer.connection = connection.elasticsearchDialer
	m_elasiticInfo.blacklistDialer.connection = connection.elasticsearchDialer
end

local function GetElasticInfoByType (elasticType)
	elasticType = (elasticType or ""):upper()
	if elasticType == "CONTACTS_DIALER" then
		return m_elasiticInfo.contactsDialer
	elseif elasticType == "BLACKLIST_DIALER" then
		return m_elasiticInfo.blacklistDialer
	end
	return m_elasiticInfo.contacts -- por defecto
end

local function MakePrettyUrlParams (url, isPrettyResponse)
	assert(url)
	if not isPrettyResponse then
		return url
	end
	return ("%s?pretty"):format(url)
end

local function MakeIndexURL (params)
	assert(type(params) == "table")
	--logger.debug("[FULLCONTACT:ES:MakeIndexURL] - Making Index URL params: '%s'.", json.encode(params))
	local elasticInfo = GetElasticInfoByType(params.elasticType)
	local indexURL = params.VCC and ("%s-%s"):format(elasticInfo.INDEX_NAME, params.VCC) or elasticInfo.INDEX_NAME
	--logger.debug("[FULLCONTACT:ES:MakeIndexURL] - elasticType: '%s', indexName: '%s', indexURL: '%s'.", elasticInfo.TYPE, elasticInfo.INDEX_NAME, indexURL)
	return indexURL
end

local function MakeTypeContactsURL (params)
	assert(type(params) == "table")
	local indexURL = MakeIndexURL(params)
	return ("%s/_doc"):format(indexURL)
end

local function MakeContactURL (params)
	assert(type(params) == "table")
	local contactsTypeURL = MakeTypeContactsURL(params)
	return ("/%s/%s"):format(contactsTypeURL, assert(params.contactId):lower())
end

local function MakeContactsBulkURL (params)
	assert(type(params) == "table")
	local indexURL = MakeIndexURL(params)
	local url = ("%s/_bulk"):format(indexURL)
	url = MakePrettyUrlParams(url, params.isPrettyResponse)
	return url, indexURL
end

local function GetIndexName (vcc, elasticType)
	local indexName = MakeIndexURL({VCC = vcc, elasticType = elasticType})
	--logger.info("[FULLCONTACT:ES:GetIndexName] Retrieving VCC '%s' Index Name for ELASTIC SEARCH. Index Name: '%s'", vcc, indexName)
	return indexName
end

local function CreateIndex (vcc, elasticType)
	local elasticInfo = GetElasticInfoByType(elasticType)
	local url = MakeIndexURL({VCC = vcc, elasticType = elasticType})

	local indexInfo = {settings = { number_of_shards = elasticInfo.connection.numberOfShards, number_of_replicas = elasticInfo.connection.numberOfReplicas}}
	local requestInfo = helpers.MakeRequestInfoForEs({
		credentials = elasticInfo.connection.credentials,
		query       = indexInfo
	})

	logger.info("[FULLCONTACT:ES:CreateIndex] Creating Index '%s' for VCC: '%s'. ELASTIC SEARCH. URL '%s' HOST '%s' PORT '%s'", elasticInfo.INDEX_NAME, vcc, url, elasticInfo.connection.host, elasticInfo.connection.port)
	logger.info("[FULLCONTACT:ES:CreateIndex]   indexInfo '%s'", requestInfo.encodedData)

	local responseBody = {}

	local ok, httpCode, responseHeaders, responseStatusLine = http.request({
		host = elasticInfo.connection.host,
		port = elasticInfo.connection.port,
		url = url,
		headers = requestInfo.headers,
		method = "PUT",
		source = requestInfo.source,
		sink = ltn12.sink.table(responseBody),
		redirect = false
	})

	httpCode = tonumber(httpCode)
	responseHeaders = responseHeaders or {}

	logger.debug("[FULLCONTACT:ES:CreateIndex] Success: %s %s\n%s\n%s\n%s", ok, httpCode, json.encode(responseHeaders), responseStatusLine, json.encode(responseBody))

	if not ok then
		logger.error("[FULLCONTACT:ES:CreateIndex] http code: %s\n%s\n%s\n%s", httpCode, json.encode(responseHeaders), responseStatusLine, json.encode(responseBody))
		return false
	end

	-- OK o CREATED o ACCEPTED
	if httpCode == 200 or httpCode == 201 or httpCode == 202 then
		responseBody = table.concat(responseBody)
		if responseHeaders["content-type"]:match("^application/json") then
			logger.info("[FULLCONTACT:ES:CreateIndex] Success created index '%s' VCC: '%s' to ELASTIC SEARCH.", elasticInfo.INDEX_NAME, vcc)
			return true, json.decode(responseBody)
		end
	end

	logger.warn("[FULLCONTACT:ES:CreateIndex] Unknown response from ELASTIC SEARCH. \nhttp code: %s\nheaders: %s\nresponse: %s\nstatus line: %s", httpCode, json.encode(responseHeaders), json.encode(responseBody), responseStatusLine)

	return false, responseBody
end

local function DropIndex (vcc, elasticType)
	local elasticInfo = GetElasticInfoByType(elasticType)
	local url = MakeIndexURL({VCC = vcc, elasticType = elasticType})
	logger.info("[FULLCONTACT:ES:DropIndex] Deleting Index '%s' for VCC: '%s'. ELASTIC SEARCH. URL '%s' HOST '%s' PORT '%s'", elasticInfo.INDEX_NAME, vcc, url, elasticInfo.connection.host, elasticInfo.connection.port)

	-- creo informacion necesaria sobre el header y demas (Ej: Authorization)
	local requestInfo = helpers.MakeRequestInfoForEs({
		credentials = elasticInfo.connection.credentials
	})

	local responseBody = {}

	local ok, httpCode, responseHeaders, responseStatusLine = http.request({
		host = elasticInfo.connection.host,
		port = elasticInfo.connection.port,
		headers = requestInfo.headers,
		url = url,
		method = "DELETE",
		sink = ltn12.sink.table(responseBody),
		redirect = false
	})

	httpCode = tonumber(httpCode)
	responseHeaders = responseHeaders or {}

	logger.debug("[FULLCONTACT:ES:DropIndex] Success: %s %s\n%s\n%s\n%s", ok, httpCode, json.encode(responseHeaders), responseStatusLine, json.encode(responseBody))

	if not ok then
		logger.warn("[FULLCONTACT:ES:DropIndex] http code: %s\n%s\n%s\n%s", httpCode, json.encode(responseHeaders), responseStatusLine, json.encode(responseBody))
		return false
	end

	-- OK o CREATED o ACCEPTED
	if httpCode == 200 or httpCode == 201 or httpCode == 202 then
		responseBody = table.concat(responseBody)
		if responseHeaders["content-type"]:match("^application/json") then
			logger.info("[FULLCONTACT:ES:DropIndex] Success deleted index '%s' VCC: '%s' to ELASTIC SEARCH.", elasticInfo.INDEX_NAME, vcc)
			return true, json.decode(responseBody)
		end
	end

	logger.warn("[FULLCONTACT:ES:DropIndex] Unknown response from ELASTIC SEARCH. \nhttp code: %s\nheaders: %s\nresponse: %s\nstatus line: %s", httpCode, json.encode(responseHeaders), json.encode(responseBody), responseStatusLine)

	return false, responseBody
end

--[[
Guia de los mapeos NESTED, OJBECT, QUERIES, ETC

https://www.elastic.co/guide/en/elasticsearch/guide/current/nested-mapping.html
https://www.elastic.co/guide/en/elasticsearch/guide/current/nested-query.html
https://www.elastic.co/guide/en/elasticsearch/reference/2.4/mapping.html
https://www.elastic.co/guide/en/elasticsearch/guide/current/mapping-intro.html
https://www.elastic.co/guide/en/elasticsearch/reference/5.5/analysis-analyzers.html

]]--
m_elasiticInfo.contacts.MakeTemplate = function(self)
	logger.info("[FULLCONTACT:ES.contacts:MakeTemplate] Making Template for CONCTACTS '%s' ELASTIC SEARCH.", self.TEMPLATE_NAME)

	return {
		index_patterns = {self.TEMPLATE_NAME}, -- index name will be like "vcc_contacts-inconcert"
		settings = {
			index = {
				analysis = {
					analyzer = {
						inconcert_content = {
							filter = {"lowercase", "asciifolding"},
							type = "custom",
							tokenizer = "standard"
						}
					}
				}
			}
		},
		mappings = {
			properties = {
				Id         = { type = "keyword"             },
				VCC        = { type = "keyword"             },
				Name       = {
					type = "text",
					fields = {
						keyword = {
							type = "keyword" }
						},
					analyzer = "inconcert_content"
				},
				CreatedAt  = {type = "date", format = "yyyy-MM-dd HH:mm:ss.SSS"},
				LastUpdate = {type = "date", format = "yyyy-MM-dd HH:mm:ss.SSS"},
				addresses = {
					type = "nested",
					properties = {
						Type = {type ="keyword"},
						Number = {type ="keyword"},
						--address = {type ="string", analyzer = "inconcert_email_analyzer"},
						SanitizedId = {type ="keyword"}
					}
				}
			}
		}
	}
end

m_elasiticInfo.contactsDialer.MakeTemplate = function(self)
	logger.info("[FULLCONTACT:ES.contactsDialer:MakeTemplate] Making Template for CONTACTS DIALER '%s' ELASTIC SEARCH.", self.TEMPLATE_NAME)

	return {
		index_patterns = {self.TEMPLATE_NAME}, -- index name will be like "vcc_contacts-inconcert"
		settings = {
			index = {
				analysis = {
					analyzer = {
						inconcert_content = {
							filter = {"lowercase", "asciifolding"},
							type = "custom",
							tokenizer = "standard"
						},
					}
				}
			}
		},
		mappings = {
			properties = {
				Id   = { type = "keyword" },
				Name = {
					type   = "text",
					fields = {
						keyword = {
							type = "keyword"
						}
					},
					analyzer = "inconcert_content"
				},
				Campaign       = { type = "keyword" },
				Category       = { type = "keyword" },
				AccountOfficer = { type = "keyword" },
				ExternalId     = { type = "keyword" },
				ImportId       = { type = "keyword" },
				AccountGroup   = { type = "keyword" },
				IsClient       = { type = "keyword" },
				CreatedAt      = { type = "date"  , format = "yyyy-MM-dd HH:mm:ss.SSS" },
				LastUpdate     = { type = "date"  , format = "yyyy-MM-dd HH:mm:ss.SSS" },
				Addresses = {
					type = "nested",
					properties = {
						Type = {type ="keyword"},
						Number = {type ="keyword"},
						--address = {type ="string", analyzer = "inconcert_email_analyzer"},
						SanitizedId = {type ="keyword"}
					}
				},
				HomeAddresses = {
					properties = {
						Value          = {type ="keyword"},
						AddressCountry = {type ="keyword"},
						AddressSubType = {type ="keyword"},
						AddressArea    = {type ="keyword"}
					}
				},
				--- Name Values Simple
				NameValuesSimple = {
					properties = {
						Name  = {type ="keyword"},
						Value = {type ="keyword"}
					}
				},
				-- Name Values Date
				NameValuesSearchDate = {
					type = "nested",
					properties = {
						Name  = {type ="keyword"},
						Value = {type ="date",format="yyyy-MM-dd HH:mm:ss.SSS||yyyy-MM-dd HH:mm:ss||yyyy-MM-dd"}
					}
				},
				-- Name Values Numeric
				NameValuesSearchNumeric = {
					type = "nested",
					properties = {
						Name  = {type ="keyword"},
						Value = {type ="double"}
					}
				},
				-- Name Values Text
				NameValuesSearchText = {
					type = "nested",
					properties = {
						Name  = {type ="keyword"},
						Value = {type ="keyword"}
					}
				},
				ContactScheduleDate = {type ="date",format="yyyy-MM-dd HH:mm:ss.SSS||yyyy-MM-dd HH:mm:ss||yyyy-MM-dd"},
				ManualScheduleDate = {type ="date",format="yyyy-MM-dd HH:mm:ss.SSS||yyyy-MM-dd HH:mm:ss||yyyy-MM-dd"},
				ScheduleDate = {type ="date",format="yyyy-MM-dd HH:mm:ss.SSS||yyyy-MM-dd HH:mm:ss||yyyy-MM-dd"},
				Agent = {type ="keyword"},
				Skills = {
					type = "nested",
					properties = {
						SkillId = {type = "keyword"},
						Value   = {type = "keyword"},
						Mode    = {type = "keyword"}
					}
				},
				--Datos para generar filtros a partir de los cuales generar lotes.
				LastContactDate        = {type = "date", format = "yyyy-MM-dd HH:mm:ss.SSS"},
				LastAddress            = {type = "keyword"},
				LastInteractionCode    = {type = "keyword"},
				LastLowLevelCode       = {type = "keyword"},
				LastManagementResult   = {type = "keyword"},
				LastAgent              = {type = "keyword"},
				LastChannel            = {type = "keyword"},
				LastKind               = {type = "keyword"},
				LastIsGoal             = {type = "boolean"},
				OutboundProcessId      = {type = "keyword"},
				BatchId                = {type = "keyword"},
				State                  = {type = "keyword"},
				StateCause             = {type = "keyword"},
				DailyRetries           = {type = "integer"},
				TotalRetries           = {type = "integer"},
				Priority               = {type = "integer"},
				NextAddress            = {type = "keyword"},
				-- migracion allegro a nativo
				MigrationType          = {type = "keyword"},
				MigrationSourceContact = {type = "keyword"}
			}
		}
	}
end

m_elasiticInfo.blacklistDialer.MakeTemplate = function(self)
	logger.info("[FULLCONTACT:ES.blacklistDialer:MakeTemplate] Making Template for BLACKLIST DIALER '%s' ELASTIC SEARCH.", self.TEMPLATE_NAME)
	return {
		index_patterns = {self.TEMPLATE_NAME}, -- index name will be like "blacklist_dialer"
		mappings = {
			properties = {
				Id       = { type = "keyword" },
				Type     = { type = "keyword" },
				Scope    = { type = "keyword" },
				VCC      = { type = "keyword" },
				Address  = { type = "keyword" },
				ImportId = { type = "keyword" },
				Key      = { type = "keyword" }
			}
		}
	}
end

--[[
	 funcion que queda por compatiblidad con la funcion generica, pero termina invocando a "MakeAddressesFilters"
]]--
m_elasiticInfo.blacklistDialer.MakeContactsFilters = function(self, filter, mustQueries, mustFilters)
	return self:MakeAddressesFilters(filter, mustQueries, mustFilters)
end

--[[
	 - Funcion que crea filtros/queries para los addresses de DIALER de blacklist
]]--
m_elasiticInfo.blacklistDialer.MakeAddressesFilters = function(self, filter, mustQueries, mustFilters)
	assert(type(mustQueries) == "table")
	assert(type(mustFilters) == "table")

	--logger.info("[FULLCONTACT:ES.blacklistDialer:MakeAddressesFilters] 1-* Making BlackList DIALER Filter ELASTIC SEARCH. filter: %s, mustQueries: %s, mustFilters: %s.", filter, mustQueries, mustFilters)

	if not filter then
		return
	end

	local SHOULD = {}
	for fieldName, fieldInfo in pairs(filter) do
		for i, value in pairs(fieldInfo.values) do
			local filterInfo
			value = utf8.lower(value)
			if fieldInfo.exact then
				--logger.debug("1 EXACT: '%s'='%s'", fieldName, value)
				filterInfo = {
					terms = {
						[fieldName] = {value}
					} 
				}
				table.insert(mustFilters, filterInfo)
			else
				filterInfo = {
					query_string = {
						default_field = fieldName,
						query = "*" .. helpers.SanitizeQueryValue(value) .. "*"
					}
				}
				table.insert(SHOULD, filterInfo)
			end
		end
	end

	if #SHOULD > 0 then
		local condition = {
			bool = {
				should = SHOULD
			}
		}
		table.insert(mustQueries, condition)
	end

	--logger.debug("[FULLCONTACT:ES.blacklistDialer:MakeAddressesFilters] 2-* Making BlackList DIALER Filter ELASTIC SEARCH. mustQueries: %s, mustFilters: %s.", json.encode(mustQueries), json.encode(mustFilters))
end

--[[
	 - Funcion que crea filtros/queries para los contactos (tradicionales)
]]--
m_elasiticInfo.contacts.MakeContactsFilters = function(self, filter, mustQueries, mustFilters)
	assert(type(mustQueries) == "table")
	assert(type(mustFilters) == "table")

	logger.info("[FULLCONTACT:ES.contacts:MakeContactsFilters] 1-- Making Contacts Filter ELASTIC SEARCH. filter: %s, mustQueries: %s.", filter, mustQueries)

	if not filter then
		return
	end

	local SHOULD = {}
	for fieldName, fieldInfo in pairs(filter) do
		if (fieldInfo.type or ""):lower() == "address" then
		-- Busqueda x Address se necesita la siguiente estructura de filtro.
			local must = {}
			if fieldInfo.addressType and fieldInfo.addressType ~= "" then
				must = {
					{
						match = {
							--["addresses.Type"] = fieldInfo.addressType
							[self.FieldsKeys.addressType] = fieldInfo.addressType
						}
					}
				}
			end
			local addressId = fieldInfo.addressId and AddressMapper.SanitizeId(fieldInfo.addressId, fieldInfo.addressType)
			if addressId and addressId ~= "" then
				if fieldInfo.exact then
					must = {
						match = {
							--["addresses.SanitizedId"] = addressId
							[self.FieldsKeys.addressNumber] = addressId
						}
					}
				else
					must = {
						query_string = {
							default_operator = "OR",
							--default_field = "addresses.SanitizedId",
							fields = {self.FieldsKeys.addressNumber},
							query = addressId
						}
					}
				end
			end

			table.insert(SHOULD, must)
		else
			for i, value in pairs(fieldInfo.values) do
				local filterInfo
				value = utf8.lower(value)
				if fieldInfo.exact then
					--logger.debug("1 EXACT: '%s'='%s'", fieldName, value)
					filterInfo = {
						terms = { 
							[fieldName] = {value}
						} 
					}
					table.insert(mustFilters, filterInfo)
				else
					filterInfo = {
						query_string = {
							default_operator = "OR",
							fields = {fieldName},
							query = value
						}
					}
					table.insert(SHOULD, filterInfo)
				end
			end
		end
	end
	if #SHOULD > 0 then
		local condition = {
			bool = {
				should = SHOULD
			}
		}
		table.insert(mustQueries, condition)
	end

	--logger.debug("[FULLCONTACT:ES.contacts:MakeContactsFilters] 2-- Making Contacts Filter ELASTIC SEARCH. mustQueries: %s, mustFilters: %s.", json.encode(mustQueries), json.encode(mustFilters))
end

--[[
	 - Funcion que crea filtros/queries para los contactos de DIALER
]]--
m_elasiticInfo.contactsDialer.MakeContactsFilters = function(self, filter, mustQueries, mustFilters)
	assert(type(mustQueries) == "table")
	assert(type(mustFilters) == "table")

	--logger.debug("[FULLCONTACT:ES.contactsDialer:MakeContactsFilters] 1-* Making Contacts DIALER Filter ELASTIC SEARCH.\nfilter: %s,\nmustQueries: %s,\nmustFilters: %s.", json.encode(filter or {}), json.encode(mustQueries), json.encode(mustFilters))

	if not filter then
		return
	end

	local SHOULD = {}
	for fieldName, fieldInfo in pairs(filter) do
		--logger.debug("      fieldName: '%s', fieldInfo: %s", fieldName, json.encode(fieldInfo))
		if (fieldInfo.type or ""):lower() == "address" then
		-- Busqueda x Address se necesita la siguiente estructura de filtro.
			local must = {}
			if fieldInfo.addressType and fieldInfo.addressType ~= "" then
				must = {
					{
						match = {
							--["addresses.Type"] = fieldInfo.addressType
							[self.FieldsKeys.addressType] = fieldInfo.addressType
						}
					}
				}
			end
			local addressId = fieldInfo.addressId and AddressMapper.SanitizeId(fieldInfo.addressId, fieldInfo.addressType)
			if addressId and addressId ~= "" then
				if fieldInfo.exact then
					table.insert(must, {
						match = {
							--["addresses.SanitizedId"] = addressId
							[self.FieldsKeys.addressNumber] = addressId
						}
					})
				else
					table.insert(must, {
						query_string = {
							--default_field = "addresses.SanitizedId",
							default_field = self.FieldsKeys.addressNumber,
							query = "*" .. helpers.SanitizeQueryValue(addressId) .. "*"
						}
					})
				end
			end

			local addressFilter = {
				nested = {
					--path = "addresses",
					path = self.FieldsKeys.path,
					query = {
						bool = {
							must = must
						}
					}
				}
			}

			table.insert(SHOULD, addressFilter)
		else
			for i, value in pairs(fieldInfo.values) do
				local filterInfo
				value = utf8.lower(value)
				if fieldInfo.exact then
					--logger.debug("1 EXACT: '%s'='%s'", fieldName, value)
					filterInfo = {
						terms = {
							[fieldName] = {value}
						}
					}
					table.insert(mustFilters, filterInfo)
				else
					filterInfo = {
						query_string = {
							default_field = fieldName,
							query = "*" .. helpers.SanitizeQueryValue(value) .. "*"
						}
					}
					table.insert(SHOULD, filterInfo)
				end
			end	
		end
	end

	if #SHOULD > 0 then
		local condition = {
			bool = {
				should = SHOULD
			}
		}
		table.insert(mustQueries, condition)
	end

	--logger.debug("[FULLCONTACT:ES.contactsDialer:MakeContactsFilters] 2-* Making Contacts DIALER Filter ELASTIC SEARCH. mustQueries: %s, mustFilters: %s.", json.encode(mustQueries), json.encode(mustFilters))
end

local function CreateTemplate (elasticType)
	local elasticInfo = GetElasticInfoByType(elasticType)
	local url = elasticInfo.TEMPLATE_URL

	logger.info("[FULLCONTACT:ES:CreateTemplate] Create Template Index. ELASTIC SEARCH. URL '%s' HOST '%s' PORT '%s'", url, elasticInfo.connection.host, elasticInfo.connection.port)

	local template = elasticInfo:MakeTemplate()
	-- creo informacion necesaria sobre el header y demas (Ej: Authorization)
	local requestInfo = helpers.MakeRequestInfoForEs({
		credentials = elasticInfo.connection.credentials,
		query       = template
	})
	logger.info("[FULLCONTACT:ES:CreateTemplate]     encodedTemplate '%s'", requestInfo.encodedData)

	local responseBody = {}

	local ok, httpCode, responseHeaders, responseStatusLine = http.request({
		host = elasticInfo.connection.host,
		port = elasticInfo.connection.port,
		url = url,
		headers = requestInfo.headers,
		method = "PUT",
		source = requestInfo.source,
		sink = ltn12.sink.table(responseBody),
		redirect = false
	})

	httpCode = tonumber(httpCode)
	responseHeaders = responseHeaders or {}

	logger.debug("[FULLCONTACT:ES:CreateTemplate] Success: %s %s\n%s\n%s\n%s", ok, httpCode, json.encode(responseHeaders), responseStatusLine, json.encode(responseBody))

	if not ok then
		logger.error("[FULLCONTACT:ES:CreateTemplate] http code: %s\n%s\n%s\n%s", httpCode, json.encode(responseHeaders), responseStatusLine, json.encode(responseBody))
		return false
	end

	-- OK o CREATED o ACCEPTED
	if httpCode == 200 or httpCode == 201 or httpCode == 202 then
		responseBody = table.concat(responseBody)
		if responseHeaders["content-type"]:match("^application/json") then
			logger.info("[FULLCONTACT:ES:CreateTemplate] Success created template ELASTIC SEARCH.")
			return true, json.decode(responseBody)
		end
	end

	logger.warn("[FULLCONTACT:ES:CreateTemplate] Unknown response from ELASTIC SEARCH. \nhttp code: %s\nheaders: %s\nresponse: %s\nstatus line: %s", httpCode, json.encode(responseHeaders), json.encode(responseBody), responseStatusLine)

	return false, responseBody
end

local function StoreContact (contact, elasticType)
	assert(contact)

	local elasticInfo = GetElasticInfoByType(elasticType)
	contact.Id = contact.Id and contact.Id:lower()
	local url = MakeContactURL({VCC = contact.VCC, contactId = contact.Id, elasticType = elasticType})

	--logger.debug("[FULLCONTACT:ES:StoreContact] - Storing contact '%s' to ELASTIC SEARCH. URL '%s'", contact.Id, url)

	if contact.Avatar then
		contact.Avatar = nil
	end

	local checkName = helpers.alltrim(contact.Name)
	if #checkName == 0 then contact.Name = "No Name" end

	local contactInfo = {}
	for name, value in pairs(contact) do
		--logger.debug("  [FULLCONTACT:ES:StoreContact] --> name:'%s', value: '%s'.", name, value)
		if type(value) == "string" then
			value = helpers.toUTF8(value)
		end
		contactInfo[name] = value
	end

	-- creo informacion necesaria sobre el header y demas (Ej: Authorization)
	local requestInfo = helpers.MakeRequestInfoForEs({
		credentials = elasticInfo.connection.credentials,
		query       = contactInfo
	})

	--logger.debug("[FULLCONTACT:ES:StoreContact] ES EXECUTE --> http.request -> url: '%s', postData: %s", url, requestInfo.encodedData)
	local responseBody = {}

	local ok, httpCode, responseHeaders, responseStatusLine = http.request({
		host = elasticInfo.connection.host,
		port = elasticInfo.connection.port,
		url = url,
		headers = requestInfo.headers,
		method = "POST",
		source = requestInfo.source,
		sink = ltn12.sink.table(responseBody),
		redirect = false
	})

	httpCode = tonumber(httpCode)
	responseHeaders = responseHeaders or {}

	--logger.debug("[FULLCONTACT:ES:StoreContact] Success: %s %s\n%s\n%s\n%s", ok, httpCode, json.encode(responseHeaders), responseStatusLine, json.encode(responseBody))

	local function MakeErrorES ()
		local err = {
			type = "ELASTIC SEARCH ERROR"
		}
		if type(responseBody) ~= "table" then
			return err
		end
		local encodedESErr = responseBody[1]
		--logger.debug("--------------> encodedESErr %s", encodedESErr)
		if type(encodedESErr) ~= "string" then
			return err
		end
		local nativeError = json.decode(encodedESErr)
		err.nativeError = nativeError
		if nativeError.error and nativeError.error.caused_by and nativeError.error.caused_by.reason then
			err.type = err.type .. " - " .. nativeError.error.caused_by.reason
		end
		return err
	end

	if not ok then
		logger.error("[FULLCONTACT:ES:StoreContact] http code: %s\n%s\n%s\n%s", httpCode, json.encode(responseHeaders), responseStatusLine, json.encode(responseBody))
		local err = MakeErrorES()
		return false, responseBody, err
	end

	-- OK o CREATED o ACCEPTED
	if httpCode == 200 or httpCode == 201 or httpCode == 202 then
		responseBody = table.concat(responseBody)
		if responseHeaders["content-type"]:match("^application/json") then
			--logger.debug("[FULLCONTACT:ES:StoreContact] Success stored contact '%s' to ELASTIC SEARCH.", contact.Id)
			return true, json.decode(responseBody)
		end
	end

	logger.warn("[FULLCONTACT:ES:StoreContact] Unknown response from ELASTIC SEARCH. \nhttp code: %s\nheaders: %s\nresponse: %s\nstatus line: %s", httpCode, json.encode(responseHeaders), json.encode(responseBody), responseStatusLine)

	local err = MakeErrorES()
	return false, responseBody, err
end

--[[
	Funcion que realiza el Request de tipo "bulk" hacia ES.
	Se debe postear en una estructura de tipo NDJSON, que es un JSON delimitado por saltos de linea.
	Ej:
		{ "create" : { "_index" : "elindice", "_id" : contactid1" } }
		{"name" :"emma"}
		{ "update" : { "_index" : "elindice", "_id" : contactid1" } }
		{ "doc": {"name" :"Emma Vallejo de Mello"} }
		{ "delete" : { "_index" : "elindice", "_id" : contactid1" } }
		..
		..
]]--
local function UpdateContactsBatchInES (vcc, contactsBatch, elasticType)
	assert(vcc)
	assert(type(contactsBatch) == "table")

	local elasticInfo = GetElasticInfoByType(elasticType)
	-- "/vcc_contacts_dialer-vcc1/_bulk?pretty", "vcc_contacts_dialer-vcc1"
	local url, indexName = MakeContactsBulkURL({VCC = vcc, elasticType = elasticType, isPrettyResponse = true})

	--logger.debug("[FULLCONTACT:ES:UpdateContactsBatchInES] 1--> Updating '%s' contacts batch to ELASTIC SEARCH. URL '%s', indexName: '%s'.", #contactsBatch, url, indexName)
	--logger.debug("[FULLCONTACT.ES:UpdateContactsBatchInES] 2--> Updating contacts batch to ELASTIC SEARCH. URL '%s', indexName: '%s'. contactsBatch: %s", url, indexName, json.encode(contactsBatch))

	if #contactsBatch == 0 then
		-- TODO: Ver si en la respuesta tienen que viajar otros valores.
		logger.info("[FULLCONTACT:ES:UpdateContactsBatchInES] --> There are no contacts to update in the batch to ELASTIC SEARCH. URL '%s', indexName: '%s'.", url, indexName)
		return true, {}, {}
	end

	local function SanitizeContact (contact)
		if contact.Avatar then
			contact.Avatar = nil
		end
		local contactInfo = {}
		for name, value in pairs(contact) do
			--logger.debug("  [FULLCONTACT:ES:UpdateContactsBatchInES] --> name:'%s', value: '%s'.", name, value)
			if type(value) == "string" then
				value = helpers.toUTF8(value)
			end
			contactInfo[name] = value
		end
		return contactInfo
	end

	local function MakeBulkAction (action, contactId)
		return string.format('{ "%s" : { "_index" : "%s", "_id" : "%s" } }', action:lower(), indexName, contactId)
	end

	local encodedContactsBatch = ''
	for i, contactInfo in ipairs(contactsBatch) do
		local contact = SanitizeContact(contactInfo.contact)
		local encodedContact = json.encode(contact)
		local action = (contactInfo.action or ""):upper()

		local encodedAction = ""
		if action == "CREATE" then
			encodedAction =	MakeBulkAction(action, contact.Id) ..
				'\n' ..
				encodedContact ..
				'\n'
		elseif action == "UPDATE" then
			encodedAction = MakeBulkAction(action, contact.Id) ..
				'\n' ..
				string.format('{ "doc" : %s }', encodedContact) ..
				'\n'
		elseif action == "DELETE" then
			encodedAction = MakeBulkAction(action, contact.Id) ..
				'\n'
		else
			assert("Unrecognized ES bulk action: " .. action)
		end
		--logger.debug(" %d - action: %s --> encodedAction: %s", i, action, encodedAction)
		encodedContactsBatch = encodedContactsBatch .. encodedAction
	end

	-- creo informacion necesaria sobre el header y demas (Ej: Authorization)
	local requestInfo = helpers.MakeRequestInfoForEs({
		credentials  = elasticInfo.connection.credentials,
		encodedQuery = encodedContactsBatch
	})

	logger.debug("[FULLCONTACT:ES:UpdateContactsBatchInES]---> ES EXECUTE --> http.request -> url: '%s', encodedContactsBatch: '%s'", url, requestInfo.encodedData)
	local responseBody = {}

	local ok, httpCode, responseHeaders, responseStatusLine = http.request({
		host = elasticInfo.connection.host,
		port = elasticInfo.connection.port,
		url = url,
		headers = requestInfo.headers,
		method = "POST",
		source = requestInfo.source,
		sink = ltn12.sink.table(responseBody),
		redirect = false
	})

	httpCode = tonumber(httpCode)
	responseHeaders = responseHeaders or {}

	--logger.debug("[FULLCONTACT:ES:UpdateContactsBatchInES] Success: %s %s\n%s\n%s\n%s", ok, httpCode, json.encode(responseHeaders), responseStatusLine, json.encode(responseBody))

	local function MakeErrorES ()
		local err = {
			type = "ELASTIC SEARCH ERROR"
		}
		if type(responseBody) ~= "table" then
			return err
		end
		local encodedESErr = responseBody[1]
		--logger.debug("--------------> encodedESErr %s", encodedESErr)
		if type(encodedESErr) ~= "string" then
			return err
		end
		local nativeError = json.decode(encodedESErr)
		err.nativeError = nativeError
		if nativeError.error and nativeError.error.caused_by and nativeError.error.caused_by.reason then
			err.type = err.type .. " - " .. nativeError.error.caused_by.reason
		end
		return err
	end

	if not ok then
		logger.error("[FULLCONTACT:ES:UpdateContactsBatchInES] http code: %s\n%s\n%s\n%s", httpCode, json.encode(responseHeaders), responseStatusLine, responseBody)
		local err = MakeErrorES()
		return false, responseBody, err
	end

	-- OK o CREATED o ACCEPTED
	if httpCode == 200 or httpCode == 201 or httpCode == 202 then
		responseBody = table.concat(responseBody)
		--logger.debug("[FULLCONTACT:ES:UpdateContactsBatchInES] http code: %s\nresponseHeaders: %s\nresponseStatusLine: %s\nresponseBody: %s", httpCode, json.encode(responseHeaders), responseStatusLine, responseBody)
		if responseHeaders["content-type"]:match("^application/json") then
			local response = json.decode(responseBody)
			local detail = {}
			local partialErrors = {}
			local hasPartialErrors = false
			for i, info in pairs(response.items or {}) do
				local action, data = next(info)
				action = action:lower()
				detail[action] = detail[action] or {}
				local result = data.result
				if result then
					-- ok
					result = result:lower()
					detail[action][result] = (detail[action][result] or 0) + 1
				else
					-- error
					detail[action].errors = detail[action].errors or {
						total = 0,
						errorsType = {}
					}
					detail[action].errors.total = detail[action].errors.total + 1
					detail[action].errors.status = detail[action].errors.status or {}
					local errorStatus = tostring(data.status or "none"):lower()
					detail[action].errors.status[errorStatus] = detail[action].errors.status[errorStatus] or {}
					table.insert(detail[action].errors.status[errorStatus], data.error or data)
					partialErrors[action] = detail[action].errors
					hasPartialErrors = true
				end
			end
			--logger.info("[FULLCONTACT:ES:UpdateContactsBatchInES] Success total '%s' to ELASTIC SEARCH. Detail: %s", #contactsBatch, json.encode(detail))
			if hasPartialErrors then
				logger.warn("[FULLCONTACT:ES:UpdateContactsBatchInES] Partial errors. Contacts '%s' to ELASTIC SEARCH. partialErrors-sample: version conflict, document already exists ..", #contactsBatch)
				--logger.debug("[FULLCONTACT:ES:UpdateContactsBatchInES] Partial errors. Contacts '%s' to ELASTIC SEARCH. partialErrors: %s", #contactsBatch, json.encode(partialErrors))
			end
			return true, response, detail
		end
	end

	logger.warn("[FULLCONTACT:ES:UpdateContactsBatchInES] Unknown response from ELASTIC SEARCH. \nhttp code: %s\nheaders: %s\nresponse: %s\nstatus line: %s", httpCode, json.encode(responseHeaders), json.encode(responseBody), responseStatusLine)

	local err = MakeErrorES()
	return false, responseBody, err
end

local function RemoveContact (contactId, vcc, mustNotShowMessage)
	assert(contactId)

	local elasticType = "CONTACTS"
	local elasticInfo = GetElasticInfoByType(elasticType)
	local url = MakeContactURL({VCC = vcc, contactId = contactId, elasticType = elasticType})

	if not mustNotShowMessage then
		logger.info("[FULLCONTACT:ES:RemoveContact] - Removing contact '%s' vcc:'%s' from ELASTIC SEARCH. URL: '%s'.", contactId, vcc, url)
	end

	-- creo informacion necesaria sobre el header y demas (Ej: Authorization)
	local requestInfo = helpers.MakeRequestInfoForEs({
		credentials = elasticInfo.connection.credentials
	})
	requestInfo.headers['Content-Type'] = nil -- debo remover el Content-Type ya que no es necesario.

	--logger.debug("[FULLCONTACT:ES:RemoveContact] ES EXECUTE --> http.request -> url: '%s'", url)

	local encodedContact = json.encode({})
	local responseBody = {}
	local ok, responseCode, responseHeaders, responseStatusLine = http.request({
		host = elasticInfo.connection.host,
		port = elasticInfo.connection.port,
		url = url,
		headers = requestInfo.headers,
		method = "DELETE",
		sink = ltn12.sink.table(responseBody),
		redirect = false
	})

	responseHeaders = responseHeaders or {}

	if responseBody then
		responseBody = table.concat(responseBody)
		if responseHeaders["content-type"]:match("^application/json") then
			responseBody = json.decode(responseBody)
		end
	end

	if not ok then
		if not mustNotShowMessage then
			logger.error("[FULLCONTACT:ES:RemoveContact] Could not remove contact with id '%s' from Elastic Search. The error message was: %s\n%s\n%s\n%s", contactId, responseCode, json.encode(responseHeaders), responseStatusLine, json.encode(responseBody))
		end
		return false, responseBody
	end

	if not mustNotShowMessage then
		logger.info("[FULLCONTACT:ES:RemoveContact] Removed contact with id: '%s' from Elastic Search.", contactId)
	end

	return true, responseBody
end

local function MakeSearchContactsURL (params)
	assert(type(params) == "table")
	local contactsTypeURL
	if "BLACKLIST_DIALER" == params.elasticType then
		contactsTypeURL = m_blackList.MakeTypeAddressURL(params)
	else
		contactsTypeURL = MakeTypeContactsURL(params)
	end
	local url = ("/%s/_search"):format(contactsTypeURL)
	url = MakePrettyUrlParams(url, params.isPrettyResponse)
	--logger.debug("[FULLCONTACT:ES:MakeSearchContactsURL] url: %s.", url)
	return url
end
 

--[[
DATA ejemplo de los POST de request de SEARCH:

1) Acá filtra x N Campañas y una dirección de MAIL especifica que se sanitiza el @ a :

{"from":0,"size":40,"sort":[{"contacts.Name":{"order":"asc"}}],"query":{"filtered":{"filter":{"bool":{"must":[{"or":[{"terms":{"campaigns":["campmail1"]}},{"terms":{"campaigns":["telborrame"]}},{"terms":{"campaigns":["camp1"]}},{"terms":{"campaigns":["camptel"]}},{"terms":{"campaigns":["contact_form"]}},{"terms":{"campaigns":["campborram1"]}},{"terms":{"campaigns":["fb"]}},{"terms":{"campaigns":["borrame"]}}]},{"nested":{"path":"addresses","query":{"bool":{"must":[{"match":{"addresses.Type":"Mail"}},{"match":{"addresses.SanitizedId":"emma_vallejo:hotmail.com"}}]}}}}]}}}}}

2) Acá filtra por una dirección de MAIL especifica que se sanitiza el @ a :
{"from":0,"size":40,"sort":[{"contacts.Name":{"order":"asc"}}],"query":{"filtered":{"filter":{"bool":{"must":[{"nested":{"path":"addresses","query":{"bool":{"must":[{"match":{"addresses.Type":"Mail"}},{"match":{"addresses.SanitizedId":"emma_vallejo:hotmail.com"}}]}}}}]}}}}}

De esta forma hay que implementar cuando se necesite para realizar búsquedas por addresses dentro del array de objetos, mirar ejemplo NESTED.
]]--

local function SearchContacts (request, elasticType)
	assert(type(request) == "table")

	local elasticInfo = GetElasticInfoByType(elasticType)

	--logger.info("[FULLCONTACT:ES:SearchContacts] Retrieving contacts from ELASTIC SEARCH. elasticType: %s, Request: '%s'", elasticType, json.encode(request))

	local from = tonumber(request.startRow) or 0 -- comenzar el paginado en la fila
	local size = tonumber(request.rowsPerPage) or 10 -- total de rows por pagina
	-----------------------------
	-- El esquelto basico de una búsqueda contra ES
	--
	local query = {
		query = {
			bool = {
				must = {},
				filter = {}
			}
		},
		from = from,
		size = size,
		sort = {}
	}

	if request.sort then
		-- Sample Sort: {{Name = {order = "asc"}}}
		query.sort = request.sort
	end

	-- 1) Agregamos los filtros
	local mustFilters = query.query.bool.filter

	-- voy por las campañas que tienen que ir entre OR de forma individual
	if request.campaigns then
		local shound_campaigns = {}
		local key = "campaigns"
		for _, campaignId in pairs(request.campaigns) do
			local simpleCampaignId = helpers.SeparateVCC(campaignId)
			table.insert(shound_campaigns, utf8.lower(simpleCampaignId))
		end
		if #shound_campaigns > 0 then
			local condition =  { terms = { [key] = shound_campaigns } }
			table.insert(mustFilters, condition)
		end
	end

	-- User Favorites - voy por los contactos que el usuario tiene como favoritos
	if request.usersFavorite then
		local shound_favorite = {}
		local key = "UsersFavorite"
		for _, userId in ipairs(request.usersFavorite) do
			local simpleUserId = helpers.SeparateVCC(userId)
			table.insert(shound_favorite, utf8.lower(simpleUserId))
		end
		if #SHOULD > 0 then
			local condition =  { terms = { [key] = shound_favorite } }
			table.insert(mustFilters, condition)
		end
	end

	-- 2) Agregamos las consultas
	local mustQueries = query.query.bool.must
	elasticInfo:MakeContactsFilters(request.filter, mustQueries, mustFilters)

	-- Este campo no puede estar vacío, por lo que se elimina si no se ha agregado ninguna consulta
	if #mustQueries == 0 then
		query.query.bool.must = nil
	end

	-- creo informacion necesaria sobre el header y demas (Ej: Authorization)
	local requestInfo = helpers.MakeRequestInfoForEs({
		credentials = elasticInfo.connection.credentials,
		query       = query
	})
	local url = MakeSearchContactsURL({VCC = request.VCC or request.vcc, elasticType = elasticType, isPrettyResponse = false})

	--logger.info("[FULLCONTACT:ES:SearchContacts] Query for ELASTIC SEARCH. URL: '%s', Query: %s", url, requestInfo.encodedData)

	local responseBody = {}

	local ok, responseCode, responseHeaders, responseStatusLine = http.request({
		host = elasticInfo.connection.host,
		port = elasticInfo.connection.port,
		url = url,
		headers = requestInfo.headers,
		method = "POST",
		source = requestInfo.source,
		sink = ltn12.sink.table(responseBody),
		redirect = false
	})
	if not ok then
		logger.warn("[FULLCONTACT:ES:SearchContacts] %s\n%s\n%s\n%s", responseCode, json.encode(responseHeaders), responseStatusLine, json.encode(responseBody))
		return false, {error = responseStatusLine, status = responseCode}
	end

	local rawResponse = table.concat(responseBody)
	if not responseHeaders["content-type"]:match("^application/json") then
		logger.warn("[FULLCONTACT:ES:SearchContacts] Unknown response from ELASTIC SEARCH. headers: %s\nresponse: %s", json.encode(responseHeaders), json.encode(rawResponse))
		return false, { error = "inconcert unknown", status = 500 }
	end

	local response = json.decode(rawResponse)
	if response.error then
		logger.warn("[FULLCONTACT:ES:SearchContacts] Request failed with the following error: '%s'", json.encode(response.error))
		return false, response
	end

	if response.timed_out then
		LogWarning("[FULLCONTACT:ES:SearchContacts] Query against ELASTIC SEARCH timed out. %s", requestInfo.encodedData)
		return false, { error = "REQUEST_TIMEDOUT", status = 502 }
	end

	local contacts = {}
	for _, hit in pairs(response.hits.hits) do
		local contact = hit._source
		table.insert(contacts, contact)
	end
	local last = from + #contacts
	--logger.debug("[FULLCONTACT:ES:SearchContacts] - Query took %s ms and returned %s hits out of %s", response.took, #response.hits.hits, response.hits.total.value)
	
	for _, contact in ipairs(contacts) do
		local checkName = helpers.alltrim(contact.Name)
		if #checkName == 0 then contact.Name = "No Name" end
	end
	
	return true, {
		contacts = contacts,
		total = response.hits.total,
		last = last
	}
end

local function GetContact (contactId, vcc, elasticType)
	assert(type(contactId) == "string")
	assert(type(vcc) == "string")

	--logger.info("[FULLCONTACT:ES:GetContact] Get contact: '%s', vcc: '%s' from ELASTIC SEARCH.", contactId, vcc)
	local elasticInfo = GetElasticInfoByType(elasticType)
	-- creo informacion necesaria sobre el header y demas (Ej: Authorization)
	local requestInfo = helpers.MakeRequestInfoForEs({
		credentials = elasticInfo.connection.credentials
	})

	local url = MakeContactURL({VCC = vcc, contactId = contactId, elasticType = elasticType})
	--logger.debug("[FULLCONTACT:ES:GetContact] 1--------> -> url: '%s'", url)

	local responseBody = {}
	local result, code, header = http.request{
		host = elasticInfo.connection.host,
		port = elasticInfo.connection.port,
		headers = requestInfo.headers,
		url = url,
		method = "GET",
		sink = ltn12.sink.table(responseBody)
	}
	local response = table.concat(responseBody, "")

	--logger.debug("[FULLCONTACT:ES:GetContact] 2--------> Result. Code: '%s'", code)
	--logger.debug("[FULLCONTACT:ES:GetContact] 3--------> -> result: '%s', code: '%s', header: '%s'.", result, code, json.encode(header))
	--logger.debug("[FULLCONTACT:ES:GetContact] 4--------> -> response: '%s'", response)

	if code == 200 then
		-- exist
		local contact = helpers.JSON_Decode(response)._source
		--logger.info("[FULLCONTACT:ES:GetContact] Success retrieved contact: '%s', vcc: '%s' from ELASTIC SEARCH. contact %ss", contactId, vcc, contact)
		return true, contact
	end
	-- contact does not exist
	--logger.warn("[FULLCONTACT:ES:GetContact] Unable to get contact: '%s', vcc: '%s' from ELASTIC SEARCH.",  contactId, vcc)
	return false, response
end

------------------------------------------
-- Contacts Dialer
--
local m_dialer = {}

--[[
	Funcion encargada de realizar validaciones de duplicados en el batch de contactos y decidir que acciones realizar (create, update, delete)
]]--
m_dialer.ValidateDupAndStoreContactsBatch = function(self, vcc, contacts, options)
	assert(type(vcc) == "string")
	assert(type(contacts) == "table")
	options = options or {}

	--logger.debug("[FULLCONTACT:ES:ValidateDupAndStoreContactsBatch] - Validating And Storing '%s' contacts BATCH to ELASTIC SEARCH. options: %s", #contacts, helpers.JSON_Encode(options))
	--logger.debug("[FULLCONTACT:ES:ValidateDupAndStoreContactsBatch] - Validating And Storing contacts BATCH '%s' to ELASTIC", helpers.JSON_Encode(contacts))

	-------------------------------------------------
	-- 1 - chequeo de duplicados (busco si existe un contacto)
	--
	local duplicateCheck = (options.DuplicateCheck or ""):upper()

	local function ValidateDuplicatedContact (contact)
		local duplicatedContact
		-- POR ID
		if duplicateCheck == "KEY" then
			local success, contactInfo = self:GetContact(contact.Id, contact.VCC)
			if success then
				--logger.debug("[FULLCONTACT_ES::ValidateDupAndStoreContactsBatch]     a---ValidateDuplicatedContact--------> duplicateCheck: 'KEY' contactId %s already exist.", contactInfo.Id)
				duplicatedContact = contactInfo
			--else
				--logger.debug("[FULLCONTACT_ES::ValidateDupAndStoreContactsBatch]     b---ValidateDuplicatedContact--------> duplicateCheck: 'KEY' contactId %s does not exist.", contact.Id)
			end

		-- POR EXTERNALID
		elseif duplicateCheck == "EXTERNALID" then
			local success, resultSearch = self:GetContactByExternalId(contact.ExternalId, contact.VCC)
			if success then
				local firstMatchedContact = resultSearch.contacts and resultSearch.contacts[1] or {}
				if firstMatchedContact.Id then
					duplicatedContact = firstMatchedContact
					--local contactId = duplicatedContact.Id
					--logger.debug("[FULLCONTACT_ES::ValidateDupAndStoreContactsBatch]     c---ValidateDuplicatedContact-----------> duplicateCheck: 'EXTERNALID' contactId %s is already exist with ExternalId '%s'", contactId, contact.ExternalId)
				end
			--else
				--logger.debug("[FULLCONTACT_ES::ValidateDupAndStoreContactsBatch]     d---ValidateDuplicatedContact--------> duplicateCheck: 'EXTERNALID' contactId %s does not exist with externalId: %s.", contact.Id, contact.ExternalId)
			end

		-- POR TELEFONO o sino POR NOMBRE DE CONTACTO Y TELEFONO
		elseif duplicateCheck == "PHONE" or duplicateCheck == "NAMEPHONE" then
			for i, address in ipairs(contact.Addresses or {}) do
				if (address.Type or ""):upper() == "PHONE" then
					local queryES = {}
					queryES.last = ""
					queryES.rowsPerPage = 1
					queryES.VCC = contact.VCC
					queryES.filter = {
						address = {
							type = "address",
							addressType = "Phone",
							addressId = address.Number,
							exact = true
						}
					}

					if duplicateCheck == "NAMEPHONE" then
						local contactName = contact.Name or ""
						queryES.filter = queryES.filter or {}
						queryES.filter.Name = {
							values = {contactName},
							exact = true
						}
					end
	
					--logger.debug("[FULLCONTACT_ES::ValidateDupAndStoreContactsBatch]      %s --ValidateDuplicatedContact---------> queryES :'%s'.", i, helpers.JSON_Encode(queryES))
					local ok, resultSearch = self:SearchContacts(queryES)
					--logger.debug("[FULLCONTACT_ES::ValidateDupAndStoreContactsBatch]      %s --ValidateDuplicatedContact---------> ok: '%s', resultSearch :'%s'.", i, ok, helpers.JSON_Encode(resultSearch or {}))
	
					if ok and resultSearch then
						local firstMatchedContact = resultSearch.contacts and resultSearch.contacts[1] or {}
						if firstMatchedContact.Id then
							-- cuando la busqueda es por teléfono únicamente corta el break de addresses
							duplicatedContact = firstMatchedContact
							--local contactId = duplicatedContact.Id
							--logger.debug("[FULLCONTACT_ES::ValidateDupAndStoreContactsBatch]     -----------> duplicateCheck: 'PHONE' contactId %s with Phone AddressId: '%s' already exist.", contactId, address.Number)
							break
						end
					end
				end
			end
		--else
			--logger.warn("[FULLCONTACT_ES::ValidateDupAndStoreContactsBatch]    Unrecognized duplicateCheck: '%s'.", duplicateCheck)
		end

		if not duplicatedContact then
			-- NO es un duplicado
			return "NOT DUPLICATED"
		end
		-- SI es un duplicado
		return "DUPLICATED", duplicatedContact
	end

	--[[
		 1.1 Recorro el batch de contactos buscando Duplicados.

		array "duplicatedContactsInfo" con la lista de todos los contactos, con los resultados de la validacion de la funcion "ValidateDuplicatedContact"
		con este array se va a decidir que accion se debe realizar contra ES (create, update, delete) en 3.1
	]]--

	--
	--logger.debug("")
	--local msg = string.format("SEARCH ES %s CONTACTS -----------------------------------------", #contacts)
	--local processId = helpers.elapsedTime:Start(msg)

	local duplicatedContactsInfo = {}
	for i, contact in ipairs(contacts) do
		--logger.debug("-----------------------------------------------------------------------------------------------")
		--logger.debug("[FULLCONTACT_ES::ValidateDupAndStoreContactsBatch] %d --VALIDANDO duplicado - contactId '%s'.", i, contact.Id)
		--logger.debug("-----------------------------------------------------------------------------------------------")
		local status, duplicatedContact = ValidateDuplicatedContact(contact)
		--logger.debug("[FULLCONTACT_ES::ValidateDupAndStoreContactsBatch]         %d -- Validacion dup - contactId '%s' status: '%s', duplicatedContact.Id: '%s'.", i, contact.Id, status, (duplicatedContact or {}).Id)
		table.insert(duplicatedContactsInfo, {
			status            = status,
			duplicatedContact = duplicatedContact
		})
	end
	--helpers.elapsedTime:End(processId, msg)
	-- logger.debug("")
	-- logger.debug("[FULLCONTACT_ES::ValidateDupAndStoreContactsBatch.UpdateCurrentContact]---> duplicatedContactsInfo: '%s'.", helpers.JSON_Encode(duplicatedContactsInfo))

	-------------------------------------------------
	-- 2 - acciones sobre los duplicados
	--
	local duplicateSolver = (options.DuplicateSolver or ""):upper()

	local function UpdateDuplicatedContact (contact, duplicatedContact)
		local function CanUpdateAttr (name, forceAttrsUpdateList)
			name = (name or ""):upper()
			if forceAttrsUpdateList then
				if forceAttrsUpdateList[name] then
					return true
				end
			end
			local notUpdateAttrsList = {
				ID         = true,
				CREATEDAT  = true,
				--LASTUPDATE = true,
				VCC        = true,
				STATE      = true
			}
			if notUpdateAttrsList[name] then
				return false
			end
			return true
		end

		local duplicatedContactId = duplicatedContact and duplicatedContact.Id
		local contactInfo
		--logger.debug("[FULLCONTACT_ES::ValidateDupAndStoreContactsBatch.UpdateCurrentContact]--->    UpdateDuplicatedContact  --> contact.Id: '%s' duplicatedContactId: '%s' duplicateSolver: %s.", contact.Id, duplicatedContactId, duplicateSolver)

		local contactIdToRemove = nil
		if not duplicatedContact or "KEEPNEW" == duplicateSolver then -- (KEEP NEW)
			-- Creo un contacto nuevo
			--logger.debug("[FULLCONTACT_ES::ValidateDupAndStoreContactsBatch]---> 1 - %s ContactId: '%s' duplicateSolver: '%s'.", duplicatedContact and " UPDATE CURRENT" or " CREATE NEW", contact.Id, duplicateSolver)
			local contactInfoNEW = {}
			
			for name, value in pairs(contact) do
				--logger.debug("  --> name:'%s', value: '%s'.", name, value)
				if not duplicatedContact then
					contactInfoNEW[name] = value
				else
					local forceAttrsUpdateList = {
						ID = true
					}
					if CanUpdateAttr(name, forceAttrsUpdateList) then
						contactInfoNEW[name] = value
					else
						-- este es el caso del ID y otros atributos que no se pueden modificar
						contactInfoNEW[name] = duplicatedContact[name]
						--logger.debug("    no lo actualizo --> name:'%s', value: '%s'. Dejo el valor anterior.", name, value, duplicatedContact[name])
					end
				end
			end
			contactInfo = contactInfoNEW

			if contactInfo.Id and duplicatedContactId then
				if contactInfo.Id:lower() ~= duplicatedContactId:lower() then
					contactIdToRemove = duplicatedContactId
				end
			end
		else
			-- Actualizo el contacto existente
			--logger.debug("[FULLCONTACT_ES::ValidateDupAndStoreContactsBatch] 2 - Update current ContactId: '%s', duplicateSolver: '%s'.", contact.Id, duplicateSolver)
			--[[
				Atributos de tipo Tablas:
					Addresses
					CustomValues
					HomeAddresses
					NameValuesSearch
					NameValuesSimple
					ContactScheduleDate
					Skills
			]]--
			local function UpdateCurrentContact (sourceContact, targetContact)
				--logger.debug("[FULLCONTACT_ES::ValidateDupAndStoreContactsBatch.UpdateCurrentContact]==> 3 - (start merge) duplicatedContact: '%s' Update current - sourceContact.Id: '%s', targetContact.Id: '%s'.", duplicatedContact, sourceContact.Id, targetContact.Id)
				--
				-- 1 - atributos que NO sean "table".
				for name, value in pairs(sourceContact) do
					if type(value) ~= "table" then
						local canUpdate = CanUpdateAttr(name)
						--logger.debug("  --> name:'%s', value: '%s'. canUpdate: %s", name, value, canUpdate)
						if canUpdate then
							targetContact[name] = value
						end
					end
				end
				-- 2 - Addresses "table"
				local newAddresses = {}
				local existingPhoneAddresses = {}
				local existingOtherAddresses = {}
				-- los addresses del contacto destino
				for _, address in ipairs(targetContact.Addresses or {}) do
					if (address.Type or ""):lower() == "phone" then
						existingPhoneAddresses[tostring(address.Number):lower()] = true
					else
						existingOtherAddresses[tostring(address.address):lower()] = true
					end
				end
				-- los addresses del contacto origen
				for _, address in ipairs(sourceContact.Addresses or {}) do
					if (address.Type or ""):lower() == "phone" then
						local addressValue = tostring(address.Number or ""):lower()
						if not existingPhoneAddresses[addressValue] then
							table.insert(newAddresses, address)
						end
					else
						local addressValue = tostring(address.address or ""):lower()
						if not existingOtherAddresses[addressValue] then
							table.insert(newAddresses, address)
						end
					end
				end
				targetContact.Addresses = targetContact.Addresses or {}
				for _, address in ipairs(newAddresses) do 
					table.insert(targetContact.Addresses, address)
				end
				-- 3 HomeAddresses
				targetContact.HomeAddresses           = helpers.MergeTables(sourceContact.HomeAddresses, targetContact.HomeAddresses)
				-- 4 Skills
				targetContact.Skills                  = helpers.MergeArrayWithoutRepeats(sourceContact.Skills, targetContact.Skills, "SkillId")
				-- 5 Name Value Simple
				targetContact.NameValuesSimple        = helpers.MergeArrayWithoutRepeats(sourceContact.NameValuesSimple, targetContact.NameValuesSimple, "Name")
				-- 6 Name Value Date
				targetContact.NameValuesSearchDate    = helpers.MergeArrayWithoutRepeats(sourceContact.NameValuesSearchDate, targetContact.NameValuesSearchDate, "Name")
				-- 7 Name Value Integer
				targetContact.NameValuesSearchNumeric = helpers.MergeArrayWithoutRepeats(sourceContact.NameValuesSearchNumeric, targetContact.NameValuesSearchNumeric, "Name")
				-- 8 Name Value Text
				targetContact.NameValuesSearchText    = helpers.MergeArrayWithoutRepeats(sourceContact.NameValuesSearchText, targetContact.NameValuesSearchText, "Name")
				-- 9 - Retorna el Contacto que se ha mergeado.
				--logger.debug("[FULLCONTACT_ES::ValidateDupAndStoreContactsBatch.UpdateCurrentContact]==> 4 - (end merge) Update current - sourceContact.Id: '%s', targetContact.Id: '%s'.", sourceContact.Id, targetContact.Id)
				return targetContact
			end

			local sourceContact, targetContact
			if "APPENDOLDTONEW" == duplicateSolver then -- (APPEND OLD TO NEW)
				sourceContact = duplicatedContact -- old
				targetContact = contact           -- new

				if sourceContact.Id and targetContact.Id then
					if sourceContact.Id:lower() ~= targetContact.Id:lower() then
						contactIdToRemove = duplicatedContactId
					end
				end
				--
				--targetContact.Id = duplicatedContactId
				targetContact.CreatedAt = duplicatedContact.CreatedAt
				duplicatedContact.LastUpdate = targetContact.LastUpdate
				sourceContact.ImportId = targetContact.ImportId

			elseif "APPENDNEWTOOLD" == duplicateSolver then -- (APPEND NEW TO OLD)
				sourceContact = contact           -- new
				targetContact = duplicatedContact -- old

				-- if sourceContact.Id and targetContact.Id then
				-- 	if sourceContact.Id:lower() ~= targetContact.Id:lower() then
				-- 		contactIdToRemove = duplicatedContactId
				-- 	end
				-- end
				--
				targetContact.ImportId = sourceContact.ImportId
	
			else -- KEEPOLD (KEEP OLD)
				--logger.debug("[FULLCONTACT_ES::ValidateDupAndStoreContactsBatch.UpdateCurrentContact] 2.4 --> Unable to Update Current ContactId: '%s', duplicateSolver: '%s'.", contact.Id, duplicateSolver)
				return false, {reason = "ALREADY EXIST", action = "KEEPOLD", type = "ALREADY EXIST", contactId = contact.Id}
			end

			-- Remuevo el contacto existente.
			--logger.debug("[FULLCONTACT_ES::ValidateDupAndStoreContactsBatch.UpdateCurrentContact]---> 4 -Removing contact: '%s' vcc: '%s'.", sourceContact.Id, sourceContact.VCC)
			--local mustNotShowMessage = true
			--local success, responseBody = self:RemoveContact(sourceContact.Id, sourceContact.VCC, mustNotShowMessage)
			--logger.debug("[FULLCONTACT_ES::ValidateDupAndStoreContactsBatch.UpdateCurrentContact]---> 5 - Removing contact: '%s' vcc: '%s', result: %s.", sourceContact.Id, sourceContact.VCC, success)

			-- Actualizo los datos del contacto existente.
			contactInfo = UpdateCurrentContact(sourceContact, targetContact)
		end

		--logger.debug("[FULLCONTACT_ES::ValidateDupAndStoreContactsBatch] ******------UpdateDuplicatedContact------> duplicateSolver: '%s' DIFERTENTES Ids: '%s', contactInfo.Id: '%s' duplicatedContactId: '%s'.", duplicateSolver, (contactInfo.Id ~= duplicatedContactId), contactInfo.Id, duplicatedContactId)

		return true, contactInfo, contactIdToRemove
	end

	--logger.debug("[FULLCONTACT_ES::ValidateDupAndStoreContactsBatch.UpdateCurrentContact]---> 6 - Storing CONTACT BATCH: contactInfo: '%s'.", helpers.JSON_Encode(contactInfo))

	-------------------------------------------------
	-- 3 - actualizaciones en Batch hacia ES.
	--

	--[[
		3.1 Recorro el batch de contactos y decido que accion contra ES se va ejecutar (create, update, delete)
			La accion a ejecutar se determina del resultado del array "duplicatedContactsInfo" que se genero en 1.1
	]]--

	--logger.debug("[FULLCONTACT_ES::ValidateDupAndStoreContactsBatch.UpdateCurrentContact]---> duplicatedContactsInfo: '%s'.", helpers.JSON_Encode(duplicatedContactsInfo))
	local contactsBatch = {}
	local contactsExcludedOfBatch = {} -- caso KEEP OLD

	for i, contact in ipairs(contacts) do
		local duplicatedContactInfo = duplicatedContactsInfo[i]
		local duplicatedContact, action
		if not duplicatedContactInfo then
			action = "CREATE"
		else
			if duplicatedContactInfo.status == "NOT DUPLICATED" then
				action = "CREATE"
			else
				action = "UPDATE"
				duplicatedContact = duplicatedContactInfo.duplicatedContact
			end
		end
		local mustUpdate, contactInfoToUpdate, contactIdToRemove = UpdateDuplicatedContact(contact, duplicatedContact)
		if mustUpdate then
			--logger.debug("[FULLCONTACT_ES::ValidateDupAndStoreContactsBatch.UpdateCurrentContact] %d -----> mustUpdate: '%s'. %s contact '%s'. remove contact '%s'.", i, mustUpdate, action, (contactInfoToUpdate or {}).Id, contactIdToRemove)
			if contactIdToRemove then
				local contactBatch = {
					action  = "DELETE",
					contact = {
						Id = contactIdToRemove
					}
				}
				table.insert(contactsBatch, contactBatch)
				action = "CREATE"
			end
			local contactBatch = {
				action  = action, --"CREATE" "UPDATE"
				contact = contactInfoToUpdate
			}
			--logger.debug("[FULLCONTACT_ES::ValidateDupAndStoreContactsBatch.UpdateCurrentContact] %d ---> contactBatch: '%s'.", i, helpers.JSON_Encode(contactBatch))
			table.insert(contactsBatch, contactBatch)
		else
			-- caso KEEP OLD
			table.insert(contactsExcludedOfBatch, contact)
			local resp = contactInfoToUpdate
			--logger.debug("[FULLCONTACT_ES::ValidateDupAndStoreContactsBatch.UpdateCurrentContact] %d -----> mustUpdate: '%s'. %s Response '%s'.", i, mustUpdate, action, json.encode(resp or {}))
		end
	end

	-- 4. Si no existen contactos en el batch para actualizar.
	if #contactsBatch == 0 then
		-- NO es un error, se puede dar en la accion KEEPOLD.
		local detail = {added = 0, updated = #contacts, errors = 0, duplicatedContactsInfo = duplicatedContactsInfo}
		--logger.debug("[FULLCONTACT_ES::ValidateDupAndStoreContactsBatch.UpdateCurrentContact] RESULT 1--> success: TRUE detail: '%s'.", json.encode(detail or {}))
		return true, {}, detail
	end

	-- 5. envio contra ES el batch de contactos a actualizar.
	local elasticType = "CONTACTS_DIALER"
	local success, contactsResult, err = UpdateContactsBatchInES(vcc, contactsBatch, elasticType) -- Guarda en ES
	if not success then
		-- error
		--logger.debug("[FULLCONTACT_ES::ValidateDupAndStoreContactsBatch.UpdateCurrentContact] RESULT 2--> success: FALSE contactsResult: '%s' err: '%s'.", json.encode(contactsResult or {}), json.encode(err or {}))
		return false, contactsResult, err
	end
	-- ok
	local detail = {added = 0, updated = #contactsExcludedOfBatch, errors = 0, duplicatedContactsInfo = duplicatedContactsInfo}
	--logger.debug("[FULLCONTACT_ES::ValidateDupAndStoreContactsBatch.UpdateCurrentContact] RESULT 3--> success: TRUE contactsResult: '%s' detail: '%s'.", json.encode(contactsResult or {}), json.encode(detail or {}))
	return true, contactsResult, detail
end

m_dialer.UpdateContactsBatch = function(self, vcc, contactsBatch)
	assert(type(vcc) == "string")
	assert(type(contactsBatch) == "table")
	--logger.debug("[FULLCONTACT:ES.Dialer:UpdateContactsBatch] UpdateContactsBatch - vcc: '%s', contactsBatch: %s", vcc, #contactsBatch)
	local elasticType = "CONTACTS_DIALER"
	return UpdateContactsBatchInES(vcc, contactsBatch, elasticType) -- Guarda en ES
end

--[[

]]--
m_dialer.MigrateContactsBatch = function(self, vcc, contactsBatch)
	assert(type(vcc) == "string")
	assert(type(contactsBatch) == "table")
	--logger.debug("[FULLCONTACT:ES.Dialer:MigrateContactsBatch] MigrateContactsBatch - vcc: '%s', contactsBatch: %s", vcc, #contactsBatch)
	return self:UpdateContactsBatch(vcc, contactsBatch) -- Guarda en ES
end

m_dialer.StoreContact = function(self, contact, options)
	assert(type(contact) == "table")
	--logger.debug("[FULLCONTACT:ES.Dialer:StoreContact] StoreContact - contact: '%s', options: %s", vcc, options)
	local elasticType = "CONTACTS_DIALER"
	return StoreContact(contact, elasticType) -- Guarda en ES
end

m_dialer.SearchContacts = function(self, request)
	assert(type(request) == "table")
	--logger.debug("[FULLCONTACT:ES.Dialer:SearchContacts] Search contacts - request: %s", json.encode(request))
	local elasticType = "CONTACTS_DIALER"
	return SearchContacts(request, elasticType)
end

m_dialer.GetContact = function(self, contactId, vcc)
	assert(type(contactId) == "string")
	assert(type(vcc) == "string")
	--logger.debug("[FULLCONTACT:ES.Dialer:GetContact] Get Contact Id '%s', vcc: '%s'", contactId, vcc)
	local elasticType = "CONTACTS_DIALER"
	return GetContact(contactId, vcc, elasticType)
end

m_dialer.GetContactByExternalId = function(self, externalId, vcc)
	assert(type(externalId) == "string")
	assert(type(vcc) == "string")
	--logger.debug("[FULLCONTACT:ES.Dialer:GetContactByExternalId] Get Contact - externalId '%s', vcc: '%s'", externalId, vcc)
	local queryES = {
		last = "",
		rowsPerPage = 1,
		VCC = vcc,
		filter = {
			ExternalId = {
				values = {externalId},
				exact = true
			}
		}
	}
	--logger.debug("[FULLCONTACT_ES::GetContactByExternalId]     --------> queryES :'%s'.", helpers.JSON_Encode(queryES))
	local ok, contact = m_dialer:SearchContacts(queryES)
	--logger.debug("[FULLCONTACT_ES::GetContactByExternalId]       ok: '%s', contact :'%s'.", ok, helpers.JSON_Encode(contact or {}))
	return ok, contact
end

--[[
	Funcion que arma la estructura de "_bulk" "delete" esperada por ES.
]]--
m_dialer.RemoveContactsBatch = function(self, vcc, contacts)
	assert(type(vcc) == "string")
	assert(type(contacts) == "table")

	logger.info("[FULLCONTACT:ES:RemoveContactsBatch] - Removing '%s' contacts BATCH to ELASTIC SEARCH. vcc: %s", #contacts, vcc)
	--logger.debug("[FULLCONTACT:ES:RemoveContactsBatch] - Removing contacts BATCH '%s' to ELASTIC", helpers.JSON_Encode(contacts))

	local contactsBatch = {}
	for i, contact in ipairs(contacts) do
		if contact.Id then
			local contactBatch = {
				action  = "DELETE",
				contact = {
					Id = contact.Id
				}
			}
			table.insert(contactsBatch, contactBatch)
			--logger.debug("[FULLCONTACT_ES::RemoveContactsBatch] %d ---> contactBatch: '%s'.", i, helpers.JSON_Encode(contactBatch))
		end
	end
	-- 2. envio el batch de contactos a actualizar.
	local elasticType = "CONTACTS_DIALER"
	local success, contactsResult, err = UpdateContactsBatchInES(vcc, contactsBatch, elasticType) -- Guarda en ES
	if not success then
		-- error
		return false, contactsResult, err
	end
	-- ok
	local detail = err
	return true, contactsResult, detail
end

------------------------------------------
-- BlackList
--

m_blackList.MakeTypeAddressURL = function(params)
	assert(type(params) == "table")
	--logger.debug("[FULLCONTACT.blacklist:ES:MakeTypeAddressURL] - Making Addresses URL params: '%s'.", json.encode(params))
	local indexURL = m_blackList.MakeIndexURL(params)
	local url = ("%s/_doc"):format(indexURL)
	--logger.debug("[FULLCONTACT.blacklist.ES:MakeTypeAddressURL] - url: '%s'.", url)
	return url
end

m_blackList.MakeIndexURL = function(params)
	assert(type(params) == "table")
	--logger.debug("[FULLCONTACT.blacklist:ES:MakeIndexURL] - Making Index URL params: '%s'.", json.encode(params))
	local elasticInfo = GetElasticInfoByType(params.elasticType)
	local indexURL = elasticInfo.INDEX_NAME
	--logger.debug("[FULLCONTACT.blacklist.ES:MakeIndexURL] - elasticType: '%s', indexName: '%s', indexURL: '%s'.", elasticInfo.TYPE, elasticInfo.INDEX_NAME, indexURL)
	return indexURL
end

m_blackList.MakeBulkURL = function(params)
	assert(type(params) == "table")
	local indexURL = m_blackList.MakeIndexURL(params)
	local url = ("%s/_bulk"):format(indexURL)
	url = MakePrettyUrlParams(url, params.isPrettyResponse)
	return url, indexURL
end

m_blackList.MakeAddressURL = function(params)
	assert(type(params) == "table")
	assert(type(params.addressKey) == "string")
	local addressURL = m_blackList.MakeTypeAddressURL(params)
	local url = ("/%s/%s"):format(addressURL, params.addressKey)
	--logger.debug("[FULLCONTACT.blacklist.ES:MakeAddressURL] Get Address URL: '%s'.", url)
	return url
end

--[[
	Funcion que realiza el Request de tipo "bulk" hacia ES.
	Se debe postear en una estructura de tipo NDJSON, que es un JSON delimitado por saltos de linea.
	Ej:
		{ "create" : { "_index" : "elindice", "_id" : contactid1" } }
		{"name" :"emma"}
		{ "update" : { "_index" : "elindice", "_id" : contactid1" } }
		{ "doc": {"name" :"Emma Vallejo de Mello"} }
		{ "delete" : { "_index" : "elindice", "_id" : contactid1" } }
		..
		..
]]--
m_blackList.UpdateAddressBatchInES = function(vcc, addressesBatch, elasticType)
	assert(vcc)
	assert(type(addressesBatch) == "table")

	local elasticInfo = GetElasticInfoByType(elasticType)
	-- "/blacklist_dialer/_bulk?pretty", "blacklist_dialer"
	local url, indexName = m_blackList.MakeBulkURL({VCC = vcc, elasticType = elasticType, isPrettyResponse = true})

	--logger.debug("[FULLCONTACT.blacklist.ES:UpdateAddressBatchInES] --> Updating '%s' addresses batch to ELASTIC SEARCH. URL '%s', indexName: '%s'.", #addressesBatch, url, indexName)
	--logger.debug("[FULLCONTACT.blacklist.ES:UpdateAddressBatchInES] --> Updating addresses batch to ELASTIC SEARCH. URL '%s', indexName: '%s'. addressesBatch: %s", url, indexName, json.encode(addressesBatch))

	if #addressesBatch == 0 then
		-- TODO: Ver si en la respuesta tienen que viajar otros valores.
		logger.info("[FULLCONTACT.blacklist.ES:UpdateAddressBatchInES] --> There are no addresses to update in the batch to ELASTIC SEARCH. URL '%s', indexName: '%s'.", url, indexName)
		return true, {}, {}
	end

	local function SanitizeAddress (address)
		local addressInfo = {}
		for name, value in pairs(address) do
			--logger.debug("  [FULLCONTACT.blacklist.ES:UpdateAddressBatchInES] --> name:'%s', value: '%s'.", name, value)
			if type(value) == "string" then
				value = helpers.toUTF8(value)
			end
			addressInfo[name] = value
		end
		return addressInfo
	end

	local function MakeBulkAction (action, id)
		return string.format('{ "%s" : { "_index" : "%s", "_id" : "%s" } }', action:lower(), indexName, id)
	end

	local encodedContactsBatch = ''
	for i, addressInfo in ipairs(addressesBatch) do
		local address = SanitizeAddress(addressInfo.address)
		local encodedAddress = json.encode(address)
		local action = (addressInfo.action or ""):upper()

		local encodedAction = ""
		if action == "CREATE" then
			encodedAction =	MakeBulkAction(action, address.Key) ..
				'\n' ..
				encodedAddress ..
				'\n'
		elseif action == "UPDATE" then
			encodedAction = MakeBulkAction(action, address.Key) ..
				'\n' ..
				string.format('{ "doc" : %s }', encodedAddress) ..
				'\n'
		elseif action == "DELETE" then
			encodedAction = MakeBulkAction(action, address.Key) ..
				'\n'
		else
			assert("Unrecognized ES bulk action: " .. action)
		end
		--logger.debug(" %d - action: %s --> encodedAction: %s", i, action, encodedAction)
		encodedContactsBatch = encodedContactsBatch .. encodedAction
	end

	-- creo informacion necesaria sobre el header y demas (Ej: Authorization)
	local requestInfo = helpers.MakeRequestInfoForEs({
		credentials  = elasticInfo.connection.credentials,
		encodedQuery = encodedContactsBatch
	})

	--logger.debug("[FULLCONTACT.blacklist.ES:UpdateAddressBatchInES]---> ES EXECUTE --> http.request -> url: '%s', encodedContactsBatch: '%s'", url, requestInfo.encodedData)
	local responseBody = {}

	local ok, httpCode, responseHeaders, responseStatusLine = http.request({
		host = elasticInfo.connection.host,
		port = elasticInfo.connection.port,
		url = url,
		headers = requestInfo.headers,
		method = "POST",
		source = requestInfo.source,
		sink = ltn12.sink.table(responseBody),
		redirect = false
	})

	httpCode = tonumber(httpCode)
	responseHeaders = responseHeaders or {}

	--logger.debug("[FULLCONTACT.blacklist.ES:UpdateAddressBatchInES] Success: %s %s\n%s\n%s\n%s", ok, httpCode, json.encode(responseHeaders), responseStatusLine, json.encode(responseBody))

	local function MakeErrorES ()
		local err = {
			type = "ELASTIC SEARCH ERROR"
		}
		if type(responseBody) ~= "table" then
			return err
		end
		local encodedESErr = responseBody[1]
		--logger.debug("--------------> encodedESErr %s", encodedESErr)
		if type(encodedESErr) ~= "string" then
			return err
		end
		local nativeError = json.decode(encodedESErr)
		err.nativeError = nativeError
		if nativeError.error and nativeError.error.caused_by and nativeError.error.caused_by.reason then
			err.type = err.type .. " - " .. nativeError.error.caused_by.reason
		end
		return err
	end

	if not ok then
		logger.error("[FULLCONTACT.blacklist.ES:UpdateAddressBatchInES] http code: %s\n%s\n%s\n%s", httpCode, json.encode(responseHeaders), responseStatusLine, responseBody)
		local err = MakeErrorES()
		return false, responseBody, err
	end

	-- OK o CREATED o ACCEPTED
	if httpCode == 200 or httpCode == 201 or httpCode == 202 then
		responseBody = table.concat(responseBody)
		--logger.debug("[FULLCONTACT.blacklist.ES:UpdateAddressBatchInES] http code: %s\nresponseHeaders: %s\nresponseStatusLine: %s\nresponseBody: %s", httpCode, json.encode(responseHeaders), responseStatusLine, responseBody)
		if responseHeaders["content-type"]:match("^application/json") then
			local response = json.decode(responseBody)
			local detail = {}
			for i, info in pairs(response.items or {}) do
				local action, data = next(info)
				action = action:lower()
				detail[action] = detail[action] or {}
				local result = (data.result or ""):lower()
				detail[action][result] = (detail[action][result] or 0) + 1
			end
			--logger.info("[FULLCONTACT.blacklist.ES:UpdateAddressBatchInES] Success total '%s' to ELASTIC SEARCH. Detail: %s", #addressesBatch, json.encode(detail))
			return true, response, detail
		end
	end

	logger.warn("[FULLCONTACT.blacklist.ES:UpdateAddressBatchInES] Unknown response from ELASTIC SEARCH. \nhttp code: %s\nheaders: %s\nresponse: %s\nstatus line: %s", httpCode, json.encode(responseHeaders), json.encode(responseBody), responseStatusLine)

	local err = MakeErrorES()
	return false, responseBody, err
end

--[[
	Funcion encargada de realizar validaciones de duplicados en el batch de addresses y decidir que acciones realizar (create, update, delete)
]]--
m_blackList.ValidateDupAndStoreAddressesBatch = function(self, vcc, addresses, options)
	assert(type(addresses) == "table")
	vcc = vcc or "system"
	options = options or {}

	--logger.debug("[FULLCONTACT.blacklist.ES:ValidateDupAndStoreAddressesBatch] (START)- Validating And Storing '%s' addresses BATCH to ELASTIC SEARCH. options: %s", #addresses, helpers.JSON_Encode(options))
	--logger.debug("[FULLCONTACT.blacklist.ES:ValidateDupAndStoreAddressesBatch] - Validating And Storing addresses BATCH '%s' to ELASTIC", helpers.JSON_Encode(addresses))

	-------------------------------------------------
	-- 1 - chequeo de duplicados (busco si existe un address)
	--
	local duplicateCheck = (options.DuplicateCheck or ""):upper()

	local function ValidateDuplicatedAddress (address)
		local duplicatedAddress
		-- POR ID
		if duplicateCheck == "KEY" then

		-- POR TELEFONO o sino POR NOMBRE DE CONTACTO Y TELEFONO
		elseif duplicateCheck == "PHONE" or duplicateCheck == "NAMEPHONE" then

		end

		if not duplicatedAddress then
			-- NO es un duplicado
			return "NOT DUPLICATED"
		end
		-- SI es un duplicado
		return "DUPLICATED", duplicatedAddress
	end

	--[[
		 1.1 Recorro el batch de addresses buscando Duplicados.

		array "duplicatedAddressesInfo" con la lista de todos los addresses, con los resultados de la validacion de la funcion "ValidateDuplicatedAddress"
		con este array se va a decidir que accion se debe realizar contra ES (create, update, delete) en 3.1
	]]--

	
	--logger.debug("")
	--local msg = string.format("SEARCH ES %s ADDRESSES -----------------------------------------", #addresses)
	--local processId = helpers.elapsedTime:Start(msg)

	local duplicatedAddressesInfo = {}
	for i, address in ipairs(addresses) do
		--logger.debug("-----------------------------------------------------------------------------------------------")
		--logger.debug("[FULLCONTACT_ES::ValidateDupAndStoreAddressesBatch] %d --VALIDANDO duplicado - addressKey '%s'.", i, address.Id)
		--logger.debug("-----------------------------------------------------------------------------------------------")
		local status, duplicatedAddress = ValidateDuplicatedAddress(address)
		--logger.debug("[FULLCONTACT_ES::ValidateDupAndStoreAddressesBatch]         %d -- Validacion dup - addressKey '%s' status: '%s', duplicatedAddress.Id: '%s'.", i, address.Id, status, (duplicatedAddress or {}).Id)
		table.insert(duplicatedAddressesInfo, {
			status            = status,
			duplicatedAddress = duplicatedAddress
		})
	end
	--helpers.elapsedTime:End(processId, msg)
	-- logger.debug("")
	-- logger.debug("[FULLCONTACT_ES::ValidateDupAndStoreAddressesBatch.UpdateCurrentContact]---> duplicatedAddressesInfo: '%s'.", helpers.JSON_Encode(duplicatedAddressesInfo))

	-------------------------------------------------
	-- 2 - acciones sobre los duplicados
	--
	local duplicateSolver = (options.DuplicateSolver or ""):upper()

	local function UpdateDuplicatedAddress (address, duplicatedAddress)
		local function CanUpdateAttr (name, forceAttrsUpdateList)
			name = (name or ""):upper()
			if forceAttrsUpdateList then
				if forceAttrsUpdateList[name] then
					return true
				end
			end
			local notUpdateAttrsList = {
				ID         = true,
				KEY        = true,
				CREATEDAT  = true,
				--LASTUPDATE = true,
				VCC        = true,
				STATE      = true
			}
			if notUpdateAttrsList[name] then
				return false
			end
			return true
		end

		local duplicatedContactKey = duplicatedAddress and duplicatedAddress.Key
		local addressInfo
		--logger.debug("[FULLCONTACT_ES::ValidateDupAndStoreAddressesBatch.UpdateCurrentContact]--->    UpdateDuplicatedAddress  --> address.Key: '%s' duplicatedContactKey: '%s' duplicateSolver: %s.", address.Key, duplicatedContactKey, duplicateSolver)

		local addressKeyToRemove = nil
		if not duplicatedAddress or "KEEPNEW" == duplicateSolver then -- (KEEP NEW)
			-- Creo un contacto nuevo
			--logger.debug("[FULLCONTACT_ES::ValidateDupAndStoreAddressesBatch]---> 1 - %s AddressKey: '%s' duplicateSolver: '%s'.", duplicatedAddress and " UPDATE CURRENT" or " CREATE NEW", address.Key, duplicateSolver)
			local contactInfoNEW = {}
			for name, value in pairs(address) do
				--logger.debug("  --> name:'%s', value: '%s'.", name, value)
				if not duplicatedAddress then
					contactInfoNEW[name] = value
				else
					local forceAttrsUpdateList = {
						ID = true
					}
					if CanUpdateAttr(name, forceAttrsUpdateList) then
						contactInfoNEW[name] = value
					else
						-- este es el caso del ID y otros atributos que no se pueden modificar
						contactInfoNEW[name] = duplicatedAddress[name]
						--logger.debug("    no lo actualizo --> name:'%s', value: '%s'. Dejo el valor anterior.", name, value, duplicatedAddress[name])
					end
				end
			end
			addressInfo = contactInfoNEW

			if addressInfo.Key and duplicatedContactKey then
				if addressInfo.Key:lower() ~= duplicatedContactKey:lower() then
					addressKeyToRemove = duplicatedContactKey
				end
			end
		else
			-- Actualizo el contacto existente
			--logger.debug("[FULLCONTACT_ES::ValidateDupAndStoreAddressesBatch] 2 - Update current ContactId: '%s', duplicateSolver: '%s'.", address.Id, duplicateSolver)
			--[[
				Atributos de tipo Tablas:
					Addresses
					CustomValues
					HomeAddresses
					NameValuesSearch
					NameValuesSimple
					ContactScheduleDate
					Skills
			]]--
			local function UpdateCurrentAddress (sourceContact, targetContact)
				--logger.debug("[FULLCONTACT_ES::ValidateDupAndStoreAddressesBatch.UpdateCurrentAddress]==> 3 - (start merge) duplicatedAddress: '%s' Update current - sourceContact.Id: '%s', targetContact.Id: '%s'.", duplicatedAddress, sourceContact.Id, targetContact.Id)
				--
				-- 1 - atributos que NO sean "table".
				for name, value in pairs(sourceContact) do
					if type(value) ~= "table" then
						local canUpdate = CanUpdateAttr(name)
						--logger.debug("  --> name:'%s', value: '%s'. canUpdate: %s", name, value, canUpdate)
						if canUpdate then
							targetContact[name] = value
						end
					end
				end
				-- 2 - Addresses "table"
				local newAddresses = {}
				local existingPhoneAddresses = {}
				local existingOtherAddresses = {}
				-- los addresses del contacto destino
				for _, address in ipairs(targetContact.Addresses or {}) do
					if (address.Type or ""):lower() == "phone" then
						existingPhoneAddresses[tostring(address.Number):lower()] = true
					else
						existingOtherAddresses[tostring(address.address):lower()] = true
					end
				end
				-- los addresses del contacto origen
				for _, address in ipairs(sourceContact.Addresses or {}) do
					if (address.Type or ""):lower() == "phone" then
						local addressValue = tostring(address.Number or ""):lower()
						if not existingPhoneAddresses[addressValue] then
							table.insert(newAddresses, address)
						end
					else
						local addressValue = tostring(address.address or ""):lower()
						if not existingOtherAddresses[addressValue] then
							table.insert(newAddresses, address)
						end
					end
				end
				targetContact.Addresses = targetContact.Addresses or {}
				for _, address in ipairs(newAddresses) do 
					table.insert(targetContact.Addresses, address)
				end

				return targetContact
			end

			local sourceContact, targetContact
			if "APPENDOLDTONEW" == duplicateSolver then -- (APPEND OLD TO NEW)
				sourceContact = duplicatedAddress -- old
				targetContact = address           -- new

				if sourceContact.Id and targetContact.Id then
					if sourceContact.Id:lower() ~= targetContact.Id:lower() then
						addressKeyToRemove = duplicatedContactKey
					end
				end
				--
				--targetContact.Id = duplicatedContactKey
				targetContact.CreatedAt = duplicatedAddress.CreatedAt
				duplicatedAddress.LastUpdate = targetContact.LastUpdate
				sourceContact.ImportId = targetContact.ImportId

			elseif "APPENDNEWTOOLD" == duplicateSolver then -- (APPEND NEW TO OLD)
				sourceContact = address           -- new
				targetContact = duplicatedAddress -- old
				--
				targetContact.ImportId = sourceContact.ImportId
	
			else -- KEEPOLD (KEEP OLD)
				--logger.debug("[FULLCONTACT_ES::ValidateDupAndStoreAddressesBatch.UpdateCurrentAddress] 2.4 --> Unable to Update Current ContactId: '%s', duplicateSolver: '%s'.", address.Id, duplicateSolver)
				return false, {reason = "ALREADY EXIST", action = "KEEPOLD", type = "ALREADY EXIST", addressKey = address.Id}
			end

			-- Actualizo los datos del contacto existente.
			addressInfo = UpdateCurrentAddress(sourceContact, targetContact)
		end

		--logger.debug("[FULLCONTACT_ES::ValidateDupAndStoreAddressesBatch] ******------UpdateDuplicatedAddress------> duplicateSolver: '%s' DIFERTENTES Ids: '%s', addressInfo.Id: '%s' duplicatedContactKey: '%s'.", duplicateSolver, (addressInfo.Id ~= duplicatedContactKey), addressInfo.Id, duplicatedContactKey)

		return true, addressInfo, addressKeyToRemove
	end

	--logger.debug("[FULLCONTACT_ES::ValidateDupAndStoreAddressesBatch.UpdateCurrentAddress]---> 6 - Storing CONTACT BATCH: addressInfo: '%s'.", helpers.JSON_Encode(addressInfo))

	-------------------------------------------------
	-- 3 - actualizaciones en Batch hacia ES.
	--

	--[[
		3.1 Recorro el batch de addresses y decido que accion contra ES se va ejecutar (create, update, delete)
			La accion a ejecutar se determina del resultado del array "duplicatedAddressesInfo" que se genero en 1.1
	]]--

	--logger.debug("[FULLCONTACT_ES::ValidateDupAndStoreAddressesBatch.UpdateCurrentAddress]---> duplicatedAddressesInfo: '%s'.", helpers.JSON_Encode(duplicatedAddressesInfo))
	local addressBatch = {}
	local addressesExcludedOfBatch = {} -- caso KEEP OLD

	for i, address in ipairs(addresses) do
		local duplicatedAddressInfo = duplicatedAddressesInfo[i]
		local duplicatedAddress, action
		if not duplicatedAddressInfo then
			action = "CREATE"
		else
			if duplicatedAddressInfo.status == "NOT DUPLICATED" then
				action = "CREATE"
			else
				action = "UPDATE"
				duplicatedAddress = duplicatedAddressInfo.duplicatedAddress
			end
		end
		local mustUpdate, addressInfoToUpdate, addressKeyToRemove = UpdateDuplicatedAddress(address, duplicatedAddress)
		if mustUpdate then
			--logger.debug("[FULLCONTACT_ES::ValidateDupAndStoreAddressesBatch.UpdateCurrentAddress] %d -----> mustUpdate: '%s'. %s address '%s'. remove address '%s'.", i, mustUpdate, action, (addressInfoToUpdate or {}).Key, addressKeyToRemove)
			if addressKeyToRemove then
				local contactBatch = {
					action  = "DELETE",
					address = {
						Key = addressKeyToRemove
					}
				}
				table.insert(addressBatch, contactBatch)
				action = "CREATE"
			end
			local contactBatch = {
				action  = action, --"CREATE" "UPDATE"
				address = addressInfoToUpdate
			}
			--logger.debug("[FULLCONTACT_ES::ValidateDupAndStoreAddressesBatch.UpdateCurrentAddress] %d ---> contactBatch: '%s'.", i, helpers.JSON_Encode(contactBatch))
			table.insert(addressBatch, contactBatch)
		else
			-- caso KEEP OLD
			table.insert(addressesExcludedOfBatch, address)
			local resp = addressInfoToUpdate
			--logger.debug("[FULLCONTACT_ES::ValidateDupAndStoreAddressesBatch.UpdateCurrentAddress] %d -----> mustUpdate: '%s'. %s Response '%s'.", i, mustUpdate, action, json.encode(resp or {}))
		end
	end

	-- 4. Si no existen addresses en el batch para actualizar.
	if #addressBatch == 0 then
		-- NO es un error, se puede dar en la accion KEEPOLD.
		local detail = {added = 0, updated = #addresses, errors = 0, duplicatedAddressesInfo = duplicatedAddressesInfo}
		--logger.debug("[FULLCONTACT_ES::ValidateDupAndStoreAddressesBatch.UpdateCurrentAddress] RESULT 1--> success: TRUE detail: '%s'.", json.encode(detail or {}))
		return true, {}, detail
	end

	-- 5. envio contra ES el batch de addresses a actualizar.
	local elasticType = "BLACKLIST_DIALER"
	local success, contactsResult, err = m_blackList.UpdateAddressBatchInES(vcc, addressBatch, elasticType) -- Guarda en ES
	if not success then
		-- error
		--logger.debug("[FULLCONTACT_ES::ValidateDupAndStoreAddressesBatch.UpdateCurrentAddress] RESULT 2--> success: FALSE contactsResult: '%s' err: '%s'.", json.encode(contactsResult or {}), json.encode(err or {}))
		return false, contactsResult, err
	end
	-- ok
	local detail = {added = 0, updated = #addressesExcludedOfBatch, errors = 0, duplicatedAddressesInfo = duplicatedAddressesInfo}
	--logger.debug("[FULLCONTACT_ES::ValidateDupAndStoreAddressesBatch.UpdateCurrentAddress] RESULT 3--> success: TRUE contactsResult: '%s' detail: '%s'.", json.encode(contactsResult or {}), json.encode(detail or {}))
	return true, contactsResult, detail
end

m_blackList.UpdateAddressesBatch = function(self, vcc, addressesBatch)
	assert(type(vcc) == "string")
	assert(type(addressesBatch) == "table")
	--logger.debug("[FULLCONTACT:ES.blacklist:UpdateAddressesBatch] Update Addresses Batch - vcc: '%s', addressesBatch: %s", vcc, #addressesBatch)
	local elasticType = "BLACKLIST_DIALER"
	return m_blackList.UpdateAddressBatchInES(vcc, addressesBatch, elasticType) -- Guarda en ES
end

m_blackList.SearchAddresses = function(self, request)
	assert(type(request) == "table")
	--logger.debug("[FULLCONTACT:ES.blacklist:SearchAddresses] Search addresses - request: %s", json.encode(request))
	local elasticType = "BLACKLIST_DIALER"
	local success, response = SearchContacts(request, elasticType)
	if not success then
		local err = response
		return false, err
	end
	response.addresses = response.contacts
	response.contacts = nil
	return true, response
end

m_blackList.GetAddress = function(self, addressESKey)
	assert(type(addressESKey) == "string")
	--logger.debug("[FULLCONTACT:ES.blacklist:GetAddress] Get address - addressESKey '%s'", addressESKey)

	local elasticType = "BLACKLIST_DIALER"
	local elasticInfo = GetElasticInfoByType(elasticType)

	local url = m_blackList.MakeAddressURL({addressKey = addressESKey, elasticType = elasticType})
	--logger.debug("[FULLCONTACT:ES.blacklist:GetAddress] 1--------> -> url: '%s'", url)

	local responseBody = {}
	local result, code, header = http.request{
		host = elasticInfo.connection.host,
		port = elasticInfo.connection.port,
		url = url,
		method = "GET",
		sink = ltn12.sink.table(responseBody)
	}
	local response = table.concat(responseBody, "")

	--logger.debug("[FULLCONTACT:ES.blacklist:GetAddress] 2--------> Result. Code: '%s'", code)
	--logger.debug("[FULLCONTACT:ES.blacklist:GetAddress] 3--------> -> result: '%s', code: '%s', header: '%s'.", result, code, json.encode(header))
	--logger.debug("[FULLCONTACT:ES.blacklist:GetAddress] 4--------> -> response: '%s'", response)

	if code == 200 then
		-- exist
		local encodedAddress = response
		local addressInfo = helpers.JSON_Decode(encodedAddress)._source
		--logger.info("[FULLCONTACT:ES.blacklist:GetAddress] Success retrieved addressId: '%s', blackListId: '%s', vcc: '%s' from ELASTIC SEARCH. address %s", addressId, vcc, blackListId, addressInfo)
		return true, addressInfo
	end
	-- address does not exist
	local err = result
	--logger.warn("[FULLCONTACT:ES.blacklist:GetAddress] Unable to get addressId: '%s', blackListId: '%s', vcc: '%s' from ELASTIC SEARCH.", addressId, blackListId, vcc)
	return false, result
end

--[[
	Funcion que arma la estructura de "_bulk" "delete" esperada por ES.
]]--
m_blackList.RemoveAddressesBatch = function(self, vcc, addresses)
	assert(type(vcc) == "string")
	assert(type(addresses) == "table")

	logger.info("[FULLCONTACT.blacklist.ES:RemoveAddressesBatch] - Removing '%s' addresses BATCH to ELASTIC SEARCH. vcc: %s", #addresses, vcc)
	--logger.debug("[FULLCONTACT.blacklist.ES:RemoveAddressesBatch] - Removing addresses BATCH '%s' to ELASTIC", helpers.JSON_Encode(addresses))

	local addressesBatch = {}
	for i, address in ipairs(addresses) do
		if address.Id then
			local addressBatch = {
				action  = "DELETE",
				address = {
					Key = address.Key
				}
			}
			table.insert(addressesBatch, addressBatch)
			--logger.debug("[FULLCONTACT_ES::RemoveAddressesBatch] %d ---> addressBatch: '%s'.", i, helpers.JSON_Encode(addressBatch))
		end
	end
	-- 2. envio el batch de addresses a actualizar.
	local elasticType = "BLACKLIST_DIALER"
	local success, addressesResult, err = m_blackList.UpdateAddressBatchInES(vcc, addressesBatch, elasticType) -- Guarda en ES
	if not success then
		-- error
		return false, addressesResult, err
	end
	-- ok
	local detail = err
	return true, addressesResult, detail
end

-----------------------------------------------------------------------------------------
--
-- Exported functions
--
return {
	CreateIndex    = CreateIndex,
	DropIndex      = DropIndex,
	CreateTemplate = CreateTemplate,
	GetContact     = GetContact,
	GetIndexName   = GetIndexName,
	RemoveContact  = RemoveContact,
	SearchContacts = SearchContacts,
	SetConnection  = SetConnection,
	StoreContact   = StoreContact,
	Dialer         = {
		GetContact                        = m_dialer.GetContact,
		GetContactByExternalId            = m_dialer.GetContactByExternalId,
		MigrateContactsBatch              = m_dialer.MigrateContactsBatch,
		RemoveContactsBatch               = m_dialer.RemoveContactsBatch,
		SearchContacts                    = m_dialer.SearchContacts,
		StoreContact                      = m_dialer.StoreContact,
		UpdateContactsBatch               = m_dialer.UpdateContactsBatch,
		ValidateDupAndStoreContactsBatch  = m_dialer.ValidateDupAndStoreContactsBatch
	},
	BlackList = {
		GetAddress                        = m_blackList.GetAddress,
		RemoveAddressesBatch              = m_blackList.RemoveAddressesBatch,
		SearchAddresses                   = m_blackList.SearchAddresses,
		UpdateAddressesBatch              = m_blackList.UpdateAddressesBatch,
		ValidateDupAndStoreAddressesBatch = m_blackList.ValidateDupAndStoreAddressesBatch
	}
}
