-- @author DoomTero 2026

require "ZomboRut/ZR_Surrender"

ZR_ZombieCloak = ZR_ZombieCloak or {}

local DEBUG = true
local function log(fmt, ...)
	if DEBUG then print(string.format("[ZR Cloak] " .. fmt, ...)) end
end

local MOODLE_NAME = "ZombieScent"
local HAS_MF = false
if getActivatedMods and getActivatedMods():contains("MoodleFramework") then
	HAS_MF = true
	require "MF_ISMoodle"
	MF.createMoodle(MOODLE_NAME)
end

local state = {}

local function _sandbox()
	return SandboxVars and SandboxVars.ZomboRut
end

local function _cloakHours()
	local sv = _sandbox()
	local h = (sv and sv.cloakDurationGameHours) or 2
	if h < 0 then h = 0 end
	return h
end

local function _enabled()
	local sv = _sandbox()
	if not sv then return false end
	if sv.enableMod == false then return false end
	if sv.cloakEnabled == false then return false end
	if _cloakHours() <= 0 then return false end
	return true
end

local function _nowHour()
	local gt = getGameTime and getGameTime()
	if not gt then return 0 end
	return gt:getWorldAgeHours()
end

local CLOAK_HOURS_PER_SCENE = 0.5

local function _stateFor(player)
	if state[player] then return state[player] end
	for p, st in pairs(state) do
		if p == player then return st end
	end
	return nil
end

local function _isPlayerCharacter(c)
	if not c then return false end
	if instanceof and instanceof(c, "IsoPlayer") then return true end
	local sp = getSpecificPlayer and getSpecificPlayer(0)
	if sp and c == sp then return true end
	return false
end

local function _moodleValueFor(remainingFrac)
	if remainingFrac <= 0 then return 0.5 end
	if remainingFrac > 0.66 then return 0.85 end
	if remainingFrac > 0.33 then return 0.7  end
	return 0.6
end

local function _setMoodle(player, value)
	if not HAS_MF then return end
	if not MF or not MF.getMoodle then return end
	local pn = (player.getPlayerNum and player:getPlayerNum()) or 0
	local m = MF.getMoodle(MOODLE_NAME, pn)
	if not m then return end
	if m.getValue and m:getValue() == value then return end
	m:setValue(value)
end

function ZR_ZombieCloak.isActive(player)
	local st = _stateFor(player)
	if not st then return false end
	if _nowHour() >= st.expireHour then return false end
	return true
end

function ZR_ZombieCloak.getRemainingHours(player)
	local st = _stateFor(player)
	if not st then return 0 end
	local r = st.expireHour - _nowHour()
	if r < 0 then return 0 end
	return r
end

function ZR_ZombieCloak.activate(player, hours)
	if not player or not player:isAlive() then return end
	hours = hours or _cloakHours()
	if hours <= 0 then return end
	state[player] = {
		expireHour     = _nowHour() + hours,
		totalHours     = hours,
		exposed        = {},
		cloakedZombies = {},
	}
	log("activated for %.2f game hours (expires at hour %.2f)",
		hours, state[player].expireHour)
	pcall(function() _setMoodle(player, _moodleValueFor(1.0)) end)
	pcall(function()
		local msg = getText("UI_ZR_ZombieCloak_Active")
		if msg and msg ~= "UI_ZR_ZombieCloak_Active" then
			player:Say(msg)
		end
	end)
end

function ZR_ZombieCloak.accumulate(player, hours)
	if not player or not player:isAlive() then return end
	hours = hours or CLOAK_HOURS_PER_SCENE
	if hours <= 0 then return end
	local cap = _cloakHours()
	if cap <= 0 then return end
	local now = _nowHour()
	local st = _stateFor(player)
	if not st then
		local newDur = math.min(hours, cap)
		state[player] = {
			expireHour     = now + newDur,
			totalHours     = newDur,
			exposed        = {},
			cloakedZombies = {},
		}
		log("cloak started (first scene) +%.2fh, remaining=%.2fh (cap=%.2fh)",
			hours, newDur, cap)
		pcall(function() _setMoodle(player, _moodleValueFor(1.0)) end)
		pcall(function()
			local msg = getText("UI_ZR_ZombieCloak_Active")
			if msg and msg ~= "UI_ZR_ZombieCloak_Active" then
				player:Say(msg)
			end
		end)
	else
		local currentRem = math.max(0, st.expireHour - now)
		local newRem = math.min(cap, currentRem + hours)
		st.expireHour = now + newRem
		st.totalHours = math.max(st.totalHours or newRem, newRem)
		st.exposed = {}
		log("cloak extended +%.2fh, remaining=%.2fh (cap=%.2fh, exposed cleared)",
			hours, newRem, cap)
		pcall(function() _setMoodle(player, _moodleValueFor(1.0)) end)
	end
