--------------------------------------------------------------------------------
-- Nanami-UI: Flight Map (FlightMap.lua)
-- Skins TaxiFrame with Nanami-UI theme, destination list, in-flight timer
--------------------------------------------------------------------------------

SFrames = SFrames or {}
SFrames.FlightMap = {}
local FM = SFrames.FlightMap
SFramesDB = SFramesDB or {}
SFramesGlobalDB = SFramesGlobalDB or {}

--------------------------------------------------------------------------------
-- Theme (Pink Cat-Paw)
--------------------------------------------------------------------------------
local T = SFrames.Theme:Extend({
    currentText   = { 0.40, 1.0, 0.40 },
    moneyGold     = { 1, 0.84, 0.0 },
    moneySilver   = { 0.78, 0.78, 0.78 },
    moneyCopper   = { 0.71, 0.43, 0.18 },
    arrivedText   = { 0.40, 1.0, 0.40 },
})

--------------------------------------------------------------------------------
-- Layout
--------------------------------------------------------------------------------
local DEST_PANEL_W  = 220
local DEST_ROW_H    = 22
local HEADER_H      = 34
local SIDE_PAD      = 10
local MAX_DEST_ROWS = 30

local BAR_W         = 160
local TRACK_H       = 170
local TRACK_X       = 20

--------------------------------------------------------------------------------
-- State
--------------------------------------------------------------------------------
local skinApplied   = false
local DestPanel     = nil
local DestRows      = {}
local FlightBar     = nil

local flightState = {
    pendingFlight = false,
    inFlight      = false,
    source        = "",
    dest          = "",
    startTime     = 0,
    estimated     = 0,
    lingerTime    = 0,
}

FM.currentSource = ""

--------------------------------------------------------------------------------
-- Helpers
--------------------------------------------------------------------------------
local function GetFont()
    if SFrames and SFrames.GetFont then return SFrames:GetFont() end
    return "Fonts\\ARIALN.TTF"
end

local function FormatMoney(copper)
    if not copper or copper <= 0 then return 0, 0, 0 end
    local g = math.floor(copper / 10000)
    local s = math.floor(math.mod(copper, 10000) / 100)
    local c = math.mod(copper, 100)
    return g, s, c
end

local function FormatTime(seconds)
    if not seconds or seconds < 0 then seconds = 0 end
    local m = math.floor(seconds / 60)
    local s = math.floor(math.mod(seconds, 60))
    return string.format("%d:%02d", m, s)
end

local function StripTextures(frame)
    if not frame or not frame.GetRegions then return end
    local regions = { frame:GetRegions() }
    for i = 1, table.getn(regions) do
        local region = regions[i]
        if region and region.SetTexture and not region._nanamiKeep then
            local drawLayer = region.GetDrawLayer and region:GetDrawLayer()
            if drawLayer == "BACKGROUND" or drawLayer == "BORDER" or drawLayer == "ARTWORK" then
                region:SetTexture(nil)
                region:SetAlpha(0)
                region:Hide()
            end
        end
    end
end

local function CreateShadow(parent)
    local s = CreateFrame("Frame", nil, parent)
    s:SetPoint("TOPLEFT", parent, "TOPLEFT", -4, 4)
    s:SetPoint("BOTTOMRIGHT", parent, "BOTTOMRIGHT", 4, -4)
    s:SetBackdrop({
        bgFile   = "Interface\\Tooltips\\UI-Tooltip-Background",
        edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
        tile = true, tileSize = 16, edgeSize = 16,
        insets = { left = 4, right = 4, top = 4, bottom = 4 },
    })
    s:SetBackdropColor(0, 0, 0, 0.45)
    s:SetBackdropBorderColor(0, 0, 0, 0.6)
    s:SetFrameLevel(math.max(0, parent:GetFrameLevel() - 1))
    return s
end

--------------------------------------------------------------------------------
-- Flight Time Database (self-learning, stored in SFramesGlobalDB)
--------------------------------------------------------------------------------
local function GetFlightDB()
    if not SFramesGlobalDB then SFramesGlobalDB = {} end
    if not SFramesGlobalDB.flightTimes then SFramesGlobalDB.flightTimes = {} end
    return SFramesGlobalDB.flightTimes
end

local function GetRouteKey(src, dst)
    return (src or "") .. "->" .. (dst or "")
end

local function GetTaxiNodeHash(nodeIndex)
    local x, y = TaxiNodePosition(nodeIndex)
    if not x then return nil end
    return tostring(math.floor(x * 100000000))
end

local function GetPlayerFaction()
    local faction = UnitFactionGroup("player")
    if faction == "Alliance" then return "Alliance" end
    return "Horde"
end

-- Hash-based index built when taxi map opens: hash -> nodeIndex
local hashToIndex = {}
local indexToHash = {}
local hashCorrection = {}  -- actualHash -> ftcHash (fuzzy match)

local function BuildHashIndex()
    hashToIndex = {}
    indexToHash = {}
    hashCorrection = {}
    local numNodes = NumTaxiNodes()
    local faction = GetPlayerFaction()
    local factionData = FTCData and FTCData[faction]

    -- Collect all hashes used in FTCData for fuzzy matching
    local ftcHashes = {}
    if factionData then
        for srcH, routes in pairs(factionData) do
            ftcHashes[srcH] = true
            for dstH in pairs(routes) do
                ftcHashes[dstH] = true
            end
        end
    end

    for i = 1, numNodes do
        local h = GetTaxiNodeHash(i)
        if h then
            hashToIndex[h] = i
            indexToHash[i] = h
            if ftcHashes[h] then
                hashCorrection[h] = h
            else
                -- Turtle WoW coords may differ slightly from Classic; try ±10
                local hNum = tonumber(h)
                if hNum then
                    for delta = -10, 10 do
                        local candidate = tostring(hNum + delta)
                        if ftcHashes[candidate] then
                            hashCorrection[h] = candidate
                            break
                        end
                    end
                end
            end
        end
    end
end

local function LookupFTCData(srcHash, dstHash)
    if not srcHash or not dstHash then return nil end
    local faction = GetPlayerFaction()
    -- Priority 1: self-learned hash DB (exact hash, no correction needed)
    local hdb = SFramesGlobalDB and SFramesGlobalDB.flightTimesHash
    if hdb and hdb[faction] then
        local lr = hdb[faction][srcHash]
        if lr and lr[dstHash] then return lr[dstHash] end
    end
    -- Priority 2: FTCData pre-recorded (with fuzzy hash correction)
    if not FTCData then return nil end
    local factionData = FTCData[faction]
    if not factionData then return nil end
    local corrSrc = hashCorrection[srcHash] or srcHash
    local corrDst = hashCorrection[dstHash] or dstHash
    local srcRoutes = factionData[corrSrc]
    if not srcRoutes then return nil end
    return srcRoutes[corrDst]
end

