--[[
    Commander 3.0 - Session Manager

    Manages command sessions with:
    - Environment variables
    - Command history
    - Working directory state
    - Session-specific settings
]]

local ADDON_NAME, C3 = ...

-------------------------------------------------------------------------------
-- Session Class
-------------------------------------------------------------------------------

local Session = C3.class()
C3.Session = Session

---Create a new session
---@param options? table
---@return table
function Session:init(options)
    options = options or {}

    self.id = options.id or self:_generateId()
    self.name = options.name or "Session " .. self.id
    self.created = GetTime()
    self.lastActive = GetTime()

    -- Environment variables
    self.env = options.env or {}

    -- Command history
    self.history = options.history or {}
    self.historyIndex = 0
    self.maxHistory = options.maxHistory or 100

    -- Working directory (separate from global VFS cwd)
    -- Default to character's home directory
    self.cwd = options.cwd or Session._getDefaultCwd()

    -- Prompt mode (default to player name until filesystem commands are used)
    self.showCwdPrompt = options.showCwdPrompt or false

    -- Session flags
    self.isActive = true
    self.isSilent = options.silent or false

    -- Last command result
    self.lastResult = nil
    self.lastError = nil
    self.lastExitCode = 0

    -- Aliases (session-specific)
    self.aliases = options.aliases or {}

    -- Custom Commander keybinds (key -> command)
    self.keybinds = options.keybinds or {}
end