end

function ZR_ZombieCloak.deactivate(player)
	local storedKey, st = nil, nil
	for p, s in pairs(state) do
		if p == player then storedKey = p; st = s; break end
	end
	if storedKey then
		log("deactivated for player")
		state[storedKey] = nil
		pcall(function() _setMoodle(player, 0.5) end)
	end
end

function ZR_ZombieCloak.expose(player, zombie)
	local st = _stateFor(player)
	if not st then return end
	if not zombie or not zombie.getID then return end
	local id = zombie:getID()
	if id == nil then return end
	if not st.exposed[id] then
		st.exposed[id] = true
		log("zombie id=%s exposed (no longer cloaked from this one)", tostring(id))
	end
	pcall(function()
		if zombie.setUseless then zombie:setUseless(false) end
		if zombie.setNoTeeth then zombie:setNoTeeth(false) end
	end)
	if st.cloakedZombies then st.cloakedZombies[id] = nil end
end

local ZomboRutScene = require "ZomboRut/ZomboRutScene"
if ZomboRutScene and ZomboRutScene.registerSceneEndCallback then
	ZomboRutScene.registerSceneEndCallback(function(player, _active, result)
		if not _enabled() then return end
		if result ~= "Released" then return end
		log("scene-end Released → +%.2fh cloak", CLOAK_HOURS_PER_SCENE)
		ZR_ZombieCloak.accumulate(player)
	end)
	log("scene-end callback registered (per-scene accumulation active)")
else
	log("WARNING: ZomboRutScene.registerSceneEndCallback unavailable — cloak will NEVER auto-activate")
end

if ZR_Surrender and ZR_Surrender.registerStartCallback then
	ZR_Surrender.registerStartCallback(function(player, _st)
		if ZR_ZombieCloak.isActive(player) then
			log("surrender started during active cloak — deactivating cloak so aggressors can engage")
			ZR_ZombieCloak.deactivate(player)
		end
	end)
end

Events.OnCharacterDeath.Add(function(character)
	state[character] = nil
end)

local function onPlayerUpdate_Expire(player)
	local st = _stateFor(player)
	if not st then return end
	local now = _nowHour()
	if now >= st.expireHour then
		log("expired naturally (hour %.2f reached)", st.expireHour)
		pcall(function()
			local msg = getText("UI_ZR_ZombieCloak_Wear_Off")
			if msg and msg ~= "UI_ZR_ZombieCloak_Wear_Off" then
				player:Say(msg)
			end
		end)
		ZR_ZombieCloak.deactivate(player)
		return
	end
	local total = st.totalHours
	if total and total > 0 then
		local frac = (st.expireHour - now) / total
		pcall(function() _setMoodle(player, _moodleValueFor(frac)) end)
	end
end
Events.OnPlayerUpdate.Add(onPlayerUpdate_Expire)

Events.OnCharacterCollide.Add(function(character, collidedCharacter)
	if not character or not collidedCharacter then return end
	local player, other
	if _isPlayerCharacter(character) then
		player, other = character, collidedCharacter
	elseif _isPlayerCharacter(collidedCharacter) then
		player, other = collidedCharacter, character
	else
		return
	end
	if not other.isZombie or not other:isZombie() then return end
	if other.getVariableBoolean and other:getVariableBoolean("Bandit") then return end
	if not ZR_ZombieCloak.isActive(player) then return end
	if ZomboRutScene.isPlayerInScene(player) then return end
	if ZomboRutScene.isPlayerInPostSceneGrace
		and ZomboRutScene.isPlayerInPostSceneGrace(player) then return end
	ZR_ZombieCloak.expose(player, other)
end)