local function GetEstimatedTime(src, dst)
    local routeKey = GetRouteKey(src, dst)
    -- Priority 1: per-account learned database (SavedVariables)
    local db = GetFlightDB()
    local learned = db[routeKey]
    if learned then return learned end

    -- Priority 2: shared learned database (FlightData.lua, cross-account)
    if NanamiLearnedFlights and NanamiLearnedFlights[routeKey] then
        return NanamiLearnedFlights[routeKey]
    end

    -- Priority 3: FTCData pre-recorded database (by hash)
    if FM.currentSourceHash and dst then
        local numNodes = NumTaxiNodes()
        for i = 1, numNodes do
            if TaxiNodeName(i) == dst then
                local dstHash = indexToHash[i] or GetTaxiNodeHash(i)
                if dstHash then
                    local ftcTime = LookupFTCData(FM.currentSourceHash, dstHash)
                    if ftcTime then return ftcTime end
                end
                break
            end
        end
    end

    return nil
end

local function GetEstimatedTimeByHash(srcHash, dstHash)
    -- Check learned DB by resolving hash to names
    local srcIdx = hashToIndex[srcHash]
    local dstIdx = hashToIndex[dstHash]
    if srcIdx and dstIdx then
        local db = GetFlightDB()
        local learned = db[GetRouteKey(TaxiNodeName(srcIdx), TaxiNodeName(dstIdx))]
        if learned then return learned end
    end
    return LookupFTCData(srcHash, dstHash)
end

local function GetFlightHashDB()
    if not SFramesGlobalDB then SFramesGlobalDB = {} end
    if not SFramesGlobalDB.flightTimesHash then SFramesGlobalDB.flightTimesHash = {} end
    return SFramesGlobalDB.flightTimesHash
end

local function SaveFlightTime(src, dst, duration, srcHash, dstHash)
    if not src or src == "" or not dst or dst == "" then return end
    if duration < 5 then return end
    local secs = math.floor(duration + 0.5)
    local db = GetFlightDB()
    db[GetRouteKey(src, dst)] = secs
    -- Also save in hash format for cross-account export
    if srcHash and dstHash then
        local faction = GetPlayerFaction()
        local hdb = GetFlightHashDB()
        if not hdb[faction] then hdb[faction] = {} end
        if not hdb[faction][srcHash] then hdb[faction][srcHash] = {} end
        hdb[faction][srcHash][dstHash] = secs
    end
end

--------------------------------------------------------------------------------
-- Money Layout (icon-based, like Merchant.lua)
--------------------------------------------------------------------------------
local function LayoutRowMoney(row, copper)
    row.gTxt:Hide(); row.gTex:Hide()
    row.sTxt:Hide(); row.sTex:Hide()
    row.cTxt:Hide(); row.cTex:Hide()

    if not copper or copper <= 0 then
        row.gTxt:SetText("--")
        row.gTxt:SetTextColor(T.dimText[1], T.dimText[2], T.dimText[3])
        row.gTxt:ClearAllPoints()
        row.gTxt:SetPoint("RIGHT", row, "RIGHT", -6, 0)
        row.gTxt:Show()
        return
    end

    local vG, vS, vC = FormatMoney(copper)
    local anchor = nil

    local function AttachPair(txt, tex, val, cr, cg, cb)
        txt:SetText(val)
        txt:SetTextColor(cr, cg, cb)
        txt:ClearAllPoints()
        if not anchor then
            txt:SetPoint("RIGHT", row, "RIGHT", -6, 0)
        else
            txt:SetPoint("RIGHT", anchor, "LEFT", -3, 0)
        end
        txt:Show()
        tex:ClearAllPoints()
        tex:SetPoint("RIGHT", txt, "LEFT", -1, 0)
        tex:Show()
        anchor = tex
    end

    if vC > 0 then AttachPair(row.cTxt, row.cTex, vC, T.moneyCopper[1], T.moneyCopper[2], T.moneyCopper[3]) end
    if vS > 0 then AttachPair(row.sTxt, row.sTex, vS, T.moneySilver[1], T.moneySilver[2], T.moneySilver[3]) end
    if vG > 0 then AttachPair(row.gTxt, row.gTex, vG, T.moneyGold[1], T.moneyGold[2], T.moneyGold[3]) end
end

--------------------------------------------------------------------------------
-- Destination Row Factory
--------------------------------------------------------------------------------
local function CreateDestRow(parent, index)
    local row = CreateFrame("Button", "NanamiFlightDest" .. index, parent)
    row:SetHeight(DEST_ROW_H)

    local font = GetFont()

    local dot = row:CreateTexture(nil, "ARTWORK")
    dot:SetTexture("Interface\\Buttons\\WHITE8X8")
    dot:SetWidth(6)
    dot:SetHeight(6)
    dot:SetPoint("LEFT", row, "LEFT", 6, 0)
    row.dot = dot

    local nameFS = row:CreateFontString(nil, "OVERLAY")
    nameFS:SetFont(font, 10, "OUTLINE")
    nameFS:SetPoint("LEFT", dot, "RIGHT", 5, 0)
    nameFS:SetPoint("RIGHT", row, "RIGHT", -112, 0)
    nameFS:SetJustifyH("LEFT")
    row.nameFS = nameFS

    local timeFS = row:CreateFontString(nil, "OVERLAY")
    timeFS:SetFont(font, 9, "OUTLINE")
    timeFS:SetPoint("RIGHT", row, "RIGHT", -78, 0)
    timeFS:SetJustifyH("RIGHT")
    timeFS:SetTextColor(T.dimText[1], T.dimText[2], T.dimText[3])
    row.timeFS = timeFS

    row.gTxt = row:CreateFontString(nil, "OVERLAY")
    row.gTxt:SetFont(font, 9, "OUTLINE")
    row.gTex = row:CreateTexture(nil, "ARTWORK")
    row.gTex:SetTexture("Interface\\MoneyFrame\\UI-MoneyIcons")
    row.gTex:SetTexCoord(0, 0.25, 0, 1)
    row.gTex:SetWidth(10); row.gTex:SetHeight(10)

    row.sTxt = row:CreateFontString(nil, "OVERLAY")
    row.sTxt:SetFont(font, 9, "OUTLINE")
    row.sTex = row:CreateTexture(nil, "ARTWORK")
    row.sTex:SetTexture("Interface\\MoneyFrame\\UI-MoneyIcons")
    row.sTex:SetTexCoord(0.25, 0.5, 0, 1)
    row.sTex:SetWidth(10); row.sTex:SetHeight(10)

    row.cTxt = row:CreateFontString(nil, "OVERLAY")
    row.cTxt:SetFont(font, 9, "OUTLINE")
    row.cTex = row:CreateTexture(nil, "ARTWORK")
    row.cTex:SetTexture("Interface\\MoneyFrame\\UI-MoneyIcons")
    row.cTex:SetTexCoord(0.5, 0.75, 0, 1)
    row.cTex:SetWidth(10); row.cTex:SetHeight(10)

    local hl = row:CreateTexture(nil, "HIGHLIGHT")
    hl:SetTexture("Interface\\QuestFrame\\UI-QuestTitleHighlight")
    hl:SetBlendMode("ADD")
    hl:SetAllPoints(row)
    hl:SetAlpha(0.15)

    row.nodeIndex = nil
    row.nodeType  = nil

    row:SetScript("OnEnter", function()
        if this.nodeType == "REACHABLE" and this.nodeIndex then
            GameTooltip:SetOwner(this, "ANCHOR_LEFT")
            GameTooltip:AddLine(TaxiNodeName(this.nodeIndex), 1, 1, 1)
            local cost = TaxiNodeCost(this.nodeIndex)
            if cost and cost > 0 then
                SetTooltipMoney(GameTooltip, cost)
            end
            local est = GetEstimatedTime(FM.currentSource, TaxiNodeName(this.nodeIndex))
            if est then
                GameTooltip:AddLine(" ")
                GameTooltip:AddLine("预计飞行: " .. FormatTime(est), 0.6, 0.8, 1.0)
            end
            GameTooltip:AddLine(" ")
            GameTooltip:AddLine("点击飞往此处", T.dimText[1], T.dimText[2], T.dimText[3])
            GameTooltip:Show()
        end
    end)

    row:SetScript("OnLeave", function()
        GameTooltip:Hide()
    end)

    row:SetScript("OnClick", function()
        if this.nodeType == "REACHABLE" and this.nodeIndex then
            TakeTaxiNode(this.nodeIndex)
        end
    end)

    return row
