-- PaladinHolyPower - logic/hp_log.lua
-- Event-driven HP log with low CPU/memory.

local PHP = _G.PaladinHolyPower
if not PHP then return end

-- ========= Upvalues =========
local GetTime                      = _G.GetTime
local UnitPower                    = _G.UnitPower
local UnitPowerMax                 = _G.UnitPowerMax
local UnitGUID                     = _G.UnitGUID
local UnitExists                   = _G.UnitExists
local UnitName                     = _G.UnitName
local UnitClassification           = _G.UnitClassification
local UnitAffectingCombat          = _G.UnitAffectingCombat
local InCombatLockdown             = _G.InCombatLockdown
local CombatLogGetCurrentEventInfo = _G.CombatLogGetCurrentEventInfo
local GetSpellInfo                 = _G.GetSpellInfo
local C_Spell                      = _G.C_Spell
local C_Timer                      = _G.C_Timer
local time                         = _G.time

-- ========= Model =========
PHP.HPLog = PHP.HPLog or { fights = {}, cur = nil, active = nil, max = 5 }

PHP.HPLogListeners = PHP.HPLogListeners or {}
function PHP.HPLog_Subscribe(fn)
  if type(fn) == "function" then table.insert(PHP.HPLogListeners, fn) end
end

local function FireTotalsChanged()
  local cf = PHP.HPLog.fights[PHP.HPLog.cur or 1]
  local g  = (cf and cf.gen) or 0
  local s  = (cf and cf.spend) or 0
  local w  = (cf and cf.wasted) or 0
  for i = 1, #PHP.HPLogListeners do
    pcall(PHP.HPLogListeners[i], g, s, w)
  end
end

-- ========= Helpers =========
local HOLY = (Enum and Enum.PowerType and Enum.PowerType.HolyPower) or 9
local function T() return (GetTime and GetTime()) or 0 end
local function HP() return (UnitPower and UnitPower("player", HOLY)) or 0 end
local function HPMAX()
  local m = UnitPowerMax and UnitPowerMax("player", HOLY)
  if m and m > 0 then return m end
  return 5
end
local function InCombat()
  if type(InCombatLockdown) == "function" then return InCombatLockdown() end
  if UnitAffectingCombat then return UnitAffectingCombat("player") or false end
  return false
end

-- Spell name cache
local SPELL_NAME = {}
local function SN(id)
  local n = SPELL_NAME[id]
  if n then return n end
  local name = GetSpellInfo and GetSpellInfo(id)
  if type(name) == "string" and name ~= "" then
    SPELL_NAME[id] = name; return name
  end
  if C_Spell and C_Spell.GetSpellInfo then
    local si = C_Spell.GetSpellInfo(id)
    if si and si.name and si.name ~= "" then
      SPELL_NAME[id] = si.name; return si.name
    end
  end
  return "Spell " .. tostring(id)
end

-- Colors/links
local COL_SPELL = "F2E699"
local COL_NUM   = "FFFF00"
local COL_TAL   = "66BBFF"
local COL_TEXT  = "b3b3b3"
local COL_WASTE = "E8615E"

local function LinkSpellColored(id, txt, hex)
  return "|cff"..hex.."|Hspell:"..tostring(id).."|h"..tostring(txt).."|h|r"
end
local function LinkCustomColored(kind, payload, shown, hex)
  return "|cff"..hex.."|H"..tostring(kind)..":"..tostring(payload).."|h"..tostring(shown).."|h|r"
end

-- Proc auras
local DP_ID   = 408458
local EP_NEW  = 406975
local EP_OLD  = 286393
local DS_ID   = 53385
local dpUp, dpLastOff = false, 0
local epUp, epLastOff = false, 0
local AURA_GRACE = 0.25
local function DPActive() local t = T(); return dpUp or (t - dpLastOff) <= AURA_GRACE end
local function EPActive() local t = T(); return epUp or (t - epLastOff) <= AURA_GRACE end

-- Light's Deliverance tag
local LD_NAME = "Light's Deliverance"
local LD_ID   = 427441

-- ========= HP delta ring buffer (1s) =========
local DELTA_MAX = 64
local dt_buf_t, dt_buf_d = {}, {}
local dt_head, dt_count  = 1, 0
local samplerHP          = HP()