Events.OnHitZombie.Add(function(zombie, attacker, _bodyPart, _weapon)
	if not zombie or not attacker then return end
	if not _isPlayerCharacter(attacker) then return end
	if zombie.getVariableBoolean and zombie:getVariableBoolean("Bandit") then return end
	if not ZR_ZombieCloak.isActive(attacker) then return end
	if ZomboRutScene.isPlayerInScene(attacker) then return end
	if ZomboRutScene.isPlayerInPostSceneGrace
		and ZomboRutScene.isPlayerInPostSceneGrace(attacker) then return end
	ZR_ZombieCloak.expose(attacker, zombie)
end)

local CLOAK_SCAN_RANGE = 18
local _setsThisSecond = 0
local _lastHeartbeatTick = 0
local _tickCounter = 0

local function onPlayerUpdate_CloakScan(player)
	_tickCounter = _tickCounter + 1

	local st = _stateFor(player)
	if not st then return end
	if _nowHour() >= st.expireHour then return end

	local phase = ZR_Surrender and ZR_Surrender.getPhase and ZR_Surrender.getPhase(player)
	if phase == "surrender" then return end

	local cell = getCell()
	local zList = cell and cell:getZombieList()
	if not zList then return end

	for i = 0, zList:size() - 1 do
		local z = zList:get(i)
		if z and z:isAlive()
			and not (z.getVariableBoolean and z:getVariableBoolean("Bandit"))
			and z:DistTo(player) <= CLOAK_SCAN_RANGE
		then
			local zid = z.getID and z:getID()
			if zid ~= nil and not st.exposed[zid] then
				pcall(function()
					if not st.cloakedZombies[zid] then
						st.cloakedZombies[zid] = true
						_setsThisSecond = _setsThisSecond + 1
					end
					if z.setUseless    then z:setUseless(true)    end
					if z.setNoTeeth    then z:setNoTeeth(true)    end
					if z.clearAggroList then z:clearAggroList() end
					if z.setTarget       then z:setTarget(nil)   end
				end)
			end
		end
	end

	if (_tickCounter - _lastHeartbeatTick) >= 60 then
		_lastHeartbeatTick = _tickCounter
		if _setsThisSecond > 0 then
			local rem = ZR_ZombieCloak.getRemainingHours(player)
			local cloaked = 0
			for _ in pairs(st.cloakedZombies) do cloaked = cloaked + 1 end
			log("heartbeat: cloaked=%d zombies, new this sec=%d, remaining=%.2fh",
				cloaked, _setsThisSecond, rem)
		end
		_setsThisSecond = 0
	end
end
Events.OnPlayerUpdate.Add(onPlayerUpdate_CloakScan)

local function _findCloakedPlayerForTarget(tgt)
	for p, st in pairs(state) do
		if p == tgt and _nowHour() < st.expireHour then
			return p, st
		end
	end
	return nil, nil
end

Events.OnZombieUpdate.Add(function(zombie)
	if not zombie or not zombie:isAlive() then return end
	if zombie.getVariableBoolean and zombie:getVariableBoolean("Bandit") then return end

	local tgt = zombie.getTarget and zombie:getTarget()
	if not tgt then return end

	local _, st = _findCloakedPlayerForTarget(tgt)
	if not st then return end

	local phase = ZR_Surrender and ZR_Surrender.getPhase and ZR_Surrender.getPhase(tgt)
	if phase == "surrender" then return end

	local zid = zombie.getID and zombie:getID()
	if zid ~= nil and st.exposed[zid] then return end

	pcall(function()
		if zid ~= nil and not st.cloakedZombies[zid] then
			st.cloakedZombies[zid] = true
		end
		if zombie.setUseless    then zombie:setUseless(true)   end
		if zombie.setNoTeeth    then zombie:setNoTeeth(true)   end
		if zombie.clearAggroList then zombie:clearAggroList() end
		if zombie.setTarget     then zombie:setTarget(nil)     end
	end)
end)

local _origDeactivate = ZR_ZombieCloak.deactivate
function ZR_ZombieCloak.deactivate(player)
	local st = _stateFor(player)
	if st and st.cloakedZombies then
		local cell = getCell()
		local zList = cell and cell:getZombieList()
		if zList then
			for i = 0, zList:size() - 1 do
				local z = zList:get(i)
				if z and z.getID then
					local zid = z:getID()
					if st.cloakedZombies[zid] and z:isAlive() then
						pcall(function()
							if z.setUseless then z:setUseless(false) end
							if z.setNoTeeth then z:setNoTeeth(false) end
						end)
					end
				end
			end
		end
	end
	_origDeactivate(player)
end

return ZR_ZombieCloak