end

--------------------------------------------------------------------------------
-- Skin TaxiFrame
--------------------------------------------------------------------------------
function FM:ApplySkin()
    if skinApplied then return end
    if not TaxiFrame then return end
    skinApplied = true

    local font = GetFont()

    StripTextures(TaxiFrame)

    if TaxiPortrait then TaxiPortrait:Hide(); TaxiPortrait:SetAlpha(0) end
    if TaxiTitleText then TaxiTitleText:Hide() end

    local origClose = TaxiCloseButton
    if origClose then
        origClose:Hide()
        origClose:SetAlpha(0)
        origClose.Show = function() end
    end

    TaxiFrame:SetBackdrop({
        bgFile   = "Interface\\Tooltips\\UI-Tooltip-Background",
        edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
        tile = true, tileSize = 16, edgeSize = 14,
        insets = { left = 3, right = 3, top = 3, bottom = 3 },
    })
    TaxiFrame:SetBackdropColor(T.panelBg[1], T.panelBg[2], T.panelBg[3], T.panelBg[4])
    TaxiFrame:SetBackdropBorderColor(T.panelBorder[1], T.panelBorder[2], T.panelBorder[3], T.panelBorder[4])

    CreateShadow(TaxiFrame)

    -- Trim empty space at the bottom (TaxiMap anchored to top, safe to shrink from bottom)
    if TaxiMap then
        local mapBottom = TaxiMap:GetBottom()
        local frameBottom = TaxiFrame:GetBottom()
        if mapBottom and frameBottom and mapBottom > frameBottom then
            local excess = mapBottom - frameBottom - 10
            if excess > 10 then
                TaxiFrame:SetHeight(TaxiFrame:GetHeight() - excess)
            end
        end
    end

    TaxiFrame:SetMovable(true)
    TaxiFrame:EnableMouse(true)
    TaxiFrame:RegisterForDrag("LeftButton")
    TaxiFrame:SetScript("OnDragStart", function() this:StartMoving() end)
    TaxiFrame:SetScript("OnDragStop", function() this:StopMovingOrSizing() end)

    local header = CreateFrame("Frame", nil, TaxiFrame)
    header:SetPoint("TOPLEFT", TaxiFrame, "TOPLEFT", 0, 0)
    header:SetPoint("TOPRIGHT", TaxiFrame, "TOPRIGHT", 0, 0)
    header:SetHeight(HEADER_H)
    header:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8X8" })
    header:SetBackdropColor(T.headerBg[1], T.headerBg[2], T.headerBg[3], T.headerBg[4])
    header:SetFrameLevel(TaxiFrame:GetFrameLevel() + 5)

    local titleIco = SFrames:CreateIcon(header, "mount", 16)
    titleIco:SetDrawLayer("OVERLAY")
    titleIco:SetPoint("LEFT", header, "LEFT", SIDE_PAD, 0)
    titleIco:SetVertexColor(T.gold[1], T.gold[2], T.gold[3])

    local titleFS = header:CreateFontString(nil, "OVERLAY")
    titleFS:SetFont(font, 14, "OUTLINE")
    titleFS:SetPoint("LEFT", titleIco, "RIGHT", 5, 0)
    titleFS:SetPoint("RIGHT", header, "RIGHT", -30, 0)
    titleFS:SetJustifyH("LEFT")
    titleFS:SetTextColor(T.gold[1], T.gold[2], T.gold[3])
    FM.titleFS = titleFS

    local closeBtn = CreateFrame("Button", nil, header)
    closeBtn:SetWidth(20)
    closeBtn:SetHeight(20)
    closeBtn:SetPoint("RIGHT", header, "RIGHT", -8, 0)
    closeBtn:SetFrameLevel(header:GetFrameLevel() + 1)
    local closeTex = closeBtn:CreateTexture(nil, "ARTWORK")
    closeTex:SetTexture("Interface\\AddOns\\Nanami-UI\\img\\icon")
    closeTex:SetTexCoord(0.25, 0.375, 0, 0.125)
    closeTex:SetAllPoints()
    closeTex:SetVertexColor(T.dimText[1], T.dimText[2], T.dimText[3])
    closeBtn:SetScript("OnClick", function() CloseTaxiMap() end)
    closeBtn:SetScript("OnEnter", function() closeTex:SetVertexColor(1, 0.6, 0.7) end)
    closeBtn:SetScript("OnLeave", function() closeTex:SetVertexColor(T.dimText[1], T.dimText[2], T.dimText[3]) end)

    local headerSep = TaxiFrame:CreateTexture(nil, "OVERLAY")
    headerSep:SetTexture("Interface\\Buttons\\WHITE8X8")
    headerSep:SetHeight(1)
    headerSep:SetPoint("TOPLEFT", TaxiFrame, "TOPLEFT", 4, -HEADER_H)
    headerSep:SetPoint("TOPRIGHT", TaxiFrame, "TOPRIGHT", -4, -HEADER_H)
    headerSep:SetVertexColor(T.divider[1], T.divider[2], T.divider[3], T.divider[4])
    headerSep._nanamiKeep = true

    if TaxiMap then
        local mapBorder = CreateFrame("Frame", nil, TaxiFrame)
        mapBorder:SetPoint("TOPLEFT", TaxiMap, "TOPLEFT", -3, 3)
        mapBorder:SetPoint("BOTTOMRIGHT", TaxiMap, "BOTTOMRIGHT", 3, -3)
        mapBorder:SetBackdrop({
            edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
            edgeSize = 12,
            insets = { left = 2, right = 2, top = 2, bottom = 2 },
        })
        mapBorder:SetBackdropBorderColor(T.panelBorder[1], T.panelBorder[2], T.panelBorder[3], T.panelBorder[4])
        mapBorder:SetFrameLevel(TaxiFrame:GetFrameLevel() + 3)
    end

    FM:CreateDestPanel()

    local origOnHide = TaxiFrame:GetScript("OnHide")
    TaxiFrame:SetScript("OnHide", function()
        if origOnHide then origOnHide() end
        if DestPanel then DestPanel:Hide() end
    end)
