local uuid = require "uuid"
local cassandra = require "lua_cassandra"
local lfs = require "lfs"
local manager = require "reporter.manage"

module(..., package.seeall)

-- Forward definition of exposed functions
function Import(cassandraIP, cassandraPort, schemaPath, vcc, importEntity) end
function Export(cassandraIP, cassandraPort, schemaPath, vcc, exportEntity) end
function Clear(cassandraIP, cassandraPort, vcc) end
function ClearData(cassandraIP, cassandraPort) end

--require("reporter.import").Export("192.168.24.172", 9160, ".", "system")
do
	-- swaps two values in a table
	function table.swap(tbl, k1, k2)
		local tmp = tbl[k1]
		tbl[k1] = tbl[k2]
		tbl[k2] = tmp
	end

	-- save table as lua code
	function table.save(tbl, file, withReturn, indent)
		indent = indent and tostring(indent) or "  "

		local function saveImpl(tbl, level, printer)
			local indentation = string.rep(indent, level+1)
			for k, v in pairs(tbl) do
				printer(indentation)

				if type(k) == "string" then -- table is an associative array
					-- quote k only when necessary
					k = k:match("^[_%a][_%w]*$") and k or string.format("[%q]", k)
					printer(k .. " = ")
				end

				if type(v) == "table" then
					if next(v) then -- optimize for empty tables
						printer("{\n")
						saveImpl(v, level + 1, printer)
						printer(indentation .."},\n")
					else
						printer("{ },\n")
					end
				else
					if type(v) == "string" then
						v = string.format("%q", v)
					end
					printer(tostring(v) ..",\n")
				end
			end
		end

		-- in case a file stream is given
		if type(file) == "string" then
			local err
			file, err = io.open(file, "wb")
			if err then return err end
		end

		file:write((withReturn and "return " or "") .."{\n")

		saveImpl(tbl, 0, function(str)
			file:write(str)
		end)

		file:write("}".. (withReturn and ";" or "") .."\n") -- new line at end of file :)

		file:close()
	end
end

-- iterates over the given directory, calling iterator(filename, fullfilename) each time
local function walkDirectory(schemaPath, dir, iterator, success)
	success = success or function() end
	local path = schemaPath .."/".. dir

	if lfs.attributes(path) then
		for file in lfs.dir(path) do
			iterator(file, path .."/".. file)
		end
		return success()
	end

	print("WARNING, sub-directory '".. dir .."' does not exist")
end

-- calls a callback(fullfilename) only if the file exists
local function ifExists(schemaPath, filename, callback)
	local path = schemaPath .."/".. filename
	if lfs.attributes(path) then
		return callback(path)
	end
	print("WARNING, file '".. filename .."' does not exist")
end

-- returns a function that when called will print @msg
local function callbackPrint(msg)
	return function()
		print(msg)
	end
end


-- ChillCode

local function CassandraSelect(keyspace, column_family, isCounter)
	local first_key = ""
	local last_key = ""
	local first_col = ""
	local last_col = ""
	local reversed_cols = false

	local result = {}
	local keys = 100
	local colsPerKey = 100
	local allKeysObtained
	local nextFirstKey = first_key
	while not allKeysObtained do
		local keyRange = keyspace:GetRangeSlices(column_family,"",nextFirstKey,last_key,
			{
				slice_range = {
					start = first_col,
					finish = last_col,
					reversed = reversed_cols,
					count = colsPerKey + 1,
				},
			},
			keys + 1,
			cassandra.CONSISTENCY_LEVEL_LOCAL_QUORUM,
			false
		)
		allKeysObtained = true
		if keyRange then
			local counter = 0
			for key, columns in keyRange:Iterator() do
				-- Iterate on the rows
				if counter < keys then
					local keyValues = {}
					-- Iterate on the columns
					while true do
						local allColsObtained = true
						local nextColumn
						for index, column in ipairs(columns) do
							if index < colsPerKey + 1 then
								--print("\tAdding "..key.."["..column.name.."] = "..column.value_as_number)
								keyValues[column.name] = isCounter and column.value_as_number or column.value
							else
								--print("\tGetting more columns for "..key..", starting with "..column.name)
								allColsObtained = false
								nextColumn = column.name
							end
						end
						if allColsObtained then break end
						columns = keyspace:GetSliceRange(key, column_family, "", { slice_range = { start = nextColumn, finish = last_col, reversed = reversed_cols, count = colsPerKey+1 } })
					end
					if next(keyValues) then
						result[key] = keyValues
					end
					counter = counter + 1
				else
					allKeysObtained = false
					nextFirstKey = key
				end
			end
		end
	end
	return result