---Get the default cwd (character's home directory)
---@return string
function Session._getDefaultCwd()
    if C3.VFSSavedVarsDriver and C3.VFSSavedVarsDriver._getCharacterHome then
        return C3.VFSSavedVarsDriver._getCharacterHome()
    end
    return "/home" -- Fallback
end

---Get the current character's home directory path
---@return string e.g. "/home/Argent Dawn/Ludwig"
function Session:GetHomePath()
    return Session._getDefaultCwd()
end

---Generate unique session ID
---@return string
function Session:_generateId()
    return string.format("%x%x", GetTime() * 1000, math.random(0xFFFF))
end

-------------------------------------------------------------------------------
-- Environment Variables
-------------------------------------------------------------------------------

---Get environment variable
---@param name string
---@return any
function Session:GetEnv(name)
    return self.env[name]
end

---Set environment variable
---@param name string
---@param value any
function Session:SetEnv(name, value)
    self.env[name] = value
    self:Touch()
end

---Unset environment variable
---@param name string
function Session:UnsetEnv(name)
    self.env[name] = nil
    self:Touch()
end

---Get all environment variables
---@return table
function Session:GetAllEnv()
    local copy = {}
    for k, v in pairs(self.env) do
        copy[k] = v
    end
    return copy
end

---Export environment variable (set and persist)
---@param name string
---@param value any
function Session:Export(name, value)
    self:SetEnv(name, value)
    -- TODO: Persist to VFS /sys/config/env
end

-------------------------------------------------------------------------------
-- Command History
-------------------------------------------------------------------------------

-- Commands to skip when navigating history (trivial/utility commands)
local HISTORY_SKIP_COMMANDS = {
    clear = true,
    cls = true,
}

---Check if a command should be skipped in history navigation
---@param command string
---@return boolean
local function shouldSkipInHistory(command)
    if not command then return false end
    local cmd = command:match("^%s*(%S+)")
    return cmd and HISTORY_SKIP_COMMANDS[cmd:lower()]
end

---Add command to history
---@param command string
function Session:AddHistory(command)
    -- Don't add empty or duplicate consecutive commands
    if command == "" then return end
    if self.history[#self.history] == command then return end

    table.insert(self.history, command)

    -- Trim if over limit
    while #self.history > self.maxHistory do
        table.remove(self.history, 1)
    end

    -- Reset history navigation
    self.historyIndex = 0
    self:Touch()
end

---Get previous command from history
---@return string?
function Session:HistoryPrev()
    if #self.history == 0 then return nil end

    -- Find next non-skipped command
    local startIndex = self.historyIndex
    while self.historyIndex < #self.history do
        self.historyIndex = self.historyIndex + 1
        local cmd = self.history[#self.history - self.historyIndex + 1]
        if not shouldSkipInHistory(cmd) then
            return cmd
        end
    end

    -- If we didn't find any, stay at current position
    if startIndex > 0 then
        self.historyIndex = startIndex
        return self.history[#self.history - self.historyIndex + 1]
    end

    -- Reset index to prevent corruption on next navigation
    self.historyIndex = 0
    return nil
end

---Get next command from history
---@return string?
function Session:HistoryNext()
    if self.historyIndex <= 0 then
        self.historyIndex = 0
        return ""
    end

    -- Find next non-skipped command going forward
    while self.historyIndex > 0 do
        self.historyIndex = self.historyIndex - 1

        if self.historyIndex == 0 then
            return ""
        end

        local cmd = self.history[#self.history - self.historyIndex + 1]
        if not shouldSkipInHistory(cmd) then
            return cmd
        end
    end

    return ""
end

---Reset history navigation
function Session:HistoryReset()
    self.historyIndex = 0
end

---Search history for pattern
---@param pattern string
---@return table matches
function Session:HistorySearch(pattern)
    local matches = {}
    pattern = pattern:lower()

    for i = #self.history, 1, -1 do
        if self.history[i]:lower():find(pattern, 1, true) then
            table.insert(matches, {
                index = i,
                command = self.history[i]
            })
        end
    end

    return matches
end

---Get history entry by index (negative = from end)
---@param index number
---@return string?
function Session:GetHistory(index)
    if index < 0 then
        index = #self.history + index + 1
    end
    return self.history[index]
end

---Clear history
function Session:ClearHistory()
    self.history = {}
    self.historyIndex = 0
end

-------------------------------------------------------------------------------
-- Working Directory
-------------------------------------------------------------------------------

---Get current working directory
---@return string
function Session:GetCwd()
    return self.cwd
end

---Set current working directory
---@param path string
---@return boolean success
---@return string? error
function Session:SetCwd(path)
    local vfs = C3:GetVFS()
    local resolved

    if C3.Paths.IsAbsolute(path) then
        resolved = C3.Paths.Normalize(path)
    else
        resolved = C3.Paths.Join(self.cwd, path)
    end

    if not vfs:Exists(resolved) then
        return false, "Directory not found: " .. resolved
    end

    if not vfs:IsDirectory(resolved) then
        return false, "Not a directory: " .. resolved
    end

    self.cwd = resolved
    self:Touch()
    return true
end

-------------------------------------------------------------------------------
-- Prompt Mode
-------------------------------------------------------------------------------

---Enable cwd prompt mode for this session
function Session:EnableCwdPrompt()
    if not self.showCwdPrompt then
        self.showCwdPrompt = true
        self:Touch()
    end
end

---Check if cwd prompt mode is enabled
---@return boolean
function Session:ShowCwdPrompt()
    return self.showCwdPrompt == true
end

-------------------------------------------------------------------------------
-- Aliases
-------------------------------------------------------------------------------

---Set alias
---@param name string
---@param command string
function Session:SetAlias(name, command)
    self.aliases[name] = command
    self:Touch()
end

---Get alias
---@param name string
---@return string?
function Session:GetAlias(name)
    return self.aliases[name]
end

---Remove alias
---@param name string
function Session:RemoveAlias(name)
    self.aliases[name] = nil
    self:Touch()
end

---Get all aliases
---@return table
function Session:GetAliases()
    local copy = {}
    for k, v in pairs(self.aliases) do
        copy[k] = v
    end
    return copy
end

---Expand aliases in command
---@param command string
---@return string
function Session:ExpandAliases(command)
    local parts = {}
    for part in command:gmatch("%S+") do
        table.insert(parts, part)
    end

    if #parts == 0 then return command end

    -- Only expand first word
    local alias = self.aliases[parts[1]]
    if alias then
        parts[1] = alias
        return table.concat(parts, " ")
    end

    return command
end

-------------------------------------------------------------------------------
-- Custom Commander Keybinds
-------------------------------------------------------------------------------

---Normalize a key string (uppercase, CONTROL -> CTRL, etc.)
---@param key string
---@return string
local function NormalizeKey(key)
    if not key then return "" end
    key = key:upper()
    -- Normalize modifier names
    key = key:gsub("CONTROL", "CTRL")
    key = key:gsub("COMMAND", "META")
    return key
end

---Set a custom Commander keybind
---@param key string Key combination (e.g., "CTRL-F5")
---@param command string Commander command to execute
function Session:SetKeybind(key, command)
    local normalizedKey = NormalizeKey(key)
    self.keybinds[normalizedKey] = command
    self:Touch()
end

---Get command bound to a key
---@param key string
---@return string?
function Session:GetKeybind(key)
    local normalizedKey = NormalizeKey(key)
    return self.keybinds[normalizedKey]
end

---Remove a custom keybind
---@param key string
---@return boolean removed
function Session:RemoveKeybind(key)
    local normalizedKey = NormalizeKey(key)
    if self.keybinds[normalizedKey] then
        self.keybinds[normalizedKey] = nil
        self:Touch()
        return true
    end
    return false
end

---Get all custom keybinds
---@return table<string, string>
function Session:GetKeybinds()
    local copy = {}
    for k, v in pairs(self.keybinds) do
        copy[k] = v
    end
    return copy
end

---Check if a key has a custom keybind
---@param key string
---@return boolean
function Session:HasKeybind(key)
    local normalizedKey = NormalizeKey(key)
    return self.keybinds[normalizedKey] ~= nil
end

-------------------------------------------------------------------------------
-- Last Command State
-------------------------------------------------------------------------------

---Store last command result
---@param result any
---@param exitCode? number
function Session:SetLastResult(result, exitCode)
    self.lastResult = result
    self.lastExitCode = exitCode or 0
    self.lastError = nil
    self:Touch()
end

---Store last command error
---@param error string
function Session:SetLastError(error)
    self.lastError = error
    self.lastExitCode = 1
    self:Touch()
end

---Get last exit code
---@return number
function Session:GetExitCode()
    return self.lastExitCode
end

-------------------------------------------------------------------------------
-- Session Lifecycle
-------------------------------------------------------------------------------

---Update last active time
function Session:Touch()
    self.lastActive = GetTime()
end

---Close session
function Session:Close()
    self.isActive = false
end

---Check if session is still active
---@return boolean
function Session:IsActive()
    return self.isActive
end

-------------------------------------------------------------------------------
-- Serialization
-------------------------------------------------------------------------------

---Serialize session for storage
---@return table
function Session:Serialize()
    return {
        id = self.id,
        name = self.name,
        created = self.created,
        lastActive = self.lastActive,
        env = self.env,
        history = self.history,
        cwd = self.cwd,
        aliases = self.aliases,
        keybinds = self.keybinds,
    }
end

---Deserialize session from storage
---@param data table
---@return Session
function Session.Deserialize(data)
    local session = Session:new({
        id = data.id,
        name = data.name,
        env = data.env,
        history = data.history,
        cwd = data.cwd,
        aliases = data.aliases,
        keybinds = data.keybinds,
    })
    session.created = data.created or GetTime()
    session.lastActive = data.lastActive or GetTime()
    return session
end

-------------------------------------------------------------------------------
-- Session Manager
-------------------------------------------------------------------------------

local SessionManager = {}
C3.SessionManager = SessionManager

SessionManager._sessions = {}
SessionManager._refCounts = {}  -- Reference counting for session lifecycle
SessionManager._activeSession = nil

-- Global config persisted across sessions (not per-session settings)
SessionManager._globalConfig = {
    lastWindowSize = nil,  -- { width, height } - persisted across logins
}

---Create a new session
---@param options? table
---@return Session
function SessionManager:Create(options)
    local session = Session:new(options)
    self._sessions[session.id] = session
    self._refCounts[session.id] = 1  -- Start with refcount 1

    if not self._activeSession then
        self._activeSession = session
    end

    C3:Debug("Session: Created " .. session.name .. " (refcount: 1)")
    return session
end

-------------------------------------------------------------------------------
-- Reference Counting
-------------------------------------------------------------------------------

---Add a reference to a session (increment refcount)
---@param id string Session ID
function SessionManager:AddRef(id)
    if not self._sessions[id] then return end

    self._refCounts[id] = (self._refCounts[id] or 0) + 1
    C3:Debug("Session: AddRef " .. id .. " (refcount: " .. self._refCounts[id] .. ")")
end

---Release a reference to a session (decrement refcount, close if zero)
---@param id string Session ID
function SessionManager:Release(id)
    if not self._sessions[id] then return end

    self._refCounts[id] = (self._refCounts[id] or 1) - 1
    C3:Debug("Session: Release " .. id .. " (refcount: " .. self._refCounts[id] .. ")")

    if self._refCounts[id] <= 0 then
        self:Close(id)
    end
end

---Get reference count for a session
---@param id string Session ID
---@return number
function SessionManager:GetRefCount(id)
    return self._refCounts[id] or 0
end

---Get session by ID
---@param id string
---@return Session?
function SessionManager:Get(id)
    return self._sessions[id]
end

---Get active session
---@return Session
function SessionManager:GetActive()
    if not self._activeSession then
        -- Try to use an existing session first
        local list = self:List()
        if list[1] then
            self._activeSession = list[1]
        else
            self._activeSession = self:Create({ name = "Default" })
        end
    end
    return self._activeSession
end

---Set active session
---@param session Session
function SessionManager:SetActive(session)
    self._activeSession = session
end

---List all sessions
---@return table
function SessionManager:List()
    local list = {}
    for _, session in pairs(self._sessions) do
        table.insert(list, session)
    end
    table.sort(list, function(a, b)
        return a.lastActive > b.lastActive
    end)
    return list
end

---Close session (internal - use Release() for proper lifecycle)
---@param id string
function SessionManager:Close(id)
    local session = self._sessions[id]
    if session then
        session:Close()
        self._sessions[id] = nil
        self._refCounts[id] = nil  -- Clean up refcount

        if self._activeSession == session then
            -- Switch to most recent session
            local list = self:List()
            self._activeSession = list[1]
        end

        C3:Debug("Session: Closed " .. (session.name or id))
    end
end

-------------------------------------------------------------------------------
-- Global Config Accessors
-------------------------------------------------------------------------------

---Get the last window size (persisted across logins)
---@return table|nil { width, height }
function SessionManager:GetLastWindowSize()
    -- Read directly from SavedVariables (bypasses VFS serialization issues)
    local db = Commander3DB
    if db and db.globalConfig and db.globalConfig.lastWindowSize then
        return db.globalConfig.lastWindowSize
    end
    return nil
end

---Set the last window size (called on window resize)
---@param width number
---@param height number
function SessionManager:SetLastWindowSize(width, height)
    -- Store directly in SavedVariables (WoW handles serialization)
    if not Commander3DB then Commander3DB = {} end
    if not Commander3DB.globalConfig then Commander3DB.globalConfig = {} end
    Commander3DB.globalConfig.lastWindowSize = { width = width, height = height }
end

---Get the last window position (persisted across logins)
---@return table|nil { point, relativeTo, relativePoint, x, y }
function SessionManager:GetLastWindowPosition()
    local db = Commander3DB
    if db and db.globalConfig and db.globalConfig.lastWindowPosition then
        return db.globalConfig.lastWindowPosition
    end
    return nil
end

---Set the last window position (called on window drag stop)
---@param point string
---@param relativePoint string
---@param x number
---@param y number
function SessionManager:SetLastWindowPosition(point, relativePoint, x, y)
    if not Commander3DB then Commander3DB = {} end
    if not Commander3DB.globalConfig then Commander3DB.globalConfig = {} end
    Commander3DB.globalConfig.lastWindowPosition = {
        point = point,
        relativePoint = relativePoint,
        x = x,
        y = y,
    }
end

-------------------------------------------------------------------------------
-- Persistence
-------------------------------------------------------------------------------

---Save sessions to VFS with backup
function SessionManager:Save()
    local vfs = C3:GetVFS()

    -- Ensure config directory exists
    vfs:MakeDirP("/sys/config")

    -- Wrap session data with version info
    local data = {
        _version = 2,  -- Bumped for globalConfig support
        _savedAt = time(),
        sessions = {},
        globalConfig = self._globalConfig,
    }
    for id, session in pairs(self._sessions) do
        data.sessions[id] = session:Serialize()
    end

    local content = C3.Serialize and C3.Serialize(data) or ""

    -- Create backup of existing file before overwriting
    local existing = vfs:Read("/sys/config/sessions.lua")
    if existing and existing ~= "" then
        vfs:Write("/sys/config/sessions.lua.bak", existing)
    end

    vfs:Write("/sys/config/sessions.lua", content)

    C3:Debug("Session: Saved " .. #self:List() .. " sessions")
end

---Load sessions from VFS with backup fallback
function SessionManager:Load()
    local vfs = C3:GetVFS()
    local content = vfs:Read("/sys/config/sessions.lua")
    local fromBackup = false

    -- Try backup if primary file is missing or empty
    if not content or content == "" then
        content = vfs:Read("/sys/config/sessions.lua.bak")
        if content and content ~= "" then
            fromBackup = true
            C3:Debug("Session: Primary file missing, loading from backup")
        end
    end

    if content and content ~= "" then
        local data = C3.Deserialize and C3.Deserialize(content)
        if data then
            -- Handle new versioned format
            if data._version and data.sessions then
                for id, sessionData in pairs(data.sessions) do
                    local session = Session.Deserialize(sessionData)
                    self._sessions[id] = session
                end
                -- Restore global config (version 2+)
                if data.globalConfig then
                    self._globalConfig = data.globalConfig
                end
            else
                -- Legacy format (no wrapper) - for backwards compatibility
                for id, sessionData in pairs(data) do
                    if id ~= "_version" and id ~= "_savedAt" then
                        local session = Session.Deserialize(sessionData)
                        self._sessions[id] = session
                    end
                end
            end
            local source = fromBackup and " (from backup)" or ""
            C3:Debug("Session: Loaded " .. #self:List() .. " sessions" .. source)
        else
            C3:Debug("Session: Failed to deserialize session data")
        end
    end
end

-------------------------------------------------------------------------------
-- Initialize
-------------------------------------------------------------------------------

function C3:GetSession()
    return C3.SessionManager:GetActive()
end

-------------------------------------------------------------------------------
-- Auto-save on Logout
-------------------------------------------------------------------------------

local sessionEventFrame = CreateFrame("Frame")
sessionEventFrame:RegisterEvent("PLAYER_LOGOUT")
sessionEventFrame:SetScript("OnEvent", function(_, event)
    if event == "PLAYER_LOGOUT" then
        C3.SessionManager:Save()
        C3:Debug("Session: Saved on logout")
    end
end)

return Session
