#!lua name=comsrv

-- ========================================
-- ComSrv Specific Functions
-- Communication Service Redis Functions
-- Patched version with critical fixes
-- ========================================

-- Redis 8.0+ has cjson as a global
-- No need to declare, just use cjson directly

-- Safe JSON decode helper
local function safe_decode(json_str, what)
	if not json_str then
		return nil, redis.error_reply("Missing " .. (what or "JSON"))
	end
	local ok, result = pcall(cjson.decode, json_str)
	if not ok then
		return nil, redis.error_reply("Invalid " .. (what or "JSON") .. ": " .. tostring(result))
	end
	return result
end

-- Batch update telemetry data for a channel
-- @function comsrv_batch_update
-- @input keys: unused
-- @input args[1]: channel_id - Channel identifier
-- @input args[2]: telemetry_type - Data type (T|S|C|A)
-- @input args[3]: updates_json - JSON object with point_id:value pairs
-- @input args[4]: raw_values_json (optional) - Unscaled raw values
-- @output string - JSON result with update count and routing status
-- @throws redis.error_reply - Invalid arguments or JSON
-- @redis-write comsrv:{channel}:{type} - Engineering values hash
-- @redis-write comsrv:{channel}:{type}:ts - Timestamps hash
-- @redis-write comsrv:{channel}:{type}:raw - Raw values hash (optional)
-- @redis-read map:c2m - Channel to model routing
-- @redis-write modsrv:{instance}:M:{point} - Routed model data
-- @redis-pubsub alarmsrv:check - Trigger alarm checking for T/S types
-- @side-effects Automatically routes data to modsrv instances via map:c2m
local function comsrv_batch_update(keys, args)
	if #args < 3 then
		return redis.error_reply("Usage: channel_id telemetry_type updates_json")
	end

	local channel_id = args[1]
	local telemetry_type = args[2] -- T, S, C, or A

	-- Validate telemetry_type
	if telemetry_type ~= "T" and telemetry_type ~= "S" and telemetry_type ~= "C" and telemetry_type ~= "A" then
		return redis.error_reply(
			"Invalid telemetry_type. Must be 'T' (Telemetry), 'S' (Signal), 'C' (Control), or 'A' (Adjustment)"
		)
	end

	-- Safe JSON decode
	local updates, err = safe_decode(args[3], "updates")
	if err then
		return err
	end
	-- Optional raw values (4th argument)
	-- Raw values are the original unscaled values from devices, stored separately for:
	-- 1. Data quality analysis and debugging
	-- 2. Historical raw data preservation
	-- 3. Future reprocessing with different scaling factors
	local raw_values = nil
	if args[4] then
		raw_values, err = safe_decode(args[4], "raw_values")
		if err then
			-- If raw values decode fails, continue without them (non-critical)
			raw_values = nil
		end
	end

	-- Get timestamp once for all updates (milliseconds for better precision)
	local time_array = redis.call("TIME")
	local now_millis = tonumber(time_array[1]) * 1000 + math.floor(tonumber(time_array[2]) / 1000)

	-- Prepare three batch updates: engineering values, timestamps, raw values
	local eng_data = {} -- Engineering values
	local ts_data = {} -- Timestamps
	local raw_data = {} -- Raw values
	local update_count = 0

	for point_id, value in pairs(updates) do
		local pid_str = tostring(point_id)

		-- Engineering values
		table.insert(eng_data, pid_str)
		if type(value) == "boolean" then
			table.insert(eng_data, value and "1" or "0")
		else
			table.insert(eng_data, tostring(value))
		end

		-- Timestamps (store as milliseconds for precision)
		table.insert(ts_data, pid_str)
		table.insert(ts_data, tostring(now_millis))

		-- Raw values (if provided)
		if raw_values and raw_values[point_id] then
			table.insert(raw_data, pid_str)
			table.insert(raw_data, tostring(raw_values[point_id]))
		end

		update_count = update_count + 1
	end

	-- Perform batch updates
	if #eng_data > 0 then
		-- Update engineering values
		local eng_key = string.format("comsrv:%s:%s", channel_id, telemetry_type)
		redis.call("HSET", eng_key, unpack(eng_data))

		-- Update timestamps
		local ts_key = string.format("comsrv:%s:%s:ts", channel_id, telemetry_type)
		redis.call("HSET", ts_key, unpack(ts_data))

		-- Update raw values (if any)
		if #raw_data > 0 then
			local raw_key = string.format("comsrv:%s:%s:raw", channel_id, telemetry_type)
			redis.call("HSET", raw_key, unpack(raw_data))
		end
	end

	-- Note: Runtime status (last_update, etc.) is maintained in memory by the service,
	-- not in Redis. Redis only stores pure data (values, timestamps, raw values).

	-- Trigger alarm checking if enabled
	if telemetry_type == "T" or telemetry_type == "S" then
		-- Publish to alarm service for threshold checking
		redis.call(
			"PUBLISH",
			"alarmsrv:check",
			cjson.encode({
				source = "comsrv",
				channel_id = channel_id,
				telemetry_type = telemetry_type,
				points = updates,
			})
		)
	end

	-- Route data to modsrv instances via routing tables (optimized batch lookup)
	-- This enables real-time data flow from comsrv to modsrv
	local comsrv_keys = {}
	local point_values = {}

	-- Build list of keys for batch lookup
	for point_id, value in pairs(updates) do
		local comsrv_key = string.format("comsrv:%s:%s:%s", channel_id, telemetry_type, point_id)
		table.insert(comsrv_keys, comsrv_key)
		table.insert(point_values, value)
	end

	-- Batch lookup routing (single Redis call instead of N calls)
	if #comsrv_keys > 0 then
		local routes = redis.call("HMGET", "route:c2m", unpack(comsrv_keys))
		local routed_count = 0
		local unmapped_count = 0

		for i, modsrv_key in ipairs(routes) do
			if modsrv_key then
				-- Parse modsrv_key format: modsrv:instance_id:type:point_id
				local instance_id, point_type, point_id = string.match(modsrv_key, "modsrv:([^:]+):([MA]):(%d+)")
				if instance_id and point_type and point_id then
					-- Route data to modsrv instance using Hash storage
					local modsrv_hash_key = string.format("modsrv:%s:%s", instance_id, point_type)
					redis.call("HSET", modsrv_hash_key, point_id, tostring(point_values[i]))
					routed_count = routed_count + 1
				end
			else
				-- Track unmapped points for configuration debugging
				unmapped_count = unmapped_count + 1
				-- Record specific unmapped points (limit to avoid memory issues)
				local unmapped_key = string.format("comsrv:stats:unmapped:%s:%s", channel_id, telemetry_type)
				local unmapped_list_size = redis.call("SCARD", unmapped_key)
				if unmapped_list_size < 100 then -- Limit set size
					-- Extract point_id from comsrv_key
					local point_id = string.match(comsrv_keys[i], ":(%d+)$")
					if point_id then
						redis.call("SADD", unmapped_key, point_id)
						redis.call("EXPIRE", unmapped_key, 3600) -- Expire after 1 hour to avoid accumulation
					end
				end
			end
		end

		-- Track statistics for monitoring
		if routed_count > 0 then
			redis.call("HINCRBY", "comsrv:stats:routed", channel_id, routed_count)
		end

		if unmapped_count > 0 then
			-- Track unmapped count for alerting
			redis.call("HINCRBY", "comsrv:stats:unmapped_total", channel_id .. ":" .. telemetry_type, unmapped_count)
			-- Record last occurrence timestamp for debugging
			redis.call("HSET", "comsrv:stats:last_unmapped", channel_id .. ":" .. telemetry_type, now_millis)
			-- Publish warning notification for real-time alerting
			redis.call(
				"PUBLISH",
				"warnings:unmapped_points",
				cjson.encode({
					service = "comsrv",
					channel_id = channel_id,
					telemetry_type = telemetry_type,
					unmapped_count = unmapped_count,
					routed_count = routed_count,
					timestamp = now_millis,
					severity = unmapped_count > 10 and "warning" or "info",
				})
			)
		end
	end

	return cjson.encode({
		status = "success",
		channel_id = channel_id,
		telemetry_type = telemetry_type,
		updated_count = update_count,
	})
