-- ============================================================================
-- Vamoose's Companion - ActivitySense
-- State machine detecting 13 player activities; routes to 3 reaction tiers.
-- Tier A: Collectors:Emit (quote pipeline)
-- Tier B: EventBus:Trigger VC_ACTIVITY_CHANGED (lighting)
-- Tier C: Mood:ApplySignal only
-- ============================================================================

VC = VC or {}
VC.ActivitySense = {}

local GetTime   = GetTime
local INTENSITY = VC.Constants.INTENSITY

-- ============================================================================
-- HEARTHSTONE SPELL IDS
-- ============================================================================

local HEARTH_SPELLS = {
    [8690]   = "Hearthstone",
    [222695] = "Dalaran Hearthstone",
    [171253] = "Garrison Hearthstone",
}

-- ============================================================================
-- STATE TABLE
-- ============================================================================

local state = {
    eating       = { active = false, lastTransition = 0 },
    drinking     = { active = false, lastTransition = 0 },
    hearthing    = { active = false, lastTransition = 0 },
    onTaxi       = { active = false, lastTransition = 0 },
    resting      = { active = false, lastTransition = 0 },
    swimming     = { active = false, lastTransition = 0 },
    lowHealth    = { active = false, lastTransition = 0 },
    mounted      = { active = false, lastTransition = 0 },
    flying       = { active = false, lastTransition = 0 },
    dead         = { active = false, lastTransition = 0 },
}

-- Debounce timestamps
local lastAuraCheck = 0

-- Idle timer handle
local idleTimer       = nil

-- Farming loot timestamps buffer
local lootTimestamps  = {}

-- Zone tracking
local currentZone     = nil
local zoneEnteredTime = 0


-- ============================================================================
-- HELPERS
-- ============================================================================

local function transition(activity, active)
    local s = state[activity]
    if not s or s.active == active then return false end
    s.active = active
    s.lastTransition = GetTime()
    return true
end

local function emitQuote(eventType, intensity, context, skipCooldown)
    if VC.Collectors and VC.Collectors.Emit then
        VC.Collectors:Emit({
            eventType    = eventType,
            intensity    = intensity,
            context      = context or {},
            timestamp    = GetTime(),
            source       = "VC_ActivitySense",
            skipCooldown = skipCooldown,
        })
    end
end

local function triggerActivity(activity, active)
    if VC.EventBus and VC.EventBus.Trigger then
        VC.EventBus:Trigger("VC_ACTIVITY_CHANGED", { activity = activity, active = active })
    end
end

local function applyMood(signal)
    if VC.Mood and VC.Mood.ApplySignal then
        VC.Mood:ApplySignal(signal)
    end
end

-- ============================================================================
-- TIER A: EATING / DRINKING (UNIT_AURA)
-- ============================================================================

local function handleAura()
    local now = GetTime()
    if now - lastAuraCheck < 0.3 then return end
    lastAuraCheck = now

    -- Detect food/drink by scanning buff names (works out of combat, pcall for safety)
    local isEating, isDrinking = false, false
    local ok = pcall(function()
        for i = 1, 40 do
            local aura = C_UnitAuras.GetAuraDataByIndex("player", i, "HELPFUL")
            if not aura then break end
            local n = aura.name
            if n == "Food" or n == "Food & Drink" then isEating = true end
            if n == "Drink" or n == "Food & Drink" then isDrinking = true end
            if isEating and isDrinking then break end
        end
    end)
    if not ok then return end -- taint in combat, skip silently

    -- Eating
    if transition("eating", isEating) and isEating then
        emitQuote("eating", INTENSITY.MEDIUM, { alsoDrinking = isDrinking }, true)
        applyMood("EATING")
    end

    -- Drinking: only fire if food is NOT active (prevent double-fire)
    if transition("drinking", isDrinking) and isDrinking and not isEating then
        emitQuote("drinking", INTENSITY.MEDIUM, {}, true)
        applyMood("DRINKING")
    end

    -- Mounted state check on aura update
    local isMounted = IsMounted()
    if transition("mounted", isMounted) then
        triggerActivity("mounted", isMounted)
    end