end

--------------------------------------------------------------------------------
-- Destination Panel
--------------------------------------------------------------------------------
function FM:CreateDestPanel()
    local font = GetFont()

    DestPanel = CreateFrame("Frame", "NanamiFlightDestPanel", UIParent)
    DestPanel:SetWidth(DEST_PANEL_W)
    DestPanel:SetPoint("TOPLEFT", TaxiFrame, "TOPRIGHT", 4, 0)
    DestPanel:SetPoint("BOTTOMLEFT", TaxiFrame, "BOTTOMRIGHT", 4, 0)
    DestPanel:SetFrameStrata(TaxiFrame:GetFrameStrata())
    DestPanel:SetFrameLevel(TaxiFrame:GetFrameLevel() + 1)
    DestPanel:SetBackdrop({
        bgFile   = "Interface\\Tooltips\\UI-Tooltip-Background",
        edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
        tile = true, tileSize = 16, edgeSize = 14,
        insets = { left = 3, right = 3, top = 3, bottom = 3 },
    })
    DestPanel:SetBackdropColor(T.listBg[1], T.listBg[2], T.listBg[3], T.listBg[4])
    DestPanel:SetBackdropBorderColor(T.listBorder[1], T.listBorder[2], T.listBorder[3], T.listBorder[4])

    CreateShadow(DestPanel)

    local panelTitle = DestPanel:CreateFontString(nil, "OVERLAY")
    panelTitle:SetFont(font, 12, "OUTLINE")
    panelTitle:SetPoint("TOP", DestPanel, "TOP", 0, -8)
    panelTitle:SetTextColor(T.gold[1], T.gold[2], T.gold[3])
    panelTitle:SetText("目的地列表")

    FM.currentLabel = DestPanel:CreateFontString(nil, "OVERLAY")
    FM.currentLabel:SetFont(font, 10, "OUTLINE")
    FM.currentLabel:SetPoint("TOPLEFT", DestPanel, "TOPLEFT", 8, -28)
    FM.currentLabel:SetPoint("RIGHT", DestPanel, "RIGHT", -8, 0)
    FM.currentLabel:SetJustifyH("LEFT")
    FM.currentLabel:SetTextColor(T.currentText[1], T.currentText[2], T.currentText[3])

    local sepLine = DestPanel:CreateTexture(nil, "OVERLAY")
    sepLine:SetTexture("Interface\\Buttons\\WHITE8X8")
    sepLine:SetHeight(1)
    sepLine:SetPoint("TOPLEFT", DestPanel, "TOPLEFT", 8, -46)
    sepLine:SetPoint("TOPRIGHT", DestPanel, "TOPRIGHT", -8, -46)
    sepLine:SetVertexColor(T.divider[1], T.divider[2], T.divider[3], T.divider[4])

    FM.noDestLabel = DestPanel:CreateFontString(nil, "OVERLAY")
    FM.noDestLabel:SetFont(font, 10, "OUTLINE")
    FM.noDestLabel:SetPoint("TOP", DestPanel, "TOP", 0, -60)
    FM.noDestLabel:SetTextColor(T.dimText[1], T.dimText[2], T.dimText[3])
    FM.noDestLabel:SetText("暂无可用航线")
    FM.noDestLabel:Hide()

    local scrollFrame = CreateFrame("ScrollFrame", "NanamiFlightDestScroll", DestPanel, "UIPanelScrollFrameTemplate")
    scrollFrame:SetPoint("TOPLEFT", DestPanel, "TOPLEFT", 2, -50)
    scrollFrame:SetPoint("BOTTOMRIGHT", DestPanel, "BOTTOMRIGHT", -20, 6)

    local scrollChild = CreateFrame("Frame", "NanamiFlightDestScrollChild", scrollFrame)
    scrollChild:SetWidth(DEST_PANEL_W - 24)
    scrollChild:SetHeight(MAX_DEST_ROWS * DEST_ROW_H + 20)
    scrollFrame:SetScrollChild(scrollChild)

    local scrollBar = getglobal("NanamiFlightDestScrollScrollBar")
    if scrollBar then
        scrollBar:SetWidth(12)
        local regions = { scrollBar:GetRegions() }
        for i = 1, table.getn(regions) do
            local region = regions[i]
            if region and region.GetObjectType and region:GetObjectType() == "Texture" then
                region:SetTexture(nil)
                region:SetAlpha(0)
            end
        end
        local thumb = scrollBar:GetThumbTexture()
        if thumb then
            thumb:SetTexture("Interface\\Buttons\\WHITE8X8")
            thumb:SetVertexColor(T.divider[1], T.divider[2], T.divider[3], 0.6)
            thumb:SetWidth(10)
            thumb:SetHeight(40)
        end
    end
    local scrollUp = getglobal("NanamiFlightDestScrollScrollBarScrollUpButton")
    if scrollUp then scrollUp:SetAlpha(0); scrollUp:SetWidth(1); scrollUp:SetHeight(1) end
    local scrollDown = getglobal("NanamiFlightDestScrollScrollBarScrollDownButton")
    if scrollDown then scrollDown:SetAlpha(0); scrollDown:SetWidth(1); scrollDown:SetHeight(1) end

    for i = 1, MAX_DEST_ROWS do
        local row = CreateDestRow(scrollChild, i)
        row:SetPoint("TOPLEFT", scrollChild, "TOPLEFT", 0, -((i - 1) * DEST_ROW_H))
        row:SetPoint("RIGHT", scrollChild, "RIGHT", 0, 0)
        row:Hide()
        DestRows[i] = row
    end

    DestPanel:Hide()
end

