--[[
  TimeLogger — login/logout events, session export, prune, and temp_logout heartbeat for crash recovery.
  Commands: /timelogger, /tlog
]]

local ADDON_NAME = ...

local HEARTBEAT_SEC = 300 -- 5 minutes

local db
local pendingPruneDays
local tempLogoutTicker
local lastPlayedRequestUnix

--- Centralized time collection using server time for timezone-independent timestamps
local function GetUnix()
  if C_DateAndTime and C_DateAndTime.GetServerTimeLocal then
    return C_DateAndTime.GetServerTimeLocal()
  end
  return time()
end

local function EnsureDB()
  if type(TimeLoggerDB) ~= "table" then
    TimeLoggerDB = {}
  end
  if type(TimeLoggerDB.events) ~= "table" then
    TimeLoggerDB.events = {}
  end
  if type(TimeLoggerDB.played_totals) ~= "table" then
    TimeLoggerDB.played_totals = {}
  end
  db = TimeLoggerDB
end

--- ISO 8601 instant in UTC (Z suffix); independent of player timezone.
local function UtcIso()
  return date("!%Y-%m-%dT%H:%M:%SZ")
end

local function CsvEscape(s)
  s = tostring(s or "")
  if s:find('["\r\n,]') then
    s = '"' .. s:gsub('"', '""') .. '"'
  end
  return s
end

local function JsonEscape(s)
  s = tostring(s or "")
  return s:gsub("\\", "\\\\"):gsub('"', '\\"'):gsub("\r", "\\r"):gsub("\n", "\\n")
end

--- Last-known-alive snapshot (same shape as an event). Updated every HEARTBEAT_SEC while in-game.
--- Used only when the last stored event is login and we need a synthetic logout after crash / missing PLAYER_LOGOUT.
local function UpdateTempLogout()
  EnsureDB()
  local char = UnitName("player") or ""
  local realm = GetRealmName() or ""
  db.temp_logout = {
    unix = GetUnix(),
    utc = UtcIso(),
    event = "logout",
    character = char,
    realm = realm,
  }
end

local function StopTempLogoutTicker()
  if tempLogoutTicker then
    tempLogoutTicker:Cancel()
    tempLogoutTicker = nil
  end
end

local function StartTempLogoutTicker()
  StopTempLogoutTicker()
  tempLogoutTicker = C_Timer.NewTicker(HEARTBEAT_SEC, UpdateTempLogout)
end

--- If the last row is login (no logout was saved), insert a logout using the last heartbeat, if valid.
local function RecoverOrphanLogout()
  EnsureDB()
  local ev = db.events
  local n = #ev
  if n == 0 then
    return
  end
  local last = ev[n]
  if (last.event or "") ~= "login" then
    return
  end
  local t = db.temp_logout
  if type(t) ~= "table" or not t.unix then
    return
  end
  local now = GetUnix()
  if t.unix < (last.unix or 0) then
    return
  end
  if t.unix >= now then
    return
  end
  ev[n + 1] = {
    unix = t.unix,
    utc = t.utc or UtcIso(),
    event = "logout",
    character = t.character or last.character,
    realm = t.realm or last.realm,
    recovery = true,
  }
end

local function Record(kind)
  EnsureDB()
  local char = UnitName("player") or ""
  local realm = GetRealmName() or ""
  db.events[#db.events + 1] = {
    unix = GetUnix(),
    utc = UtcIso(),
    event = kind,
    character = char,
    realm = realm,
  }
end

local function CurrentCharKey()
  return (GetRealmName() or "") .. "|" .. (UnitName("player") or "")
end

-- OPTIMIZED: Linear Session Builder
local function BuildSessions()
    local sessions = {}
    local openSessions = {}

    for i, e in ipairs(db.events) do
        local key = e.character .. "-" .. e.realm
        
        if e.event == "login" then
            if openSessions[key] then
                local prevIdx = openSessions[key]
                sessions[prevIdx].status = "no_logout"
            end
            
            table.insert(sessions, {
                start_unix = e.unix,
                start_utc = e.utc,
                character = e.character,
                realm = e.realm,
                status = "open",
                end_unix = 0,
                end_utc = ""
            })
            openSessions[key] = #sessions
            
        elseif e.event == "logout" then
            if openSessions[key] then
                local sIdx = openSessions[key]
                local s = sessions[sIdx]
                -- FIXED: Removed trailing commas that caused the syntax error
                s.end_unix = e.unix
                s.end_utc = e.utc
                s.duration_sec = e.unix - s.start_unix
                s.status = e.recovery and "recovered" or "closed"
                openSessions[key] = nil
            end
        end
    end
    return sessions
