local json = require "cjson"

local REPORT_TYPE_SUMMARY			= "summary"
local REPORT_TYPE_DETAIL			= "detail"
local REPORT_TYPE_REALTIME			= "realtime"
local REPORT_TYPE_DIALERBATCH		= "dialerbatch"
local REPORT_TYPE_DIALERCONTACTS	= "dialercontacts"
local REPORT_TYPE_LIVEDETAIL		= "livedetail"

-- in case it's available
local LogWarning = LogWarning or function(msg)
	print("Warning", msg)
end


--[[!
-- Map with columns families indexed by entity type
--]]
local columnFamilies = {
	report                 = "Reports",
	dimension              = "Dimensions",
	cut                    = "SummaryCuts",
	filter                 = "DetailFilters",
	compositeIndicator     = "CompositeIndicators",
	summaryIndicator       = "SummaryIndicators",
	summaryIndicatorHelper = "SummaryIndicatorHelpers",
	fieldType              = "FieldTypes"
}

--[[!
-- Default max length in characters of Dimensions, Indicators and 
-- Composite Indicators depending on the type of field's value.
--
-- A T E N T I O N: Each time this map changes we must update the database 
-- by executing corresponding migration scripts like importSchema.sh
--]]
local DEFAULT_FIELD_MAX_LENGTH = {
	TIME       = 8,
	DATE       = 19,
	STRING     = 30,
	NUMBER     = 6,
	BOOLEAN	   = 5,
	ENUM       = 25,
	PERCENTAGE = 7,
	DECIMAL    = 9,
	DEFAULT    = 6  -- like NUMBER
}

--------------------------------------------------------------------------------
-- AUXILIAR FUNCTIONS
--------------------------------------------------------------------------------

local function contains(str, stringArray)
	for _, s in ipairs(stringArray) do
		if s == str then return true end
	end
	return false
end

local function rawGet(connection, entityType, vcc, id)
	return connection:GetColumnValue(vcc, columnFamilies[entityType], id)
end

local function vccGet(connection, entityType, candidateVcc, id)
	local systemGot = rawGet(connection, entityType, "system", id)
	if systemGot then
		return "system", systemGot
	end
	if candidateVcc ~= "system" then
		local vccGot = rawGet(connection, entityType, candidateVcc, id)
		if vccGot then
			return candidateVcc, vccGot
		end
	end
	return nil, entityType .. ": " .. id .. " do not exists in vcc " .. candidateVcc
end

local function resolveAll(connection, entityType, candidateVcc, ids)
	for _, id in pairs(ids) do
		local res, errMessage = vccGet(connection, entityType, candidateVcc, id)
		if not res then
			return nil, errMessage
		end
	end
	return true
end

local function listAll(connection, vcc, entityType)
	local cf = columnFamilies[entityType]

	local entities = {}
	local blockSize  = 100
	local rangeSize  = blockSize
	local skipNext   = false
	local lastColumn = nil
	while rangeSize == blockSize do
		local range = connection:GetSliceRange(vcc, cf,
		"", {
			column_names = {},
			slice_range = {
				start  = lastColumn,
				finish = "",
				reversed = false,
				count = blockSize
			}
		})
		rangeSize = 0
		for _, column in pairs(range) do
			if skipNext then
				skipNext = false
			else
				table.insert(entities, column.name)
			end
			lastColumn = column.name
			rangeSize = rangeSize + 1
		end
		skipNext = true
	end
	return entities
end

local function storeEntity(connection, element, entityType, vcc, id)
	if vcc ~= "system" and rawGet(connection, entityType, "system", id) then
		return nil, entityType .. " already exists in system"
	end
	connection:InsertColumn(vcc, columnFamilies[entityType], id, json.encode(element))
	return true
end

local function getEntity(connection, entityType, vcc, id)
	local entityVCC, entity = vccGet(connection, entityType, vcc, id)
	if not entityVCC then
		return nil, "could not find " .. entityType .. ": " .. id
	end
	local ok, decoded = pcall(json.decode, entity)
	if not ok then
		return nil, entityType .. " " .. id .. " is incorrectly encoded."
	end
	decoded.id = id
	decoded.vcc = entityVCC
	return decoded
end

local function removeEntity(connection, entityType, vcc, id)
	if not (vcc and id) then
		return false, "Invalid argument"
	end
	connection:RemoveColumn(vcc, columnFamilies[entityType], id)
	return true
end

--------------------------------------------------------------------------------
-- DETAIL SCRIPTS
--------------------------------------------------------------------------------
local function setDetailScript(connection, scriptType, script)
	connection:InsertColumn("ComputeFields", "DetailScripts", scriptType, script)