local function _ring_push(tnow, d)
  local tail = ((dt_head + dt_count - 1) % DELTA_MAX) + 1
  dt_buf_t[tail] = tnow
  dt_buf_d[tail] = d
  if dt_count < DELTA_MAX then
    dt_count = dt_count + 1
  else
    dt_head = (dt_head % DELTA_MAX) + 1
  end
  local cut = tnow - 1.0
  while dt_count > 0 do
    local ot = dt_buf_t[dt_head]
    if not ot or ot >= cut then break end
    dt_buf_t[dt_head] = nil
    dt_buf_d[dt_head] = nil
    dt_head = (dt_head % DELTA_MAX) + 1
    dt_count = dt_count - 1
  end
end

local function RecordHPDelta(newHP)
  local old = samplerHP or newHP
  local d = newHP - old
  if d ~= 0 then _ring_push(T(), d) end
  samplerHP = newHP
end

local function HPDropSince(t0, window)
  local endt = t0 + window
  local drop = 0
  for k = 0, dt_count - 1 do
    local idx = ((dt_head + dt_count - 1 - k - 1) % DELTA_MAX) + 1
    local tt  = dt_buf_t[idx]; if not tt then break end
    if tt < t0 then break end
    if tt <= endt then
      local dd = dt_buf_d[idx]
      if dd and dd < 0 then drop = drop + (-dd) end
    end
  end
  return drop
end

-- ========= Fights =========
local function Boss()
  for i = 1, 5 do
    local u = "boss"..i
    if UnitExists and UnitExists(u) then
      local n = UnitName and UnitName(u)
      if n and n ~= "" then return n, true end
    end
  end
  if UnitExists and UnitExists("target")
     and UnitClassification and UnitClassification("target") == "worldboss" then
    local n = UnitName and UnitName("target")
    if n and n ~= "" then return n, true end
  end
  return "Trash", false
end

local function NewFight()
  local name, isBoss = Boss()
  local f = {
    started      = T(),
    ended        = nil,
    startedLocal = time(),
    endedLocal   = nil,
    lines        = {},
    gen          = 0,
    spend        = 0,
    wasted       = 0,
    cap          = HPMAX(),
    bossName     = name,
    isBoss       = isBoss,
  }
  table.insert(PHP.HPLog.fights, 1, f)
  while #PHP.HPLog.fights > (PHP.HPLog.max or 5) do table.remove(PHP.HPLog.fights) end
  PHP.HPLog.active = 1
  if PHP.HPLog.cur == nil then PHP.HPLog.cur = 1 end
  FireTotalsChanged()
  return f
end

local function CurFight()
  return PHP.HPLog.fights[PHP.HPLog.cur or 1]
end

local function EnsureActiveFight()
  local idx = PHP.HPLog.active or 1
  local f = PHP.HPLog.fights[idx]
  if f then return f end
  return NewFight()
end

-- UI refresh helper (throttled)
local lastRefresh = 0
local function SafeRefresh()
  if not PHP.HPLog_Refresh then return end
  local w = PHP.HPLogWindow
  if not (w and w.IsShown and w:IsShown()) then return end
  local now = T()
  if (now - lastRefresh) >= 0.12 then
    pcall(PHP.HPLog_Refresh)
    lastRefresh = now
  end
end

function PHP.HPLog_GetTotals()
  local cf = CurFight(); if not cf then return 0,0,0 end
  return (cf.gen or 0), (cf.spend or 0), (cf.wasted or 0)
end

function PHP.HPLog_GetHeader()
  local cf = CurFight(); if not cf then return "Trash", "0:00" end
  local e = (cf.ended or T()) - (cf.started or T())
  local m = math.max(0, math.floor(e + 0.5))
  local label = (cf.bossName ~= "" and cf.bossName) or "Trash"
  return label, string.format("%d:%02d", math.floor(m/60), m%60)
end

-- ========= Spenders =========
local TEMPLARS_VERDICT        = 85256
local FINAL_VERDICT           = 383328
local DIVINE_STORM            = 53385
local WORD_OF_GLORY           = 85673
local SHIELD_OF_THE_RIGHTEOUS = 53600
local LIGHT_OF_DAWN           = 85222
local RADIANT_DECREE          = 385438
local JUSTICARS_VENGEANCE     = 215661
local HAMMER_OF_LIGHT         = 427453
local DIVINE_HAMMER           = 198034