end

-- ============================================================================
-- TIER A: HEARTHING (UNIT_SPELLCAST_*)
-- ============================================================================

local function handleSpellCastStart(unitTarget, castGUID, spellID)
    if unitTarget ~= "player" then return end
    if HEARTH_SPELLS[spellID] then
        if transition("hearthing", true) then
            emitQuote("hearthing", INTENSITY.LOW, { hearthName = HEARTH_SPELLS[spellID] })
            applyMood("HEARTHING")
        end
    end
end

local function handleSpellCastEnd(unitTarget, castGUID, spellID)
    if unitTarget ~= "player" then return end
    if HEARTH_SPELLS[spellID] then
        transition("hearthing", false)
    end
end

-- ============================================================================
-- TIER A: FLIGHT PATH (PLAYER_STARTED/STOPPED_MOVING)
-- ============================================================================

local function checkTaxi(started)
    if started then
        local onTaxi = UnitOnTaxi("player")
        if transition("onTaxi", onTaxi) and onTaxi then
            emitQuote("flightpath", INTENSITY.LOW, {})
            applyMood("HEARTHING") -- mild positive for travel
        end
    else
        -- stopped moving: check if taxi ended
        local onTaxi = UnitOnTaxi("player")
        transition("onTaxi", onTaxi)
    end
end

-- ============================================================================
-- TIER A: REST ZONE (PLAYER_UPDATE_RESTING)
-- ============================================================================

local function handleResting()
    local resting = IsResting()
    if transition("resting", resting) then
        if resting then
            emitQuote("restzone", INTENSITY.LOW, {})
            applyMood("RESTING")
            triggerActivity("resting", true)
        else
            triggerActivity("resting", false)
        end
    end
end

-- ============================================================================
-- TIER A: RARE SPAWN (VIGNETTE_MINIMAP_UPDATED)
-- ============================================================================

local function handleVignette(vignetteGUID, onMinimap)
    if not onMinimap then return end
    if not C_VignetteInfo or not C_VignetteInfo.GetVignetteInfo then return end
    local info = C_VignetteInfo.GetVignetteInfo(vignetteGUID)
    if not info then return end
    local atlas = info.atlasName or ""
    if atlas:find("[Vv]ignette[Kk]ill") or atlas:find("[Vv]ignette[Ee]lite") then
        emitQuote("rarespawn", INTENSITY.HIGH, { rareName = info.name }, true)
        applyMood("RARE_SPOTTED")
    end
end

-- ============================================================================
-- TIER A: LONG IDLE (PLAYER_STARTED/STOPPED_MOVING + timer)
-- ============================================================================

local function cancelIdleTimer()
    if idleTimer then
        idleTimer:Cancel()
        idleTimer = nil
    end
end

local function startIdleTimer()
    cancelIdleTimer()
    idleTimer = C_Timer.NewTimer(90, function()
        idleTimer = nil

        -- Midnight secret-value context guard (per Reference/MIDNIGHT_SECRET_VALUES.md).
        -- UnitIsAFK, UnitIsDead and similar return "secret booleans" in M+,
        -- encounters, PvP, and instanced combat. Any boolean test on such a
        -- value taints the addon, and pcall isn't always sufficient (the
        -- taint can fire on subsequent reads of already-tainted values).
        -- Cheapest reliable fix: skip the entire idle path in secret contexts.
        -- Same pattern VC_Portrait uses for its intent evaluator.
        local inSecretContext = false
        if C_ChallengeMode and C_ChallengeMode.IsChallengeModeActive
           and C_ChallengeMode.IsChallengeModeActive() then inSecretContext = true end
        if not inSecretContext and IsEncounterInProgress
           and IsEncounterInProgress() then inSecretContext = true end
        if not inSecretContext then
            local _, instanceType = IsInInstance()
            if instanceType == "pvp" or instanceType == "arena" then
                inSecretContext = true
            elseif InCombatLockdown() and (instanceType == "party" or instanceType == "raid") then
                inSecretContext = true
            end
        end
        if inSecretContext then return end

        if UnitIsAFK("player") then return end -- don't talk to an empty chair
        emitQuote("idle", INTENSITY.LOW, { idleSeconds = 90 })
        applyMood("IDLE_LONG")
    end)