--------------------------------------------------------------------------------
-- Update Destinations
--------------------------------------------------------------------------------
function FM:UpdateDestinations()
    if not DestPanel then return end

    local numNodes = NumTaxiNodes()
    local currentName = ""
    local reachable = {}
    local unreachableCount = 0

    for i = 1, numNodes do
        local name = TaxiNodeName(i)
        local ntype = TaxiNodeGetType(i)
        local cost = TaxiNodeCost(i)

        if ntype == "CURRENT" then
            currentName = name
        elseif ntype == "REACHABLE" then
            table.insert(reachable, { index = i, name = name, cost = cost })
        elseif ntype == "NONE" then
            unreachableCount = unreachableCount + 1
        end
    end

    FM.currentSource = currentName

    -- Build hash index for FTCData lookup
    BuildHashIndex()
    FM.currentSourceHash = nil
    for i = 1, numNodes do
        if TaxiNodeGetType(i) == "CURRENT" then
            FM.currentSourceHash = indexToHash[i] or GetTaxiNodeHash(i)
            break
        end
    end

    table.sort(reachable, function(a, b) return a.cost < b.cost end)

    if table.getn(reachable) == 0 then
        CloseTaxiMap()
        return
    end

    local npcName = UnitName("NPC") or "飞行管理员"
    FM.titleFS:SetText(npcName .. " - 飞行路线")

    if currentName ~= "" then
        FM.currentLabel:SetText("|cFF66E666*|r " .. currentName)
    else
        FM.currentLabel:SetText("|cFF66E666*|r 未知")
    end

    local rowIdx = 0

    for _, dest in ipairs(reachable) do
        rowIdx = rowIdx + 1
        if rowIdx <= MAX_DEST_ROWS then
            local row = DestRows[rowIdx]
            row.nodeIndex = dest.index
            row.nodeType = "REACHABLE"
            row.dot:SetVertexColor(T.nameText[1], T.nameText[2], T.nameText[3])
            row.nameFS:SetText(dest.name)
            row.nameFS:SetTextColor(T.nameText[1], T.nameText[2], T.nameText[3])
            LayoutRowMoney(row, dest.cost)
            local est = GetEstimatedTime(currentName, dest.name)
            if not est and FM.currentSourceHash then
                local dstHash = indexToHash[dest.index] or GetTaxiNodeHash(dest.index)
                if dstHash then est = LookupFTCData(FM.currentSourceHash, dstHash) end
            end
            if est then
                row.timeFS:SetText(FormatTime(est))
                row.timeFS:Show()
            else
                row.timeFS:SetText("")
                row.timeFS:Hide()
            end
            row:Show()
        end
    end

    if unreachableCount > 0 then
        rowIdx = rowIdx + 1
        if rowIdx <= MAX_DEST_ROWS then
            local row = DestRows[rowIdx]
            row.nodeIndex = nil
            row.nodeType = "NONE"
            row.dot:SetVertexColor(T.dimText[1], T.dimText[2], T.dimText[3])
            row.nameFS:SetText("(" .. unreachableCount .. " 个未发现)")
            row.nameFS:SetTextColor(T.dimText[1], T.dimText[2], T.dimText[3])
            row.gTxt:Hide(); row.gTex:Hide()
            row.sTxt:Hide(); row.sTex:Hide()
            row.cTxt:Hide(); row.cTex:Hide()
            row.timeFS:SetText(""); row.timeFS:Hide()
            row:Show()
        end
    end

    for i = rowIdx + 1, MAX_DEST_ROWS do
        DestRows[i]:Hide()
    end

    if rowIdx == 0 then
        FM.noDestLabel:Show()
    else
        FM.noDestLabel:Hide()
    end

    local child = getglobal("NanamiFlightDestScrollChild")
    if child then
        child:SetHeight(math.max(rowIdx * DEST_ROW_H + 10, 50))
    end

    DestPanel:Show()
end

--------------------------------------------------------------------------------
-- Hook Node Buttons (tooltip only)
--------------------------------------------------------------------------------
function FM:HookNodeButtons()
    local numNodes = NumTaxiNodes()
    for i = 1, numNodes do
        local btn = getglobal("TaxiButton" .. i)
        if btn and not btn._nanamiHooked then
            btn._nanamiHooked = true

            local origEnter = btn:GetScript("OnEnter")
            btn:SetScript("OnEnter", function()
                if origEnter then origEnter() end
                local id = this:GetID()
                if TaxiNodeGetType(id) == "REACHABLE" then
                    local cost = TaxiNodeCost(id)
                    if cost and cost > 0 then
                        GameTooltip:AddLine(" ")
                        SetTooltipMoney(GameTooltip, cost)
                    end
                    local est = GetEstimatedTime(FM.currentSource, TaxiNodeName(id))
                    if est then
                        GameTooltip:AddLine("预计飞行: " .. FormatTime(est), 0.6, 0.8, 1.0)
                    end
                    GameTooltip:Show()
                end
                for _, row in ipairs(DestRows) do
                    if row:IsShown() and row.nodeIndex == id then
                        row.nameFS:SetTextColor(T.gold[1], T.gold[2], T.gold[3])
                    end
                end
            end)

            local origLeave = btn:GetScript("OnLeave")
            btn:SetScript("OnLeave", function()
                if origLeave then origLeave() end
                for _, row in ipairs(DestRows) do
                    if row:IsShown() and row.nodeType == "REACHABLE" then
                        row.nameFS:SetTextColor(T.nameText[1], T.nameText[2], T.nameText[3])
                    end
                end
            end)
        end
    end
end

--------------------------------------------------------------------------------
-- Flight Progress Bar (vertical axis: departure → destination with countdown)
--------------------------------------------------------------------------------
local COLLAPSED_H = 34
local barCollapsed = false

local function SetBarCollapsed(collapsed)
    barCollapsed = collapsed
    if not FlightBar then return end
    local bar = FlightBar
    if collapsed then
        bar:SetHeight(COLLAPSED_H)
        for _, el in ipairs(bar.expandElements) do el:Hide() end
        bar.collapseBtn.label:SetText("+")
        -- Vertically center all visible elements
        bar.titleFS:ClearAllPoints()
        bar.titleFS:SetPoint("LEFT", bar, "LEFT", 10, 0)
        bar.compactFS:ClearAllPoints()
        bar.compactFS:SetPoint("RIGHT", bar, "RIGHT", -28, 0)
        bar.collapseBtn:ClearAllPoints()
        bar.collapseBtn:SetPoint("RIGHT", bar, "RIGHT", -6, 0)
        bar.compactFS:Show()
    else
        bar:SetHeight(bar.expandedH)
        for _, el in ipairs(bar.expandElements) do el:Show() end
        bar.collapseBtn.label:SetText("-")
        -- Restore top-aligned positions
        bar.titleFS:ClearAllPoints()
        bar.titleFS:SetPoint("TOPLEFT", bar, "TOPLEFT", 10, -8)
        bar.compactFS:ClearAllPoints()
        bar.compactFS:SetPoint("RIGHT", bar, "RIGHT", -28, -9)
        bar.collapseBtn:ClearAllPoints()
        bar.collapseBtn:SetPoint("TOPRIGHT", bar, "TOPRIGHT", -6, -6)
        bar.compactFS:Hide()
    end
end

