-- KiraUI-Plugin_Shroud.lua
-- Shroud of Concealment: count the 15 seconds down in /say, 15 to 1.
--
-- Shroud is a group stealth on a hard window. Everyone has to keep moving and
-- nobody can afford to guess how much is left, so the rogue who cast it says
-- the numbers out loud.
--
-- THE TRIGGER IS THE CAST, AND ONLY THE CAST
--
-- 114018 is the spell you press. 115834 is the aura it puts on people - a
-- different ID for a different thing, and watching the aura was the earlier
-- mistake here.
--
-- Casting is also the better signal on its own merits. Your own casts stay
-- readable in every kind of content, dungeons and keys included, while aura
-- data goes secret inside a key and would have failed exactly where it is
-- needed. And because only a rogue can cast 114018, matching the ID gates the
-- feature to rogues by itself - no class check that could read wrong.
--
-- ONE LINE PER SECOND
--
-- Fifteen messages. The client throttles addon chat, so if the tail goes
-- missing in practice the fix is fewer lines, not more retries.

local addonName, ns = ...
local _G = _G

local CreateFrame = CreateFrame
local issecretvalue = issecretvalue
local pairs, pcall, tostring = pairs, pcall, tostring

ns.hasShroud = true

-- The CAST, not the buff it applies.
local SHROUD_SPELL = 114018
local DURATION = 15

local function secret(v) return issecretvalue and issecretvalue(v) end

----------------------------------------------------------------------
-- Settings
----------------------------------------------------------------------
local DEFAULTS = {
	enable = false,
	spellID = SHROUD_SPELL,
	duration = DURATION,
	channel = 'SAY',
	dungeonOnly = true,   -- keeps a 15-line burst out of cities
}

-- Say and Yell float above your character, which is the whole point: nobody
-- has to be watching chat. Both are range-limited, Yell reaching further.
local CHANNELS = {
	{ value = 'SAY',           text = 'Say (above your head)' },
	{ value = 'YELL',          text = 'Yell (above your head, wider range)' },
	{ value = 'PARTY',         text = 'Party chat' },
	{ value = 'INSTANCE_CHAT', text = 'Instance chat' },
}
ns.SHChannels = CHANNELS

-- Migration: earlier builds watched the aura ID and never fired.
local function Migrate(cfg)
	if cfg.spellID == 115834 then cfg.spellID = SHROUD_SPELL end
end

local function GetCfg() return ns.ModuleCfg('shroud', DEFAULTS, Migrate) end
ns.SHGetCfg = GetCfg

----------------------------------------------------------------------
-- Debug
----------------------------------------------------------------------
local debugOn = false
local function Dbg(...)
	if not debugOn then return end
	print('|cffff7d0aKiraShroud dbg|r', ...)
end

----------------------------------------------------------------------
-- Where we are allowed to speak
----------------------------------------------------------------------
local function InDungeon()
	local f = _G.IsInInstance
	if not f then return false end
	local ok, inside, kind = pcall(f)
	if not ok then return false end
	if secret(inside) then return false end
	if not inside then return false end
	if secret(kind) then return true end
	return kind == 'party' or kind == 'scenario'
end

-- Returns false plus the REASON, so silence can always be explained.
local function MaySpeak()
	local cfg = GetCfg()
	if not cfg.enable then return false, 'the feature is not enabled' end
	-- Shroud is an out-of-combat tool and a countdown running through a pull
	-- is noise at the worst possible moment.
	if _G.InCombatLockdown and _G.InCombatLockdown() then return false, 'you are in combat' end
	if cfg.dungeonOnly and not InDungeon() then return false, 'you are not in a dungeon' end
	return true
end

----------------------------------------------------------------------
-- Sending
--
-- Both the namespaced call and the legacy global are tried. Which one an
-- addon may use has moved between patches, and preferring the current one
-- with a fallback costs nothing.
----------------------------------------------------------------------
local function Say(text)
	local chan = GetCfg().channel or 'SAY'
	local primary = (_G.C_ChatInfo and _G.C_ChatInfo.SendChatMessage) or _G.SendChatMessage
	if not primary then Dbg('no SendChatMessage available') return end
	local ok, err = pcall(primary, text, chan)
	if ok then return end
	Dbg('send failed on', chan, '-', tostring(err))
	local alt = _G.SendChatMessage
	if primary == alt then alt = _G.C_ChatInfo and _G.C_ChatInfo.SendChatMessage end
	if alt and alt ~= primary then
		local ok2, err2 = pcall(alt, text, chan)
		if not ok2 then Dbg('fallback also failed -', tostring(err2)) end
	end