end

local function deleteDetailScript(connection, scriptType, script)
	connection:RemoveColumn("ComputeFields", "DetailScripts", scriptType)
end
--------------------------------------------------------------------------------
-- INDICATORS - GENERIC
--------------------------------------------------------------------------------
local function cleanupIndicatorId(indicatorId)
	return string.match(indicatorId, '[^|]+')
end
--------------------------------------------------------------------------------
-- COMPOSITE INDICATORS
--------------------------------------------------------------------------------
local function listCompositeIndicators(connection, vcc)
	return listAll(connection, vcc, "compositeIndicator")
end
--------------------------------------------------------------------------------
local function checkCompositeIndicator(indicator)
	if type(indicator.indicators) ~= "table" then
		return nil, "indicators must be a table"
	end

	if type(indicator.formula) ~= "string" then
		return nil, "formula must be string"
	end

	for _, v in pairs(indicator.indicators) do
		if type(v) ~= "string" then
			return nil, "indicators must be string"
		end
	end
	
	if indicator.maxLength ~= nil and (
			type(indicator.maxLength) ~= "number" or 
			indicator.maxLength ~= math.floor(indicator.maxLength) or 
			indicator.maxLength < 1) then
		return nil, "maxLength must be an integer greater than or equal to 1"
	end

	return true
end
--------------------------------------------------------------------------------
local function deleteCompositeIndicator(connection, vcc, id)
	return removeEntity(connection, "compositeIndicator", vcc, id)
end
--------------------------------------------------------------------------------
local function existsCompositeIndicator(connection, vcc, id)
	return vccGet(connection, "compositeIndicator", vcc, id)
end
--------------------------------------------------------------------------------
--local function setCompositeIndicator(connection, compositeIndicator, vcc, id)
local function setCompositeIndicator(connection, vcc, id, compositeIndicator)
	if type(vcc) == "table" then
		LogWarning("deprecated usage, you should update to setCompositeIndicator(connection, vcc, id, data)")
		local tmp = vcc
		vcc = id
		id = compositeIndicator
		compositeIndicator = tmp
	end

	local valid, message = checkCompositeIndicator(compositeIndicator)
	if not valid then
		return nil, message
	end
	
	if not compositeIndicator.maxLength then
		compositeIndicator.maxLength = DEFAULT_FIELD_MAX_LENGTH[
			compositeIndicator.format and compositeIndicator.format.type:upper() or 
			"DEFAULT"] or
			DEFAULT_FIELD_MAX_LENGTH.DEFAULT
	end

	return storeEntity(connection, compositeIndicator, "compositeIndicator", vcc, id)
end
--------------------------------------------------------------------------------
local function getCompositeIndicator(connection, vcc, id)
	return getEntity(connection, "compositeIndicator", vcc, id)
end
--------------------------------------------------------------------------------
-- CUTS AND FILTERS
--------------------------------------------------------------------------------
local function checkFilter(filter)
	if not filter.scope then return nil, "there is no scope in filter" end

	if type(filter.scope) ~= "string" then
		return nil, "scope must be string " .. type(filter.scope)
	end

	if type(filter.mandatoryFields) == "table" then
		for _, v in ipairs(filter.mandatoryFields) do
			if type(v) ~= "string" then
				return nil, "all mandatoryFields must be string " .. type(v)
			end
		end
	else
		return nil, "mandatoryFields must be an array of string"
	end

	if type(filter.optionalFields) == "table" then
		for _, v in ipairs(filter.optionalFields) do
			if type(v) ~= "string" then
				return nil, "all optionalFields must be string " .. type(v)
			end
		end
	else
		return nil, "optionalFields must be an array of string"
	end

	if type(filter.sortFields) == "table" then
		for _, v in pairs(filter.sortFields) do
			if type(v) ~= "string" then
				return nil, "all sortFields must be string"
			end
		end
	else
		return nil, "sortFields must be an array of string"
	end

	if type(filter.dateField) ~= "string" then
		return nil, "dateField must be a string"
	end

	if filter.requiredFields and type(filter.requiredFields) ~= "table" then
		return nil, "requiredFields, if present, must be a table"
	end

	return true
end
--------------------------------------------------------------------------------
local function existsFilter(connection, vcc, id)
	return vccGet(connection, "filter", vcc, id)
end
--------------------------------------------------------------------------------
local function deleteFilter(connection, vcc, id)
	return removeEntity(connection, "filter", vcc, id)