end

-- ============================================================================
-- TIER B: DELAYED STATE CHECK (shared pattern for swimming/flying)
-- ============================================================================

local function delayedStateCheck(timerRef, activity, pollFn, mood)
    if timerRef[1] then timerRef[1]:Cancel() end
    timerRef[1] = C_Timer.NewTimer(0.5, function()
        timerRef[1] = nil
        local active = pollFn()
        if transition(activity, active) then
            triggerActivity(activity, active)
            if active and mood then applyMood(mood) end
        end
    end)
end

local swimTimer, flyTimer = {}, {}
local function checkSwimming() delayedStateCheck(swimTimer, "swimming", IsSwimming, "SWIMMING") end
local function checkFlying()   delayedStateCheck(flyTimer,  "flying",   IsFlying,   nil)        end

-- ============================================================================
-- TIER B: LOW HEALTH (1s fast poll, avoids UNIT_HEALTH taint)
-- ============================================================================

local healthTicker = nil

local function startHealthPoll()
    if healthTicker then healthTicker:Cancel() end
    healthTicker = C_Timer.NewTicker(1.0, function()
        local ok, pct = pcall(function()
            local max = UnitHealthMax("player")
            if not max or max == 0 then return nil end
            return UnitHealth("player") / max
        end)
        if not ok or not pct then return end

        if pct < 0.25 then
            if transition("lowHealth", true) then triggerActivity("lowhealth", true) end
        elseif pct > 0.35 then
            if transition("lowHealth", false) then triggerActivity("lowhealth", false) end
        end
    end)
end

-- ============================================================================
-- TIER C: EXTENDED FARMING (CHAT_MSG_LOOT)
-- ============================================================================

local FARM_WINDOW = 1800  -- 30 minutes
local FARM_THRESHOLD = 15