local BASE = PHP.HolyPowerSpenders or {
  [TEMPLARS_VERDICT]        = 3,
  [FINAL_VERDICT]           = 3,
  [DIVINE_STORM]            = 3,
  [WORD_OF_GLORY]           = 3,
  [SHIELD_OF_THE_RIGHTEOUS] = 3,
  [LIGHT_OF_DAWN]           = 3,
  [RADIANT_DECREE]          = 3,
  [JUSTICARS_VENGEANCE]     = 5,
  [HAMMER_OF_LIGHT]         = 5,
  [DIVINE_HAMMER]           = 3,
}
local function IsSpender(id) return BASE[id] ~= nil end
local function Baseline(id)  return BASE[id] end

local function BuildLine(spellID, spent, reason)
  local spellPart = LinkSpellColored(spellID, SN(spellID), COL_SPELL)
  local spentPart = "|cff"..COL_TEXT.." Spent |r"
  local numPart   = LinkCustomColored("phpower", "spend:"..tostring(spent), tostring(spent), COL_NUM)
  local hpWord    = "|cff"..COL_TEXT.." Holy Power|r"
  local tail      = ""
  if spent == 0 and reason and reason ~= "" then
    local rid
    if     reason == "Divine Purpose" then rid = DP_ID
    elseif reason == "Empyrean Power" then rid = EP_NEW
    elseif reason == LD_NAME          then rid = LD_ID end
    if rid then
      tail = "|cff"..COL_TEXT.." (|r"..LinkSpellColored(rid, reason, COL_TAL).."|cff"..COL_TEXT..")|r"
    else
      tail = "|cff"..COL_TEXT.." ("..reason..")|r"
    end
  end
  return spellPart .. spentPart .. numPart .. hpWord .. tail
end

-- ========= Pending spends =========
local pending = {}
local seq     = 0
local PRE_WINDOW     = 0.45
local POST_WINDOW    = 0.45
local FINALIZE_DELAY = 0.45

local function MakeKey(castGUID, id)
  if castGUID and castGUID ~= "" then return castGUID end
  seq = (seq or 0) + 1
  return tostring(id) .. ":" .. tostring(seq)
end

