questLogController = Controller:new()

-- @ todo
-- test tracker onUpdateQuestTracker
-- test 14.10
-- questLogController:bindKeyPress('Down' // 'up') focusNextChild // focusPreviousChild
-- PopMenu Miniwindows "Remove completed quests // Automatically track new quests // Automatically untrack completed quests"

-- @  windows
local trackerMiniWindow = nil
local questLogButton = nil
local buttonQuestLogTrackerButton = nil

-- @widgets
local UICheckBox = {
    showComplete = nil,
    showShidden = nil,
    showInQuestTracker = nil
}

local UIlabel = {
    numberQuestComplete = nil,
    numberQuestHidden = nil
}

local UITextList = {
    questLogList = nil,
    questLogLine = nil,
    questLogInfo = nil
}

local UITextEdit = {
    search = nil
}

-- variable
local settings = {}
local namePlayer = ""
local currentQuestId = nil  -- Track the currently selected quest ID
local missionToQuestMap = {} -- Map missionId to questId for navigation
local isNavigating = false   -- Flag to prevent checkbox events during navigation
local isUpdatingCheckbox = false  -- Flag to prevent recursive checkbox events
local isReceivingQuestTracker = false -- Prevent stale local cache from being sent while applying server tracker data
local questTrackerSettingsLoaded = false
local saveTimer = nil
local questLogCache = {
    items = {},
    completed = 0,
    hidden = 0,
    visible = 0
}

-- const
local COLORS = {
    BASE_1 = "#484848",
    BASE_2 = "#414141",
    SELECTED = "#585858"
}
local file = "/settings/questtracking.json"
local settingsDirectory = "/settings/"

--[[=================================================
=               Local Functions                     =
=================================================== ]] --

local function isIdInTracker(key, id)
    if not settings[key] then
        return false
    end
    return table.findbyfield(settings[key], 1, tonumber(id)) ~= nil
end

local function addUniqueIdQuest(key, questId, missionId, missionName, missionDescription)
    if not settings[key] then
        settings[key] = {}
    end

    if not isIdInTracker(key, missionId) then
        table.insert(settings[key], {tonumber(missionId), missionName, missionDescription or missionName, tonumber(questId)})
    end
end

local function removeNumber(key, id)
    if settings[key] then
        table.remove_if(settings[key], function(_, v)
            return v[1] == tonumber(id)
        end)
    end
end

local function load()
    if g_resources.fileExists(file) then
        local status, result = pcall(function()
            return json.decode(g_resources.readFileContents(file))
        end)
        if not status then
            return g_logger.error(
                "Error while reading profiles file. To fix this problem you can delete storage.json. Details: " ..
                    result)
        end
        return result or {}
    end
end

local function ensureQuestTrackerState()
    if g_game.getClientVersion() < 1280 then
        return false
    end

    if not namePlayer or namePlayer == "" then
        local characterName = g_game.getCharacterName()
        if not characterName or characterName == "" then
            return false
        end

        namePlayer = characterName:lower()
    end

    if not questTrackerSettingsLoaded then
        settings = load() or {}
        questTrackerSettingsLoaded = true

        g_logger.debug(string.format(
            "[QuestTracker] loaded local cache player=%s missions=%d autoTrack=%s autoUntrack=%s",
            namePlayer,
            settings[namePlayer] and #settings[namePlayer] or 0,
            settings.autoTrackNewQuests == true and "true" or "false",
            settings.autoUntrackCompleted == true and "true" or "false"
        ))
    end

    if settings.autoTrackNewQuests == nil then
        settings.autoTrackNewQuests = false
    end
    if settings.autoUntrackCompleted == nil then
        settings.autoUntrackCompleted = false
    end
    if not settings[namePlayer] then
        settings[namePlayer] = {}
    end

    return true
end

local function save()
    if not namePlayer or namePlayer == "" then
        g_logger.debug("[QuestTracker] skipped local cache save without player name")
        return false
    end

    local status, result = pcall(function()
        return json.encode(settings, 2)
    end)
    if not status then
        g_logger.error("Error while saving quest tracker settings. Data won't be saved. Details: " .. result)
        return false
    end
    if result:len() > 100 * 1024 * 1024 then
        g_logger.error("Something went wrong, quest tracker settings file is above 100MB, won't be saved")
        return false
    end

    if not g_resources.directoryExists(settingsDirectory) then
        g_resources.makeDir(settingsDirectory)
    end
    
    local writeStatus, writeResult = pcall(function()
        return g_resources.writeFileContents(file, result)
    end)
    
    if not writeStatus then
        g_logger.error("Could not save quest tracker settings: " .. tostring(writeResult))
        return false
    end

    if not writeResult then
        g_logger.error("Could not save quest tracker settings: writeFileContents returned false")
        return false
    end

    g_logger.debug(string.format(
        "[QuestTracker] saved local cache player=%s missions=%d autoTrack=%s autoUntrack=%s",
        namePlayer or "",
        namePlayer and settings[namePlayer] and #settings[namePlayer] or 0,
        settings.autoTrackNewQuests == true and "true" or "false",
        settings.autoUntrackCompleted == true and "true" or "false"
    ))

    return true
end

local function deferredSave()
    if saveTimer then
        saveTimer:cancel()
    end

    saveTimer = scheduleEvent(function()
        saveTimer = nil
        save()
    end, 500)
end

local sortFunctions = {
    ["Alphabetically (A-Z)"] = function(a, b)
        return a:getText() < b:getText()
    end,
    ["Alphabetically (Z-A)"] = function(a, b)
        return a:getText() > b:getText()
    end,
    ["Completed on Top"] = function(a, b)
        local aCompleted = a.isComplete or false
        local bCompleted = b.isComplete or false

        if aCompleted and not bCompleted then
            return true
        elseif not aCompleted and bCompleted then
            return false
        else
            return a:getText() < b:getText()
        end
    end,
    ["Completed on Bottom"] = function(a, b)
        local aCompleted = a.isComplete or false
        local bCompleted = b.isComplete or false

        if aCompleted and not bCompleted then
            return false
        elseif not aCompleted and bCompleted then
            return true
        else
            return a:getText() < b:getText()
        end
    end
}

local function sendQuestTracker(listToMap)
    if not ensureQuestTrackerState() then
        g_logger.debug("[QuestTracker] skipped 0xD0 send before local cache was loaded")
        return
    end

    local missionIds = {}
    local added = {}

    for _, entry in ipairs(listToMap or {}) do
        local missionId = tonumber(entry[1])
        if missionId and not added[missionId] then
            missionIds[#missionIds + 1] = missionId
            added[missionId] = true

            if #missionIds >= 255 then
                break
            end
        end
    end

    local extra = math.random(0, 0xff)

    g_logger.debug(string.format(
        "[QuestTracker] sending 0xD0 payload count=%d autoTrack=%s autoUntrack=%s extra=0x%02X",
        #missionIds,
        settings.autoTrackNewQuests == true and "true" or "false",
        settings.autoUntrackCompleted == true and "true" or "false",
        extra
    ))

    g_game.sendRequestTrackerQuestLog(
        missionIds,
        settings.autoTrackNewQuests == true,
        settings.autoUntrackCompleted == true,
        extra
    )
end

local function rebuildTrackerFromSettings()
    if not trackerMiniWindow or not settings[namePlayer] then
        return
    end
    
    trackerMiniWindow.contentsPanel.list:destroyChildren()
    
    for i, entry in ipairs(settings[namePlayer]) do
        local missionId, missionName, missionDescription, questId = unpack(entry)
        
        if not questId or questId == 0 then
            questId = missionToQuestMap[tonumber(missionId)] or 0
        end
        
        local trackerLabel = g_ui.createWidget('QuestTrackerLabel', trackerMiniWindow.contentsPanel.list)
        trackerLabel:setId(tostring(missionId))
        trackerLabel.questId = questId
        trackerLabel.missionId = missionId
        trackerLabel.description:setText(missionDescription or missionName)
    end
end

local function findQuestIdForMission(missionId)
    -- Try to find the questId by looking through all quest items and their missions
    if not UITextList.questLogList then
        return nil
    end
    
    for i = 1, UITextList.questLogList:getChildCount() do
        local questItem = UITextList.questLogList:getChildByIndex(i)
        local questId = questItem:getId()
        
        -- We'd need to request each quest line to check, but that would be too expensive
        -- For now, return nil and rely on server data
    end
    
    return nil
end

local function debugTrackerLabels()
    if not trackerMiniWindow or not trackerMiniWindow.contentsPanel or not trackerMiniWindow.contentsPanel.list then
        return
    end
    
    local childCount = trackerMiniWindow.contentsPanel.list:getChildCount()
    
    for i = 1, childCount do
        local child = trackerMiniWindow.contentsPanel.list:getChildByIndex(i)
        local questId = child.questId
        local missionId = child.missionId
        local widgetId = child:getId()
        local description = child.description:getText()
    end
end

local function destroyWindows(windows)
    if type(windows) == "table" then
        for _, window in pairs(windows) do
            if window and not window:isDestroyed() then
                window:destroy()
            end
        end
    else
        if windows and not windows:isDestroyed() then
            windows:destroy()
        end
    end
    return nil
end

local function resetItemCategorySelection(list)
    for _, child in pairs(list:getChildren()) do
        child:setChecked(false)
        child:setBackgroundColor(child.BaseColor)
        if child.iconShow then
            child.iconShow:setVisible(child.isHiddenQuestLog)
        end
        if child.iconPin then
            child.iconPin:setVisible(child.isPinned)
        end
    end
end

local function createQuestItem(parent, id, text, color, icon)
    local item = g_ui.createWidget("QuestLogLabel", parent)
    item:setId(id)
    item:setText(text)
    item:setBackgroundColor(color)
    item:setPhantom(false)
    item:setFocusable(true)
    item.BaseColor = color
    item.isPinned = false
    item.isComplete = false
    if icon then
        item:setIcon(icon)
    end
    if parent == UITextList.questLogList then
        table.insert(questLogCache.items, item)
        if icon ~= "" then
            item.isComplete = true
            questLogCache.completed = questLogCache.completed + 1
        end
    end
    return item
end

local function updateQuestCounter()
    UIlabel.numberQuestComplete:setText(questLogCache.completed)
    UIlabel.numberQuestHidden:setText(questLogCache.hidden)
end

local function recolorVisibleItems()
    local categoryColor = COLORS.BASE_1
    local visibleIndex = 0

    for _, item in pairs(questLogCache.items) do
        if item:isVisible() then
            visibleIndex = visibleIndex + 1
            item:setBackgroundColor(visibleIndex % 2 == 1 and COLORS.BASE_1 or COLORS.BASE_2)
            item.BaseColor = item:getBackgroundColor()
        end
    end
end

local function sortQuestList(questList, sortOrder)
    questLogController.currentSortOrder = sortOrder
    local pinnedItems = {}
    local regularItems = {}
    for _, child in pairs(questLogCache.items) do
        if child.isPinned then
            table.insert(pinnedItems, child)
        else
            table.insert(regularItems, child)
        end
    end
    local sortFunc = sortFunctions[sortOrder]
    if sortFunc then
        table.sort(regularItems, sortFunc)
    end
    questLogCache.items = {}
    local index = 1
    for _, item in ipairs(pinnedItems) do
        questList:moveChildToIndex(item, index)
        table.insert(questLogCache.items, item)
        index = index + 1
    end
    for _, item in ipairs(regularItems) do
        questList:moveChildToIndex(item, index)
        table.insert(questLogCache.items, item)
        index = index + 1
    end
    recolorVisibleItems()
    updateQuestCounter()
end


local function setupQuestItemClickHandler(item, isQuestList)
    function item:onClick()
        local list = isQuestList and UITextList.questLogList or UITextList.questLogLine
        resetItemCategorySelection(list)
        self:setChecked(true)
        self:setBackgroundColor(COLORS.SELECTED)
        if isQuestList then
            g_game.requestQuestLine(self:getId())
            self.iconShow:setVisible(true)
            self.iconPin:setVisible(true)
            questLogController.ui.panelQuestLineSelected:setText(self:getText())
        else
            UITextList.questLogInfo:setText(self.description)
            -- Update the tracker checkbox state for the selected mission (but not during navigation)
            if not isNavigating then
                local playerName = namePlayer or g_game.getCharacterName():lower()
                local missionId = tonumber(self:getId())
                
                -- Simple check: is this specific mission ID in our tracked list?
                local isThisMissionTracked = false
                if settings[playerName] and settings[playerName] then
                    for _, entry in ipairs(settings[playerName]) do
                        if entry[1] == missionId then
                            isThisMissionTracked = true
                            break
                        end
                    end
                end
                
                -- Set checkbox state WITHOUT triggering events
                isUpdatingCheckbox = true
                UICheckBox.showInQuestTracker:setChecked(isThisMissionTracked)
                isUpdatingCheckbox = false
            else
                -- Skipping checkbox update during navigation
            end
        end
    end

    if isQuestList then
        function item.iconPin:onClick(mousePos)
            local parent = self:getParent()
            parent.isPinned = not parent.isPinned
            if parent.isPinned then
                self:setImageColor("#00ff00")
                local list = UITextList.questLogList
                list:removeChild(parent)
                list:insertChild(1, parent)

                table.removevalue(questLogCache.items, parent)
                table.insert(questLogCache.items, 1, parent)
                recolorVisibleItems()
            else
                self:setImageColor("#ffffff")
                self:setVisible(false)
                sortQuestList(UITextList.questLogList, questLogController.currentSortOrder or "Alphabetically (A-Z)")
            end
            return true
        end

        function item.iconShow:onClick(mousePos, mouseButton)
            local parent = self:getParent()
            parent.isHiddenQuestLog = not parent.isHiddenQuestLog
            if parent.isHiddenQuestLog then
                questLogCache.hidden = questLogCache.hidden + 1
                self:setImageColor("#ff0000")
                if not UICheckBox.showShidden:isChecked() then
                    parent:setVisible(false)
                    questLogCache.visible = questLogCache.visible - 1
                end
            else
                questLogCache.hidden = questLogCache.hidden - 1
                self:setImageColor("#ffffff")
                if UICheckBox.showShidden:isChecked() then
                    parent:setVisible(false)
                    questLogCache.visible = questLogCache.visible - 1
                else
                    local isCompleted = parent.isComplete
                    local shouldBeVisible = UICheckBox.showComplete:isChecked() or not isCompleted
                    parent:setVisible(shouldBeVisible)
                    if shouldBeVisible then
                        questLogCache.visible = questLogCache.visible + 1
                    end
                end
            end

            if parent.iconShow then
                parent.iconShow:setVisible(parent.isHiddenQuestLog)
            end
            if parent.iconPin then
                parent.iconPin:setVisible(parent.isPinned)
            end

            updateQuestCounter()
            recolorVisibleItems()
            return true
        end
    end
end

--[[=================================================
=                        Windows                     =
=================================================== ]] --
local function hide()
    if not questLogController.ui then
        return
    end
    questLogController.ui:hide()
    if questLogButton then
        questLogButton:setOn(false)
    end
end

function show()
    if not questLogController.ui then
        return
    end
    g_game.requestQuestLog()
    questLogController.ui:show()
    questLogController.ui:raise()
    questLogController.ui:focus()
    if questLogButton then
        questLogButton:setOn(true)
    end
end

local function toggle()
    if not questLogController.ui then
        return
    end
    if questLogController.ui:isVisible() then
        return hide()
    end
    show()
end

local function toggleTracker()
    if trackerMiniWindow:isOn() then
        trackerMiniWindow:close()
    else
        if not trackerMiniWindow:getParent() then
            local panel = modules.game_interface
                              .findContentPanelAvailable(trackerMiniWindow, trackerMiniWindow:getMinimumHeight())
            if not panel then
                return
            end
            panel:addChild(trackerMiniWindow)
        end
        trackerMiniWindow:open()
    end
end
--[[=================================================
=                        miniWindows                     =
=================================================== ]] --
function onOpenTracker()
    if buttonQuestLogTrackerButton then
        buttonQuestLogTrackerButton:setOn(true)
    end
end

function onCloseTracker()
    if buttonQuestLogTrackerButton then
        buttonQuestLogTrackerButton:setOn(false)
    end
end

local function isCompletedMissionText(text)
    if not text then
        return false
    end

    return string.find(string.lower(text), "%(completed%)") ~= nil
end

local function ensureQuestTrackerButton()
    if buttonQuestLogTrackerButton then
        return
    end

    buttonQuestLogTrackerButton = modules.game_mainpanel.addToggleButton("QuestLogTracker",
        tr("Open QuestLog Tracker"), "/images/options/button_questlog_tracker", function()
            questLogController:toggleMiniWindowsTracker()
        end, false, 1001)
end

local function showQuestTracker()
    ensureQuestTrackerButton()

    if trackerMiniWindow then
        toggleTracker()
        return
    end
    trackerMiniWindow = g_ui.createWidget('QuestLogTracker')
    
    -- Hide all standard miniwindow buttons that we don't want
    local toggleFilterButton = trackerMiniWindow:recursiveGetChildById('toggleFilterButton')
    if toggleFilterButton then
        toggleFilterButton:setVisible(false)
    end
    
    -- Hide the custom menuButton since we'll use the standard contextMenuButton
    local menuButton = trackerMiniWindow:getChildById('menuButton')
    if menuButton then
        menuButton:setVisible(false)
    end
    
    -- Set up the miniwindow title and icon
    local titleWidget = trackerMiniWindow:getChildById('miniwindowTitle')
    if titleWidget then
        titleWidget:setText('Quest Tracker')
    else
        -- Fallback to old method if miniwindowTitle doesn't exist
        trackerMiniWindow:setText('Quest Tracker')
    end
    
    local iconWidget = trackerMiniWindow:getChildById('miniwindowIcon')
    if iconWidget then
        iconWidget:setImageSource('/images/topbuttons/icon-questtracker-widget')
    end
    
    -- Position contextMenuButton where toggleFilterButton was (to the left of minimize button)
    local contextMenuButton = trackerMiniWindow:recursiveGetChildById('contextMenuButton')
    local minimizeButton = trackerMiniWindow:recursiveGetChildById('minimizeButton')
    
    if contextMenuButton and minimizeButton then
        contextMenuButton:setVisible(true)
        contextMenuButton:breakAnchors()
        contextMenuButton:addAnchor(AnchorTop, minimizeButton:getId(), AnchorTop)
        contextMenuButton:addAnchor(AnchorRight, minimizeButton:getId(), AnchorLeft)
        contextMenuButton:setMarginRight(7)  -- Same margin as toggleFilterButton had
        contextMenuButton:setMarginTop(0)
        contextMenuButton:setSize({width = 12, height = 12})
    end
    
    -- Position newWindowButton to the left of contextMenuButton
    local newWindowButton = trackerMiniWindow:recursiveGetChildById('newWindowButton')
    
    if newWindowButton and contextMenuButton then
        newWindowButton:setVisible(true)
        newWindowButton:breakAnchors()
        newWindowButton:addAnchor(AnchorTop, contextMenuButton:getId(), AnchorTop)
        newWindowButton:addAnchor(AnchorRight, contextMenuButton:getId(), AnchorLeft)
        newWindowButton:setMarginRight(2)  -- Same margin as other buttons
        newWindowButton:setMarginTop(0)
    end
    
    -- Position lockButton to the left of newWindowButton
    local lockButton = trackerMiniWindow:recursiveGetChildById('lockButton')
    
    if lockButton and newWindowButton then
        lockButton:breakAnchors()
        lockButton:addAnchor(AnchorTop, newWindowButton:getId(), AnchorTop)
        lockButton:addAnchor(AnchorRight, newWindowButton:getId(), AnchorLeft)
        lockButton:setMarginRight(2)  -- Same margin as other buttons
        lockButton:setMarginTop(0)
    end

    -- Set up contextMenuButton click handler (moved from menuButton)
    if contextMenuButton then
        contextMenuButton.onClick = function(widget, mousePos)
            local menu = g_ui.createWidget('PopupMenu')
            menu:setGameMenu(true)
            menu:addOption('Remove All quest', function()
                if settings[namePlayer] then
                    -- Store the mission IDs that are being removed for checkbox updates
                    local removedMissionIds = {}
                    for _, entry in ipairs(settings[namePlayer]) do
                        local missionId = entry[1]
                        table.insert(removedMissionIds, missionId)
                    end
                    
                    -- Clear the settings and mapping
                    table.clear(settings[namePlayer])
                    table.clear(missionToQuestMap)  -- Clear the mapping as well
                    save()
                    sendQuestTracker(settings[namePlayer])
                    
                    -- Clear the tracker display
                    trackerMiniWindow.contentsPanel.list:destroyChildren()
                    
                    -- Update the checkbox in Quest Log window if it's open and a mission is selected
                    if questLogController.ui and questLogController.ui:isVisible() then
                        if UITextList.questLogLine and UITextList.questLogLine:hasChildren() then
                            -- Update checkbox for any currently selected mission
                            if UITextList.questLogLine:getFocusedChild() then
                                local currentMissionId = tonumber(UITextList.questLogLine:getFocusedChild():getId())
                                isUpdatingCheckbox = true
                                UICheckBox.showInQuestTracker:setChecked(false)
                                isUpdatingCheckbox = false
                            end
                            
                            -- Force refresh of checkbox state for all visible missions
                            -- This ensures that when user navigates to other missions, they show correct state
                        end
                    end
                    
                    -- Update layouts
                    trackerMiniWindow.contentsPanel.list:getLayout():enableUpdates()
                    trackerMiniWindow.contentsPanel.list:getLayout():update()
                end
            end)
            menu:addOption('Remove completed quests', function()
                if not settings[namePlayer] then
                    return
                end

                local removedMissionIds = {}

                for i = #settings[namePlayer], 1, -1 do
                    local missionId, missionName, missionDescription = unpack(settings[namePlayer][i])
                    local trackerLabel = trackerMiniWindow.contentsPanel.list:getChildById(tostring(missionId))
                    local trackerText = trackerLabel and trackerLabel.description and trackerLabel.description:getText() or nil

                    if isCompletedMissionText(missionName) or isCompletedMissionText(missionDescription) or isCompletedMissionText(trackerText) then
                        removedMissionIds[#removedMissionIds + 1] = missionId
                        table.remove(settings[namePlayer], i)
                        missionToQuestMap[tonumber(missionId)] = nil

                        if trackerLabel then
                            trackerLabel:destroy()
                        end
                    end
                end

                if #removedMissionIds == 0 then
                    return
                end

                if questLogController.ui and questLogController.ui:isVisible() and UITextList.questLogLine and UITextList.questLogLine:hasChildren() then
                    local focusedChild = UITextList.questLogLine:getFocusedChild()
                    if focusedChild then
                        local currentMissionId = tonumber(focusedChild:getId())
                        for _, removedId in ipairs(removedMissionIds) do
                            if currentMissionId == removedId then
                                isUpdatingCheckbox = true
                                UICheckBox.showInQuestTracker:setChecked(false)
                                isUpdatingCheckbox = false
                                break
                            end
                        end
                    end
                end

                trackerMiniWindow.contentsPanel.list:getLayout():enableUpdates()
                trackerMiniWindow.contentsPanel.list:getLayout():update()

                save()
                sendQuestTracker(settings[namePlayer])
            end)
            menu:addSeparator()
            menu:addCheckBox('Automatically track new quests', settings.autoTrackNewQuests or false, function(widget, checked)
                settings.autoTrackNewQuests = checked
                save()
                sendQuestTracker(settings[namePlayer] or {})
            end)
            menu:addCheckBox('Automatically untrack completed quests', settings.autoUntrackCompleted or false, function(widget, checked)
                settings.autoUntrackCompleted = checked
                save()
                sendQuestTracker(settings[namePlayer] or {})
            end)

            menu:display(mousePos)
            return true
        end
    end
    
    -- Set up newWindowButton click handler to open Quest Log window
    if newWindowButton then
        newWindowButton.onClick = function()
            show()
            return true
        end
    end
    
    trackerMiniWindow:setContentMinimumHeight(80)
    trackerMiniWindow:setup()
    
    -- Rebuild tracker from saved settings when first created
    if not isReceivingQuestTracker and settings[namePlayer] and #settings[namePlayer] > 0 then
        rebuildTrackerFromSettings()
    end
    
    toggleTracker()

end

--[[=================================================
=                      onParse                      =
=================================================== ]] --
local function onQuestLog(questList)
    UITextList.questLogList:destroyChildren()

    questLogCache = {
        items = {},
        completed = 0,
        hidden = 0,
        visible = #questList
    }

    local categoryColor = COLORS.BASE_1
    for _, data in pairs(questList) do
        local id, questName, questCompleted = unpack(data)
        if _ == 2 and true then
            questCompleted = false
        end
        local icon = questCompleted and "/game_cyclopedia/images/checkmark-icon" or ""
        local itemCat = createQuestItem(UITextList.questLogList, id, questName, categoryColor, icon)
        setupQuestItemClickHandler(itemCat, true)
        categoryColor = categoryColor == COLORS.BASE_1 and COLORS.BASE_2 or COLORS.BASE_1
    end
    sortQuestList(UITextList.questLogList, "Alphabetically (A-Z)")
    updateQuestCounter()
end

local function onQuestLine(questId, questMissions)
    currentQuestId = questId  -- Store the current quest ID
    UITextList.questLogLine:destroyChildren()
    
    -- Always start with checkbox unchecked when loading a new quest line
    isUpdatingCheckbox = true
    UICheckBox.showInQuestTracker:setChecked(false)
    isUpdatingCheckbox = false
    
    local categoryColor = COLORS.BASE_1
    for _, data in pairs(questMissions) do
        local missionName, missionDescription, missionId = unpack(data)
        local itemCat = createQuestItem(UITextList.questLogLine, missionId, missionName, categoryColor)
        itemCat.description = missionDescription
        setupQuestItemClickHandler(itemCat, false)
        categoryColor = categoryColor == COLORS.BASE_1 and COLORS.BASE_2 or COLORS.BASE_1
    end
    
    -- Auto-select the first mission but prevent checkbox updates during this automatic selection
    if UITextList.questLogLine:hasChildren() then
        local firstChild = UITextList.questLogLine:getChildByIndex(1)
        if firstChild then
            -- Set navigation flag to prevent checkbox updates during automatic selection
            isNavigating = true
            firstChild:onClick()  -- This will show the mission description but won't update checkbox
            -- Reset navigation flag after a brief delay
            scheduleEvent(function()
                isNavigating = false
            end, 100)
        end
    end
end

local function onQuestTracker(remainingQuests, missions)
    if not ensureQuestTrackerState() then
        return
    end

    if not trackerMiniWindow then
        isReceivingQuestTracker = true
        showQuestTracker()
        isReceivingQuestTracker = false
    end

    trackerMiniWindow.contentsPanel.list:destroyChildren()
    table.clear(settings[namePlayer])
    table.clear(missionToQuestMap)

    if not missions then
        save()
        return
    end

    for _, mission in ipairs(missions) do
        local questId, missionId, questName, missionName, missionDesc = unpack(mission)
        questId = tonumber(questId) or 0
        missionId = tonumber(missionId)

        if missionId then
            missionToQuestMap[missionId] = questId
            settings[namePlayer][#settings[namePlayer] + 1] = {missionId, missionName, missionDesc or missionName, questId}

            local trackerLabel = g_ui.createWidget('QuestTrackerLabel', trackerMiniWindow.contentsPanel.list)
            trackerLabel:setId(tostring(missionId))
            trackerLabel.questId = questId
            trackerLabel.missionId = missionId
            trackerLabel.description:setText(missionDesc or missionName)
        end
    end

    deferredSave()
end

local function onUpdateQuestTracker(questId, missionId, questName, missionName, missionDesc)
    if not ensureQuestTrackerState() then
        return
    end

    if not trackerMiniWindow then
        isReceivingQuestTracker = true
        showQuestTracker()
        isReceivingQuestTracker = false
    end

    questId = tonumber(questId) or 0
    missionId = tonumber(missionId)
    if not missionId then
        return
    end

    missionToQuestMap[missionId] = questId

    local trackerLabel = trackerMiniWindow.contentsPanel.list:getChildById(tostring(missionId))
    if not trackerLabel then
        trackerLabel = g_ui.createWidget('QuestTrackerLabel', trackerMiniWindow.contentsPanel.list)
        trackerLabel:setId(tostring(missionId))
    end

    trackerLabel.questId = questId
    trackerLabel.missionId = missionId
    trackerLabel.description:setText(missionDesc or missionName)

    local updated = false
    for i, entry in ipairs(settings[namePlayer]) do
        if entry[1] == missionId then
            settings[namePlayer][i] = {missionId, missionName, missionDesc or missionName, questId}
            updated = true
            break
        end
    end

    if not updated then
        settings[namePlayer][#settings[namePlayer] + 1] = {missionId, missionName, missionDesc or missionName, questId}
    end

    deferredSave()
end

--[[=================================================
=               onCall otui / html                  =
=================================================== ]] --
function filterQuestList(searchText)
    local showComplete = UICheckBox.showComplete:isChecked()
    local showHidden = UICheckBox.showShidden:isChecked()
    local searchPattern = searchText and string.lower(searchText) or nil
    questLogCache.visible = 0
    for _, child in pairs(questLogCache.items) do
        local isCompleted = child.isComplete
        local isHidden = child.isHiddenQuestLog
        local text = child:getText()
        local visible = true
        if searchPattern and text then
            visible = string.find(string.lower(text), searchPattern) ~= nil
        end
        if not showComplete and isCompleted then
            visible = false
        end
        if showHidden then
            visible = visible and isHidden
        else
            visible = visible and not isHidden
        end
        child:setVisible(visible)
        if visible then
            questLogCache.visible = questLogCache.visible + 1
        end
        if child.iconShow then
            child.iconShow:setVisible(child.isHiddenQuestLog)
        end
    end
    recolorVisibleItems()
end

function questLogController:onCheckChangeQuestTracker(event)
    -- Ignore checkbox changes during navigation or when we're just updating the display
    if isNavigating then
        return
    end
    
    if isUpdatingCheckbox then
        return
    end

    if not ensureQuestTrackerState() then
        return
    end
    
    -- Make sure tracker window exists
    if not trackerMiniWindow then
        showQuestTracker()
        if not trackerMiniWindow then
            return
        end
    end
    
    -- Make sure we have a selected mission
    if not UITextList.questLogLine:hasChildren() or not UITextList.questLogLine:getFocusedChild() then
        return
    end
    
    local focusedChild = UITextList.questLogLine:getFocusedChild()
    local missionId = tonumber(focusedChild:getId())
    local missionName = focusedChild:getText()
    local missionDescription = focusedChild.description or missionName
    
    -- Make sure we have a valid currentQuestId
    if not currentQuestId or currentQuestId == 0 then
        if UITextList.questLogList and UITextList.questLogList:getFocusedChild() then
            currentQuestId = tonumber(UITextList.questLogList:getFocusedChild():getId())
        end
        
        if not currentQuestId or currentQuestId == 0 then
            return
        end
    end
    
    if event.checked then
        -- User wants to TRACK this mission
        
        -- Update the mission to quest mapping
        missionToQuestMap[missionId] = currentQuestId
        
        -- Ensure tracker window is visible
        if not trackerMiniWindow:isVisible() then
            showQuestTracker()
        end
        
        -- Add to our settings
        addUniqueIdQuest(namePlayer, currentQuestId, missionId, missionName, missionDescription)
        
        -- Add to tracker display (if not already there)
        local existingLabel = trackerMiniWindow.contentsPanel.list:getChildById(tostring(missionId))
        if not existingLabel then
            local trackerLabel = g_ui.createWidget('QuestTrackerLabel', trackerMiniWindow.contentsPanel.list)
            trackerLabel:setId(tostring(missionId))
            trackerLabel.questId = currentQuestId
            trackerLabel.missionId = missionId
            trackerLabel.description:setText(missionDescription)
        else
            -- Update existing label
            existingLabel.questId = currentQuestId
            existingLabel.missionId = missionId
            existingLabel.description:setText(missionDescription)
        end
        
    else
        -- User wants to UNTRACK this mission
        
        -- Remove from our settings
        removeNumber(namePlayer, missionId)
        
        -- Remove from tracker display
        local trackerLabel = trackerMiniWindow.contentsPanel.list:getChildById(tostring(missionId))
        if trackerLabel then
            trackerLabel:destroy()
        end
        
        -- Remove from mapping
        missionToQuestMap[missionId] = nil
    end
    
    -- Send updated tracker state to server and save
    save()
    if settings[namePlayer] then
        sendQuestTracker(settings[namePlayer])
    end
end

function questLogController:onFilterQuestLog(event)
    if sortFunctions[event.text] then
        sortQuestList(UITextList.questLogList, event.text)
    end
end

function questLogController:close()
    hide()
end

function questLogController:toggleMiniWindowsTracker()
    if not trackerMiniWindow then
        showQuestTracker()
        return
    end
    if trackerMiniWindow:isVisible() then
        if buttonQuestLogTrackerButton then
            buttonQuestLogTrackerButton:setOn(false)
        end
        return trackerMiniWindow:hide()
    end
    if buttonQuestLogTrackerButton then
        buttonQuestLogTrackerButton:setOn(true)
    end
    showQuestTracker()
end

function questLogController:filterQuestListShowComplete()
    filterQuestList()
end

function questLogController:filterQuestListShowHidden()
    filterQuestList()
end

function onSearchTextChange(text)
    if text and text:len() > 0 then
        filterQuestList(text)
    else
        filterQuestList()
    end
end

function onQuestLogMousePress(widget, mousePos, mouseButton)
    if mouseButton ~= MouseRightButton then
        return
    end
    local menu = g_ui.createWidget('PopupMenu')
    menu:setGameMenu(true)
    menu:addOption(tr('remove'), function()
        local missionId = widget:getParent():getId()  -- This is actually the missionId, not questId
        removeNumber(namePlayer, missionId)
        save()
        if settings[namePlayer] then
            sendQuestTracker(settings[namePlayer])
        end
        widget:getParent():destroy()
        
        -- Also remove from the mapping
        if missionToQuestMap[tonumber(missionId)] then
            missionToQuestMap[tonumber(missionId)] = nil
        end
        
        -- Update the checkbox in the quest log if that mission is currently selected
        if UITextList.questLogLine:hasChildren() and UITextList.questLogLine:getFocusedChild() then
            local currentId = UITextList.questLogLine:getFocusedChild():getId()
            if tostring(currentId) == tostring(missionId) then
                isUpdatingCheckbox = true
                UICheckBox.showInQuestTracker:setChecked(false)
                isUpdatingCheckbox = false
            end
        end
    end)
    menu:display(mousePos)
    return true
end

function onQuestTrackerDescriptionClick(widget, mousePos, mouseButton)
    if mouseButton == MouseRightButton then
        -- Handle right-click for context menu (same as before)
        return onQuestLogMousePress(widget, mousePos, mouseButton)
    elseif mouseButton == MouseLeftButton then
        -- Handle left-click to open Quest Log and navigate to the quest
        local trackerLabel = widget:getParent()
        local questId = trackerLabel.questId
        local missionId = trackerLabel.missionId
        
        -- Try to get questId from mapping if not available on the label
        if (not questId or questId == 0) and missionId then
            questId = missionToQuestMap[tonumber(missionId)]
            if questId then
                -- Update the label for future use
                trackerLabel.questId = questId
            end
        end
        
        local labelIndex = trackerLabel:getParent():getChildIndex(trackerLabel)
        
        -- Always open the Quest Log window
        show()
        
        if questId and questId ~= 0 and missionId then
            -- We have both quest ID and mission ID - do full navigation
            -- Create a function to check if quest list is populated and navigate
            local function attemptNavigation(attempts)
                attempts = attempts or 0
                if attempts > 20 then  -- Max 2 seconds of attempts
                    return
                end
                
                scheduleEvent(function()
                    if UITextList.questLogList and UITextList.questLogList:getChildCount() > 0 then
                        -- Quest list is populated, try to find our quest
                        
                        local questItem = UITextList.questLogList:getChildById(tostring(questId))
                        if questItem then
                            -- Found the quest, click it to load missions
                            questItem:onClick()
                            
                            -- Now wait for the mission list to be populated
                            local function attemptMissionSelection(missionAttempts)
                                missionAttempts = missionAttempts or 0
                                if missionAttempts > 10 then  -- Max 1 second for mission selection
                                    return
                                end
                                
                                scheduleEvent(function()
                                    if UITextList.questLogLine and UITextList.questLogLine:getChildCount() > 0 then
                                        
                                        local missionItem = UITextList.questLogLine:getChildById(tostring(missionId))
                                        if missionItem then
                                            -- Clear the navigation flag temporarily to allow checkbox update
                                            isNavigating = false
                                            missionItem:onClick()  -- Select the specific mission and update checkbox
                                            
                                            -- Since this mission is from the tracker, ensure checkbox is checked
                                            scheduleEvent(function()
                                                isUpdatingCheckbox = true
                                                UICheckBox.showInQuestTracker:setChecked(true)
                                                isUpdatingCheckbox = false
                                            end, 50)
                                        else
                                            -- Mission not found yet, try again
                                            attemptMissionSelection(missionAttempts + 1)
                                        end
                                    else
                                        -- Mission list not populated yet, try again
                                        attemptMissionSelection(missionAttempts + 1)
                                    end
                                end, 100)
                            end
                            
                            -- Start attempting mission selection
                            attemptMissionSelection()
                        else
                            attemptNavigation(attempts + 1)
                        end
                    else
                        -- Quest list not populated yet, try again
                        attemptNavigation(attempts + 1)
                    end
                end, 100)
            end
            
            -- Start attempting navigation
            attemptNavigation()
        else
            -- Fallback: just open the Quest Log (maybe for old tracked quests without quest ID)
        end
        return true
    end
    return false
end

--[[=================================================
=               Controller                     =
=================================================== ]] --
function questLogController:onInit()
    g_ui.importStyle("styles/game_questlog.otui")
    questLogController:loadHtml('game_questlog.html')
    hide()

    UITextList.questLogList = questLogController.ui.panelQuestLog.areaPanelQuestList.questList
    UITextList.questLogLine = questLogController.ui.panelQuestLineSelected.ScrollAreaQuestList.questList
    UITextList.questLogInfo = questLogController.ui.panelQuestLineSelected.panelQuestInfo.questList
    UITextList.questLogInfo:setBackgroundColor('#363636')

    UITextEdit.search = questLogController.ui.panelQuestLog.textEditSearchQuest
    UIlabel.numberQuestComplete = questLogController:findWidget("#lblCompleteNumber")
    UIlabel.numberQuestHidden = questLogController:findWidget("#lblHiddenNumber")
    UICheckBox.showComplete = questLogController:findWidget("#checkboxShowComplete")
    UICheckBox.showShidden = questLogController:findWidget("#checkboxShowHidden")
    UICheckBox.showInQuestTracker = questLogController.ui.panelQuestLineSelected.checkboxShowInQuestTracker

    questLogController:registerEvents(g_game, {
        onQuestLog = onQuestLog,
        onQuestLine = onQuestLine,
        onQuestTracker = onQuestTracker,
        onUpdateQuestTracker = onUpdateQuestTracker
    })

    questLogButton = modules.game_mainpanel.addToggleButton('questLogButton', tr('Quest Log'),
        '/images/options/button_questlog', function()
            toggle()
        end, false, 1000)
    Keybind.new("Windows", "Show/hide quest Log", "", "")
    Keybind.bind("Windows", "Show/hide quest Log", {{
        type = KEY_DOWN,
        callback = function()
            show()
        end
    }})
end

function questLogController:onTerminate()
    questLogButton, trackerMiniWindow, buttonQuestLogTrackerButton = destroyWindows(
        {questLogButton, trackerMiniWindow, buttonQuestLogTrackerButton})
    Keybind.delete("Windows", "Show/hide quest Log")
end

function questLogController:onGameStart()
    if g_game.getClientVersion() >= 1280 then
        if not ensureQuestTrackerState() then
            return
        end

        ensureQuestTrackerButton()

        if trackerMiniWindow then
            trackerMiniWindow:setupOnStart()
            -- Rebuild tracker from saved settings
            rebuildTrackerFromSettings()
        end

        -- Send local tracker cache on login, matching the official client behavior.
        -- The server reconciles this payload during the initial sync window.
        sendQuestTracker(settings[namePlayer])
    else
        UICheckBox.showInQuestTracker:setVisible(false)
        questLogController.ui.buttonsPanel.trackerButton:setVisible(false)
    end
end

function questLogController:onGameEnd()
    if g_game.getClientVersion() >= 1280 then
        save()
    end
    hide()
    if trackerMiniWindow then
        trackerMiniWindow:setParent(nil, true)
    end
    -- Clear the mission to quest mapping
    missionToQuestMap = {}
    questTrackerSettingsLoaded = false
    namePlayer = ""
end
