-- @author SolarEdge 2026

DebugLog.log("[ZR Debug] ZR_DebugSpawnDummy.lua loaded")

local ZomboRutScene = require "ZomboRut/ZomboRutScene"

local DUMMY_TAG = "ZomboRutDummy"

local _dummiesTouched = false

local banditsAvailable = require("ZomboRut/ContextMenu/ZR_MenuUtil").banditsAvailable
local setIcon = require("ZomboRut/ContextMenu/ZR_MenuUtil").icon

local function findDeadBodyAt(worldobjects)
	for i = 1, #worldobjects do
		local obj = worldobjects[i]
		if instanceof(obj, "IsoDeadBody") then return obj end
	end
	for i = 1, #worldobjects do
		local obj = worldobjects[i]
		local sq = obj and obj.getSquare and obj:getSquare()
		if sq then
			local statics = sq:getStaticMovingObjects()
			if statics then
				for j = 0, statics:size() - 1 do
					local s = statics:get(j)
					if instanceof(s, "IsoDeadBody") then return s end
				end
			end
		end
	end
	return nil
end

local function pickSquareFromContext(worldobjects)
	for _, v in ipairs(worldobjects) do
		if v and v.getSquare then
			local sq = v:getSquare()
			if sq then return sq end
		end
	end
	return nil
end

local function silenceZombie(zombie)
	local desc = zombie:getDescriptor()
	if desc and desc.setVoicePrefix then
		desc:setVoicePrefix("NotAZombie")
	end
	local emitter = zombie:getEmitter()
	if emitter and emitter.stopSoundByName then
		emitter:stopSoundByName("FemaleZombieVoiceA")
		emitter:stopSoundByName("FemaleZombieVoiceB")
		emitter:stopSoundByName("FemaleZombieVoiceC")
		emitter:stopSoundByName("MaleZombieVoiceA")
		emitter:stopSoundByName("MaleZombieVoiceB")
		emitter:stopSoundByName("MaleZombieVoiceC")
	end
end

local function spawnDummyZombie(worldobjects, playerObj, square, genderChoice)
	DebugLog.log("[ZR Debug] spawnDummyZombie fired: gender=" .. tostring(genderChoice))
	if not square then
		playerObj:Say("ZR Debug: no valid square under the cursor")
		return
	end

	local wantFemale
	if genderChoice == "female" then
		wantFemale = true
	elseif genderChoice == "male" then
		wantFemale = false
	else
		wantFemale = not playerObj:isFemale()
	end

	local zombie
	pcall(function()
		if createZombie then
			zombie = createZombie(
				square:getX(), square:getY(), square:getZ(),
				nil, 0, IsoDirections.S
			)
		end
	end)
	if not zombie then
		playerObj:Say("ZR Debug: createZombie failed")
		DebugLog.log("[ZR Debug] createZombie returned nil — abort")
		return
	end

	_dummiesTouched = true
	zombie:setVariable(DUMMY_TAG, true)
	local md = zombie:getModData()
	md.zomborutDummy = true
	md.zrDummyZombie = true
	md.zrDummyZombieConfigured = true

	if zombie.setFemale then zombie:setFemale(wantFemale) end
	if zombie.resetModelNextFrame then zombie:resetModelNextFrame() end

	pcall(function()
		zombie:setUseless(true)
		zombie:setNoTeeth(true)
		zombie:setCanWalk(false)
		zombie:clearAggroList()
		zombie:setTarget(nil)
	end)

	playerObj:Say("ZR Debug: dummy zombie spawned (" ..
		(wantFemale and "F" or "M") .. ")")
	DebugLog.log("[ZR Debug] dummy zombie configured: id=" .. zombie:getID())
end