end
--------------------------------------------------------------------------------
--local function setFilter(connection, filter, vcc, id)
local function setFilter(connection, vcc, id, filter)
	if type(vcc) == "table" then
		LogWarning("deprecated usage, you should update to setFilter(connection, vcc, id, data)")
		local tmp = vcc
		vcc = id
		id = filter
		filter = tmp
	end
	local valid, message = checkFilter(filter)
	if not valid then
		return nil, message
	end

	return storeEntity(connection, filter, "filter", vcc, id)
end
--------------------------------------------------------------------------------
local function getFilter(connection, vcc, id)
	local filter, message = getEntity(connection, "filter", vcc, id)
	if not filter then
		return nil, message
	end

	local ok, errMessage = checkFilter(filter)
	if not ok then
		return nil, errMessage
	end

	return filter
end
--------------------------------------------------------------------------------
local function listFilters(connection, vcc)
	return listAll(connection, vcc, "filter")
end
--------------------------------------------------------------------------------
local function listDetailFilters(connection, vcc)
	--LogDebug("[REPORTER.manage:listDetailFilters] Adding detail filters for VCC: '%s'.", vcc)
	local allFilters, err = listAll(connection, vcc, "filter")
	if not allFilters then
		return allFilters, err
	end
	local detailFilters = {}
	for _, filterId in pairs(allFilters) do
		local filter = getFilter(connection, vcc, filterId)
		if filter then
			local filterType = (filter.filterType or ""):upper()
			if filterType == "DETAIL" or filterType == "" then
				table.insert(detailFilters, filterId)
			end
		end
	end
	return detailFilters
end
--------------------------------------------------------------------------------
local function listLiveDetailFilters(connection, vcc)
	--LogDebug("[REPORTER.manage:listLiveDetailFilters] Adding live detail filters for VCC: '%s'.", vcc)
	local allFilters, err = listAll(connection, vcc, "filter")
	if not allFilters then
		return allFilters, err
	end
	local liveDetailFilters = {}
	for _, filterId in pairs(allFilters) do
		--LogDebug("---> i: '%s', filterId: '%s'.",_, filterId)
		local filter = getFilter(connection, vcc, filterId)
		if filter then
			--LogDebug("-------> filter: '%s'.", json.encode(filter))
			local filterType = (filter.filterType or ""):upper()
			if filterType == "LIVE DETAIL" then
				--LogDebug("[REPORTER.manage:listLiveDetailFilters] Adding live detail filters '%s' for VCC: '%s'.", filterId, vcc)
				table.insert(liveDetailFilters, filterId)
			end
		end
	end
	return liveDetailFilters
end
--------------------------------------------------------------------------------
local function checkCut(cut)
	if type(cut.columns) == "table" then
		for _, v in pairs(cut.columns) do
			if type(v) ~= "string" then
				return nil, "Columns must be an array of strings"
			end
		end
	else
		return nil, "Columns must be table"
	end

	if type(cut.filter) == "table" then
		for _, v in pairs(cut.filter) do
			if type(v) ~= "string" then
				return nil, "Filter must be an array of strings"
			end
		end
	else
		return nil, "Filter must be table"
	end

	return true
end
--------------------------------------------------------------------------------
local function deleteCut(connection, vcc, id)
	return removeEntity(connection, "cut", vcc, id)
end
--------------------------------------------------------------------------------
local function existsCut(connection, vcc, id)
	return vccGet(connection, "cut", vcc, id)
end
--------------------------------------------------------------------------------
--local function setCut(connection, cut, vcc, id)
local function setCut(connection, vcc, id, cut)
	if type(vcc) == "table" then
		LogWarning("deprecated usage, you should update to setCut(connection, vcc, id, data)")
		local tmp = vcc
		vcc = id
		id = cut
		cut = tmp
	end

	local valid, message = checkCut(cut)
	if not valid then
		return nil, message
	end

	return storeEntity(connection, cut, "cut", vcc, id)
end
--------------------------------------------------------------------------------
local function getCut(connection, vcc, id)
	local cut, message = getEntity(connection, "cut", vcc, id)

	if not cut then return nil, message end

	local ok, errorMessage = checkCut(cut)
	if not ok then
		return nil, errorMessage
	end

	return cut
end
--------------------------------------------------------------------------------
local function listCuts(connection, vcc)
	return listAll(connection, vcc, "cut")