end

-- Process command trigger (YK/YT)
local function comsrv_command_trigger(keys, args)
	if #args < 3 then
		return redis.error_reply("Usage: channel_id command_type command_json")
	end

	local channel_id = args[1]
	local command_type = args[2] -- "C" for Control (YK) or "A" for Adjustment (YT)
	-- Safe JSON decode
	local command, err = safe_decode(args[3], "command")
	if err then
		return err
	end

	-- Validate command type
	if command_type ~= "C" and command_type ~= "A" then
		return redis.error_reply("Invalid command type. Must be 'C' (Control) or 'A' (Adjustment)")
	end

	-- Get current timestamp in milliseconds
	local now = redis.call("TIME")
	local ts = tonumber(now[1])
	local us = tonumber(now[2])
	local timestamp_ms = ts * 1000 + math.floor(us / 1000)

	-- Generate command ID without external counters (ts_us based)
	local command_id = string.format("%s_%s_%d_%d", channel_id, command_type, ts, us)

	-- Store command in queue (unified naming: comsrv:{channel}:{type}:TODO)
	local queue_key = string.format("comsrv:%s:%s:TODO", channel_id, command_type)
	local command_data = cjson.encode({
		command_id = command_id,
		channel_id = channel_id,
		command_type = command_type, -- Add command_type field for proper deserialization
		point_id = command.point_id,
		value = command.value,
		timestamp = timestamp_ms, -- Store as milliseconds
		source = command.source or "manual",
		priority = command.priority or 5,
	})

	-- Use FIFO queue: RPUSH items, consumer uses BLPOP
	redis.call("RPUSH", queue_key, command_data)

	-- Control queue length to avoid unbounded growth
	-- Keep only the newest 500 items (RPUSH appends to tail, BLPOP pops from head)
	-- Using negative indices preserves the last 500 items
	redis.call("LTRIM", queue_key, -500, -1)

	-- (Event publish removed) No Pub/Sub broadcast for comsrv:command

	return cjson.encode({
		status = "success",
		command_id = command_id,
		channel_id = channel_id,
		command_type = command_type,
		queued = true,
	})