local function spawnAttackingSprinter(worldobjects, playerObj)
	DebugLog.log("[ZR Debug] spawnAttackingSprinter fired")

	local fx, fy = 0, 1
	pcall(function()
		local fwd = playerObj:getForwardDirection()
		if fwd then fx, fy = fwd:getX(), fwd:getY() end
	end)
	local pz    = math.floor(playerObj:getZ())
	local baseX = playerObj:getX() - fx * 4
	local baseY = playerObj:getY() - fy * 4
	local cell  = getCell()
	local sq
	for _, off in ipairs({ {0,0}, {1,0}, {-1,0}, {0,1}, {0,-1}, {1,1}, {-1,-1} }) do
		local s = cell and cell:getGridSquare(math.floor(baseX) + off[1], math.floor(baseY) + off[2], pz)
		if s and s:canStand() then sq = s; break end
	end
	if not sq then
		playerObj:Say("ZR Debug: no walkable tile behind you")
		return
	end

	local zombie
	pcall(function()
		zombie = createZombie(sq:getX(), sq:getY(), sq:getZ(), nil, 100, IsoDirections.N)
	end)
	if not zombie then
		playerObj:Say("ZR Debug: createZombie failed")
		return
	end

	_dummiesTouched = true
	zombie:setVariable(DUMMY_TAG, true)
	local md = zombie:getModData()
	md.zomborutDummy    = true
	md.zrAttackSprinter = true

	pcall(function()
		zombie:doZombieSpeed(1)
		zombie:setWalkType("sprint1")
		zombie:setUseless(false)
		zombie:setCanWalk(true)
		zombie:setNoTeeth(false)
		zombie:resetModelNextFrame()
		zombie:spotted(playerObj, true)
		zombie:setTarget(playerObj)
		zombie:addAggro(playerObj, 100)
		zombie:pathToCharacter(playerObj)
	end)

	playerObj:Say("ZR Debug: attacking sprinter spawned behind you")
	DebugLog.log("[ZR Debug] attacking sprinter spawned: id=" .. zombie:getID())
end

local HORDE_COUNT = 8
local HORDE_AHEAD = 8

local function configureHordeZombie(zombie, playerObj, sprinter)
	_dummiesTouched = true
	zombie:setVariable(DUMMY_TAG, true)
	local md = zombie:getModData()
	md.zomborutDummy    = true
	md.zrAttackSprinter = true
	pcall(function()
		if sprinter then
			zombie:doZombieSpeed(1)
			zombie:setWalkType("sprint1")
		else
			zombie:doZombieSpeed(2)
		end
		zombie:setUseless(false)
		zombie:setCanWalk(true)
		zombie:setNoTeeth(false)
		zombie:resetModelNextFrame()
		zombie:spotted(playerObj, true)
		zombie:setTarget(playerObj)
		zombie:addAggro(playerObj, 100)
		zombie:pathToCharacter(playerObj)
	end)
end