end
--------------------------------------------------------------------------------
-- DIMENSIONS
--------------------------------------------------------------------------------
local function checkDimension(dimension)
	if type(dimension.type) ~= "string" then
		return false, "dimension.type must be string"
	end
	if dimension.type == "enum" and (type(dimension.domain) ~= "table" or #dimension.domain == 0) then
		return false, "dimension type es ENUM but domain is invalid"
	end
	if dimension.maxLength ~= nil and (
			type(dimension.maxLength) ~= "number" or 
			dimension.maxLength ~= math.floor(dimension.maxLength) or 
			dimension.maxLength < 1) then
		return false, "dimension.maxLength must be an integer greater than or equal to 1"
	end
	return true
end
--------------------------------------------------------------------------------
local function deleteDimension(connection, vcc, id)
	return removeEntity(connection, "dimension", vcc, id)
end
--------------------------------------------------------------------------------
local function existsDimension(connection, vcc, id)
	return vccGet(connection, "dimension", vcc, id)
end
--------------------------------------------------------------------------------
local function setDimension(connection, vcc, id, dimension)
	local valid, message = checkDimension(dimension)
	if not valid then
		return valid, message
	end
	if not dimension.maxLength then
		dimension.maxLength = DEFAULT_FIELD_MAX_LENGTH[dimension.type:upper()] or DEFAULT_FIELD_MAX_LENGTH.STRING
	end
	return storeEntity(connection, dimension, "dimension", vcc, id)
end
--------------------------------------------------------------------------------
local function getDimension(connection, vcc, id)
	local dimension, message = getEntity(connection, "dimension", vcc, id)
	if not dimension then return nil, message end
	local ok, errorMessage = checkDimension(dimension)
	if not ok then
		return nil, errorMessage
	end
	return dimension
end
--------------------------------------------------------------------------------
local function listAllDimensions(connection, vcc)
	return listAll(connection, vcc, "dimension")
end
--------------------------------------------------------------------------------
local function listArchiveDimensions(connection, vcc)
	LogDebug("[REPORTER.manage:listArchiveDimensions] Adding ARCHIVE Dimensions for VCC: '%s'.", vcc)
	local allDimensions, err = listAllDimensions(connection, vcc)
	if not allDimensions then
		return allDimensions, err
	end
	local archiveDimensions = {}
	for _, dimensionId in pairs(allDimensions) do
		local dimension = getDimension(connection, vcc, dimensionId)
		if dimension then
			local belongsToReportType = (dimension.belongsToReportType or "ARCHIVE"):upper()
			if belongsToReportType == "ARCHIVE" or belongsToReportType == "ARCHIVE_AND_LIVE" then
				table.insert(archiveDimensions, dimensionId)
			end
		end
	end
	return archiveDimensions
end
--------------------------------------------------------------------------------
local function listLiveDimensions(connection, vcc)
	LogDebug("[REPORTER.manage:listLiveDimensions] Adding LIVE Dimensions for VCC: '%s'.", vcc)
	local allDimensions, err = listAllDimensions(connection, vcc)
	if not allDimensions then 
		return allDimensions, err
	end
	local liveDimensions = {}
	for _, dimensionId in pairs(allDimensions) do
		--LogDebug("---> i: '%s', dimensionId: '%s'.",_, dimensionId)
		local dimension = getDimension(connection, vcc, dimensionId)
		if dimension then
			--LogDebug("-------> dimension: '%s'.", json.encode(dimension))
			--LogDebug("-------> dimension: '%s' - dimension.belongsToReportType: '%s'.", dimensionId, dimension.belongsToReportType)
			local belongsToReportType = (dimension.belongsToReportType or "ARCHIVE"):upper()
			if belongsToReportType == "LIVE" or belongsToReportType == "ARCHIVE_AND_LIVE"then
				--LogDebug("[REPORTER.manage:listLiveDimensions] Adding LIVE Dimensions '%s' for VCC: '%s'.", dimensionId, vcc)
				table.insert(liveDimensions, dimensionId)
			end
		end
	end
	return liveDimensions
end
--------------------------------------------------------------------------------
-- REPORT FUNCTIONS
--------------------------------------------------------------------------------
local function isMutable(connection, vcc, id)
	local report, errMessage = getEntity(connection, "report", vcc, id)
	if not report then return nil, errMessage end

	return report.mutable
end
--------------------------------------------------------------------------------
local function getReportType(connection, vcc, id)
	local report, errMessage = getEntity(connection, "report", vcc, id)
	if not report then return nil, errMessage end

	return report.type
end
--------------------------------------------------------------------------------
local function checkDetailReport(connection, report, vcc)
	if type(report.projectFields) ~= "table" or #report.projectFields == 0 then
		return nil, "missing projectFields"
	end

	if type(report.filterName) == "string" then
		local ok, errMessage = existsFilter(connection, vcc, report.filterName)
		if not ok then return nil, errMessage end
	else
		return nil, "missing filterName"
	end

	--filters in a report are optional
	if report.filters then
		if type(report.filters) == "table" then
			for name, value in pairs(report.filters) do
				if type(name) ~= "string" or type(value) ~= "string" then
					return nil, "filters should be a table of strings"
				end
			end
		else
			return nil, "filters must be a table"
		end
	end

	if type(report.mutable) ~= "boolean" then
		return nil, "mutable must be boolean"
	end

	return true
end
--------------------------------------------------------------------------------
local function checkSummaryReport(connection, report, vcc)
	if report.indicators and type(report.indicators) == "table" then
		for _, v in pairs(report.indicators) do
			if type(v) ~= "string" then
				return nil, "indicators must be string"
			end
		end
	else
		return nil, "indicators must be present"
	end

	if type(report.cut) ~= "string" then
		return nil, "missing cut"
	end

	local cut, errMessage = getEntity(connection, "cut", vcc, report.cut)
	if not cut then return nil, errMessage end

	if report.columnDetails and type(report.columnDetails) == "table" then
		for indicatorName, detail in pairs(report.columnDetails) do
			if not contains(indicatorName, report.indicators) and
			   not contains(indicatorName, cut.columns) then
				return nil, indicatorName .. " undeclared"
			else
				if detail.composite then
					if type(detail.composite) ~= "table" then
						return nil, "columnDetail for " .. indicatorName .. ": composite must be a table"
					else
						if type(detail.composite.indicators) ~= "table" then
							return nil, "composite.indicators must be a table"
						end
						if type(detail.composite.formula) ~= "string" then
							return nil, "composite.formula must be a string"
						end
					end
				end
			end
		end
	else
		return nil, "missing columnDetails"
	end

	if type(report.mutable) ~= "boolean" then
		return nil, "mutable must be boolean"
	end

	return true
end
--------------------------------------------------------------------------------
local function deleteReport(connection, vcc, id, force)
	if not isMutable(connection, vcc, id) and not force then
		return nil, "could not delete report is not mutable"
	end
	return removeEntity(connection, "report", vcc, id)
end
--------------------------------------------------------------------------------
local function storeSummaryReport(connection, report, vcc, id, force)
	local valid, message = checkSummaryReport(connection, report, vcc)
	if not valid then
		return nil, message
	end
	report.type = REPORT_TYPE_SUMMARY

	for name, columnDetail in pairs(report.columnDetails)  do
		if columnDetail.composite then
			local ok, errMessage = setCompositeIndicator(connection, vcc, name, columnDetail.composite)
			if not ok then
				return nil, errMessage
			end
			columnDetail.composite = true
		end
	end

	return storeEntity(connection, report, "report", vcc, id)
end
--------------------------------------------------------------------------------
local function storeDetailReport(connection, report, vcc, id, force)
	local valid, message = checkDetailReport(connection, report, vcc)
	if not valid then return nil, message end

	report.type = REPORT_TYPE_DETAIL
	return storeEntity(connection, report, "report", vcc, id)
end
--------------------------------------------------------------------------------
local function storeRealtimeReport(connection, report, vcc, id, force)
	report.type = REPORT_TYPE_REALTIME
	return storeEntity(connection, report, "report", vcc, id)
end
--------------------------------------------------------------------------------
local function storeDialerBatchReport(connection, report, vcc, id, force)
	report.type = REPORT_TYPE_DIALERBATCH
	return storeEntity(connection, report, "report", vcc, id)
end
--------------------------------------------------------------------------------
local function storeDialerContactsReport(connection, report, vcc, id, force)
	report.type = REPORT_TYPE_DIALERCONTACTS
	return storeEntity(connection, report, "report", vcc, id)
end
--------------------------------------------------------------------------------
local function storeLiveDetailReport(connection, report, vcc, id, force)
	report.type = REPORT_TYPE_LIVEDETAIL
	return storeEntity(connection, report, "report", vcc, id)
end
--------------------------------------------------------------------------------
local function filterReportsByType(connection, reports, reportType, vcc)
	local filteredReports = {}
	for _, reportID in ipairs(reports) do
		local currentReportType, message = getReportType(connection, vcc, reportID)
		if not currentReportType then
			return nil, message
		end
		if currentReportType == reportType then
			table.insert(filteredReports, reportID)
		end
	end
	return filteredReports
end
--------------------------------------------------------------------------------
-- SUMMARY BASE INDICATORS
--------------------------------------------------------------------------------
local function setSummaryIndicator(connection, vcc, id, data)
	if type(data.scope) ~= "string" or data.scope == "" then
		return nil, "Argument data.scope is invalid or missing"
	end
	if type(data.script) ~= "string" or data.script == "" then
		return nil, "Argument data.script is invalid or missing"
	end
	if data.sub_scope and type(data.sub_scope) ~= "string" then
		return nil, "Argument data.sub_scope is invalid"
	end
	if data.requiredFields and type(data.requiredFields) ~= "table" then
		return nil, "Argument data.requiredFields is invalid"
	end
	if data.extras and type(data.extras) ~= "table" then
		return nil, "Argument data.extras is invalid"
	end
	if data.format and type(data.format) ~= "table" then
		return nil, "Argument data.format is invalid"
	end
	if data.maxLength ~= nil and (
			type(data.maxLength) ~= "number" or 
			data.maxLength ~= math.floor(data.maxLength) or 
			data.maxLength < 1) then
		return nil, "Argument data.maxLength must be an integer greater than or equal to 1"
	end
	
	if not data.maxLength then
		data.maxLength = DEFAULT_FIELD_MAX_LENGTH[data.format and data.format.type:upper() or "DEFAULT"] or
											DEFAULT_FIELD_MAX_LENGTH.DEFAULT
	end

	return storeEntity(connection, data, "summaryIndicator", vcc, id)
end

local function getSummaryIndicator(connection, vcc, id)
	return getEntity(connection, "summaryIndicator", vcc, id)
end

local function deleteSummaryIndicator(connection, vcc, id)
	return removeEntity(connection, "summaryIndicator", vcc, id)
end

local function existsSummaryIndicator(connection, vcc, id)
	return vccGet(connection, "summaryIndicator", vcc, id)
end

local function listSummaryIndicators(connection, vcc)
	return listAll(connection, vcc, "summaryIndicator")
end

--------------------------------------------------------------------------------
local function setSummaryIndicatorHelper(connection, vcc, id, script)
	local entity = {script = script}

	return storeEntity(connection, entity, "summaryIndicatorHelper", vcc, id)
end

local function getSummaryIndicatorHelper(connection, vcc, id)
	local entity, errMessage = getEntity(connection, "summaryIndicatorHelper", vcc, id)
	if not entity then return nil, errMessage end
	return entity.script
end

local function listSummaryIndicatorHelpers(connection, vcc)
	return listAll(connection, vcc, "summaryIndicatorHelper")
end

local function deleteSummaryIndicatorHelper(connection, vcc, id)
	connection:RemoveColumn(id .. "@" .. vcc, "SummaryIndicatorHelpers", "script")
	return true
end

local function existsSummaryIndicatorHelper(connection, vcc, id)
	return vccGet(connection, "summaryIndicatorHelper", vcc, id)
end

--------------------------------------------------------------------------------
-- FILTER FIELD TYPES
--------------------------------------------------------------------------------
--filterFieldType = {
--  name
--  validValues
--  format
--  caption
--  comment
--}
local function checkFieldType(fieldType)
	if type(fieldType.name) ~= "string" then
		return nil, "name must be string"
	end

	if type(fieldType.format) ~= "string" then
		return nil, "format must be string"
	end
	if type(fieldType.caption) ~= "string" then
		return nil, "caption must be string"
	end
	if type(fieldType.comment) ~= "string" then
		return nil, "comment must be string"
	end

	if type(fieldType.validValues) == "table" then
		for _, v in pairs(fieldType.validValues) do
			if type(v) ~= "string" then
				return nil, "validValues must be an array of strings"
			end
		end
	else
		return nil, "validValues must be a table"
	end

	return true
end
--------------------------------------------------------------------------------
local function existsFieldType(connection, vcc, id)
	return vccGet(connection, "fieldType", vcc, id)
end
--------------------------------------------------------------------------------
local function deleteFieldType(connection, vcc, id)
	return removeEntity(connection, "fieldType", vcc, id)
end
--------------------------------------------------------------------------------
local function setFieldType(connection, fieldType, vcc, id)
	local valid, message = checkFieldType(fieldType)
	if not valid then
		return nil, message
	end
	return storeEntity(connection, fieldType, "fieldType", vcc, id)
end
--------------------------------------------------------------------------------
local function getFieldType(connection, vcc, id)
	local fieldType, errMessage = getEntity(connection, "fieldType", vcc, id)
	if not fieldType then return nil, errMessage end

	local ok, message = checkFieldType(fieldType)
	if not ok then return nil, message end

	return fieldType
end
--------------------------------------------------------------------------------
local function listFieldTypes(connection, vcc)
	return listAll(connection, vcc, "fieldType")
end
--------------------------------------------------------------------------------
local function filterReportsByMutability(connection, reports, mutability, vcc)
	local filteredReports = {}
	for _, id in ipairs(reports) do
		local mutable, message = isMutable(connection, vcc, id)
		if not mutable then
			return nil, message
		end
		if mutable == mutability then
			table.insert(filteredReports, id)
		end
	end
	return filteredReports
end
--------------------------------------------------------------------------------
local function existsReport(connection, vcc, id)
	return vccGet(connection, "report", vcc, id)
end
--------------------------------------------------------------------------------
-- A summary report it's a table:
-- cut:
--            name of the filter used to query
--
-- indicators:
--            list of fields that the query projects
--
-- columnNames:
--
-- mutable:
--           "true" if its mutable, "false" otherwise
--------------------------------------------------------------------------------
local function getSummaryReport(connection, vcc, id, strictCheck)
	strictCheck = strictCheck == nil or strictCheck

	if not existsReport(connection, vcc, id) then
		return nil, "report does not exists"
	end

	local summaryReport, errMessage = getEntity(connection, "report", vcc, id)

	if not summaryReport then return nil, errMessage end

	for id, columnDetail in pairs(summaryReport.columnDetails) do
		if columnDetail.composite then
			local composite, errMessage = getCompositeIndicator(connection, vcc, id)
			if composite then
				columnDetail.composite = composite
			else
				return nil, errMessage
			end
		end
	end

	if summaryReport.mutable and type(summaryReport.mutable) == "string" then
		summaryReport.mutable = (summaryReport.mutable == "true")
	end

	local ok, errMessage = checkSummaryReport(connection, summaryReport, vcc)
	if not ok and strictCheck then
			return nil, errMessage
	end
	return summaryReport, errMessage
end
--------------------------------------------------------------------------------
-- A detail report it's a table:
-- filterName:
--            name of the filter used to query
--
-- projectedFields:
--            list of fields that the query projects
--
-- mutable:
--           "true" if its mutable, "false" otherwise
--
-- filters:
--           a table of strings that represents field and values that will be
--           applied to the filter. These filters are prefilled for the query
--
--------------------------------------------------------------------------------
local function getDetailReport(connection, vcc, id)
	return getEntity(connection, "report", vcc, id)
end
local function getRealtimeReport(connection, vcc, id)
	return getEntity(connection, "report", vcc, id)
end
local function getDialerBatchReport(connection, vcc, id)
	return getEntity(connection, "report", vcc, id)
end
local function getDialerContactsReport(connection, vcc, id)
	return getEntity(connection, "report", vcc, id)
end
local function getLiveDetailReport(connection, vcc, id)
	return getEntity(connection, "report", vcc, id)
end
--------------------------------------------------------------------------------
local function getReport(connection, vcc, id, strictCheck)
	local reportType, message = getReportType(connection, vcc, id)

	if not reportType then
		return nil, message
	end

	if reportType == REPORT_TYPE_SUMMARY then
		return getSummaryReport(connection, vcc, id, strictCheck)
	elseif reportType == REPORT_TYPE_DETAIL then
		return getDetailReport(connection, vcc, id, strictCheck)
	elseif reportType == REPORT_TYPE_REALTIME then
		return getRealtimeReport(connection, vcc, id, strictCheck)
	elseif reportType == REPORT_TYPE_DIALERBATCH then
		return getDialerBatchReport(connection, vcc, id, strictCheck)
	elseif reportType == REPORT_TYPE_DIALERCONTACTS then
		return getDialerContactsReport(connection, vcc, id, strictCheck)
	elseif reportType == REPORT_TYPE_LIVEDETAIL then
		return getLiveDetailReport(connection, vcc, id, strictCheck)
	else
		return nil, "unknown report type: " .. reportType
	end
end
--------------------------------------------------------------------------------
--local function setReport(connection, report, vcc, id, force)
local function setReport(connection, vcc, id, report, force)
	if type(vcc) == "table" then
		LogWarning("deprecated usage, you should update to setReport(connection, vcc, id, data)")
		local tmp = vcc
		vcc = id
		id = report
		report = tmp
	end

	if not report then
		return nil, "report is nil"
	end

	if not report.type then
		return nil, "report type is nil"
	end

	if existsReport(connection, vcc, id) and not isMutable(connection, vcc, id) and not force then
		return nil, "report already exists and it's not mutable"
	end

	if report.type == REPORT_TYPE_DETAIL then
		return storeDetailReport(connection, report, vcc, id, force)
	elseif report.type == REPORT_TYPE_SUMMARY then
		return storeSummaryReport(connection, report, vcc, id, force)
	elseif report.type == REPORT_TYPE_REALTIME then
		return storeRealtimeReport(connection, report, vcc, id, force)
	elseif report.type == REPORT_TYPE_DIALERBATCH then
		return storeDialerBatchReport(connection, report, vcc, id, force)
	elseif report.type == REPORT_TYPE_DIALERCONTACTS then
		return storeDialerContactsReport(connection, report, vcc, id, force)
	elseif report.type == REPORT_TYPE_LIVEDETAIL then
		return storeLiveDetailReport(connection, report, vcc, id, force)
	else
		return nil, "report " .. id .. " invalid type: " .. report.type
	end
end
--------------------------------------------------------------------------------
-- returns an array of reports names
--------------------------------------------------------------------------------
--local function listReports(connection, mutability, reportType, vcc)
local function listReports(connection, vcc, reportType, mutability)
	if type(vcc) ~= "string" then
		LogWarning("deprecated usage, you should update to listReports(connection, vcc, reportType, mutability)")
		local tmp = vcc
		vcc = mutability
		mutability = tmp
	end

	local reports, errMessage = listAll(connection, vcc, "report")
	if not reports then return nil, errMessage end

	if mutability ~= nil then
		reports = filterReportsByMutability(connection, reports, mutability, vcc)
	end

	if reportType ~= nil then
		reports = filterReportsByType(connection, reports, reportType, vcc)
	end

	return reports
end
--------------------------------------------------------------------------------
return {
-- dimensions
	setDimension					= setDimension,
	getDimension					= getDimension,
	listAllDimensions				= listAllDimensions,
	listArchiveDimensions			= listArchiveDimensions,
	listLiveDimensions				= listLiveDimensions,
	deleteDimension					= deleteDimension,
	existsDimension					= existsDimension,
-- detail scripts
	setDetailScript					= setDetailScript,
	deleteDetailScript				= deleteDetailScript,
-- reports
	setReport						= setReport,
	getReport						= getReport,
	listReports						= listReports,
	deleteReport					= deleteReport,
	existsReport					= existsReport,
	isMutable						= isMutable,
-- detail filters
	setFilter						= setFilter,
	getFilter						= getFilter,
	listFilters						= listFilters,
	listDetailFilters				= listDetailFilters,
	listLiveDetailFilters			= listLiveDetailFilters,
	deleteFilter					= deleteFilter,
	existsFilter					= existsFilter,
-- summary cuts
	setCut							= setCut,
	getCut							= getCut,
	listCuts						= listCuts,
	deleteCut						= deleteCut,
	existsCut						= existsCut,
-- indicators, generic
	cleanupIndicatorId				= cleanupIndicatorId,
-- summary indicators
	setSummaryIndicator				= setSummaryIndicator,
	getSummaryIndicator				= getSummaryIndicator,
	listSummaryIndicators			= listSummaryIndicators,
	deleteSummaryIndicator			= deleteSummaryIndicator,
	existsSummaryIndicator			= existsSummaryIndicator,
-- summary indicator helpers
	setSummaryIndicatorHelper		= setSummaryIndicatorHelper,
	getSummaryIndicatorHelper		= getSummaryIndicatorHelper,
	listSummaryIndicatorHelpers		= listSummaryIndicatorHelpers,
	deleteSummaryIndicatorHelper	= deleteSummaryIndicatorHelper,
	existsSummaryIndicatorHelper	= existsSummaryIndicatorHelper,
-- summary composite indicators
	setCompositeIndicator			= setCompositeIndicator,
	getCompositeIndicator			= getCompositeIndicator,
	listCompositeIndicators			= listCompositeIndicators,
	deleteCompositeIndicator		= deleteCompositeIndicator,
	existsCompositeIndicator		= existsCompositeIndicator,
-- filter types
	setFieldType					= setFieldType,
	getFieldType					= getFieldType,
	listFieldTypes					= listFieldTypes,
	deleteFieldType					= deleteFieldType,
	existsFieldType					= existsFieldType,
-- constants
	REPORT_TYPE_SUMMARY				= REPORT_TYPE_SUMMARY,
	REPORT_TYPE_DETAIL				= REPORT_TYPE_DETAIL,
	REPORT_TYPE_REALTIME			= REPORT_TYPE_REALTIME,
	REPORT_TYPE_DIALERBATCH			= REPORT_TYPE_DIALERBATCH,
	REPORT_TYPE_DIALERCONTACTS		= REPORT_TYPE_DIALERCONTACTS,
	REPORT_TYPE_LIVEDETAIL			= REPORT_TYPE_LIVEDETAIL,
-- default values
	DEFAULT_FIELD_MAX_LENGTH			= DEFAULT_FIELD_MAX_LENGTH,
}
