local cassandra = require "lua_cassandra"
local logger = require "ticketek.logger"
local stp = require "StackTracePlus"
local struct = require "struct"
local wrapredis = require "wrapredis"
local pl = {
	tablex = require "pl.tablex"
}
local m_cassandra
local m_redis
local helpers = require "ticketek.helpers"

-------------------------------
-- I can't get enough (helpers)
--

local function pack (...)
	return { n = select("#", ...), ... }
end

local function try (finally, ok, ...)
	finally()
	if ok then
		return ...
	end
	logger.error("%s", ...)
	return nil, ...
end

---
-- Calls a given function providing a CassandraConnection bound to a given keyspace or "Agent" by default.
-- Either on success or error, the connection is closed.
--
-- Could not implement this in terms of the other due to keyspace being optional.
--
local function with_connection_do (keyspace, f, ...)
	local connection, args

	if type(keyspace) == "function" then
		args = pack(f, ...)
		f = keyspace
		connection = m_cassandra:CassandraConnection("Agent")
	else
		connection = m_cassandra:CassandraConnection(keyspace)
		args = pack(...)
	end

	local function finally()
		connection:Close()
	end

	return try(
		finally, xpcall( function() return f(connection, unpack(args, 1, args.n)) end, stp.stacktrace )
	)
end

---
--
local function should_be_numeric (column_name)
	return column_name == "base_number" or column_name == "number_width"
end

---
-- given an emitter, returns a table with the values that can be stored in Cassandra.
-- This just skips anything not one of string, number or boolean.
--
local function get_values_to_store (emitter)

	return pl.tablex.pairmap(function(key, value)
		local value_type = type(value)
		if value_type == "string" or value_type == "number" or value_type == "boolean"
		then
			return value, key
		end
	end, emitter)
end

--
-- End of helpers
------------------

---
--
local function save_new (connection, emitter)
	logger.debug("[TICKETEK:data_access] - Saving new emitter: '%s'", emitter.id)

	local id = emitter.id

	for key, value in pairs(get_values_to_store(emitter)) do
		connection:InsertColumn(id, "TicketEmitter", key, value)
	end

	local vcc = helpers.extract_vcc(id)
	m_redis:hset("TicketCounters::" .. vcc, id, emitter.base_number - 1)

	return true
end

---
-- We only retrieve the id and the description. This function is to be used for providing a list of emitters
-- of a given vcc
--
local function get_by_vcc (connection, vcc)

	local range = connection:GetRangeSlices(
		"TicketEmitter",
		"",
		"",
		"",
		{ column_names = { "description" }, slice_range = {start = "", finish = "", reversed = false, count = 1000} },
		500
	)

	local result = {}
	for row_id, columns in range:Iterator() do
		-- skip tombstones
		if columns[1] and helpers.extract_vcc(row_id) == vcc then
			result[#result + 1] = { id = row_id, description = columns[1].value }
		end
	end

	return result
end

---
-- Retrieves emitter data from Cassandra, given its id.
--
local function get_by_id (connection, emitter_id)
	local columns = connection:GetSliceRange(
		emitter_id,
		"TicketEmitter",
		"",
		{
			column_names = {},
			slice_range = { start = "", finish = "", reversed = false, count = 200 }
		}
	)

	if not columns then
		return
	end
	local result = {}
	for _, column in ipairs(columns) do
		if should_be_numeric(column.name) then
			-- do *almost* like libCassandra::deserializeLong. gets us a number back from a string
			-- note: Lua 5.1 can represent numbers as high as 2^52 so don't get crazy with this numbers
			result[column.name] = struct.unpack(">I8", column.value)
		else
			result[column.name] = column.value
		end
	end
	-- If no columns are projected this is a tombstone and should not be returned
	if not next(result) then
		return nil, "Emitter not found"
	end
	return result
end

---
-- Actual counters are stored in Redis, since Cassandra ones won't do what we need
-- (that is get the actual value of the counter immediately after increasing it)
--
local function increment (emitter_id)

	-- we don't make sure the emitter_id corresponds to a valid value since
	-- that would imply a lookup in Cassandra and we don't want that penalty
	-- in the common case. So please, go configure things properly.

	logger.debug("[TICKETEK:data_access] - Incrementing emitter: '%s'", emitter_id)
	local vcc = helpers.extract_vcc(emitter_id)
	return m_redis:hincrby("TicketCounters::" .. vcc, emitter_id, 1)
end

local function delete_emitter (connection, emitter_id)
	logger.debug("[TICKETEK:data_access] - Deletting emitter: '%s'", emitter_id)

	connection:RemoveColumn(emitter_id, "TicketEmitter")

	local vcc = helpers.extract_vcc(emitter_id)
	m_redis:hdel("TicketCounters::" .. vcc, emitter_id)
	return true
end

---
--
local function update (connection, emitter_id, new_data)
	logger.debug("[TICKETEK:data_access] - Saving new emitter: '%s'", emitter_id)

	local current_emittter = get_by_id(connection, emitter_id)

	-- remove values that have been deleted (present in old emitter, not in new one)
	for key, value in pairs(current_emittter or {}) do
		if new_data[key] == nil then
			connection:RemoveColumn(emitter_id, "TicketEmitter", key)
		end
	end

	for key, value in pairs(new_data) do
		if not current_emittter or value ~= current_emittter[key] then
			connection:InsertColumn(emitter_id, "TicketEmitter", key, value)
		end
	end

	-- only update the base number on Redis if it actually changed
	if current_emittter.base_number ~= new_data.base_number then
		local vcc = helpers.extract_vcc(emitter_id)
		m_redis:hset("TicketCounters::" .. vcc, emitter_id, new_data.base_number - 1)
	end

	return true
end

local data_access = {}

function data_access.init (cassandra_options, redis_options)
	m_cassandra = cassandra
	m_cassandra.seed(cassandra_options.host, tonumber(cassandra_options.port))
	m_redis = wrapredis(redis_options.host, redis_options.port, redis_options.pwd)
end

function data_access.save_new (emitter_id)
	return with_connection_do(save_new, emitter_id)
end

function data_access.update (emitter_id, new_data)
	return with_connection_do(update, emitter_id, new_data)
end

function data_access.get_by_vcc (vcc)
	return with_connection_do(get_by_vcc, vcc)
end

function data_access.get_by_id (emitter_id)
	return with_connection_do(get_by_id, emitter_id)
end

function data_access.increment (emitter_id)
	return increment(emitter_id)
end

function data_access.delete (emitter_id)
	return with_connection_do(delete_emitter, emitter_id)
end

function data_access.reset (emitter_id, base_number)
	local vcc = helpers.extract_vcc(emitter_id)
	m_redis:hset("TicketCounters::" .. vcc, emitter_id, base_number - 1)
end

function data_access.link (ticket, vcc, value)
	-- Ahora el link se realiza en el IR/Elastic Search
	assert(false, "ticketek: 'data_access.link' Method is Deprecated")
end

function data_access.lookup (tickets, vcc)
	-- Ahora el lookup se realiza en los controllers de la app "Interaction"/Elastic Search
	assert(false, "ticketek: 'data_access.lookup' Method is Deprecated")
end

return data_access