end

local function GetFileContent(filename)
	local f = io.open(filename, "r")
	assert(f, "Failed to read "..filename)
	local result = f:read("*a")
	f:close()
	return result
end

--[[
-- Used in order to prevent breaking old migrations
--]]
local function ExistsColumnFamily(ks, cf)
	local test_key = "test_".. uuid.new()
	local res = pcall(ks.InsertColumn, ks, test_key, cf, "test_colum", "")
	if res then
		local ok, err = pcall(ks.RemoveColumn, ks, test_key, cf)
		if not ok then
			print("\n\tWARNING: Failed to delete temporary key '".. test_key .."' in CF '".. cf .. "'.\n")
		end
	end
	return res
end

-- loads all entities of an specific type
local function GetAllEntities(summarizerKS, vcc, listFunction, getFunction, asArray, iterator)
	iterator = iterator or function() end
	local ids, err = listFunction(summarizerKS, vcc)
	if not ids then return nil, err end

	local result = {}
	for _, id in pairs(ids) do
		local e, err = getFunction(summarizerKS, vcc, id)
		if not e then
			print("WARNING, could not get entity ".. id .." it may be deleted already. ", err)
		else
			if iterator(e) ~= false then
				e.vcc = nil
				result[asArray and (#result + 1) or id] = e
			end
		end
	end
	return result
end

local entityHandlers = {
	{
		name = "dimensions",
		import = function(summarizerKS, schemaPath, vcc)

			-- TODO: Delete this code in January '14?
			if not ExistsColumnFamily(summarizerKS, "Dimensions") then
				print("\n\tWARNING: Column Family 'Dimensions' was not found\n")
				return
			end

			print("Adding dimensions")
			ifExists(schemaPath, "dimensions.lua", function(filename)
				local dimensions = dofile(filename)
				for id, dimension in pairs(dimensions) do
					--print("- "..id)
					local ok, errMessage = manager.setDimension(summarizerKS, vcc, id, dimension)
					if not ok then
						error("Error saving dimension "..id..": " .. errMessage)
					end
				end
				print("Dimensions added")
			end)
		end,

		export = function(summarizerKS, schemaPath, vcc)
			-- TODO: Delete this code in January '14?
			if not ExistsColumnFamily(summarizerKS, "Dimensions") then
				print("\n\tWARNING: Column Family 'Dimensions' was not found\n")
				return
			end

			local dimensions, err = GetAllEntities(summarizerKS, vcc, manager.listAllDimensions, manager.getDimension)
			if not dimensions then
				error("Failed to list dimensions, ".. err)
			end
			local dimensionsPath = schemaPath.."/dimensions.lua"
			print("Saving dimensions to file ".. dimensionsPath)
			table.save(dimensions, dimensionsPath, true)
		end
	},
	{
		name = "filters",
		import = function(summarizerKS, schemaPath, vcc)
			local import = function(filename, filterType)
				local filters = dofile(filename)
				for name, filter in pairs(filters) do
					if vcc ~= "system" then
						filter.requiredFields = filter.requiredFields or {}
						filter.requiredFields.VCC = vcc
					end
					--print("- "..name)
					local ok, errMessage = manager.setFilter(summarizerKS, vcc, name, filter)
					if not ok then
						error("Error saving " .. tostring(filterType) .. " filter " .. name .. ": " .. errMessage)
					end
				end
				print(tostring(filterType) .. " filters added")
			end

			print("Adding DETAIL filters")
			ifExists(schemaPath, "detail_filters.lua", function(filename) import(filename, "DETAIL") end)

			print("Adding LIVE DETAIL filters")
			ifExists(schemaPath, "live_detail_filters.lua", function(filename) import(filename, "LIVE DETAIL") end)
		end,

		export = function(summarizerKS, schemaPath, vcc)
			local allFilters, err = GetAllEntities(summarizerKS, vcc, manager.listFilters, manager.getFilter)
			if not allFilters then
				error("Failed to list filters, ".. err)
			end
			local detailFilters = {}
			local liveDetailFilters = {}
			for filterId, filter in pairs(allFilters) do
				if filter.filterType == "LIVE DETAIL" then
					liveDetailFilters[filterId] = filter
				else
					detailFilters[filterId] = filter
				end
			end
			-- Saving Detail Filters
			local detailFiltersPath = schemaPath.."/detail_filters.lua"
			print("1 - Saving DETAIL filters to file ".. detailFiltersPath)
			table.save(detailFilters, detailFiltersPath, true)
			-- Saving Live Detail Filters
			local liveDetailFiltersPath = schemaPath.."/live_detail_filters.lua"
			print("2 - Saving LIVE DETAIL filters to file ".. liveDetailFiltersPath)
			table.save(liveDetailFilters, liveDetailFiltersPath, true)
		end
	},
	{
		name = "cuts",
		import = function(summarizerKS, schemaPath, vcc)
			print("Adding summary cuts")
			ifExists(schemaPath, "cuts.lua", function(filename)
				local cuts = dofile(filename)
				for _, cut in ipairs(cuts) do
					if vcc ~= "system" then
						cut.requiredFields = cut.requiredFields or {}
						cut.requiredFields.VCC = vcc
					end
					--print("- "..cut.ID)
					local cutId = cut.ID cut.ID = nil
					local ok, errMessage = manager.setCut(summarizerKS, vcc, cutId, cut)
					if not ok then
						error("Error saving cut "..cutId..": " .. errMessage)
					end
				end
				print("Summary cuts added")
			end)
		end,

		export = function(summarizerKS, schemaPath, vcc, filterFunction)
			local cuts, err = GetAllEntities(summarizerKS, vcc, manager.listCuts, manager.getCut, true, function(m)
					if not filterFunction(m) then
						return false
					end
					table.swap(m, "id", "ID")
					m.id = nil
				end)
			if not cuts then
				error("Failed to list cuts, ".. err)
			end
			local cutsPath = schemaPath.."/cuts.lua"
			print("Saving cuts to file ".. cutsPath)
			table.save(cuts, cutsPath, true)
		end
	},
	{
		name = "metrics",
		import = function(summarizerKS, schemaPath, vcc)
			print("Adding summary indicators")
			ifExists(schemaPath, "indicators.lua", function(filename)
				local indicators = dofile(filename)
				for _, indicator in ipairs(indicators) do
					--print("- "..indicator.ID)
					local indicatorId = indicator.ID indicator.ID = nil
					indicator.script = indicator.formula or GetFileContent(schemaPath.."/scripts/"..indicator.script)
					if vcc ~= "system" then
						indicator.requiredFields = indicator.requiredFields or {}
						indicator.requiredFields.VCC = vcc
					end
					local ok, errMessage = manager.setSummaryIndicator(summarizerKS, vcc, indicatorId, indicator)
					if not ok then
						error("Error saving summary indicator "..indicatorId..": " .. errMessage)
					end
				end
				print("Summary indicators added")
			end)

			print("Adding composite metrics")
			ifExists(schemaPath, "compositeMetrics.lua", function(filename)
				local indicators = dofile(filename)
				for _, indicator in ipairs(indicators) do
					--print("- "..indicator.ID)
					local indicatorId = indicator.ID
					indicator.ID = nil
					local ok, errMessage = manager.setCompositeIndicator(summarizerKS, vcc, indicatorId, indicator)
					if not ok then
						error("Error saving composite metric "..indicatorId..": " .. errMessage)
					end
				end
				print("Composite metrics added")
			end)
		end,

		export = function(summarizerKS, schemaPath, vcc, filterFunction)
			local metrics, err = GetAllEntities(
				summarizerKS, vcc, manager.listSummaryIndicators, manager.getSummaryIndicator, true, function(m)
					if not filterFunction(m) then
						return false
					end
					table.swap(m, "id", "ID")
					m.id = nil
					table.swap(m, "formula", "script")
				end)
			if not metrics then
				error("Failed to list metrics, ".. err)
			end
			local indicatorsPath = schemaPath.."/indicators.lua"
			print("Saving indicators to file ".. indicatorsPath)
			table.save(metrics, indicatorsPath, true)

			metrics, err = GetAllEntities(
				summarizerKS, vcc, manager.listCompositeIndicators, manager.getCompositeIndicator, true, function(m)
					if not filterFunction(m) then
						return false
					end
					table.swap(m, "id", "ID")
					m.id = nil
				end)
			if not metrics then
				error("Failed to list composite metrics, ".. err)
			end
			indicatorsPath = schemaPath.."/compositeMetrics.lua"
			print("Saving composite metrics to file ".. indicatorsPath)
			table.save(metrics, indicatorsPath, true)
		end
	},
	{
		name = "reports",
		import = function(summarizerKS, schemaPath, vcc)

			print("Adding detail reports")
			walkDirectory(schemaPath, "detailReports", function(filename, fullfilename)
				local luaFile = string.match(filename, "([^\.]*)\.lua$")
				if luaFile then
					--print("- "..luaFile)
					local report = dofile(fullfilename)
					if type(_G[luaFile]) == "function" then
						report = _G[luaFile]({})
						report.id      = luaFile
						report.type    = manager.REPORT_TYPE_DETAIL
						report.mutable = false
						report.__designer = nil -- make sure this flag is not set
					end
					local reportName = report.id
					local ok, errMessage = manager.setReport(summarizerKS, vcc, reportName, report, true)
					if not ok then
						error("Error saving detail report " .. reportName .. ": " .. errMessage)
					end
				end
			end, callbackPrint("Detail reports added"))

			print("Adding summary reports and composite indicators")
			walkDirectory(schemaPath, "reports", function(filename, fullfilename)
				local luaFile = string.match(filename, "([^%.]*)\.lua$")
				if luaFile then
					--print("- "..luaFile)
					local report = dofile(fullfilename)

					-- cope with builtin reports
					if type(_G[luaFile]) == "function" then
						report = _G[luaFile]({})
						report.id      = luaFile
						report.type    = manager.REPORT_TYPE_SUMMARY
						report.mutable = false
						report.__designer = nil -- make sure this flag is not set
					end

					local reportName = report.id
					local ok, errMessage = manager.setReport(summarizerKS, vcc, reportName, report, true)
					if not ok then
						error("Error saving summary report " .. reportName .. ": " .. errMessage)
					end

					-- Check if there's a withShift version (only cut changes)
					--if type(_G[luaFile]) == "function" then
					--	local reportWithShift = _G[luaFile] { withShift = true }
					--	if report.cut ~= reportWithShift.cut then
					--		local reportName = luaFile.."WithShifts"
					--		reportWithShift.type = manager.REPORT_TYPE_SUMMARY
					--		reportWithShift.mutable = false
					--		local ok, errMessage = manager.setReport(summarizerKS, vcc, reportName, reportWithShift, true)
					--		if not ok then
					--			error("Error saving summary report " .. reportName .. ": " .. errMessage)
					--		end
					--	end
					--end
				end
			end, callbackPrint("Summary reports and composite indicators added"))
		end,

		export = function(summarizerKS, schemaPath, vcc, filterFunction)
			lfs.mkdir(schemaPath.."/reports/")
			lfs.mkdir(schemaPath.."/detailReports/")
			local reports, err = GetAllEntities(summarizerKS, vcc, manager.listReports, manager.getReport, true, function(r)
				if not filterFunction(r) then
					return false
				end
			end)
			if not reports then
				error("Failed to list reports, ".. err)
			end
			print("Saving reports")
			for _,report in pairs(reports) do
				local filename = string.format("%s/%s/%s.lua", schemaPath, (report.type == "detail" and "/detailReports/" or "/reports/"), report.id)
				table.save(report, filename, true)
			end
			print(tostring(#reports).." reports saved")
		end
	},
	-- this stuff cannot be exported
	{
		name = "system",
		export = function() end,
		import = function(summarizerKS, schemaPath, vcc)
			if vcc ~= "system" then
				return
			end

			-- Detail scripts are files inside the 'detailScripts' directory, with the structure <type>Detail.lua
			-- They must be stored by type
			print("Adding detail scripts")
			walkDirectory(schemaPath, "detailScripts", function(filename, fullfilename)
				local scriptType = string.match(filename, "([^\.]*)Detail\.lua$")
				if scriptType then
					--print("- "..scriptType)
					manager.setDetailScript(summarizerKS, scriptType, GetFileContent(fullfilename))
				end
			end, callbackPrint("Detail scripts added"))

			-- Summary helpers are files inside the 'helpers' directory.
			-- They must be stored by filename, without extension.
			print("Adding summary indicator helpers")
			walkDirectory(schemaPath, "helpers", function(filename, fullfilename)
				local luaFile = string.match(filename, "([^\.]*)\.lua$")
				if luaFile then
					--print("- "..luaFile)
					manager.setSummaryIndicatorHelper(summarizerKS, vcc, luaFile, GetFileContent(fullfilename))
				end
			end, callbackPrint("Summary indicator helpers added"))
		end,
	}
}

local function getHandlers(entities)
	return entities and (function()
		print("Looking for ".. entities .." handler")
		for _, v in ipairs(entityHandlers) do
			if v.name == entities then
				return { v }
			end
		end
		error("invalid entity name: ".. tostring(entities))
	end)() or entityHandlers
end

local function execCommon(action, cassandraIP, cassandraPort, schemaPath, vcc, entities, filterFunction)
	filterFunction = filterFunction or function() return true end

	print("Connecting with Cassandra... "..cassandraIP.." port "..cassandraPort)
	local summarizerKS = cassandra.seed(cassandraIP, cassandraPort) and
		cassandra:CassandraConnection("SummarizerData") or error("Connection with Cassandra failed.")

	local handlers = getHandlers(entities)
	for _,handler in ipairs(handlers) do
		handler[action](summarizerKS, schemaPath, vcc, filterFunction)
	end

	summarizerKS:Close()
end

Import = function(cassandraIP, cassandraPort, schemaPath, vcc, importEntity)
	vcc = vcc or "system"
	if not importEntity then print("Adding summarizer schema for vcc ".. vcc .." to cassandra") end
	execCommon("import", cassandraIP, cassandraPort, schemaPath, vcc, importEntity)
	if not importEntity then print("Summarizer schema stored in cassandra") end
end

Export = function(cassandraIP, cassandraPort, schemaPath, vcc, exportEntity, filterFunction)
	vcc = vcc or "system"
	execCommon("export", cassandraIP, cassandraPort, schemaPath, vcc, exportEntity, filterFunction)
end


Clear = function(cassandraIP, cassandraPort, vcc)
	print("Removing summarizer schema from cassandra")
	print("Connecting with Cassandra... "..cassandraIP.." port "..cassandraPort)
	local summarizerKS = cassandra.seed(cassandraIP, cassandraPort) and
		cassandra:CassandraConnection("SummarizerData") or error("Connection with Cassandra failed.")

	-- Clear everything old

	local function CleanCF(CF)
		print(" > Clear CF ".. CF)
		local keys = CassandraSelect(summarizerKS, CF)
		for key in pairs(keys) do
			if not vcc or vcc == key then
				summarizerKS:RemoveColumn(key, CF)
			end
		end
	end
	CleanCF("SummaryCuts")
	CleanCF("SummaryIndicators")
	CleanCF("SummaryIndicatorHelpers")
	CleanCF("DetailFilters")
	CleanCF("DetailScripts")
	CleanCF("Reports")
	CleanCF("CompositeIndicators")
	CleanCF("FieldTypes")
	-- TODO: delete this check
	if ExistsColumnFamily(summarizerKS, "Dimensions") then
		CleanCF("Dimensions")
	end

	summarizerKS:Close()
	summarizerKS = nil
	print("Summarizer schema removed from cassandra")
end

ClearData = function(cassandraIP, cassandraPort)
	print("Removing summarizer data from cassandra")
	print("Connecting with Cassandra... "..cassandraIP.." port "..cassandraPort)
	if not cassandra.seed(cassandraIP, cassandraPort) then
		print("Connection with Cassandra failed.")
		return
	end
	local summarizerKS = cassandra:CassandraConnection("SummarizerData")

	-- Clear everything old

	summarizerKS:Truncate("Summaries")
	summarizerKS:Truncate("SummaryDomains")
	summarizerKS:Truncate("DetailIncrements")
	summarizerKS:Truncate("SummaryMinMax")
	summarizerKS:Truncate("DetailMinMax")
	summarizerKS:Truncate("IndexesDetailData")
	summarizerKS:Truncate("DetailsData")
	summarizerKS:Truncate("ReverseIndexesDetailData")

	summarizerKS:Close()
	summarizerKS = nil
	print("Summarizer data removed from cassandra")
end