local function CreateFlightBar()
    if FlightBar then return end

    local font = GetFont()
    local panelH = 8 + 18 + 6 + 12 + 6 + TRACK_H + 6 + 12 + 10 + 14 + 4 + 14 + 4 + 14 + 10

    local bar = CreateFrame("Frame", "NanamiFlightBar", UIParent)
    bar:SetWidth(BAR_W)
    bar:SetHeight(panelH)
    bar.expandedH = panelH
    bar:SetPoint("TOPRIGHT", UIParent, "TOPRIGHT", -80, -300)
    bar:SetFrameStrata("HIGH")
    bar:SetMovable(true)
    bar:EnableMouse(true)
    bar:RegisterForDrag("LeftButton")
    bar:SetScript("OnDragStart", function() this:StartMoving() end)
    bar:SetScript("OnDragStop", function() this:StopMovingOrSizing() end)
    bar:SetBackdrop({
        bgFile   = "Interface\\Tooltips\\UI-Tooltip-Background",
        edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
        tile = true, tileSize = 16, edgeSize = 14,
        insets = { left = 3, right = 3, top = 3, bottom = 3 },
    })
    bar:SetBackdropColor(T.panelBg[1], T.panelBg[2], T.panelBg[3], T.panelBg[4])
    bar:SetBackdropBorderColor(T.panelBorder[1], T.panelBorder[2], T.panelBorder[3], T.panelBorder[4])
    CreateShadow(bar)

    bar.expandElements = {}
    local yOff = -8

    -- Title (always visible)
    bar.titleFS = bar:CreateFontString(nil, "OVERLAY")
    bar.titleFS:SetFont(font, 13, "OUTLINE")
    bar.titleFS:SetPoint("TOPLEFT", bar, "TOPLEFT", 10, yOff)
    bar.titleFS:SetTextColor(T.gold[1], T.gold[2], T.gold[3])
    bar.titleFS:SetText("飞行中...")

    -- Compact remaining time (shown only when collapsed, next to title)
    bar.compactFS = bar:CreateFontString(nil, "OVERLAY")
    bar.compactFS:SetFont(font, 13, "OUTLINE")
    bar.compactFS:SetPoint("RIGHT", bar, "RIGHT", -28, yOff - 1)
    bar.compactFS:SetJustifyH("RIGHT")
    bar.compactFS:SetTextColor(T.gold[1], T.gold[2], T.gold[3])
    bar.compactFS:Hide()

    -- Collapse / expand button
    local colBtn = CreateFrame("Button", nil, bar)
    colBtn:SetWidth(18)
    colBtn:SetHeight(18)
    colBtn:SetPoint("TOPRIGHT", bar, "TOPRIGHT", -6, -6)
    colBtn:SetFrameLevel(bar:GetFrameLevel() + 3)
    local colLabel = colBtn:CreateFontString(nil, "OVERLAY")
    colLabel:SetFont(font, 14, "OUTLINE")
    colLabel:SetPoint("CENTER", 0, 1)
    colLabel:SetTextColor(T.dimText[1], T.dimText[2], T.dimText[3])
    colLabel:SetText("-")
    colBtn.label = colLabel
    colBtn:SetScript("OnClick", function()
        SetBarCollapsed(not barCollapsed)
    end)
    colBtn:SetScript("OnEnter", function()
        colLabel:SetTextColor(1, 0.7, 0.85)
    end)
    colBtn:SetScript("OnLeave", function()
        colLabel:SetTextColor(T.dimText[1], T.dimText[2], T.dimText[3])
    end)
    bar.collapseBtn = colBtn

    yOff = yOff - 18

    -- Separator
    local sep1 = bar:CreateTexture(nil, "OVERLAY")
    sep1:SetTexture("Interface\\Buttons\\WHITE8X8")
    sep1:SetHeight(1)
    sep1:SetPoint("TOPLEFT", bar, "TOPLEFT", 8, yOff - 2)
    sep1:SetPoint("TOPRIGHT", bar, "TOPRIGHT", -8, yOff - 2)
    sep1:SetVertexColor(T.divider[1], T.divider[2], T.divider[3], T.divider[4])
    table.insert(bar.expandElements, sep1)
    yOff = yOff - 6

    -- Source dot + name
    local srcDot = bar:CreateTexture(nil, "OVERLAY")
    srcDot:SetTexture("Interface\\Buttons\\WHITE8X8")
    srcDot:SetWidth(9)
    srcDot:SetHeight(9)
    srcDot:SetPoint("TOPLEFT", bar, "TOPLEFT", TRACK_X, yOff)
    srcDot:SetVertexColor(T.currentText[1], T.currentText[2], T.currentText[3])
    bar.srcDot = srcDot
    table.insert(bar.expandElements, srcDot)

    bar.srcNameFS = bar:CreateFontString(nil, "OVERLAY")
    bar.srcNameFS:SetFont(font, 10, "OUTLINE")
    bar.srcNameFS:SetPoint("LEFT", srcDot, "RIGHT", 6, 0)
    bar.srcNameFS:SetPoint("RIGHT", bar, "RIGHT", -8, 0)
    bar.srcNameFS:SetJustifyH("LEFT")
    bar.srcNameFS:SetTextColor(T.currentText[1], T.currentText[2], T.currentText[3])
    table.insert(bar.expandElements, bar.srcNameFS)

    yOff = yOff - 14

    -- Vertical track area
    local trackTop = yOff
    local trackBg = bar:CreateTexture(nil, "BACKGROUND")
    trackBg:SetTexture("Interface\\Buttons\\WHITE8X8")
    trackBg:SetWidth(3)
    trackBg:SetHeight(TRACK_H)
    trackBg:SetPoint("TOPLEFT", bar, "TOPLEFT", TRACK_X + 3, trackTop)
    trackBg:SetVertexColor(T.sectionBg[1], T.sectionBg[2], T.sectionBg[3], T.sectionBg[4])
    bar.trackBg = trackBg
    table.insert(bar.expandElements, trackBg)

    -- Track fill (grows from top downward)
    local trackFillFrame = CreateFrame("Frame", nil, bar)
    trackFillFrame:SetPoint("TOPLEFT", trackBg, "TOPLEFT", 0, 0)
    trackFillFrame:SetWidth(3)
    trackFillFrame:SetHeight(1)
    local trackFill = trackFillFrame:CreateTexture(nil, "ARTWORK")
    trackFill:SetTexture("Interface\\Buttons\\WHITE8X8")
    trackFill:SetAllPoints(trackFillFrame)
    trackFill:SetVertexColor(T.progressFill[1], T.progressFill[2], T.progressFill[3], T.progressFill[4])
    bar.trackFillFrame = trackFillFrame
    bar.trackFill = trackFill
    table.insert(bar.expandElements, trackFillFrame)

    -- Progress indicator
    local progDot = bar:CreateTexture(nil, "OVERLAY")
    progDot:SetTexture("Interface\\Buttons\\WHITE8X8")
    progDot:SetWidth(13)
    progDot:SetHeight(5)
    progDot:SetPoint("CENTER", trackBg, "TOP", 0, 0)
    progDot:SetVertexColor(T.accent[1], T.accent[2], T.accent[3], T.accent[4])
    bar.progDot = progDot
    table.insert(bar.expandElements, progDot)

    -- Progress glow
    local progGlow = bar:CreateTexture(nil, "ARTWORK")
    progGlow:SetTexture("Interface\\Buttons\\UI-ActionButton-Border")
    progGlow:SetBlendMode("ADD")
    progGlow:SetWidth(28)
    progGlow:SetHeight(28)
    progGlow:SetPoint("CENTER", progDot, "CENTER", 0, 0)
    progGlow:SetVertexColor(T.progressFill[1], T.progressFill[2], T.progressFill[3], 0.4)
    bar.progGlow = progGlow
    table.insert(bar.expandElements, progGlow)

    -- Elapsed time
    bar.elapsedFS = bar:CreateFontString(nil, "OVERLAY")
    bar.elapsedFS:SetFont(font, 11, "OUTLINE")
    bar.elapsedFS:SetPoint("LEFT", trackBg, "RIGHT", 12, 0)
    bar.elapsedFS:SetTextColor(T.bodyText[1], T.bodyText[2], T.bodyText[3])
    table.insert(bar.expandElements, bar.elapsedFS)

    -- Remaining time (follows progress dot)
    bar.remainFS = bar:CreateFontString(nil, "OVERLAY")
    bar.remainFS:SetFont(font, 14, "OUTLINE")
    bar.remainFS:SetPoint("LEFT", progDot, "RIGHT", 10, 0)
    bar.remainFS:SetTextColor(T.gold[1], T.gold[2], T.gold[3])
    table.insert(bar.expandElements, bar.remainFS)

    yOff = trackTop - TRACK_H

    -- Destination dot + name
    local dstDot = bar:CreateTexture(nil, "OVERLAY")
    dstDot:SetTexture("Interface\\Buttons\\WHITE8X8")
    dstDot:SetWidth(9)
    dstDot:SetHeight(9)
    dstDot:SetPoint("TOPLEFT", bar, "TOPLEFT", TRACK_X, yOff - 4)
    dstDot:SetVertexColor(T.nameText[1], T.nameText[2], T.nameText[3])
    bar.dstDot = dstDot
    table.insert(bar.expandElements, dstDot)

    bar.dstNameFS = bar:CreateFontString(nil, "OVERLAY")
    bar.dstNameFS:SetFont(font, 10, "OUTLINE")
    bar.dstNameFS:SetPoint("LEFT", dstDot, "RIGHT", 6, 0)
    bar.dstNameFS:SetPoint("RIGHT", bar, "RIGHT", -8, 0)
    bar.dstNameFS:SetJustifyH("LEFT")
    bar.dstNameFS:SetTextColor(T.nameText[1], T.nameText[2], T.nameText[3])
    table.insert(bar.expandElements, bar.dstNameFS)

    yOff = yOff - 18

    -- Bottom info
    local sep2 = bar:CreateTexture(nil, "OVERLAY")
    sep2:SetTexture("Interface\\Buttons\\WHITE8X8")
    sep2:SetHeight(1)
    sep2:SetPoint("TOPLEFT", bar, "TOPLEFT", 8, yOff)
    sep2:SetPoint("TOPRIGHT", bar, "TOPRIGHT", -8, yOff)
    sep2:SetVertexColor(T.divider[1], T.divider[2], T.divider[3], T.divider[4])
    table.insert(bar.expandElements, sep2)

    yOff = yOff - 6

    bar.totalFS = bar:CreateFontString(nil, "OVERLAY")
    bar.totalFS:SetFont(font, 10, "OUTLINE")
    bar.totalFS:SetPoint("TOPLEFT", bar, "TOPLEFT", 12, yOff)
    bar.totalFS:SetTextColor(T.dimText[1], T.dimText[2], T.dimText[3])
    table.insert(bar.expandElements, bar.totalFS)

    bar:Hide()
    FlightBar = bar