local function handleLoot()
    local now = GetTime()
    lootTimestamps[#lootTimestamps + 1] = now

    -- Trim entries older than 30 minutes (in-place from front)
    local cutoff = now - FARM_WINDOW
    while #lootTimestamps > 0 and lootTimestamps[1] < cutoff do
        table.remove(lootTimestamps, 1)
    end

    if #lootTimestamps >= FARM_THRESHOLD then
        applyMood("FARMING_LONG")
        lootTimestamps = {} -- reset after signal
    end
end

-- ============================================================================
-- TIER C: SAME ZONE LONG TIME (ZONE_CHANGED_NEW_AREA)
-- ============================================================================

local ZONE_LONG_THRESHOLD = 1800 -- 30 minutes

local function handleZoneChanged()
    local zone = GetZoneText()
    local now  = GetTime()

    if zone ~= currentZone then
        -- Check if we stayed in the previous zone long enough
        if currentZone and (now - zoneEnteredTime) >= ZONE_LONG_THRESHOLD then
            applyMood("ZONE_LONG")
        end
        currentZone    = zone
        zoneEnteredTime = now
    end
end

-- ============================================================================
-- MOVEMENT HANDLER (shared across multiple activities)
-- ============================================================================

local function handleMovement(started)
    if started then
        cancelIdleTimer()
        checkTaxi(true)
        checkSwimming()
        checkFlying()
    else
        checkTaxi(false)
        checkSwimming()
        checkFlying()
        startIdleTimer()
    end
end

-- ============================================================================
-- INITIALIZATION
-- ============================================================================

function VC.ActivitySense:Initialize()
    self.frame = CreateFrame("Frame")

    self.frame:RegisterEvent("UNIT_AURA")
    self.frame:RegisterEvent("UNIT_SPELLCAST_START")
    self.frame:RegisterEvent("UNIT_SPELLCAST_STOP")
    self.frame:RegisterEvent("UNIT_SPELLCAST_INTERRUPTED")
    self.frame:RegisterEvent("UNIT_SPELLCAST_FAILED")
    self.frame:RegisterEvent("PLAYER_UPDATE_RESTING")
    self.frame:RegisterEvent("VIGNETTE_MINIMAP_UPDATED")
    self.frame:RegisterEvent("PLAYER_STARTED_MOVING")
    self.frame:RegisterEvent("PLAYER_STOPPED_MOVING")
    self.frame:RegisterEvent("CHAT_MSG_LOOT")
    self.frame:RegisterEvent("ZONE_CHANGED_NEW_AREA")
    self.frame:RegisterEvent("PLAYER_DEAD")
    self.frame:RegisterEvent("PLAYER_ALIVE")
    self.frame:RegisterEvent("PLAYER_UNGHOST")

    -- Seed zone tracking, idle timer, health poll
    currentZone    = GetZoneText()
    zoneEnteredTime = GetTime()
    startIdleTimer()
    startHealthPoll()

    -- Bootstrap current state (handles /reload while swimming, dead, etc.)
    C_Timer.After(1, function()
        -- Skip the dead/ghost bootstrap in secret-value contexts (M+, encounters,
        -- PvP, instance combat) -- UnitIsDead/UnitIsGhost taint via boolean test.
        local inSecretContext = false
        if C_ChallengeMode and C_ChallengeMode.IsChallengeModeActive
           and C_ChallengeMode.IsChallengeModeActive() then inSecretContext = true end
        if not inSecretContext and IsEncounterInProgress
           and IsEncounterInProgress() then inSecretContext = true end
        if not inSecretContext then
            local _, instanceType = IsInInstance()
            if instanceType == "pvp" or instanceType == "arena" then
                inSecretContext = true
            elseif InCombatLockdown() and (instanceType == "party" or instanceType == "raid") then
                inSecretContext = true
            end
        end
        if not inSecretContext and (UnitIsDead("player") or UnitIsGhost("player")) then
            if transition("dead", true) then triggerActivity("dead", true) end
        end
        if IsSwimming() then
            if transition("swimming", true) then triggerActivity("swimming", true) end
        end
        if IsResting() then
            if transition("resting", true) then triggerActivity("resting", true) end
        end
        if IsMounted() then
            if transition("mounted", true) then triggerActivity("mounted", true) end
        end
        if IsFlying() then
            if transition("flying", true) then triggerActivity("flying", true) end
        end
    end)

    self.frame:SetScript("OnEvent", function(_, event, ...)
        if event == "UNIT_AURA" then
            local unit = ...
            if unit == "player" then handleAura() end

        elseif event == "UNIT_SPELLCAST_START" then
            handleSpellCastStart(...)

        elseif event == "UNIT_SPELLCAST_STOP"
            or event == "UNIT_SPELLCAST_INTERRUPTED"
            or event == "UNIT_SPELLCAST_FAILED" then
            handleSpellCastEnd(...)

        elseif event == "PLAYER_UPDATE_RESTING" then
            handleResting()

        elseif event == "VIGNETTE_MINIMAP_UPDATED" then
            handleVignette(...)

        elseif event == "PLAYER_STARTED_MOVING" then
            handleMovement(true)

        elseif event == "PLAYER_STOPPED_MOVING" then
            handleMovement(false)

        elseif event == "CHAT_MSG_LOOT" then
            handleLoot()

        elseif event == "ZONE_CHANGED_NEW_AREA" then
            handleZoneChanged()

        elseif event == "PLAYER_DEAD" then
            if transition("dead", true) then
                triggerActivity("dead", true)
            end

        elseif event == "PLAYER_ALIVE" or event == "PLAYER_UNGHOST" then
            if transition("dead", false) then
                triggerActivity("dead", false)
            end
        end
    end)
end