end

-- Get complete point information (value, timestamp, raw value)
local function comsrv_get_point_info(keys, args)
	if #args < 3 then
		return redis.error_reply("Usage: channel_id telemetry_type point_id")
	end

	local channel_id = args[1]
	local telemetry_type = args[2]
	local point_id = args[3]

	local eng_key = string.format("comsrv:%s:%s", channel_id, telemetry_type)
	local ts_key = string.format("comsrv:%s:%s:ts", channel_id, telemetry_type)
	local raw_key = string.format("comsrv:%s:%s:raw", channel_id, telemetry_type)

	local eng_value = redis.call("HGET", eng_key, point_id)
	local timestamp = redis.call("HGET", ts_key, point_id)
	local raw_value = redis.call("HGET", raw_key, point_id)

	return cjson.encode({
		value = eng_value,
		timestamp = timestamp,
		raw = raw_value,
		channel_id = channel_id,
		telemetry_type = telemetry_type,
		point_id = point_id,
	})
end

-- NOTE: Removed 6 unused functions:
-- comsrv_get_four_telemetry, comsrv_server_handle_read, comsrv_server_handle_write,
-- comsrv_sync_channels, comsrv_get_channel_stats, comsrv_cleanup_channel
-- These functions were defined but never registered, adding unnecessary complexity

-- Register minimal functions required by runtime
redis.register_function("comsrv_batch_update", comsrv_batch_update)
redis.register_function("comsrv_command_trigger", comsrv_command_trigger)
redis.register_function("comsrv_get_point_info", comsrv_get_point_info)