local function FinalizePending(key)
  local p = pending[key]; if not p or p.done then return end
  p.done = true
  pending[key] = nil

  local drop = HPDropSince(p.t0, PRE_WINDOW + POST_WINDOW)
  local spent, reason
  if drop >= 1 then
    spent = p.base or Baseline(p.id) or 0
  else
    spent = 0
    reason = p.reasonPred or (DPActive() and "Divine Purpose") or (p.id == DIVINE_STORM and EPActive() and "Empyrean Power") or nil
  end
  if p.id == HAMMER_OF_LIGHT and spent == 0 then reason = LD_NAME end

  local cf = EnsureActiveFight()
  cf.spend = (cf.spend or 0) + spent
  cf.lines[#cf.lines + 1] = BuildLine(p.id, spent or 0, reason)

  SafeRefresh()
  FireTotalsChanged()
end

local function EnqueueFinalize(key, delay)
  if not (C_Timer and C_Timer.After) then
    FinalizePending(key)
    return
  end
  C_Timer.After(delay or FINALIZE_DELAY, function() FinalizePending(key) end)
end

-- ========= Events =========
local f = CreateFrame("Frame")
f:RegisterEvent("PLAYER_LOGIN")
f:RegisterEvent("PLAYER_REGEN_DISABLED")
f:RegisterEvent("PLAYER_REGEN_ENABLED")
f:RegisterEvent("ENCOUNTER_START")
f:RegisterEvent("ENCOUNTER_END")
f:RegisterUnitEvent("UNIT_SPELLCAST_SUCCEEDED", "player")
f:RegisterUnitEvent("UNIT_POWER_UPDATE", "player")
f:RegisterEvent("COMBAT_LOG_EVENT_UNFILTERED")

local playerGUID = nil

f:SetScript("OnEvent", function(_, ev, ...)
  if ev == "PLAYER_LOGIN" then
    playerGUID = UnitGUID and UnitGUID("player") or nil
    samplerHP  = HP()
    return

  elseif ev == "PLAYER_REGEN_DISABLED" then
    -- If a pre-pull event already created a live fight, reuse it.
    local top = PHP.HPLog.fights[1]
    if not top or top.ended then
      NewFight()          -- no open fight -> start now
    else
      PHP.HPLog.active = 1 -- continue logging into the existing pre-pull fight
    end
    return

  elseif ev == "PLAYER_REGEN_ENABLED" then
    local af = PHP.HPLog.fights[PHP.HPLog.active or 1]
    if af and not af.ended then af.ended = T(); af.endedLocal = time() end
    for key in pairs(pending) do FinalizePending(key) end
    SafeRefresh()
    FireTotalsChanged()
    return

  elseif ev == "ENCOUNTER_START" then
    local _, name = ...
    if name and name ~= "" then
      local af = PHP.HPLog.fights[PHP.HPLog.active or 1]
      if af then af.bossName, af.isBoss = name, true end
      SafeRefresh()
    end
    return

  elseif ev == "ENCOUNTER_END" then
    SafeRefresh()
    FireTotalsChanged()
    return

  elseif ev == "UNIT_POWER_UPDATE" then
    local unit, powerType = ...
    if unit == "player" and (powerType == "HOLY_POWER" or powerType == "9" or powerType == nil) then
      RecordHPDelta(HP())
    end
    return

  elseif ev == "COMBAT_LOG_EVENT_UNFILTERED" then
    local _, sub, _, srcGUID, _, _, _, dstGUID, _, _, _, spellId, name, _, amt, over, ptype =
      CombatLogGetCurrentEventInfo()

    if dstGUID == playerGUID then
      if sub == "SPELL_AURA_APPLIED" or sub == "SPELL_AURA_REFRESH" then
        if spellId == DP_ID then dpUp = true
        elseif spellId == EP_NEW or spellId == EP_OLD then epUp = true end
      elseif sub == "SPELL_AURA_REMOVED" or sub == "SPELL_AURA_REMOVED_DOSE" then
        if spellId == DP_ID then dpUp = false; dpLastOff = T()
        elseif spellId == EP_NEW or spellId == EP_OLD then epUp = false; epLastOff = T() end
      end
    end

    if (sub == "SPELL_ENERGIZE" or sub == "SPELL_PERIODIC_ENERGIZE") and srcGUID == playerGUID then
      local isHP = (ptype == HOLY or ptype == 9 or name == "Holy Power")
      if isHP then
        local add = amt or 0
        if PHP.GetHPGeneratedFromSpell then
          local forced = PHP.GetHPGeneratedFromSpell(spellId, add)
          if forced ~= nil then add = forced end
        end
        if add > 0 then
          local cf = EnsureActiveFight()
          cf.gen = cf.gen + add
          local curHP = HP()
          local room  = math.max(0, (cf.cap or HPMAX()) - curHP)
          local waste = math.max(0, add - room)
          cf.wasted   = (cf.wasted or 0) + waste

          local line = LinkSpellColored(spellId, name or SN(spellId), COL_SPELL)
                     .. "|cff"..COL_TEXT.." Generated |r"
                     .. LinkCustomColored("phpower", "gen:"..tostring(add), tostring(add), COL_NUM)
                     .. "|cff"..COL_TEXT.." Holy Power|r"
          if waste > 0 then
            line = line .. "|cff"..COL_TEXT.." (|r"
                 .. LinkCustomColored("phpower", "wasted", tostring(waste), COL_WASTE)
                 .. "|cff"..COL_TEXT.." wasted)|r"
          end
          cf.lines[#cf.lines + 1] = line

          SafeRefresh()
          FireTotalsChanged()
        end
      end
    end
    return

  elseif ev == "UNIT_SPELLCAST_SUCCEEDED" then
    local _, castGUID, spellID = ...
    if not spellID or not IsSpender(spellID) then return end
    local key = MakeKey(castGUID, spellID)
    if pending[key] and not pending[key].done then return end

    local reasonPred = nil
    if DPActive() then
      reasonPred = "Divine Purpose"
    elseif spellID == DIVINE_STORM and EPActive() then
      reasonPred = "Empyrean Power"
    end

    pending[key] = {
      id = spellID,
      base = Baseline(spellID) or 0,
      t0 = T() - 0.45,
      enq = T(),
      reasonPred = reasonPred,
      done = false,
    }

    EnqueueFinalize(key, 0.45)
    return
  end
end)