end

-- Export Helpers
local function BuildCSV()
    local lines = {"unix,utc_iso,event,character,realm,recovery"}
    for _, e in ipairs(db.events) do
        table.insert(lines, string.format("%d,%s,%s,%s,%s,%d",
            e.unix or 0,
            CsvEscape(e.utc),
            CsvEscape(e.event),
            CsvEscape(e.character),
            CsvEscape(e.realm),
            e.recovery and 1 or 0))
    end
    return table.concat(lines, "\n")
end

local function BuildSessionsCSV()
    local sessions = BuildSessions()
    local lines = {"session_id,start_unix,start_utc,end_unix,end_utc,duration_sec,character,realm,status"}
    for i, s in ipairs(sessions) do
        table.insert(lines, string.format("%d,%d,%s,%d,%s,%d,%s,%s,%s",
            i,
            s.start_unix,
            CsvEscape(s.start_utc),
            s.end_unix,
            CsvEscape(s.end_utc),
            s.duration_sec or 0,
            CsvEscape(s.character),
            CsvEscape(s.realm),
            CsvEscape(s.status)))
    end
    return table.concat(lines, "\n")
end

local function BuildSessionsJSON()
    local sessions = BuildSessions()
    local lines = {}
    for i, s in ipairs(sessions) do
        table.insert(lines, string.format(
          '  {"id":%d,"start_unix":%d,"start_utc":"%s","end_unix":%d,"end_utc":"%s","duration":%d,"char":"%s","realm":"%s","status":"%s"}',
          i,
          s.start_unix,
          JsonEscape(s.start_utc),
          s.end_unix,
          JsonEscape(s.end_utc),
          s.duration_sec or 0,
          JsonEscape(s.character),
          JsonEscape(s.realm),
          JsonEscape(s.status)))
    end
    return "[\n" .. table.concat(lines, ",\n") .. "\n]"
end

local function FormatDuration(sec)
  sec = math.max(0, math.floor(sec or 0))
  local h = math.floor(sec / 3600)
  local m = math.floor((sec % 3600) / 60)
  local s = sec % 60
  return string.format("%02d:%02d:%02d", h, m, s)
end

local function GetCurrentSessionDurationSec()
  EnsureDB()
  local char = UnitName("player") or ""
  local realm = GetRealmName() or ""
  for i = #db.events, 1, -1 do
    local e = db.events[i]
    if e.character == char and e.realm == realm then
      if e.event == "logout" then
        return nil
      end
      if e.event == "login" then
        return GetUnix() - (e.unix or 0)
      end
    end
  end
  return nil
end

local function BuildCurrentSessionLabel()
  local sec = GetCurrentSessionDurationSec()
  if not sec then
    return "Current Session Duration: N/A"
  end
  return "Current Session Duration: " .. FormatDuration(sec)
end

local function SaveCurrentCharacterPlayed(totalSec)
  EnsureDB()
  local n = tonumber(totalSec)
  if not n then
    return
  end
  db.played_totals[CurrentCharKey()] = math.max(0, math.floor(n))
end

local function RequestPlayedTotals(force)
  if not RequestTimePlayed then
    return
  end
  local now = GetUnix()
  if not force and lastPlayedRequestUnix and (now - lastPlayedRequestUnix) < 60 then
    return
  end
  lastPlayedRequestUnix = now
  RequestTimePlayed()
end

local function BuildPlaytimeTotals()
  EnsureDB()
  local allTotal = 0
  for _, sec in pairs(db.played_totals) do
    allTotal = allTotal + (sec or 0)
  end

  local currentTotal = db.played_totals[CurrentCharKey()] or 0
  return currentTotal, allTotal