end

local function ShowFlightBar()
    CreateFlightBar()
    local bar = FlightBar

    bar.srcNameFS:SetText(flightState.source)
    bar.dstNameFS:SetText(flightState.dest)

    if flightState.estimated > 0 then
        bar.totalFS:SetText("预计: " .. FormatTime(flightState.estimated))
    else
        bar.totalFS:SetText("首次飞行 - 记录中...")
    end

    bar.titleFS:SetText("飞行中...")
    bar.titleFS:SetTextColor(T.gold[1], T.gold[2], T.gold[3])
    bar:SetAlpha(1)
    bar.compactFS:SetText("")

    -- Reset track fill
    bar.trackFillFrame:SetHeight(1)
    bar.progDot:ClearAllPoints()
    bar.progDot:SetPoint("CENTER", bar.trackBg, "TOP", 0, 0)
    bar.progGlow:ClearAllPoints()
    bar.progGlow:SetPoint("CENTER", bar.progDot, "CENTER", 0, 0)
    bar.elapsedFS:SetText("0:00")
    bar.remainFS:SetText("")

    -- Restore collapse state
    SetBarCollapsed(barCollapsed)

    bar:Show()
end

local function UpdateFlightBar()
    if not FlightBar or not FlightBar:IsShown() then return end
    local bar = FlightBar

    local elapsed = GetTime() - flightState.startTime
    local estimated = flightState.estimated
    local progress = 0
    local compactText = FormatTime(elapsed)

    if estimated > 0 then
        progress = math.min(1, elapsed / estimated)
        local remain = math.max(0, estimated - elapsed)
        bar.remainFS:SetText(FormatTime(remain))
        compactText = FormatTime(remain)
        if remain <= 0 then
            bar.remainFS:SetText("即将到达")
            bar.remainFS:SetTextColor(T.arrivedText[1], T.arrivedText[2], T.arrivedText[3])
            compactText = "即将到达"
        else
            bar.remainFS:SetTextColor(T.gold[1], T.gold[2], T.gold[3])
        end
    else
        bar.remainFS:SetText("记录中...")
        bar.remainFS:SetTextColor(T.dimText[1], T.dimText[2], T.dimText[3])
        progress = math.mod(elapsed / 10, 1) * 0.8
    end

    bar.compactFS:SetText(compactText)
    bar.elapsedFS:SetText("已飞行 " .. FormatTime(elapsed))

    if not barCollapsed then
        local fillH = math.max(1, progress * TRACK_H)
        bar.trackFillFrame:SetHeight(fillH)
        bar.progDot:ClearAllPoints()
        bar.progDot:SetPoint("CENTER", bar.trackBg, "TOP", 0, -fillH)
        bar.progGlow:ClearAllPoints()
        bar.progGlow:SetPoint("CENTER", bar.progDot, "CENTER", 0, 0)
    end
end

local function OnFlightArrived()
    if not FlightBar then return end
    local bar = FlightBar

    local elapsed = GetTime() - flightState.startTime
    SaveFlightTime(flightState.source, flightState.dest, elapsed, flightState.srcHash, flightState.dstHash)

    bar.titleFS:SetText("已到达!")
    bar.titleFS:SetTextColor(T.arrivedText[1], T.arrivedText[2], T.arrivedText[3])
    bar.remainFS:SetText("")
    bar.elapsedFS:SetText("飞行用时 " .. FormatTime(elapsed))
    bar.totalFS:SetText("")

    -- Fill track to 100%
    bar.trackFillFrame:SetHeight(TRACK_H)
    bar.progDot:ClearAllPoints()
    bar.progDot:SetPoint("CENTER", bar.trackBg, "BOTTOM", 0, 0)
    bar.progGlow:ClearAllPoints()
    bar.progGlow:SetPoint("CENTER", bar.progDot, "CENTER", 0, 0)
    bar.progDot:SetVertexColor(T.arrivedText[1], T.arrivedText[2], T.arrivedText[3])

    flightState.lingerTime = 4
end