local function spawnHorde(worldobjects, playerObj, sprinter, count)
	count = count or HORDE_COUNT
	DebugLog.log("[ZR Debug] spawnHorde fired: sprinter=" .. tostring(sprinter) .. " count=" .. tostring(count))
	local fx, fy = 0, 1
	pcall(function()
		local fwd = playerObj:getForwardDirection()
		if fwd then fx, fy = fwd:getX(), fwd:getY() end
	end)
	local cell = getCell()
	if not cell then playerObj:Say("ZR Debug: no cell"); return end

	local pz = math.floor(playerObj:getZ())
	local cx = math.floor(playerObj:getX() + fx * HORDE_AHEAD)
	local cy = math.floor(playerObj:getY() + fy * HORDE_AHEAD)

	local tiles = {}
	for r = 0, 4 do
		for dx = -r, r do
			for dy = -r, r do
				if math.max(math.abs(dx), math.abs(dy)) == r then
					local s = cell:getGridSquare(cx + dx, cy + dy, pz)
					if s and s:canStand() then tiles[#tiles + 1] = s end
				end
			end
		end
		if #tiles >= count then break end
	end
	if #tiles == 0 then
		playerObj:Say("ZR Debug: no walkable tiles " .. HORDE_AHEAD .. " ahead")
		return
	end

	local spawned = 0
	for i = 1, math.min(count, #tiles) do
		local s = tiles[i]
		local fc = (i % 2 == 0) and 100 or 0
		local z
		pcall(function()
			z = createZombie(s:getX(), s:getY(), s:getZ(), nil, fc, IsoDirections.S)
		end)
		if z then
			configureHordeZombie(z, playerObj, sprinter)
			spawned = spawned + 1
		end
	end

	playerObj:Say(string.format("ZR Debug: spawned %d %s %d tiles ahead",
		spawned, sprinter and "sprinters" or "walkers", HORDE_AHEAD))
	DebugLog.log(string.format("[ZR Debug] horde spawned: %d %s", spawned,
		sprinter and "sprinter(s)" or "walker(s)"))
end

local function findZombieNear(square, maxDist)
	if not square then return nil end
	maxDist = maxDist or 3
	local cell = getCell()
	local zl = cell and cell:getZombieList()
	if not zl then return nil end
	local sx, sy = square:getX(), square:getY()
	local best, bestD = nil, maxDist
	for i = 0, zl:size() - 1 do
		local z = zl:get(i)
		if z and z:isAlive() then
			local dx, dy = z:getX() - sx, z:getY() - sy
			local d = math.sqrt(dx * dx + dy * dy)
			if d <= bestD then best, bestD = z, d end
		end
	end
	return best
end

local function debugStartTakedown(worldobjects, playerObj, square)
	DebugLog.log("[ZR Debug] debugStartTakedown fired")
	local zombie = findZombieNear(square, 4)
	if not zombie then
		playerObj:Say("ZR Debug: no live zombie near the click")
		return
	end
	if not (ZR_SprinterTakedown and ZR_SprinterTakedown.launch) then
		playerObj:Say("ZR Debug: ZR_SprinterTakedown not loaded")
		return
	end
	local ok = ZR_SprinterTakedown.launch(playerObj, zombie)
	playerObj:Say(ok and "ZR Debug: takedown launched"
		or "ZR Debug: takedown refused (already in a scene?)")
	DebugLog.log("[ZR Debug] debugStartTakedown launch=" .. tostring(ok)
		.. " zombie_id=" .. tostring(zombie:getID()))
end

local AVOID_CLAN_KEYWORDS = {
	"army", "militia", "military", "legion", "officer", "soldier",
	"spike",
	"medieval",
	"prison",
}

local function pickNonSoldierClanId()
	if not banditsAvailable() then return nil end
	BanditCustom.Load()
	local clans = BanditCustom.ClanGetAllSorted()

	local candidates = {}
	for cid, clan in pairs(clans) do
		local name = (clan and clan.general and clan.general.name or ""):lower()
		local skip = false
		for _, kw in ipairs(AVOID_CLAN_KEYWORDS) do
			if string.find(name, kw, 1, true) then
				skip = true
				break
			end
		end
		if not skip and name ~= "" then
			candidates[#candidates + 1] = { cid = cid, name = name }
		end
	end

	if #candidates > 0 then
		local pick = candidates[ZombRand(#candidates) + 1]
		DebugLog.log("[ZR Debug] clan pick: " .. pick.name .. " (cid=" .. tostring(pick.cid) .. ")")
		return pick.cid
	end

	DebugLog.log("[ZR Debug] WARN no non-soldier clan found, using first available")
	for cid, _ in pairs(clans) do return cid end
end

local function spawnDummyBandit(worldobjects, playerObj, square, genderChoice)
	DebugLog.log("[ZR Debug] spawnDummyBandit fired: gender=" .. tostring(genderChoice))
	if not square then
		playerObj:Say("ZR Debug: no valid square under the cursor")
		return
	end
	if not banditsAvailable() then
		playerObj:Say("ZR Debug: Bandits mod required for dummy bandits")
		DebugLog.log("[ZR Debug] Bandits not loaded — abort bandit spawn")
		return
	end

	local cid = pickNonSoldierClanId()
	if not cid then
		playerObj:Say("ZR Debug: no bandit clans available")
		DebugLog.log("[ZR Debug] no clans loaded — abort bandit spawn")
		return
	end

	_dummiesTouched = true
	local preExisting = {}
	local zl = getWorld():getCell():getZombieList()
	for i = 0, zl:size() - 1 do
		local z = zl:get(i)
		if z:getVariableBoolean("Bandit") then
			preExisting[z:getID()] = true
		end
	end

	sendClientCommand(playerObj, 'Spawner', 'Clan', {
		cid = cid,
		x = square:getX(), y = square:getY(), z = square:getZ(),
		program = "Companion", size = 1,
		hostile = false, hostileP = false,
	})

	local wantFemale
	if genderChoice == "female" then
		wantFemale = true
	elseif genderChoice == "male" then
		wantFemale = false
	else
		wantFemale = not playerObj:isFemale()
	end

	local setupHandler
	setupHandler = function(zombie)
		if not zombie:getVariableBoolean("Bandit") then return end
		if preExisting[zombie:getID()] then return end
		if zombie:DistTo(playerObj) > 10 then return end
		if zombie:getModData().zrDummyBanditConfigured then return end

		zombie:getModData().zrDummyBanditConfigured = true
		zombie:setVariable(DUMMY_TAG, true)
		zombie:getModData().zomborutDummy = true
		zombie:getModData().zrDummyBandit = true

		Bandit.SetHostile(zombie, false); Bandit.SetHostileP(zombie, false)
		Bandit.ForceStationary(zombie, true)
		Bandit.ClearTasks(zombie)

		zombie:setPrimaryHandItem(nil); zombie:setSecondaryHandItem(nil)
		local brain = BanditBrain.Get(zombie)
		if brain and brain.weapons then
			brain.weapons.melee = "Base.BareHands"
			brain.weapons.primary = { bulletsLeft = 0, magCount = 0 }
			brain.weapons.secondary = { bulletsLeft = 0, magCount = 0 }
		end
		zombie:setVariable("BanditPrimary", "")
		zombie:setVariable("BanditSecondary", "")
		zombie:setVariable("BanditPrimaryType", "")

		if brain then brain.zrDummyBandit = true end

		if zombie.setFemale then zombie:setFemale(wantFemale) end
		if zombie.resetModelNextFrame then zombie:resetModelNextFrame() end

		Events.OnZombieUpdate.Remove(setupHandler)

		playerObj:Say("ZR Debug: pacified dummy bandit spawned (" ..
			(wantFemale and "F" or "M") .. ")")
		DebugLog.log("[ZR Debug] dummy bandit configured: id=" .. zombie:getID() ..
			" name=" .. tostring(brain and brain.fullname))
	end
	Events.OnZombieUpdate.Add(setupHandler)
end

local function removeAllDummies(worldobjects, playerObj)
	local removed = 0
	local cell = getWorld():getCell()
	local zList = cell and cell:getZombieList()
	if zList then
		for i = zList:size() - 1, 0, -1 do
			local z = zList:get(i)
			if z and z:getVariableBoolean(DUMMY_TAG) then
				pcall(function()
					if z:getVariableBoolean("Bandit") then
						local brain = BanditBrain and BanditBrain.Get and BanditBrain.Get(z)
						if brain and brain.id then
							local args = { id = brain.id }
							if isClient() then
								sendClientCommand(playerObj, 'Commands', 'BanditRemove', args)
							elseif BanditServer and BanditServer.Commands and BanditServer.Commands.BanditRemove then
								BanditServer.Commands.BanditRemove(playerObj, args)
							end
						end
						if BanditBrain and BanditBrain.Remove then BanditBrain.Remove(z) end
						z:setVariable("Bandit", false)
						z:setVariable("BanditPrimary", "")
						z:setVariable("BanditSecondary", "")
					end
					z:setVariable(DUMMY_TAG, false)
				end)
				pcall(function() if z.removeFromSquare then z:removeFromSquare() end end)
				z:removeFromWorld()
				removed = removed + 1
			end
		end
	end
	playerObj:Say("ZR Debug: removed " .. tostring(removed) .. " dummy NPC(s)")
	DebugLog.log("[ZR Debug] removeAllDummies: " .. tostring(removed))
end

local function onFillWorldObjectContextMenu(playerIndex, context, worldobjects, test)
	if test == true then return end
	if not isDebugEnabled() then return end

	local playerObj = getSpecificPlayer(playerIndex)
	if not playerObj then return end

	local square = pickSquareFromContext(worldobjects)

	local rootOption = context:addOption("ZR Debug", worldobjects, nil)
	setIcon(rootOption, "Debug")
	local rootSub = ISContextMenu:getNew(context)
	context:addSubMenu(rootOption, rootSub)

	local hasBandits = banditsAvailable()

	if square then
		local spawnHeader = rootSub:addOption("Spawn Dummy Zombie", worldobjects, nil)
		setIcon(spawnHeader, "SpawnZombie")
		local spawnSub = ISContextMenu:getNew(rootSub)
		rootSub:addSubMenu(spawnHeader, spawnSub)

		setIcon(spawnSub:addOption("Opposite Gender (pairing)", worldobjects,
			spawnDummyZombie, playerObj, square, "opposite"), "Opposite")
		setIcon(spawnSub:addOption("Male", worldobjects,
			spawnDummyZombie, playerObj, square, "male"), "Male")
		setIcon(spawnSub:addOption("Female", worldobjects,
			spawnDummyZombie, playerObj, square, "female"), "Female")

		if hasBandits then
			local spawnBanditHeader = rootSub:addOption("Spawn Dummy Bandit", worldobjects, nil)
			setIcon(spawnBanditHeader, "SpawnBandit")
			local spawnBanditSub = ISContextMenu:getNew(rootSub)
			rootSub:addSubMenu(spawnBanditHeader, spawnBanditSub)

			setIcon(spawnBanditSub:addOption("Opposite Gender (pairing)", worldobjects,
				spawnDummyBandit, playerObj, square, "opposite"), "Opposite")
			setIcon(spawnBanditSub:addOption("Male", worldobjects,
				spawnDummyBandit, playerObj, square, "male"), "Male")
			setIcon(spawnBanditSub:addOption("Female", worldobjects,
				spawnDummyBandit, playerObj, square, "female"), "Female")
		else
			local disabledBandit = rootSub:addOption("Spawn Dummy Bandit (Bandits mod required)", worldobjects, nil)
			disabledBandit.notAvailable = true
			setIcon(disabledBandit, "SpawnBandit")
		end
	else
		local disabled = rootSub:addOption("Spawn Dummy Zombie (click on a tile)", worldobjects, nil)
		disabled.notAvailable = true
		setIcon(disabled, "SpawnZombie")
		local disabledBandit = rootSub:addOption("Spawn Dummy Bandit (click on a tile)", worldobjects, nil)
		disabledBandit.notAvailable = true
		setIcon(disabledBandit, "SpawnBandit")
	end

	setIcon(rootSub:addOption("Spawn Attacking Sprinter (from behind)", worldobjects,
		spawnAttackingSprinter, playerObj), "SpawnZombie")

	setIcon(rootSub:addOption("Spawn Single Walker (8 ahead)", worldobjects,
		spawnHorde, playerObj, false, 1), "SpawnZombie")

	setIcon(rootSub:addOption("Spawn Single Sprinter (8 ahead)", worldobjects,
		spawnHorde, playerObj, true, 1), "SpawnZombie")

	setIcon(rootSub:addOption("Spawn Walker Horde (8 ahead)", worldobjects,
		spawnHorde, playerObj, false), "SpawnZombie")

	setIcon(rootSub:addOption("Spawn Sprinter Horde (8 ahead)", worldobjects,
		spawnHorde, playerObj, true), "SpawnZombie")

	setIcon(rootSub:addOption("Start Sprinter Takedown (on nearby zombie)", worldobjects,
		debugStartTakedown, playerObj, square), "SpawnZombie")

	setIcon(rootSub:addOption("Remove All Dummies", worldobjects, removeAllDummies, playerObj), "Remove")

end

Events.OnFillWorldObjectContextMenu.Add(onFillWorldObjectContextMenu)

local ORPHAN_GRACE_TICKS = 120
local _orphanTicks = {}

local function pinTaggedDummy(zombie)
	if not zombie then return end
	if not _dummiesTouched and not (isDebugEnabled and isDebugEnabled()) then return end

	if zombie:getModData() and zombie:getModData().zrAttackSprinter then return end

	local isDummyZombie = zombie:getVariableBoolean(DUMMY_TAG)
	local isDummyBandit = false
	local hasBanditVar = zombie:getVariableBoolean("Bandit")
	local brain = nil
	if hasBanditVar and BanditBrain and BanditBrain.Get then
		brain = BanditBrain.Get(zombie)
		if brain and brain.zrDummyBandit then isDummyBandit = true end
	end

	if hasBanditVar and not brain then
		local zid = zombie:getID()
		local seen = (_orphanTicks[zid] or 0) + 1
		if seen >= ORPHAN_GRACE_TICKS then
			_orphanTicks[zid] = nil
			pcall(function() zombie:setVariable("Bandit", false) end)
			pcall(function() zombie:setVariable("BanditPrimary", "") end)
			pcall(function() zombie:setVariable("BanditSecondary", "") end)
			DebugLog.log("[ZR Heal] cleared Bandit=true on brain-less zombie id=" .. tostring(zid)
				.. " (brain-less for " .. ORPHAN_GRACE_TICKS .. " ticks)")
		else
			_orphanTicks[zid] = seen
		end
	elseif hasBanditVar then
		local zid = zombie:getID()
		if _orphanTicks[zid] then _orphanTicks[zid] = nil end
	end

	if isDummyZombie or isDummyBandit then
		zombie:clearAggroList()
		zombie:setTarget(nil)
		zombie:setCanWalk(false)
	end

	if isDummyZombie and not isDummyBandit then
		pcall(function()
			zombie:setUseless(true)
			zombie:setNoTeeth(true)
			zombie:setCollidable(true)
			zombie:setInvincible(false)
		end)
	end

	if isDummyBandit then
		if not isDummyZombie then
			zombie:setVariable(DUMMY_TAG, true)
		end
		local md = zombie:getModData()
		md.zrDummyBandit = true
		md.zomborutDummy = true
		md.zrDummyBanditConfigured = true
		if Bandit and Bandit.IsHostile and Bandit.IsHostile(zombie) then
			Bandit.SetHostile(zombie, false)
		end
		if Bandit and Bandit.IsForceStationary and not Bandit.IsForceStationary(zombie) then
			Bandit.ForceStationary(zombie, true)
		end
	end
end
Events.OnZombieUpdate.Add(pinTaggedDummy)
