local _, Addon = ...

local Routes = {}
Addon.Routes = Routes
local L = Addon.L

local MAX_IMPORT_BYTES = 2 * 1024 * 1024
local MAX_PULLS = 300
local MAX_ASSIGNMENTS = 15000
local MAX_ROUTES_PER_DUNGEON = 100

local fallbackColors = {
    "ff3eff",
    "3eff9e",
    "3e9eff",
    "fffb3e",
    "ff3e3e",
    "a83eff",
    "ff9b3e",
    "3efff2",
}

local routeLookup
local lookupRoute
local progressByRoute = setmetatable({}, { __mode = "k" })
local candidateCache = setmetatable({}, { __mode = "k" })
local trackedUnits = {}
local importFrame
local importEditBox
local importStatus
local currentRouteText
local visibleCheck
local clearButton
local resetProgressButton
local routeNameEditBox
local renameButton
local dungeonSelector
local routeSelector
local dialogDungeonIndex

local function RefreshRouteDisplay()
    routeLookup = nil
    lookupRoute = nil
    if Addon.Markers and Addon.Markers.RefreshContentMarkers then
        Addon.Markers:RefreshContentMarkers()
    end
    if Addon.AddonButtons and Addon.AddonButtons.UpdateRouteButton then
        Addon.AddonButtons:UpdateRouteButton()
    end
end