end

local function BuildJSON()
  local parts = { "[" }
  local n = #db.events
  for i = 1, n do
    local e = db.events[i]
    local rec = e.recovery and ',"recovery":true' or ""
    local chunk = string.format(
      '{"unix":%d,"utc":"%s","event":"%s","character":"%s","realm":"%s"%s}',
      e.unix or 0,
      JsonEscape(e.utc),
      JsonEscape(e.event),
      JsonEscape(e.character),
      JsonEscape(e.realm),
      rec
    )
    if i < n then
      chunk = chunk .. ","
    end
    parts[#parts + 1] = chunk
  end
  parts[#parts + 1] = "]"
  return table.concat(parts, "\n")
end

local function CopyEventsSnapshot()
  local t = {}
  for i = 1, #db.events do
    local e = db.events[i]
    if CopyTable then
      t[i] = CopyTable(e)
    else
      t[i] = {
        unix = e.unix,
        utc = e.utc,
        event = e.event,
        character = e.character,
        realm = e.realm,
        recovery = e.recovery,
      }
    end
  end
  return t
end

local function DoPrune(days)
  EnsureDB()
  local cutoff = GetUnix() - (days * 86400)
  
  -- Only create backup if one doesn't exist for this session or if it's old
  if not db.events_backup or not db.events_backup_time or (GetUnix() - db.events_backup_time) > 3600 then
    db.events_backup = CopyEventsSnapshot()
    db.events_backup_time = GetUnix()
  end
  
  local kept = {}
  for i = 1, #db.events do
    local e = db.events[i]
    if (e.unix or 0) >= cutoff then
      kept[#kept + 1] = e
    end
  end
  db.events = kept
  print(
    string.format(
      "|cff00ff00TimeLogger:|r Pruned events older than %d days. Kept %d rows; full pre-prune snapshot in events_backup (%d rows).",
      days,
      #db.events,
      #db.events_backup
    )
  )
end

-- Export UI -----------------------------------------------------------------

local exportFrame
local exportEdit
local sessionDurationLabel
local currentCharacterTotalLabel
local allCharactersTotalLabel
local exportMode = "events_csv"

local function ResizeExportEdit()
  if not exportEdit or not exportFrame then
    return
  end
  local scroll = exportFrame.scroll
  local w = math.max(scroll:GetWidth() - 24, 1)
  exportEdit:SetWidth(w)
  local insetL, insetR, insetT, insetB = exportEdit:GetTextInsets()
  insetL = insetL or 0
  insetR = insetR or 0
  insetT = insetT or 0
  insetB = insetB or 0
  local contentW = math.max(w - insetL - insetR, 1)
  local fs = exportFrame.measureFS
  local font, size, flags = exportEdit:GetFont()
  if font then
    fs:SetFont(font, size, flags)
  end
  fs:SetWidth(contentW)
  if fs.SetWordWrap then
    fs:SetWordWrap(true)
  end
  fs:SetText(exportEdit:GetText() or "")
  local textH = fs:GetStringHeight() or 0
  local h = math.max(textH + insetT + insetB + 4, scroll:GetHeight())
  exportEdit:SetHeight(h)
end

local function GetExportText()
  EnsureDB()
  if exportMode == "events_json" then
    return BuildJSON()
  elseif exportMode == "sessions_csv" then
    return BuildSessionsCSV()
  elseif exportMode == "sessions_json" then
    return BuildSessionsJSON()
  end
  return BuildCSV()
end

local function RefreshExportText()
  if not exportEdit then
    return
  end
  exportEdit:SetText(GetExportText())
  ResizeExportEdit()
end

local function RefreshCurrentSessionLabel()
  if sessionDurationLabel then
    sessionDurationLabel:SetText(BuildCurrentSessionLabel())
  end
end

local function RefreshPlaytimeSummaryLabels()
  if not currentCharacterTotalLabel and not allCharactersTotalLabel then
    return
  end
  local currentTotal, allTotal = BuildPlaytimeTotals()
  if currentCharacterTotalLabel then
    currentCharacterTotalLabel:SetText("Total This Character: " .. FormatDuration(currentTotal))
  end
  if allCharactersTotalLabel then
    allCharactersTotalLabel:SetText("Total All Characters: " .. FormatDuration(allTotal))
  end
end

local function SaveCurrentPlayedFromEventsFallback()
  EnsureDB()
  local key = CurrentCharKey()
  local total = 0
  local openStart
  local char = UnitName("player") or ""
  local realm = GetRealmName() or ""
  for i = 1, #db.events do
    local e = db.events[i]
    if e.character == char and e.realm == realm then
      if e.event == "login" then
        openStart = e.unix or 0
      elseif e.event == "logout" then
        local endUnix = e.unix or 0
        if openStart and endUnix > openStart then
          total = total + (endUnix - openStart)
        end
        openStart = nil
      end
    end
  end
  local now = GetUnix()
  if openStart and now > openStart then
    total = total + (now - openStart)
  end
  if total > 0 and not db.played_totals[key] then
    db.played_totals[key] = math.floor(total)
  end
end

local function CreateExportUI()
  local f = CreateFrame("Frame", "TimeLoggerExportFrame", UIParent, "BackdropTemplate")
  f:SetSize(860, 620)
  f:SetPoint("CENTER")
  f:SetFrameStrata("DIALOG")
  f:SetMovable(true)
  f:EnableMouse(true)
  f:RegisterForDrag("LeftButton")
  f:SetScript("OnDragStart", f.StartMoving)
  f:SetScript("OnDragStop", f.StopMovingOrSizing)
  f:SetBackdrop({
    bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
    edgeFile = "Interface\\DialogFrame\\UI-DialogBox-Border",
    tile = true,
    tileSize = 16,
    edgeSize = 32,
    insets = { left = 8, right = 8, top = 10, bottom = 8 },
  })
  f:SetBackdropColor(0.04, 0.05, 0.08, 0.95)
  f:SetBackdropBorderColor(0.45, 0.5, 0.65, 1)

  local title = f:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
  title:SetPoint("TOPLEFT", 20, -16)
  title:SetJustifyH("LEFT")
  title:SetText("TimeLogger Export")

  local subtitle = f:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
  subtitle:SetPoint("TOPLEFT", title, "BOTTOMLEFT", 0, -4)
  subtitle:SetText("Events and sessions in CSV/JSON")

  local sessionLabel = f:CreateFontString(nil, "OVERLAY", "GameFontNormal")
  sessionLabel:SetPoint("TOPLEFT", 20, -52)
  sessionLabel:SetText("Current Session Duration: N/A")
  sessionDurationLabel = sessionLabel

  local closeBtn = CreateFrame("Button", nil, f, "UIPanelCloseButton")
  closeBtn:SetPoint("TOPRIGHT", -4, -4)
  closeBtn:SetScript("OnClick", function()
    f:Hide()
  end)

  local evCsv = CreateFrame("Button", nil, f, "UIPanelButtonTemplate")
  evCsv:SetSize(104, 22)
  evCsv:SetPoint("TOPLEFT", 20, -78)
  evCsv:SetText("Events CSV")
  evCsv:SetScript("OnClick", function()
    exportMode = "events_csv"
    RefreshExportText()
  end)

  local evJson = CreateFrame("Button", nil, f, "UIPanelButtonTemplate")
  evJson:SetSize(104, 22)
  evJson:SetPoint("LEFT", evCsv, "RIGHT", 8, 0)
  evJson:SetText("Events JSON")
  evJson:SetScript("OnClick", function()
    exportMode = "events_json"
    RefreshExportText()
  end)

  local sessCsv = CreateFrame("Button", nil, f, "UIPanelButtonTemplate")
  sessCsv:SetSize(112, 22)
  sessCsv:SetPoint("LEFT", evJson, "RIGHT", 8, 0)
  sessCsv:SetText("Sessions CSV")
  sessCsv:SetScript("OnClick", function()
    exportMode = "sessions_csv"
    RefreshExportText()
  end)

  local sessJson = CreateFrame("Button", nil, f, "UIPanelButtonTemplate")
  sessJson:SetSize(112, 22)
  sessJson:SetPoint("LEFT", sessCsv, "RIGHT", 8, 0)
  sessJson:SetText("Sessions JSON")
  sessJson:SetScript("OnClick", function()
    exportMode = "sessions_json"
    RefreshExportText()
  end)

  local copyBtn = CreateFrame("Button", nil, f, "UIPanelButtonTemplate")
  copyBtn:SetSize(140, 22)
  copyBtn:SetPoint("TOPRIGHT", -36, -78)
  copyBtn:SetText("Copy to clipboard")
  copyBtn:SetScript("OnClick", function()
    local text = exportEdit:GetText() or ""
    if C_ChatInfo and C_ChatInfo.CopyStringToClipboard then
      C_ChatInfo.CopyStringToClipboard(text)
      print("|cff00ff00TimeLogger:|r Copied " .. #text .. " characters to clipboard.")
    else
      exportEdit:SetFocus()
      exportEdit:HighlightText()
      print("|cffff9900TimeLogger:|r Select all (Ctrl+A) and copy (Ctrl+C).")
    end
  end)

  local topDivider = f:CreateTexture(nil, "ARTWORK")
  topDivider:SetColorTexture(0.3, 0.35, 0.45, 0.45)
  topDivider:SetPoint("TOPLEFT", 20, -108)
  topDivider:SetPoint("TOPRIGHT", -20, -108)
  topDivider:SetHeight(1)

  local pruneLabel = f:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
  pruneLabel:SetPoint("BOTTOMLEFT", 20, 44)
  pruneLabel:SetText("Data cleanup:")

  local pruneDays = CreateFrame("EditBox", nil, f, "InputBoxTemplate")
  pruneDays:SetSize(50, 20)
  pruneDays:SetPoint("LEFT", pruneLabel, "RIGHT", 8, 0)
  pruneDays:SetAutoFocus(false)
  pruneDays:SetText("30")
  if pruneDays.SetNumeric then
    pruneDays:SetNumeric(true)
  end
  f.pruneDaysEdit = pruneDays

  local daysSuffix = f:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
  daysSuffix:SetPoint("LEFT", pruneDays, "RIGHT", 6, 0)
  daysSuffix:SetText("days old")

  local pruneBtn = CreateFrame("Button", nil, f, "UIPanelButtonTemplate")
  pruneBtn:SetSize(72, 22)
  pruneBtn:SetPoint("LEFT", daysSuffix, "RIGHT", 12, 0)
  pruneBtn:SetText("Prune")
  pruneBtn:SetScript("OnClick", function()
    local d = tonumber(pruneDays:GetText())
    if not d or d < 1 then
      print("|cffff9900TimeLogger:|r Enter a number of days (1 or more).")
      return
    end
    pendingPruneDays = math.floor(d)
    StaticPopup_Show("TIMELOGGER_PRUNE_CONFIRM", tostring(pendingPruneDays))
  end)

  local totalsHeader = f:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
  totalsHeader:SetPoint("BOTTOMRIGHT", -20, 62)
  totalsHeader:SetText("Total Playtime Summary")

  local currentTotalLabel = f:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
  currentTotalLabel:SetPoint("TOPRIGHT", totalsHeader, "BOTTOMRIGHT", 0, -4)
  currentTotalLabel:SetText("Total This Character: 00:00:00")
  currentCharacterTotalLabel = currentTotalLabel

  local allTotalLabel = f:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
  allTotalLabel:SetPoint("TOPRIGHT", currentTotalLabel, "BOTTOMRIGHT", 0, -2)
  allTotalLabel:SetText("Total All Characters: 00:00:00")
  allCharactersTotalLabel = allTotalLabel

  local bottomDivider = f:CreateTexture(nil, "ARTWORK")
  bottomDivider:SetColorTexture(0.3, 0.35, 0.45, 0.45)
  bottomDivider:SetPoint("BOTTOMLEFT", 20, 74)
  bottomDivider:SetPoint("BOTTOMRIGHT", -20, 74)
  bottomDivider:SetHeight(1)

  local scroll = CreateFrame("ScrollFrame", nil, f, "UIPanelScrollFrameTemplate")
  scroll:SetPoint("TOPLEFT", 20, -116)
  scroll:SetPoint("BOTTOMRIGHT", -36, 82)
  f.scroll = scroll

  local measureFS = f:CreateFontString(nil, "ARTWORK", "ChatFontNormal")
  measureFS:SetPoint("TOPLEFT", UIParent, "BOTTOMRIGHT", 10000, 10000)
  measureFS:Hide()
  f.measureFS = measureFS

  local edit = CreateFrame("EditBox", nil, scroll)
  edit:SetMultiLine(true)
  edit:SetAutoFocus(false)
  edit:SetFontObject(ChatFontNormal)
  edit:SetWidth(460)
  edit:SetTextInsets(8, 8, 8, 8)
  edit:SetScript("OnEscapePressed", function()
    f:Hide()
  end)
  edit:SetScript("OnTextChanged", function(self, userInput)
    if userInput then
      return
    end
    ResizeExportEdit()
  end)
  scroll:SetScrollChild(edit)

  exportFrame = f
  exportEdit = edit
  f.sessionRefreshElapsed = 0
  f:SetScript("OnUpdate", function(self, elapsed)
    self.sessionRefreshElapsed = (self.sessionRefreshElapsed or 0) + elapsed
    if self.sessionRefreshElapsed >= 1 then
      self.sessionRefreshElapsed = 0
      RefreshCurrentSessionLabel()
      RefreshPlaytimeSummaryLabels()
    end
  end)
end

local function ShowExport()
  EnsureDB()
  if not exportFrame then
    CreateExportUI()
  end
  RequestPlayedTotals(false)
  RefreshExportText()
  RefreshCurrentSessionLabel()
  RefreshPlaytimeSummaryLabels()
  exportFrame:Show()
  exportFrame:Raise()
  C_Timer.After(0, function()
    if exportFrame and exportFrame:IsShown() then
      ResizeExportEdit()
    end
  end)
end

StaticPopupDialogs["TIMELOGGER_PRUNE_CONFIRM"] = {
  text = "Prune events older than %s days?|nYour full current event list will be copied to events_backup first.",
  button1 = YES,
  button2 = NO,
  OnAccept = function()
    if pendingPruneDays then
      DoPrune(pendingPruneDays)
      pendingPruneDays = nil
      if exportFrame and exportFrame:IsShown() then
        RefreshExportText()
      end
    end
  end,
  OnCancel = function()
    pendingPruneDays = nil
  end,
  timeout = 0,
  whileDead = true,
  hideOnEscape = true,
}

-- Events --------------------------------------------------------------------

local eventFrame = CreateFrame("Frame")
eventFrame:RegisterEvent("ADDON_LOADED")
eventFrame:RegisterEvent("PLAYER_LOGIN")
eventFrame:RegisterEvent("PLAYER_LOGOUT")
eventFrame:RegisterEvent("TIME_PLAYED_MSG")

eventFrame:SetScript("OnEvent", function(_, event, arg1)
  if event == "ADDON_LOADED" and arg1 == ADDON_NAME then
    EnsureDB()
  elseif event == "PLAYER_LOGIN" then
    RecoverOrphanLogout()
    Record("login")
    UpdateTempLogout()
    StartTempLogoutTicker()
    SaveCurrentPlayedFromEventsFallback()
    C_Timer.After(2, function()
      RequestPlayedTotals(true)
    end)
  elseif event == "PLAYER_LOGOUT" then
    StopTempLogoutTicker()
    EnsureDB()
    RequestPlayedTotals(true)
    db.temp_logout = nil
    Record("logout")
  elseif event == "TIME_PLAYED_MSG" then
    local totalTime = tonumber(arg1)
    if totalTime then
      SaveCurrentCharacterPlayed(totalTime)
      if exportFrame and exportFrame:IsShown() then
        RefreshPlaytimeSummaryLabels()
      end
    end
  end
end)

SLASH_TIMELOGGER1 = "/timelogger"
SLASH_TIMELOGGER2 = "/tlog"
SlashCmdList["TIMELOGGER"] = function()
  ShowExport()
end