end

----------------------------------------------------------------------
-- The countdown: 15, 14, 13 ... 1
----------------------------------------------------------------------
local ticker, remaining

local function Stop()
	if ticker then ticker:Cancel() ticker = nil end
	remaining = nil
end
ns.SHStop = Stop

local function Start(why)
	Dbg('trigger:', why or '?')
	if ticker then Dbg('  ignored - already counting') return end
	local ok, reason = MaySpeak()
	if not ok then Dbg('  BLOCKED -', reason) return end

	local n = GetCfg().duration or DURATION
	remaining = n

	Say(tostring(remaining))          -- 15 immediately
	remaining = remaining - 1

	ticker = _G.C_Timer.NewTicker(1, function()
		-- rechecked every tick, not just at the start: getting pulled
		-- mid-shroud is the normal way this ends and it should go quiet
		local okTick, whyTick = MaySpeak()
		if not okTick then Dbg('stopped mid-count -', whyTick) Stop() return end
		if not remaining or remaining < 1 then Stop() return end
		Say(tostring(remaining))      -- 14 ... 1
		remaining = remaining - 1
		if remaining < 1 then Stop() end
	end, n)
end
ns.SHStart = Start

----------------------------------------------------------------------
-- Trigger
----------------------------------------------------------------------
local evt = CreateFrame('Frame')
evt:RegisterUnitEvent('UNIT_SPELLCAST_SUCCEEDED', 'player')
evt:RegisterEvent('PLAYER_REGEN_DISABLED')

evt:SetScript('OnEvent', function(_, event, _, _, spellID)
	if event == 'PLAYER_REGEN_DISABLED' then Stop() return end
	if secret(spellID) then Dbg('own cast, spellID SECRET') return end
	if spellID == (GetCfg().spellID or SHROUD_SPELL) then Start('own cast') end
end)

----------------------------------------------------------------------
-- Diagnostic
----------------------------------------------------------------------
_G.SLASH_KIRASHROUD1 = '/kirashroud'
_G.SlashCmdList.KIRASHROUD = function(msg)
	local cfg = GetCfg()
	local function p(t) print('|cffff7d0aKiraShroud|r ' .. t) end
	msg = (msg or ''):lower():gsub('^%s+', ''):gsub('%s+$', '')

	if msg == 'debug' then
		debugOn = not debugOn
		p('debug ' .. (debugOn and 'ON - cast a shroud and watch this frame' or 'off'))
		return
	end

	-- A real send on the configured channel, ignoring the dungeon gate. Three
	-- numbers rather than fifteen: enough to prove the message reaches the
	-- channel and floats above your head, short enough to run anywhere.
	if msg == 'test' then
		local chan = cfg.channel or 'SAY'
		p('sending 3 real messages to ' .. chan .. ' - watch above your character, not this frame')
		local i = 3
		Say(tostring(i))
		_G.C_Timer.NewTicker(1, function()
			i = i - 1
			if i < 1 then return end
			Say(tostring(i))
		end, 2)
		return
	end

	local ok, reason = MaySpeak()
	p(('enabled: %s | spell %d | %ds | channel %s'):format(
		cfg.enable and 'yes' or 'NO', cfg.spellID or SHROUD_SPELL,
		cfg.duration or DURATION, cfg.channel or 'SAY'))
	p(('may speak right now: %s%s'):format(
		ok and '|cff00ff00yes|r' or '|cffff5555no|r',
		ok and '' or ('  - ' .. tostring(reason))))
	p(('dungeon-only %s | debug %s'):format(
		cfg.dungeonOnly and 'on' or 'off', debugOn and 'ON' or 'off'))
	p('|cffffd200/kirashroud test|r sends 3 real messages, |cffffd200/kirashroud debug|r traces the cast.')
end