local function NormalizeColor(color, pullIndex)
    if type(color) == "string" then
        color = string.lower(string.gsub(color, "#", ""))
        if #color == 6 and not string.find(color, "[^0-9a-f]") then
            return color
        end
    end
    return fallbackColors[((pullIndex - 1) % #fallbackColors) + 1]
end

local function IsPositiveInteger(value)
    return type(value) == "number" and value >= 1 and value == math.floor(value)
end

local function DecodePreset(text)
    if type(text) ~= "string" then
        return nil, L.ROUTE_PASTE
    end

    text = string.gsub(strtrim(text), "%s+", "")
    if text == "" then
        return nil, L.ROUTE_PASTE
    end
    if #text > MAX_IMPORT_BYTES then
        return nil, L.ROUTE_TOO_LARGE
    end
    if string.sub(text, 1, 1) ~= "!" then
        return nil, L.ROUTE_FORMAT_OLD
    end

    local deflate = LibStub and LibStub:GetLibrary("LibDeflate", true)
    local serializer = LibStub and LibStub:GetLibrary("AceSerializer-3.0", true)
    if not deflate or not serializer then
        return nil, L.ROUTE_LIBRARIES_FAILED
    end

    local ok, decoded = pcall(deflate.DecodeForPrint, deflate, string.sub(text, 2))
    if not ok or not decoded then
        return nil, L.ROUTE_DECODE_FAILED
    end

    local decompressed
    ok, decompressed = pcall(deflate.DecompressDeflate, deflate, decoded)
    if not ok or not decompressed then
        return nil, L.ROUTE_DAMAGED
    end

    local success, preset
    ok, success, preset = pcall(serializer.Deserialize, serializer, decompressed)
    if not ok or not success or type(preset) ~= "table" then
        return nil, L.ROUTE_INVALID_PRESET
    end
    return preset
end

local function SanitizePreset(preset)
    local value = preset and preset.value
    local dungeonIndex = value and tonumber(value.currentDungeonIdx)
    local sourcePulls = value and value.pulls
    if not IsPositiveInteger(dungeonIndex) or type(sourcePulls) ~= "table" then
        return nil, L.ROUTE_INVALID
    end

    local data = Addon.DungeonEnemyData
    local dungeon = data and data.dungeons and data.dungeons[dungeonIndex]
    if not dungeon then
        return nil, L.ROUTE_DUNGEON_UNSUPPORTED
    end

    local pullCount = #sourcePulls
    if pullCount < 1 or pullCount > MAX_PULLS then
        return nil, L.ROUTE_INVALID_PULL_COUNT
    end

    local route = {
        name = type(preset.text) == "string" and string.sub(string.gsub(preset.text, "|", ""), 1, 80)
            or L.ROUTE_DEFAULT_NAME,
        uid = type(preset.uid) == "string" and string.sub(string.gsub(preset.uid, "|", ""), 1, 80)
            or nil,
        dungeonIndex = dungeonIndex,
        dungeonName = dungeon.name,
        addonVersion = tonumber(preset.addonVersion),
        pulls = {},
    }
    local assignments = 0

    for pullIndex, sourcePull in ipairs(sourcePulls) do
        if type(sourcePull) ~= "table" then
            return nil, Addon:Format("ROUTE_INVALID_PULL", pullIndex)
        end
        local pull = {
            color = NormalizeColor(sourcePull.color, pullIndex),
            enemies = {},
        }
        for enemyIndex, sourceClones in pairs(sourcePull) do
            if IsPositiveInteger(enemyIndex) and type(sourceClones) == "table" then
                local clones = {}
                local seen = {}
                for _, cloneIndex in ipairs(sourceClones) do
                    if IsPositiveInteger(cloneIndex) and not seen[cloneIndex] then
                        clones[#clones + 1] = cloneIndex
                        seen[cloneIndex] = true
                        assignments = assignments + 1
                        if assignments > MAX_ASSIGNMENTS then
                            return nil, L.ROUTE_TOO_MANY_ENEMIES
                        end
                    end
                end
                if #clones > 0 then
                    pull.enemies[enemyIndex] = clones
                end
            end
        end
        route.pulls[pullIndex] = pull
    end

    route.assignmentCount = assignments
    return route
end

local function BuildLookup(route)
    if lookupRoute == route and routeLookup then
        return routeLookup
    end

    routeLookup = {}
    lookupRoute = route
    for pullIndex, pull in ipairs(route and route.pulls or {}) do
        for enemyIndex, clones in pairs(pull.enemies or {}) do
            for _, cloneIndex in ipairs(clones) do
                local key = tostring(enemyIndex) .. ":" .. tostring(cloneIndex)
                if not routeLookup[key] then
                    routeLookup[key] = {
                        pull = pullIndex,
                        color = pull.color,
                    }
                end
            end
        end
    end
    return routeLookup
end

local function IsValidStoredRoute(route, dungeonIndex)
    return type(route) == "table"
        and IsPositiveInteger(route.dungeonIndex)
        and route.dungeonIndex == dungeonIndex
        and type(route.pulls) == "table"
end

local function EnsureStorage()
    if not Addon.db then
        return
    end
    if type(Addon.db.routesByDungeon) ~= "table" then
        Addon.db.routesByDungeon = {}
    end
    if type(Addon.db.selectedRouteByDungeon) ~= "table" then
        Addon.db.selectedRouteByDungeon = {}
    end

    -- Migrate the single route used by 0.10.0 without losing the user's import.
    local oldRoute = Addon.db.route
    if type(oldRoute) == "table" and IsPositiveInteger(oldRoute.dungeonIndex) then
        local dungeonIndex = oldRoute.dungeonIndex
        local routes = Addon.db.routesByDungeon[dungeonIndex]
        if type(routes) ~= "table" then
            routes = {}
            Addon.db.routesByDungeon[dungeonIndex] = routes
        end
        if IsValidStoredRoute(oldRoute, dungeonIndex) then
            routes[#routes + 1] = oldRoute
            Addon.db.selectedRouteByDungeon[dungeonIndex] = #routes
            Addon.db.routeDungeonSelection = dungeonIndex
        end
    end
    Addon.db.route = nil

    if tonumber(Addon.db.routeLibraryVersion) ~= 1 then
        for dungeonIndex, routes in pairs(Addon.db.routesByDungeon) do
            if not IsPositiveInteger(dungeonIndex) or type(routes) ~= "table" then
                Addon.db.routesByDungeon[dungeonIndex] = nil
                Addon.db.selectedRouteByDungeon[dungeonIndex] = nil
            else
                for routeIndex = #routes, 1, -1 do
                    if not IsValidStoredRoute(routes[routeIndex], dungeonIndex) then
                        table.remove(routes, routeIndex)
                    end
                end
                if #routes == 0 then
                    Addon.db.routesByDungeon[dungeonIndex] = nil
                    Addon.db.selectedRouteByDungeon[dungeonIndex] = nil
                else
                    local selected = tonumber(Addon.db.selectedRouteByDungeon[dungeonIndex]) or 1
                    Addon.db.selectedRouteByDungeon[dungeonIndex] = Addon:Clamp(
                        math.floor(selected),
                        1,
                        #routes
                    )
                end
            end
        end
        Addon.db.routeLibraryVersion = 1
    end
end

function Routes:InitializeStorage()
    EnsureStorage()
end

function Routes:GetRoutes(dungeonIndex)
    EnsureStorage()
    return Addon.db.routesByDungeon[dungeonIndex]
end

function Routes:GetSelectedRoute(dungeonIndex)
    local routes = self:GetRoutes(dungeonIndex)
    if type(routes) ~= "table" or #routes == 0 then
        return nil
    end
    local routeIndex = tonumber(Addon.db.selectedRouteByDungeon[dungeonIndex]) or 1
    routeIndex = Addon:Clamp(math.floor(routeIndex), 1, #routes)
    Addon.db.selectedRouteByDungeon[dungeonIndex] = routeIndex
    return routes[routeIndex], routeIndex
end

function Routes:GetCurrentDungeonIndex()
    local mapID = Addon.state and Addon.state.mapID
    if not mapID and C_Map and C_Map.GetBestMapForUnit then
        mapID = C_Map.GetBestMapForUnit("player")
    end
    return Addon.DungeonEnemyData
        and Addon.DungeonEnemyData.maps
        and Addon.DungeonEnemyData.maps[mapID]
end

function Routes:GetDungeonList()
    EnsureStorage()
    local dungeons = {}
    for dungeonIndex, dungeon in pairs(Addon.DungeonEnemyData.dungeons or {}) do
        dungeons[#dungeons + 1] = {
            index = dungeonIndex,
            name = dungeon.name or ("Dungeon " .. tostring(dungeonIndex)),
            routeCount = #(Addon.db.routesByDungeon[dungeonIndex] or {}),
        }
    end
    table.sort(dungeons, function(left, right)
        if (left.routeCount > 0) ~= (right.routeCount > 0) then
            return left.routeCount > 0
        end
        return string.lower(left.name) < string.lower(right.name)
    end)
    return dungeons
end

function Routes:GetDialogDungeonIndex()
    EnsureStorage()
    local dungeons = Addon.DungeonEnemyData.dungeons or {}
    if dialogDungeonIndex and dungeons[dialogDungeonIndex] then
        return dialogDungeonIndex
    end
    local saved = tonumber(Addon.db.routeDungeonSelection)
    local current = self:GetCurrentDungeonIndex()
    dialogDungeonIndex = dungeons[current] and current
        or (dungeons[saved] and saved)
        or (self:GetDungeonList()[1] and self:GetDungeonList()[1].index)
    return dialogDungeonIndex
end

function Routes:SelectDungeon(dungeonIndex)
    if not Addon.DungeonEnemyData.dungeons[dungeonIndex] then
        return false
    end
    dialogDungeonIndex = dungeonIndex
    Addon.db.routeDungeonSelection = dungeonIndex
    if importFrame and importFrame:IsShown() then
        self:RefreshImportDialog()
    end
    return true
end

function Routes:SelectRoute(dungeonIndex, routeIndex)
    local routes = self:GetRoutes(dungeonIndex)
    if type(routes) ~= "table" or not routes[routeIndex] then
        return false
    end
    Addon.db.selectedRouteByDungeon[dungeonIndex] = routeIndex
    self:SelectDungeon(dungeonIndex)
    RefreshRouteDisplay()
    return true
end

function Routes:RenameSelected(name)
    local dungeonIndex = self:GetDialogDungeonIndex()
    local route = dungeonIndex and self:GetSelectedRoute(dungeonIndex)
    name = type(name) == "string" and strtrim(string.gsub(name, "|", "")) or ""
    if not route or name == "" then
        return nil, L.ROUTE_ENTER_NAME
    end
    route.name = string.sub(name, 1, 80)
    RefreshRouteDisplay()
    if importFrame and importFrame:IsShown() then
        self:RefreshImportDialog()
    end
    return route
end

function Routes:HasRoutes()
    EnsureStorage()
    for _, routes in pairs(Addon.db.routesByDungeon) do
        if type(routes) == "table" and #routes > 0 then
            return true
        end
    end
    return false
end

function Routes:GetRouteCount()
    EnsureStorage()
    local count = 0
    for _, routes in pairs(Addon.db.routesByDungeon) do
        if type(routes) == "table" then
            count = count + #routes
        end
    end
    return count
end

function Routes:ImportString(text)
    local preset, errorMessage = DecodePreset(text)
    if not preset then
        return nil, errorMessage
    end

    local route
    route, errorMessage = SanitizePreset(preset)
    if not route then
        return nil, errorMessage
    end

    EnsureStorage()
    local routes = Addon.db.routesByDungeon[route.dungeonIndex]
    if type(routes) ~= "table" then
        routes = {}
        Addon.db.routesByDungeon[route.dungeonIndex] = routes
    end

    local routeIndex
    if route.uid then
        for index, savedRoute in ipairs(routes) do
            if savedRoute.uid == route.uid then
                routeIndex = index
                break
            end
        end
    end
    if routeIndex then
        routes[routeIndex] = route
    else
        if #routes >= MAX_ROUTES_PER_DUNGEON then
            return nil, L.ROUTE_TOO_MANY_SAVED
        end
        routes[#routes + 1] = route
        routeIndex = #routes
    end

    Addon.db.selectedRouteByDungeon[route.dungeonIndex] = routeIndex
    Addon.db.routeDungeonSelection = route.dungeonIndex
    dialogDungeonIndex = route.dungeonIndex
    Addon.db.showRoute = true
    Addon.db.showAdds = true
    RefreshRouteDisplay()
    if Addon.AddonButtons and Addon.AddonButtons.UpdateAddsButton then
        Addon.AddonButtons:UpdateAddsButton()
    end
    return route
end

function Routes:GetRoute()
    EnsureStorage()
    local currentDungeonIndex = self:GetCurrentDungeonIndex()
    local route = currentDungeonIndex and self:GetSelectedRoute(currentDungeonIndex)
    if route then
        return route
    end
    local dungeonIndex = self:GetDialogDungeonIndex()
    route = dungeonIndex and self:GetSelectedRoute(dungeonIndex)
    if route then
        return route
    end
    for _, dungeonInfo in ipairs(self:GetDungeonList()) do
        if dungeonInfo.routeCount > 0 then
            return self:GetSelectedRoute(dungeonInfo.index)
        end
    end
end

function Routes:GetRouteForMap(mapID)
    if not Addon.db.showRoute then
        return nil
    end
    local dungeonIndex = Addon.DungeonEnemyData
        and Addon.DungeonEnemyData.maps
        and Addon.DungeonEnemyData.maps[mapID]
    return dungeonIndex and self:GetSelectedRoute(dungeonIndex)
end

function Routes:GetPullAssignment(route, enemyIndex, cloneIndex)
    if not route or not enemyIndex or not cloneIndex then
        return nil
    end
    return BuildLookup(route)[tostring(enemyIndex) .. ":" .. tostring(cloneIndex)]
end

local function EnemyKey(enemyIndex, cloneIndex)
    return tostring(enemyIndex) .. ":" .. tostring(cloneIndex)
end

local function BuildRouteCandidates(route)
    if candidateCache[route] then
        return candidateCache[route]
    end

    local result = { all = {}, byNPC = {} }
    local data = Addon.DungeonEnemyData
    local dungeon = data.dungeons[route.dungeonIndex]
    local enemyIndices = data.enemyIndices and data.enemyIndices[route.dungeonIndex]
    local dungeonCloneIndices = data.cloneIndices and data.cloneIndices[route.dungeonIndex]
    for enemyPosition, enemy in ipairs(dungeon and dungeon.enemies or {}) do
        local enemyIndex = enemyIndices and enemyIndices[enemyPosition] or enemyPosition
        local cloneIndices = dungeonCloneIndices and dungeonCloneIndices[enemyPosition]
        for clonePosition in ipairs(enemy.clones or {}) do
            local cloneIndex = cloneIndices and cloneIndices[clonePosition] or clonePosition
            local assignment = Routes:GetPullAssignment(route, enemyIndex, cloneIndex)
            if assignment then
                local candidate = {
                    key = EnemyKey(enemyIndex, cloneIndex),
                    enemyIndex = enemyIndex,
                    cloneIndex = cloneIndex,
                    npcID = enemy.npcID,
                    pull = assignment.pull,
                }
                result.all[#result.all + 1] = candidate
                result.byNPC[enemy.npcID] = result.byNPC[enemy.npcID] or {}
                result.byNPC[enemy.npcID][#result.byNPC[enemy.npcID] + 1] = candidate
            end
        end
    end
    table.sort(result.all, function(left, right)
        if left.pull ~= right.pull then
            return left.pull < right.pull
        end
        if left.enemyIndex ~= right.enemyIndex then
            return left.enemyIndex < right.enemyIndex
        end
        return left.cloneIndex < right.cloneIndex
    end)
    for _, candidates in pairs(result.byNPC) do
        table.sort(candidates, function(left, right)
            if left.pull ~= right.pull then
                return left.pull < right.pull
            end
            return left.cloneIndex < right.cloneIndex
        end)
    end
    candidateCache[route] = result
    return result
end

local function GetProgress(route, create)
    local progress = route and progressByRoute[route]
    if not progress and route and create then
        progress = { killed = {}, guids = {}, killedCount = 0 }
        progressByRoute[route] = progress
    end
    return progress
end

function Routes:IsEnemyKilled(route, enemyIndex, cloneIndex)
    local progress = GetProgress(route, false)
    return progress and progress.killed[EnemyKey(enemyIndex, cloneIndex)] or false
end

function Routes:GetKilledCount(route)
    local progress = GetProgress(route, false)
    return progress and progress.killedCount or 0
end

function Routes:GetTrackableCount(route)
    return route and #BuildRouteCandidates(route).all or 0
end

function Routes:ResetProgress(route)
    route = route or self:GetRoute()
    if not route then
        return false
    end
    progressByRoute[route] = nil
    if Addon.Markers and Addon.Markers.ScheduleContentRefresh then
        Addon.Markers:ScheduleContentRefresh()
    end
    if importFrame and importFrame:IsShown() then
        self:RefreshImportDialog()
    end
    return true
end

local function FindNearestCandidate(route, candidates, mapID, playerX, playerY)
    if not playerX or not playerY or not Addon.DungeonEnemies then
        return candidates[1]
    end
    local enemies = Addon.DungeonEnemies:GetEnemies(mapID)
    local positions = {}
    for _, enemy in ipairs(enemies or {}) do
        positions[EnemyKey(enemy.enemyIndex, enemy.cloneIndex)] = enemy
    end
    local nearest
    local nearestDistance
    for _, candidate in ipairs(candidates) do
        local enemy = positions[candidate.key]
        if enemy then
            local deltaX = enemy.mapX - playerX
            local deltaY = enemy.mapY - playerY
            local distance = (deltaX * deltaX) + (deltaY * deltaY)
            if not nearestDistance or distance < nearestDistance then
                nearest = candidate
                nearestDistance = distance
            end
        end
    end
    return nearest or candidates[1]
end

function Routes:MarkEnemyKilled(npcID, guid)
    local dungeonIndex = self:GetCurrentDungeonIndex()
    local route = dungeonIndex and self:GetSelectedRoute(dungeonIndex)
    if not route then
        return nil
    end
    local progress = GetProgress(route, true)
    if guid and progress.guids[guid] then
        return nil
    end

    local available = {}
    local earliestPull
    for _, candidate in ipairs(BuildRouteCandidates(route).byNPC[npcID] or {}) do
        if not progress.killed[candidate.key] then
            if not earliestPull then
                earliestPull = candidate.pull
            end
            if candidate.pull == earliestPull then
                available[#available + 1] = candidate
            end
        end
    end
    if #available == 0 then
        return nil
    end

    local mapID = Addon.state and Addon.state.mapID
    local playerX, playerY
    if mapID and Addon.Coordinates and Addon.Coordinates.GetUnitMapPosition then
        local ok
        ok, playerX, playerY = pcall(
            Addon.Coordinates.GetUnitMapPosition,
            Addon.Coordinates,
            mapID,
            "player"
        )
        if not ok then
            playerX, playerY = nil, nil
        end
    end
    local candidate = FindNearestCandidate(route, available, mapID, playerX, playerY)
    progress.killed[candidate.key] = true
    progress.killedCount = progress.killedCount + 1
    if guid then
        progress.guids[guid] = true
    end
    if Addon.Markers and Addon.Markers.ScheduleContentRefresh then
        Addon.Markers:ScheduleContentRefresh()
    end
    if importFrame and importFrame:IsShown() then
        self:RefreshImportDialog()
    end
    return candidate
end

local function GetNPCIDFromGUID(guid)
    if not guid or Addon:IsSecret(guid) or type(guid) ~= "string" then
        return
    end
    local unitType, _, _, _, _, npcID = strsplit("-", guid)
    if unitType ~= "Creature" and unitType ~= "Vehicle" then
        return
    end
    return tonumber(npcID)
end

local function IsTrackedUnitDead(unit)
    if UnitIsDeadOrGhost then
        local ok, dead = pcall(UnitIsDeadOrGhost, unit)
        if ok and not Addon:IsSecret(dead) and dead == true then
            return true
        end
    end
    if UnitHealth then
        local ok, health = pcall(UnitHealth, unit)
        if ok and not Addon:IsSecret(health) and type(health) == "number" and health <= 0 then
            return true
        end
    end
    return false
end

function Routes:TrackUnit(unit)
    if type(unit) ~= "string" or not UnitGUID then
        return nil
    end
    local ok, guid = pcall(UnitGUID, unit)
    local npcID = ok and GetNPCIDFromGUID(guid)
    if not npcID then
        trackedUnits[unit] = nil
        return nil
    end
    trackedUnits[unit] = { guid = guid, npcID = npcID }
    return trackedUnits[unit]
end

function Routes:HandleTrackedUnitHealth(unit)
    local tracked = trackedUnits[unit]
    if not tracked then
        if type(unit) ~= "string"
            or (unit ~= "target" and not string.match(unit, "^nameplate%d+$")) then
            return nil
        end
        tracked = self:TrackUnit(unit)
    end
    if not tracked or not IsTrackedUnitDead(unit) then
        return nil
    end
    trackedUnits[unit] = nil
    return self:MarkEnemyKilled(tracked.npcID, tracked.guid)
end

function Routes:HandleTrackedUnitRemoved(unit)
    local killed = self:HandleTrackedUnitHealth(unit)
    trackedUnits[unit] = nil
    return killed
end

function Routes:ClearTrackedUnits()
    for unit in pairs(trackedUnits) do
        trackedUnits[unit] = nil
    end
end

function Routes:HandleUnitDied(guid)
    local npcID = GetNPCIDFromGUID(guid)
    if npcID then
        return self:MarkEnemyKilled(npcID, guid)
    end
end

function Routes:HandleCombatLogEvent()
    local _, subevent, _, _, _, _, _, destinationGUID = CombatLogGetCurrentEventInfo()
    if subevent == "UNIT_DIED" then
        self:HandleUnitDied(destinationGUID)
    end
end

function Routes:GetBossPullAssignment(route, dungeonIndex, npcID, displayID)
    local dungeon = Addon.DungeonEnemyData
        and Addon.DungeonEnemyData.dungeons
        and Addon.DungeonEnemyData.dungeons[dungeonIndex]
    local bestAssignment
    for _, boss in ipairs(dungeon and dungeon.bosses or {}) do
        local matches = (npcID and boss.npcID == npcID)
            or (displayID and boss.displayID == displayID)
        if matches then
            local assignment = self:GetPullAssignment(route, boss.enemyIndex, boss.cloneIndex)
            if assignment and (not bestAssignment or assignment.pull < bestAssignment.pull) then
                bestAssignment = assignment
            end
        end
    end
    return bestAssignment
end

function Routes:GetColor(color)
    color = NormalizeColor(color, 1)
    return tonumber(string.sub(color, 1, 2), 16) / 255,
        tonumber(string.sub(color, 3, 4), 16) / 255,
        tonumber(string.sub(color, 5, 6), 16) / 255
end

function Routes:GetTextColor(color)
    local red, green, blue = self:GetColor(color)
    if (red * 0.299) + (green * 0.587) + (blue * 0.114) > 0.62 then
        return 0.03, 0.04, 0.05
    end
    return 1, 1, 1
end

function Routes:SetVisible(visible)
    Addon.db.showRoute = not not visible
    RefreshRouteDisplay()
    if importFrame and importFrame:IsShown() then
        self:RefreshImportDialog()
    end
end

function Routes:Clear(dungeonIndex)
    dungeonIndex = dungeonIndex or self:GetDialogDungeonIndex()
    local routes = dungeonIndex and self:GetRoutes(dungeonIndex)
    if type(routes) ~= "table" or #routes == 0 then
        return nil
    end
    local routeIndex = tonumber(Addon.db.selectedRouteByDungeon[dungeonIndex]) or 1
    routeIndex = Addon:Clamp(math.floor(routeIndex), 1, #routes)
    local removed = table.remove(routes, routeIndex)
    progressByRoute[removed] = nil
    candidateCache[removed] = nil
    if #routes == 0 then
        Addon.db.routesByDungeon[dungeonIndex] = nil
        Addon.db.selectedRouteByDungeon[dungeonIndex] = nil
    else
        Addon.db.selectedRouteByDungeon[dungeonIndex] = math.min(routeIndex, #routes)
    end
    routeLookup = nil
    lookupRoute = nil
    RefreshRouteDisplay()
    if importFrame and importFrame:IsShown() then
        self:RefreshImportDialog()
    end
    return removed
end

local function SetImportStatus(message, isError)
    importStatus:SetText(message or "")
    if isError then
        importStatus:SetTextColor(1, 0.3, 0.25)
    else
        importStatus:SetTextColor(0.35, 0.9, 0.7)
    end
end

local function ImportFromEditBox()
    local route, errorMessage = Routes:ImportString(importEditBox:GetText())
    if not route then
        SetImportStatus(errorMessage, true)
        return
    end
    importEditBox:SetText("")
    SetImportStatus(L.ROUTE_ADDED_SELECTED, false)
    Routes:RefreshImportDialog()
    Addon:Print(Addon:Format(
        "ROUTE_IMPORTED",
        route.name,
        #route.pulls,
        route.assignmentCount
    ))
end

local function RenameFromEditBox()
    local route, errorMessage = Routes:RenameSelected(routeNameEditBox:GetText())
    if not route then
        SetImportStatus(errorMessage, true)
        return
    end
    routeNameEditBox:ClearFocus()
    SetImportStatus(L.ROUTE_NAME_SAVED, false)
end

local function Shorten(text, maximum)
    text = tostring(text or "")
    if #text > maximum then
        local cutoff = maximum - 3
        while cutoff > 0 do
            local nextByte = string.byte(text, cutoff + 1)
            if not nextByte or nextByte < 128 or nextByte >= 192 then
                break
            end
            cutoff = cutoff - 1
        end
        return string.sub(text, 1, cutoff) .. "..."
    end
    return text
end

local function SetSelectorText(button, text)
    button:SetText(Shorten(text, 34) .. "  |cffb8c7d9▼|r")
end

local function FormatRouteCount(count)
    return string.format("%d %s", count, count == 1 and L.ROUTE_SINGULAR or L.ROUTE_PLURAL)
end

local function OpenDungeonSelector()
    local dungeons = Routes:GetDungeonList()
    if MenuUtil and MenuUtil.CreateContextMenu then
        MenuUtil.CreateContextMenu(dungeonSelector, function(_, rootDescription)
            rootDescription:CreateTitle(L.DUNGEON)
            for _, dungeonInfo in ipairs(dungeons) do
                local dungeonIndex = dungeonInfo.index
                local label = string.format("%s (%d)", dungeonInfo.name, dungeonInfo.routeCount)
                rootDescription:CreateRadio(
                    label,
                    function()
                        return Routes:GetDialogDungeonIndex() == dungeonIndex
                    end,
                    function()
                        Routes:SelectDungeon(dungeonIndex)
                    end
                )
            end
        end)
        return
    end

    for index, dungeonInfo in ipairs(dungeons) do
        if dungeonInfo.index == Routes:GetDialogDungeonIndex() then
            local nextDungeon = dungeons[(index % #dungeons) + 1]
            Routes:SelectDungeon(nextDungeon.index)
            return
        end
    end
end

local function OpenRouteSelector()
    local dungeonIndex = Routes:GetDialogDungeonIndex()
    local routes = Routes:GetRoutes(dungeonIndex) or {}
    if #routes == 0 then
        return
    end
    local _, selectedIndex = Routes:GetSelectedRoute(dungeonIndex)
    if MenuUtil and MenuUtil.CreateContextMenu then
        MenuUtil.CreateContextMenu(routeSelector, function(_, rootDescription)
            rootDescription:CreateTitle(L.ROUTE)
            for routeIndex, route in ipairs(routes) do
                local index = routeIndex
                local label = Addon:Format("ROUTE_WITH_PULLS", route.name, #route.pulls)
                rootDescription:CreateRadio(
                    label,
                    function()
                        return select(2, Routes:GetSelectedRoute(dungeonIndex)) == index
                    end,
                    function()
                        Routes:SelectRoute(dungeonIndex, index)
                    end
                )
            end
        end)
        return
    end

    Routes:SelectRoute(dungeonIndex, (selectedIndex % #routes) + 1)
end

local function CreateImportDialog()
    importFrame = CreateFrame("Frame", "MapaEnMiniaturaRouteImportFrame", UIParent, "BackdropTemplate")
    importFrame:SetSize(500, 370)
    importFrame:SetPoint("CENTER")
    importFrame:SetFrameStrata("DIALOG")
    importFrame:SetFrameLevel(80)
    importFrame:SetClampedToScreen(true)
    importFrame:SetToplevel(true)
    importFrame:SetBackdrop({
        bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background-Dark",
        edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
        tile = true,
        tileSize = 16,
        edgeSize = 14,
        insets = { left = 4, right = 4, top = 4, bottom = 4 },
    })
    importFrame:SetBackdropColor(0.012, 0.018, 0.026, 0.98)
    importFrame:SetBackdropBorderColor(0.2, 0.72, 0.9, 1)
    importFrame:EnableMouse(true)
    importFrame:SetMovable(true)
    importFrame:RegisterForDrag("LeftButton")
    importFrame:SetScript("OnDragStart", importFrame.StartMoving)
    importFrame:SetScript("OnDragStop", importFrame.StopMovingOrSizing)

    local title = importFrame:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
    title:SetPoint("TOPLEFT", 18, -16)
    title:SetText(L.ROUTES_WINDOW_TITLE)

    local close = CreateFrame("Button", nil, importFrame, "UIPanelCloseButton")
    close:SetPoint("TOPRIGHT", -3, -3)

    local dungeonLabel = importFrame:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    dungeonLabel:SetPoint("TOPLEFT", 18, -50)
    dungeonLabel:SetText(L.DUNGEON)

    dungeonSelector = CreateFrame("Button", nil, importFrame, "UIPanelButtonTemplate")
    dungeonSelector:SetSize(222, 26)
    dungeonSelector:SetPoint("TOPLEFT", dungeonLabel, "BOTTOMLEFT", 0, -5)
    dungeonSelector:SetScript("OnClick", OpenDungeonSelector)

    local routeLabel = importFrame:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    routeLabel:SetPoint("TOPLEFT", 260, -50)
    routeLabel:SetText(L.ROUTE)

    routeSelector = CreateFrame("Button", nil, importFrame, "UIPanelButtonTemplate")
    routeSelector:SetSize(222, 26)
    routeSelector:SetPoint("TOPLEFT", routeLabel, "BOTTOMLEFT", 0, -5)
    routeSelector:SetScript("OnClick", OpenRouteSelector)

    currentRouteText = importFrame:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    currentRouteText:SetPoint("TOPLEFT", 18, -111)
    currentRouteText:SetPoint("RIGHT", importFrame, "RIGHT", -18, 0)
    currentRouteText:SetJustifyH("LEFT")

    local routeNameLabel = importFrame:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    routeNameLabel:SetPoint("TOPLEFT", 18, -143)
    routeNameLabel:SetText(L.ROUTE_NAME)

    renameButton = CreateFrame("Button", nil, importFrame, "UIPanelButtonTemplate")
    renameButton:SetSize(90, 24)
    renameButton:SetPoint("TOPRIGHT", -18, -164)
    renameButton:SetText(L.RENAME)
    renameButton:SetScript("OnClick", RenameFromEditBox)

    routeNameEditBox = CreateFrame("EditBox", nil, importFrame, "InputBoxTemplate")
    routeNameEditBox:SetPoint("TOPLEFT", routeNameLabel, "BOTTOMLEFT", 5, -6)
    routeNameEditBox:SetPoint("RIGHT", renameButton, "LEFT", -10, 0)
    routeNameEditBox:SetHeight(30)
    routeNameEditBox:SetAutoFocus(false)
    routeNameEditBox:SetMaxLetters(80)
    routeNameEditBox:SetScript("OnEnterPressed", RenameFromEditBox)
    routeNameEditBox:SetScript("OnEscapePressed", function(editBox)
        editBox:ClearFocus()
        Routes:RefreshImportDialog()
    end)

    local importLabel = importFrame:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    importLabel:SetPoint("TOPLEFT", 18, -213)
    importLabel:SetText(L.IMPORT_NEW_ROUTE)

    importEditBox = CreateFrame("EditBox", nil, importFrame, "InputBoxTemplate")
    importEditBox:SetPoint("TOPLEFT", importLabel, "BOTTOMLEFT", 5, -6)
    importEditBox:SetPoint("RIGHT", importFrame, "RIGHT", -24, 0)
    importEditBox:SetHeight(30)
    importEditBox:SetAutoFocus(false)
    importEditBox:SetMaxBytes(MAX_IMPORT_BYTES)
    importEditBox:SetScript("OnEscapePressed", function()
        importFrame:Hide()
    end)
    importEditBox:SetScript("OnEnterPressed", ImportFromEditBox)
    importEditBox:SetScript("OnTextChanged", function(_, userInput)
        if userInput then
            SetImportStatus("", false)
        end
    end)

    importStatus = importFrame:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    importStatus:SetPoint("TOPLEFT", importEditBox, "BOTTOMLEFT", -5, -10)
    importStatus:SetPoint("RIGHT", importFrame, "RIGHT", -18, 0)
    importStatus:SetHeight(30)
    importStatus:SetJustifyH("LEFT")
    importStatus:SetJustifyV("TOP")

    visibleCheck = CreateFrame("CheckButton", nil, importFrame, "UICheckButtonTemplate")
    visibleCheck:SetPoint("BOTTOMLEFT", 14, 14)
    local visibleLabel = importFrame:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
    visibleLabel:SetPoint("LEFT", visibleCheck, "RIGHT", 2, 0)
    visibleLabel:SetText(L.SHOW_PULLS)
    visibleCheck:SetScript("OnClick", function(button)
        Routes:SetVisible(button:GetChecked())
    end)

    local importButton = CreateFrame("Button", nil, importFrame, "UIPanelButtonTemplate")
    importButton:SetSize(90, 24)
    importButton:SetPoint("BOTTOMRIGHT", -18, 16)
    importButton:SetText(L.IMPORT)
    importButton:SetScript("OnClick", ImportFromEditBox)

    clearButton = CreateFrame("Button", nil, importFrame, "UIPanelButtonTemplate")
    clearButton:SetSize(100, 24)
    clearButton:SetPoint("RIGHT", importButton, "LEFT", -8, 0)
    clearButton:SetText(L.DELETE_ROUTE)
    clearButton:SetScript("OnClick", function()
        local removed = Routes:Clear()
        importEditBox:SetText("")
        SetImportStatus(
            removed and Addon:Format("ROUTE_DELETED", removed.name) or L.NO_SELECTED_ROUTE,
            not removed
        )
    end)

    resetProgressButton = CreateFrame("Button", nil, importFrame, "UIPanelButtonTemplate")
    resetProgressButton:SetSize(112, 24)
    resetProgressButton:SetPoint("RIGHT", clearButton, "LEFT", -8, 0)
    resetProgressButton:SetText(L.RESET_KILLS)
    resetProgressButton:SetScript("OnClick", function()
        local dungeonIndex = Routes:GetDialogDungeonIndex()
        local route = dungeonIndex and Routes:GetSelectedRoute(dungeonIndex)
        local reset = route and Routes:ResetProgress(route) or false
        SetImportStatus(
            reset and L.KILL_PROGRESS_RESET or L.NO_SELECTED_ROUTE,
            not reset
        )
    end)

    table.insert(UISpecialFrames, importFrame:GetName())
    importFrame:Hide()
end

function Routes:RefreshImportDialog()
    if not importFrame then
        return
    end
    local dungeonIndex = self:GetDialogDungeonIndex()
    local dungeon = dungeonIndex and Addon.DungeonEnemyData.dungeons[dungeonIndex]
    local routes = dungeonIndex and self:GetRoutes(dungeonIndex) or {}
    local route, routeIndex
    if dungeonIndex then
        route, routeIndex = self:GetSelectedRoute(dungeonIndex)
    end
    SetSelectorText(
        dungeonSelector,
        string.format("%s (%s)", dungeon and dungeon.name or L.NO_DUNGEON, FormatRouteCount(#routes))
    )
    if route then
        currentRouteText:SetText(Addon:Format(
            "SELECTED_ROUTE_STATUS",
            Shorten(route.name, 48),
            #route.pulls,
            self:GetKilledCount(route),
            self:GetTrackableCount(route)
        ))
        SetSelectorText(routeSelector, string.format("%d. %s", routeIndex, route.name))
    else
        currentRouteText:SetText(L.NO_ROUTES_DUNGEON)
        SetSelectorText(routeSelector, L.NO_ROUTES)
    end
    routeSelector:SetEnabled(route ~= nil)
    routeNameEditBox:SetText(route and route.name or "")
    routeNameEditBox:SetEnabled(route ~= nil)
    renameButton:SetEnabled(route ~= nil)
    visibleCheck:SetChecked(Addon.db.showRoute)
    visibleCheck:SetEnabled(self:HasRoutes())
    clearButton:SetEnabled(route ~= nil)
    resetProgressButton:SetEnabled(route ~= nil and self:GetKilledCount(route) > 0)
end

function Routes:OpenImportDialog()
    if not importFrame then
        CreateImportDialog()
    end
    local currentDungeonIndex = self:GetCurrentDungeonIndex()
    if currentDungeonIndex then
        self:SelectDungeon(currentDungeonIndex)
    end
    self:RefreshImportDialog()
    importEditBox:SetText("")
    SetImportStatus(L.ROUTE_PASTE_AND_IMPORT, false)
    importFrame:Show()
    importFrame:Raise()
    importEditBox:SetFocus()
end