local function HideFlightBar()
    if FlightBar then
        FlightBar:Hide()
        FlightBar.progDot:SetVertexColor(T.accent[1], T.accent[2], T.accent[3], T.accent[4])
    end
end

--------------------------------------------------------------------------------
-- Initialize
--------------------------------------------------------------------------------
function FM:Initialize()
    -- TaxiMap skin updater
    local updater = CreateFrame("Frame")
    updater:RegisterEvent("TAXIMAP_OPENED")
    updater.needsUpdate = false

    updater:SetScript("OnEvent", function()
        if event == "TAXIMAP_OPENED" then
            this.needsUpdate = true
        end
    end)

    updater:SetScript("OnUpdate", function()
        if this.needsUpdate then
            this.needsUpdate = false
            FM:ApplySkin()
            FM:UpdateDestinations()
            FM:HookNodeButtons()
        end
    end)

    -- Hook TakeTaxiNode to capture route info before flight starts
    local origTakeTaxiNode = TakeTaxiNode
    TakeTaxiNode = function(index)
        local numNodes = NumTaxiNodes()
        local srcName = ""
        local srcHash = nil
        for i = 1, numNodes do
            if TaxiNodeGetType(i) == "CURRENT" then
                srcName = TaxiNodeName(i)
                srcHash = indexToHash[i] or GetTaxiNodeHash(i)
                break
            end
        end
        local dstName = TaxiNodeName(index) or ""
        local dstHash = indexToHash[index] or GetTaxiNodeHash(index)

        flightState.source = srcName
        flightState.dest = dstName
        flightState.srcHash = srcHash
        flightState.dstHash = dstHash
        flightState.pendingFlight = true
        flightState.inFlight = false
        flightState.lingerTime = 0

        -- Lookup estimated time: learned DB first, then FTCData
        local est = GetEstimatedTime(srcName, dstName)
        if not est and srcHash and dstHash then
            est = LookupFTCData(srcHash, dstHash)
        end
        flightState.estimated = est or 0

        origTakeTaxiNode(index)
    end

    -- Flight state monitor
    local monitor = CreateFrame("Frame")
    monitor:SetScript("OnUpdate", function()
        local dt = arg1 or 0
        local onTaxi = UnitOnTaxi("player")

        if flightState.pendingFlight and onTaxi then
            flightState.pendingFlight = false
            flightState.inFlight = true
            flightState.startTime = GetTime()
            ShowFlightBar()
        end

        if flightState.inFlight then
            if not onTaxi then
                flightState.inFlight = false
                OnFlightArrived()
            else
                UpdateFlightBar()
            end
        end

        -- Linger after arrival then hide
        if flightState.lingerTime > 0 then
            flightState.lingerTime = flightState.lingerTime - dt
            if flightState.lingerTime <= 1 and FlightBar then
                FlightBar:SetAlpha(math.max(0, flightState.lingerTime))
            end
            if flightState.lingerTime <= 0 then
                flightState.lingerTime = 0
                HideFlightBar()
            end
        end
    end)
end

--------------------------------------------------------------------------------
-- Bootstrap
--------------------------------------------------------------------------------
local bootstrap = CreateFrame("Frame")
bootstrap:RegisterEvent("PLAYER_LOGIN")
bootstrap:SetScript("OnEvent", function()
    if event == "PLAYER_LOGIN" then
        if SFramesDB.enableFlightMap == nil then
            SFramesDB.enableFlightMap = true
        end
        if SFramesDB.enableFlightMap ~= false then
            FM:Initialize()
        end
    end
end)

--------------------------------------------------------------------------------
-- Debug: /ftcdebug  (open taxi map first, then type this command)
--------------------------------------------------------------------------------
SLASH_FTCDEBUG1 = "/ftcdebug"
SlashCmdList["FTCDEBUG"] = function()
    local faction = GetPlayerFaction()
    DEFAULT_CHAT_FRAME:AddMessage("|cFF00FF00[NanamiFlightDebug]|r faction=" .. tostring(faction) .. "  FTCData=" .. tostring(FTCData ~= nil))
    if FTCData then
        DEFAULT_CHAT_FRAME:AddMessage("|cFF00FF00[NanamiFlightDebug]|r FTCData[" .. faction .. "]=" .. tostring(FTCData[faction] ~= nil))
    end
    local numNodes = NumTaxiNodes()
    if not numNodes or numNodes == 0 then
        DEFAULT_CHAT_FRAME:AddMessage("|cFFFF6666[NanamiFlightDebug]|r Taxi map not open!")
        return
    end
    DEFAULT_CHAT_FRAME:AddMessage("|cFF00FF00[NanamiFlightDebug]|r Nodes=" .. numNodes .. "  srcHash=" .. tostring(FM.currentSourceHash))
    for i = 1, numNodes do
        local name = TaxiNodeName(i)
        local ntype = TaxiNodeGetType(i)
        local x, y = TaxiNodePosition(i)
        local h = tostring(math.floor(x * 100000000))
        local corr = hashCorrection[h]
        local corrTag = ""
        if corr and corr ~= h then
            corrTag = " |cFFFFFF00->fuzzy:" .. corr .. "|r"
        elseif corr then
            corrTag = " |cFF00FF00exact|r"
        else
            corrTag = " |cFFFF6666NO_MATCH|r"
        end
        local tag = ""
        if ntype == "CURRENT" then tag = " |cFF66E666<< YOU|r" end
        DEFAULT_CHAT_FRAME:AddMessage("  #" .. i .. " [" .. ntype .. "] " .. name .. "  h=" .. h .. corrTag .. tag)
    end
end

--------------------------------------------------------------------------------
-- Export: /ftcexport  (prints learned flight times in FlightData.lua format)
--------------------------------------------------------------------------------
SLASH_FTCEXPORT1 = "/ftcexport"
SlashCmdList["FTCEXPORT"] = function()
    local hdb = SFramesGlobalDB and SFramesGlobalDB.flightTimesHash
    if not hdb then
        DEFAULT_CHAT_FRAME:AddMessage("|cFFFF6666[NanamiFlightExport]|r No learned flight times yet.")
        return
    end
    local count = 0
    for faction, sources in pairs(hdb) do
        DEFAULT_CHAT_FRAME:AddMessage("|cFF00FF00[NanamiFlightExport]|r -- " .. faction .. " learned routes:")
        for srcHash, dests in pairs(sources) do
            for dstHash, secs in pairs(dests) do
                DEFAULT_CHAT_FRAME:AddMessage("  FTCData." .. faction .. "['" .. srcHash .. "']['" .. dstHash .. "'] = " .. secs)
                count = count + 1
            end
        end
    end
    if count == 0 then
        DEFAULT_CHAT_FRAME:AddMessage("|cFFFF6666[NanamiFlightExport]|r No learned flight times yet. Fly some routes first!")
    else
        DEFAULT_CHAT_FRAME:AddMessage("|cFF00FF00[NanamiFlightExport]|r Total: " .. count .. " routes. Copy lines above into FlightData.lua for permanent cross-account sharing.")
    end
end
