-- ============================================================
-- RealNibba.Pack v1.0
-- Farming Tracker + Overlay + Route Recorder
-- WoW 3.3.5a (Interface 30300)
-- ============================================================

local _rnpp_carbonite = IsAddOnLoaded("Carbonite") or IsAddOnLoaded("Carbonite_Core") or (Nx ~= nil)
local function SafeSetMapToCurrentZone()
    if _rnpp_carbonite then return end
    SetMapToCurrentZone()
end

-- ============================================================
-- [1] CONSTANTS + COLORS
-- ============================================================
local ADDON_VER    = "1.0"
local RIBBON_LEVELS = { 0.003, 0.0045, 0.006 }
local RIBBON        = 0.006
local MAX16         = 65535
local SAMPLE_RATE   = 0.5
local MIN_DIST_MAP  = 0.008
local WP_RATE       = 0.05
local ARRIVE_DIST   = 40
local twopi         = math.pi * 2

local ZONE_DATA = {
    [492] = { w=6270.833, h=4181.25 },
    [495] = { w=8493.75,  h=5662.5  },
}

local TYPE_COLOR = {
    Herbs = { r=0.10, g=0.85, b=0.10 },
    Mines = { r=0.10, g=0.40, b=1.00 },
    Mixed = { r=1.00, g=0.55, b=0.00 },
}

local QUALITY_COLORS = {
    [0]={0.62,0.62,0.62}, [1]={1,1,1},     [2]={0.12,1,0},
    [3]={0,0.44,0.87},    [4]={0.64,0.21,0.93}, [5]={1,0.5,0},
}

local C = {
    bg       = {0.04, 0.04, 0.05, 0.97},
    bgMed    = {0.08, 0.08, 0.10, 1.00},
    bgLight  = {0.12, 0.12, 0.14, 1.00},
    red      = {0.80, 0.10, 0.10, 1.00},
    redDark  = {0.28, 0.03, 0.03, 1.00},
    gold     = {1.00, 0.82, 0.10, 1.00},
    white    = {0.95, 0.95, 0.95, 1.00},
    grey     = {0.55, 0.55, 0.55, 1.00},
    green    = {0.10, 0.70, 0.15, 1.00},
    orange   = {1.00, 0.50, 0.00, 1.00},
    divider  = {0.40, 0.05, 0.05, 0.80},
}

-- ============================================================
-- [2] DB SETUP
-- ============================================================
local DB
local session

local DEFDATA = {
    startTime  = nil, duration  = 0,   zoneName   = nil,
    moneyCash  = 0,   moneyItems= 0,   moneyQuests= 0,
    countItems = 0,   countMobs = 0,   countHerbs= 0,   countMines= 0,
    lootedItems= {},  killedMobs= {},
}

local function CopyDefaults(dst, src)
    for k, v in pairs(src) do
        if dst[k] == nil then
            dst[k] = (type(v) == "table") and {} or v
        end
    end
end

local function InitDB()
    RealNibbaPack_DB = RealNibbaPack_DB or {}
    DB = RealNibbaPack_DB
    DB.routes      = DB.routes      or {}
    DB.enabled     = DB.enabled     or { Herbs=true, Mines=true, Mixed=true }
    -- Per-quality price sources (0=grey .. 5=orange)
    DB.priceByQuality = DB.priceByQuality or {
        [0]="Vendor", [1]="Vendor", [2]="Atr",
        [3]="Atr",    [4]="Atr",   [5]="Atr",
    }
    DB.customPrices   = DB.customPrices   or {}
    DB.blacklist      = DB.blacklist      or {}
    -- Quality filter: which qualities to track (default all)
    DB.trackQualities = DB.trackQualities or {[0]=true,[1]=true,[2]=true,[3]=true,[4]=true,[5]=true}
    DB.session     = DB.session     or {}
    DB.total       = DB.total       or {}
    CopyDefaults(DB.session, DEFDATA)
    session = DB.session
    DB.sessions = DB.sessions or {}
end

-- ============================================================
-- [3] UTILITIES
-- ============================================================
local function prt(msg)
    DEFAULT_CHAT_FRAME:AddMessage("|cffCC2222[RNPack]|r " .. tostring(msg))
end

local function FormatMoney(copper)
    if not copper or copper <= 0 then return "0g" end
    local g = math.floor(copper / 10000)
    local s = math.floor((copper % 10000) / 100)
    local c = copper % 100
    if g > 0 then
        return string.format("%dg %ds %dc", g, s, c)
    elseif s > 0 then
        return string.format("%ds %dc", s, c)
    else
        return string.format("%dc", c)
    end
end

local function FormatMoneyShort(copper)
    if not copper or copper <= 0 then return "0g" end
    local g = copper / 10000
    if g >= 1000 then
        return string.format("%.1fkg", g/1000)
    elseif g >= 1 then
        return string.format("%.1fg", g)
    else
        return string.format("%ds", math.floor(copper/100))
    end
end

local function FormatTime(sec)
    sec = math.floor(sec or 0)
    if sec >= 3600 then
        return string.format("%dh %02dm", math.floor(sec/3600), math.floor((sec%3600)/60))
    else
        return string.format("%dm %02ds", math.floor(sec/60), sec%60)
    end
end

local function GetSessionDuration()
    local d = session.duration or 0
    if session.startTime then d = d + (time() - session.startTime) end
    return d
end

local function GetTotalMoney()
    return (session.moneyCash or 0) + (session.moneyItems or 0) + (session.moneyQuests or 0)
end

local function GetGoldPerHour()
    local dur = GetSessionDuration()
    if dur <= 0 then return 0 end
    return math.floor(GetTotalMoney() / dur * 3600)
end

-- ============================================================
-- [4] PRICE SOURCES (Auctionator / TSM / Vendor)
-- ============================================================
local GetItemPrice
do
    local initDone = false
    local AtrGetPrice

    local function InitSources()
        if initDone then return end
        initDone = true
        if Atr_GetAuctionPrice then
            AtrGetPrice = function(name) return Atr_GetAuctionPrice(name) end
        end
    end

    function GetItemPrice(itemLink)
        InitSources()
        if not itemLink then return 0, nil, nil end
        local itemID = tonumber(string.match(itemLink, "item:(%d+):"))
        if not itemID then return 0, nil, nil end
        local name, _, rarity, _, _, _, _, _, _, _, vendorPrice = GetItemInfo(itemLink)
        if not name then return 0, nil, nil end
        -- Custom manual price overrides first
        if DB and DB.customPrices and DB.customPrices[itemLink] then
            return DB.customPrices[itemLink], rarity, name
        end
        local src = "Vendor"
        if DB and DB.priceByQuality then
            src = DB.priceByQuality[rarity or 0] or "Vendor"
        end
        local price = 0
        if src == "Atr" and AtrGetPrice then
            price = AtrGetPrice(name) or 0
        elseif src == "TSM" and TSM_API and TSM_API.GetCustomPriceValue then
            price = TSM_API.GetCustomPriceValue("dbmarket", "i:"..itemID) or 0
        elseif src == "Vendor" then
            price = vendorPrice or 0
        end
        if price <= 0 then price = vendorPrice or 0 end
        return price, rarity, name
    end
end

-- ============================================================
-- [5] SESSION TRACKING
-- ============================================================
local sessionActive = false
local sessionPaused = false
local curZone       = ""
local instanceLog   = {}
local eventFrame

local function ResetSession()
    session.startTime   = nil
    session.duration    = 0
    session.zoneName    = nil
    session.moneyCash   = 0
    session.moneyItems  = 0
    session.moneyQuests = 0
    session.countItems  = 0
    session.countMobs   = 0
    session.lootedItems = {}
    session.killedMobs  = {}
end

local function SessionStart()
    if sessionActive and not sessionPaused then return end
    if not sessionPaused then ResetSession() end
    session.startTime = time()
    session.zoneName  = session.zoneName or curZone
    sessionActive     = true
    sessionPaused     = false
    if eventFrame then
        eventFrame:RegisterEvent("CHAT_MSG_LOOT")
        eventFrame:RegisterEvent("CHAT_MSG_MONEY")
        eventFrame:RegisterEvent("QUEST_TURNED_IN")
        eventFrame:RegisterEvent("COMBAT_LOG_EVENT_UNFILTERED")
    end
    prt("Session started - " .. (curZone ~= "" and curZone or "Unknown Zone"))
end

local function SessionPause()
    if not sessionActive or sessionPaused then return end
    session.duration = (session.duration or 0) + (time() - (session.startTime or time()))
    session.startTime = nil
    sessionPaused = true
    if eventFrame then
        eventFrame:UnregisterEvent("CHAT_MSG_LOOT")
        eventFrame:UnregisterEvent("CHAT_MSG_MONEY")
        eventFrame:UnregisterEvent("QUEST_TURNED_IN")
        eventFrame:UnregisterEvent("COMBAT_LOG_EVENT_UNFILTERED")
    end
    prt("Session paused")
end

local function SessionStop()
    if not sessionActive then return end
    if session.startTime then
        session.duration = (session.duration or 0) + (time() - session.startTime)
        session.startTime = nil
    end
    sessionActive = false
    sessionPaused = false
    if eventFrame then
        eventFrame:UnregisterEvent("CHAT_MSG_LOOT")
        eventFrame:UnregisterEvent("CHAT_MSG_MONEY")
        eventFrame:UnregisterEvent("QUEST_TURNED_IN")
        eventFrame:UnregisterEvent("COMBAT_LOG_EVENT_UNFILTERED")
    end
    for k, v in pairs(DEFDATA) do
        if type(v) == "number" and type(session[k]) == "number" then
            DB.total[k] = (DB.total[k] or 0) + session[k]
        end
    end
    -- Save to history with full item list (max 15 sessions)
    if GetTotalMoney() > 0 or session.countItems > 0 then
        -- Snapshot top 30 items by value
        local itemSnapshot = {}
        for link, data in pairs(session.lootedItems) do
            itemSnapshot[#itemSnapshot+1] = {link=link, value=data[1], count=data[2]}
        end
        table.sort(itemSnapshot, function(a,b) return a.value > b.value end)
        local savedItems = {}
        for i=1, math.min(30, #itemSnapshot) do
            savedItems[i] = itemSnapshot[i]
        end
        local entry = {
            zone     = session.zoneName or "Unknown",
            duration = session.duration,
            cash     = session.moneyCash,
            items    = session.moneyItems,
            quests   = session.moneyQuests,
            mobs     = session.countMobs,
            herbs    = session.countHerbs or 0,
            mines    = session.countMines or 0,
            itemCount= session.countItems,
            timestamp= time(),
            lootedItems = savedItems,
        }
        table.insert(DB.sessions, 1, entry)
        if #DB.sessions > 15 then table.remove(DB.sessions) end
    end
    prt("Session ended | " .. FormatTime(session.duration) .. " | " .. FormatMoney(GetTotalMoney()))
end

-- ============================================================
-- [6] LOOT EVENTS
-- ============================================================
local function OnChatLoot(msg)
    if not sessionActive or sessionPaused then return end
    -- Skip crafted items: "You create", "You have created", etc.
    -- Only track actual loot: "You receive loot:", "You receive item:"
    local lowerMsg = string.lower(msg)
    if string.find(lowerMsg, "you create") or string.find(lowerMsg, "you have created") then return end
    if not string.find(lowerMsg, "receive") then return end
    local itemLink = string.match(msg, "|H(item:[^|]+)|h")
    if not itemLink then return end
    itemLink = "|H"..itemLink.."|h"
    local quantity = tonumber(string.match(msg, "x(%d+)%.?$")) or 1
    -- Check blacklist
    if DB.blacklist and DB.blacklist[itemLink] then return end
    local price, rarity = GetItemPrice(itemLink)
    -- Check quality filter
    if rarity and DB.trackQualities and DB.trackQualities[rarity] == false then return end
    if price and price > 0 then
        local money = price * quantity
        local data  = session.lootedItems[itemLink]
        if data then
            data[1] = data[1] + money
            data[2] = data[2] + quantity
        else
            session.lootedItems[itemLink] = { money, quantity }
        end
        session.moneyItems = session.moneyItems + money
        session.countItems = session.countItems + quantity
    end
    -- Track herb and mine node gathers by item subtype
    local _, _, _, _, _, _, subType = GetItemInfo(itemLink)
    if subType then
        local st = string.lower(subType)
        if string.find(st,"herb") then
            session.countHerbs = (session.countHerbs or 0) + 1
        elseif string.find(st,"metal") or string.find(st,"stone") then
            session.countMines = (session.countMines or 0) + 1
        end
    end
end

local function OnChatMoney(msg)
    if not sessionActive or sessionPaused then return end
    local copper = 0
    local g = tonumber(string.match(msg, "(%d+) Gold")) or 0
    local s = tonumber(string.match(msg, "(%d+) Silver")) or 0
    local c = tonumber(string.match(msg, "(%d+) Copper")) or 0
    copper = g*10000 + s*100 + c
    if copper > 0 then session.moneyCash = session.moneyCash + copper end
end

local function OnQuestTurnedIn(money)
    if not sessionActive or sessionPaused then return end
    if money and money > 0 then
        session.moneyQuests = session.moneyQuests + money
    end
end

local function OnCombatLog(...)
    if not sessionActive or sessionPaused then return end
    local timestamp, subevent, _, srcGUID, srcName, srcFlags, _,
          dstGUID, dstName, dstFlags = ...
    -- PARTY_KILL = player/group got kill credit (exact "Kills" filter event)
    -- UNIT_DIED fires for all nearby deaths - massively over-counts
    if subevent == "PARTY_KILL" and dstFlags then
        local isNPC     = bit.band(dstFlags, 0x00000800) > 0
        local isHostile = bit.band(dstFlags, 0x00000040) > 0
        if isNPC and isHostile then
            session.countMobs = session.countMobs + 1
        end
    end
end

-- ============================================================
-- [7] ROUTE ENCODER / DECODER
-- ============================================================
local B64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
local B64D = {}
for i = 1, #B64 do B64D[string.sub(B64,i,i)] = i-1 end

local function ToBase64(s)
    local out = {}
    local bytes = {string.byte(s, 1, -1)}
    for i = 1, #bytes, 3 do
        local b1,b2,b3 = bytes[i] or 0, bytes[i+1] or 0, bytes[i+2] or 0
        local n = b1*65536 + b2*256 + b3
        out[#out+1] = string.sub(B64, math.floor(n/262144)%64+1, math.floor(n/262144)%64+1)
        out[#out+1] = string.sub(B64, math.floor(n/4096)%64+1,   math.floor(n/4096)%64+1)
        out[#out+1] = (i+1 <= #bytes) and string.sub(B64, math.floor(n/64)%64+1, math.floor(n/64)%64+1) or "="
        out[#out+1] = (i+2 <= #bytes) and string.sub(B64, n%64+1, n%64+1) or "="
    end
    return table.concat(out)
end

local function FromBase64(s)
    local out = {}
    for i = 1, #s, 4 do
        local c1 = B64D[string.sub(s,i,i)]     or 0
        local c2 = B64D[string.sub(s,i+1,i+1)] or 0
        local c3 = B64D[string.sub(s,i+2,i+2)]
        local c4 = B64D[string.sub(s,i+3,i+3)]
        local n  = c1*262144 + c2*4096 + (c3 or 0)*64 + (c4 or 0)
        out[#out+1] = string.char(math.floor(n/65536)%256)
        if string.sub(s,i+2,i+2) ~= "=" then out[#out+1] = string.char(math.floor(n/256)%256) end
        if string.sub(s,i+3,i+3) ~= "=" then out[#out+1] = string.char(n%256) end
    end
    return table.concat(out)
end

local function WaypointsToBytes(pts)
    local out = {}
    for _, p in ipairs(pts) do
        local xi = math.floor(math.max(0, math.min(1, p[1])) * MAX16 + 0.5)
        local yi = math.floor(math.max(0, math.min(1, p[2])) * MAX16 + 0.5)
        out[#out+1] = string.char(math.floor(xi/256), xi%256, math.floor(yi/256), yi%256)
    end
    return table.concat(out)
end

local function BytesToWaypoints(bytes)
    local pts = {}
    for i = 1, #bytes-3, 4 do
        local b = {string.byte(bytes, i, i+3)}
        pts[#pts+1] = {(b[1]*256+b[2])/MAX16, (b[3]*256+b[4])/MAX16}
    end
    return pts
end

local function EncodeRoute(pts)
    local N = #pts
    if N < 2 then return nil, 0 end
    local MITER = 4.0

    local function pdist(p, a, b)
        local dx, dy = b[1]-a[1], b[2]-a[2]
        if dx==0 and dy==0 then return math.sqrt((p[1]-a[1])^2+(p[2]-a[2])^2) end
        return math.abs(dy*p[1]-dx*p[2]+b[1]*a[2]-b[2]*a[1]) / math.sqrt(dx*dx+dy*dy)
    end
    local function rdp(t, eps)
        if #t < 3 then return t end
        local dmax, idx = 0, 0
        for i = 2, #t-1 do
            local d = pdist(t[i], t[1], t[#t])
            if d > dmax then dmax, idx = d, i end
        end
        if dmax > eps then
            local l = rdp({unpack(t, 1, idx)}, eps)
            local r = rdp({unpack(t, idx, #t)}, eps)
            local out = {}
            for _, v in ipairs(l) do out[#out+1] = v end
            for i = 2, #r do out[#out+1] = r[i] end
            return out
        end
        return {t[1], t[#t]}
    end

    local simp = rdp(pts, 0.001)
    N = #simp
    if N < 2 then return nil, 0 end

    local normals, dirs = {}, {}
    for i = 1, N-1 do
        local dx = simp[i+1][1]-simp[i][1]; local dy = simp[i+1][2]-simp[i][2]
        local l  = math.sqrt(dx*dx+dy*dy); if l < 1e-9 then l = 1e-9 end
        normals[i] = {-dy/l*RIBBON, dx/l*RIBBON}; dirs[i] = {dx/l, dy/l}
    end

    local function isect(p1,d1,p2,d2)
        local cross = d1[1]*d2[2]-d1[2]*d2[1]
        if math.abs(cross) < 1e-10 then return nil end
        local dx,dy = p2[1]-p1[1], p2[2]-p1[2]
        local t = (dx*d2[2]-dy*d2[1])/cross
        return {p1[1]+t*d1[1], p1[2]+t*d1[2]}
    end

    local lp, rp = {}, {}
    lp[1]={simp[1][1]+normals[1][1], simp[1][2]+normals[1][2]}
    rp[1]={simp[1][1]-normals[1][1], simp[1][2]-normals[1][2]}
    lp[N]={simp[N][1]+normals[N-1][1], simp[N][2]+normals[N-1][2]}
    rp[N]={simp[N][1]-normals[N-1][1], simp[N][2]-normals[N-1][2]}

    for i = 2, N-1 do
        local p=simp[i]; local n1,n2=normals[i-1],normals[i]; local d1,d2=dirs[i-1],dirs[i]
        local p1l={p[1]+n1[1],p[2]+n1[2]}; local p2l={p[1]+n2[1],p[2]+n2[2]}
        local ml = isect(p1l,d1,p2l,d2)
        if ml then
            if math.sqrt((ml[1]-p[1])^2+(ml[2]-p[2])^2) > RIBBON*MITER then
                ml={(p1l[1]+p2l[1])/2,(p1l[2]+p2l[2])/2}
            end
        else ml=p1l end
        local p1r={p[1]-n1[1],p[2]-n1[2]}; local p2r={p[1]-n2[1],p[2]-n2[2]}
        local mr = isect(p1r,{-d1[1],-d1[2]},p2r,{-d2[1],-d2[2]})
        if mr then
            if math.sqrt((mr[1]-p[1])^2+(mr[2]-p[2])^2) > RIBBON*MITER then
                mr={(p1r[1]+p2r[1])/2,(p1r[2]+p2r[2])/2}
            end
        else mr=p1r end
        lp[i]=ml; rp[i]=mr
    end

    local function enc16(v)
        local n = math.floor(math.max(0,math.min(1,v))*MAX16+0.5)
        return string.char(math.floor(n/256), n%256)
    end
    local out = {}
    for i = 1, N-1 do
        local tl,bl,tr,br = lp[i],rp[i],lp[i+1],rp[i+1]
        out[#out+1]=enc16(tl[1])..enc16(tl[2])..enc16(bl[1])..enc16(bl[2])..enc16(tr[1])..enc16(tr[2])
        out[#out+1]=enc16(bl[1])..enc16(bl[2])..enc16(br[1])..enc16(br[2])..enc16(tr[1])..enc16(tr[2])
    end
    return table.concat(out), (N-1)*2
end

local function ExportString(route)
    if not route.pts or #route.pts == 0 then return nil end
    return "RNR~"..route.name.."~"..route.rtype.."~"..tostring(route.mapID).."~"..ToBase64(WaypointsToBytes(route.pts))
end

local function ImportString(str)
    str = string.gsub(str or "", "^%s+", "")
    str = string.gsub(str, "%s+$", "")
    if not string.find(str, "^RNR~") then return nil, "Not a RealNibba route string" end
    local _, _, name, rtype, mapID, b64 = string.find(str, "^RNR~([^~]+)~([^~]+)~(%d+)~(.+)$")
    if not name then return nil, "Malformed string" end
    if not TYPE_COLOR[rtype] then return nil, "Unknown type: "..rtype end
    local wpBytes = FromBase64(b64)
    if #wpBytes == 0 or #wpBytes % 4 ~= 0 then return nil, "Corrupted data" end
    local pts = BytesToWaypoints(wpBytes)
    if #pts < 2 then return nil, "Not enough points" end
    local data, tris = EncodeRoute(pts)
    if not data then return nil, "Encoding failed" end
    return { name=name, rtype=rtype, mapID=tonumber(mapID), data=data, tris=tris, pts=pts }
end

-- ============================================================
-- [8] ROUTE RECORDER
-- ============================================================
local rec = {
    active=false, name="", rtype="Herbs", pts={},
    lastX=nil, lastY=nil, timer=0, mapID=0, wpShown=false,
}

local function StartRecording(name, rtype)
    rec.active=true; rec.name=name; rec.rtype=rtype
    rec.pts={}; rec.lastX=nil; rec.lastY=nil; rec.timer=0; rec.wpShown=false
    SafeSetMapToCurrentZone()
    rec.mapID = GetCurrentMapAreaID() - 1
end

local function StopRecording(thickness)
    rec.active = false
    if _G["RNPack_WP"] then _G["RNPack_WP"].Hide() end
    if #rec.pts < 2 then return nil, "Not enough points" end

    local pts = rec.pts
    local zd = ZONE_DATA[rec.mapID] or {w=6000,h=4000}

    local function dist(a, b)
        local dx=(a[1]-b[1])*zd.w; local dy=(a[2]-b[2])*zd.h
        return math.sqrt(dx*dx+dy*dy)
    end

    local start = pts[1]
    -- Only trim overshoot if route came back near start (within 75 yards)
    -- and the closest return point is in the last 25% of the route
    local quarterLen = math.floor(#pts * 0.75)
    local minDist, trimAt = math.huge, #pts
    for i = quarterLen, #pts do
        local d = dist(pts[i], start)
        if d < minDist then minDist=d; trimAt=i end
    end
    -- Only trim if: close to start (< 75 yds) AND saves at least 3 points
    local endDist = dist(pts[#pts], start)
    if minDist <= 75 and trimAt < #pts - 2 and minDist < endDist * 0.6 then
        local trimmed = {}
        for i = 1, trimAt do trimmed[i] = pts[i] end
        pts = trimmed
    end

    -- Only close loop if within 75 yards - allows straight paths too
    local closeDist = dist(pts[#pts], start)
    if closeDist <= 75 then
        pts[#pts] = start  -- snap to close loop
    end
    -- If > 75 yards away, leave end point as-is (straight path)

    RIBBON = RIBBON_LEVELS[thickness or 3]
    local data, tris = EncodeRoute(pts)
    if not data then return nil, "Encoding failed" end
    return { name=rec.name, rtype=rec.rtype, mapID=rec.mapID,
             data=data, tris=tris, thickness=thickness or 3, pts=pts }
end

-- ============================================================
-- [9] WAYPOINT ARROW (TomTom BLP spritesheet)
-- Textures: Interface\AddOns\RealNibba.Pack\Images\Arrow.blp
--           Interface\AddOns\RealNibba.Pack\Images\Arrow-UP.blp
-- NOTE: Requires FULL game restart (not /reload) to load BLP files
-- Arrow.blp  : 108 frames, 9x12 grid, 56x42px each, 512x512
-- Arrow-UP.blp: 55 frames, 9x7 grid,  53x70px each, 512x512
-- ============================================================
local WP = {}
_G["RNPack_WP"] = WP

local ZONE_WIDTHS  = { [492]=6270.833, [495]=8493.75 }
local ZONE_HEIGHTS = { [492]=4181.25,  [495]=5662.5  }
local wpFrame, wpArrow, wpDistText, wpTitleText, wpTtaText
local wpTimer=0; local flashTimer=0; local flashOn=false
local lastDist=0; local ttaTimer=0; local showDown=false; local arrowCount=0

-- Confirmed path: Interface\AddOns\RealNibba.Pack\Images-- WoW accepts forward slashes as path separators
local ARROW_PATH = [[Interface\AddOns\RealNibba.Pack\Images\]]

local function GetArrowTexCoord(angle)
    -- Arrow.blp: 108 frames, 9 cols x 12 rows, 56x42px in 512x512
    local cell = math.floor(angle / twopi * 108 + 0.5) % 108
    local col  = cell % 9
    local row  = math.floor(cell / 9)
    return (col*56)/512, ((col+1)*56)/512, (row*42)/512, ((row+1)*42)/512
end

local function GetArrowUPTexCoord(cell)
    -- Arrow-UP.blp: 55 frames, 9 cols x 7 rows, 53x70px in 512x512
    local col = cell % 9
    local row = math.floor(cell / 9)
    return (col*53)/512, ((col+1)*53)/512, (row*70)/512, ((row+1)*70)/512
end

local function CreateWPArrow()
    wpFrame = CreateFrame("Frame","RNPackWPArrow",UIParent)
    wpFrame:SetSize(56,42)
    wpFrame:SetPoint("CENTER",UIParent,"CENTER",0,150)
    wpFrame:SetMovable(true); wpFrame:EnableMouse(true)
    wpFrame:RegisterForDrag("LeftButton")
    wpFrame:SetScript("OnDragStart",function() wpFrame:StartMoving() end)
    wpFrame:SetScript("OnDragStop", function() wpFrame:StopMovingOrSizing() end)
    wpFrame:SetClampedToScreen(true)
    wpFrame:SetFrameStrata("HIGH"); wpFrame:Hide()

    -- Arrow texture from RealNibba.Pack\Images\Arrow.blp
    wpArrow = wpFrame:CreateTexture(nil,"ARTWORK")
    wpArrow:SetTexture(ARROW_PATH.."Arrow")  -- loads Arrow.blp
    wpArrow:SetSize(56,42)
    wpArrow:SetPoint("CENTER",wpFrame,"CENTER",0,0)
    wpArrow:SetTexCoord(0, 56/512, 0, 42/512)  -- first frame
    wpArrow:SetVertexColor(0.2,1.0,0.2,1)

    -- Text labels in a child frame below the arrow
    local tf = CreateFrame("Frame",nil,wpFrame)
    tf:SetPoint("TOP",wpFrame,"BOTTOM",0,0)
    tf:SetSize(120,52)

    wpTitleText = tf:CreateFontString(nil,"OVERLAY","GameFontHighlightSmall")
    wpTitleText:SetPoint("TOP",tf,"TOP",0,0)
    wpTitleText:SetText("Start Point")

    wpDistText = tf:CreateFontString(nil,"OVERLAY","GameFontNormalSmall")
    wpDistText:SetPoint("TOP",wpTitleText,"BOTTOM",0,-1)
    wpDistText:SetTextColor(1,0.82,0,1)
    wpDistText:SetText("-- yds")

    wpTtaText = tf:CreateFontString(nil,"OVERLAY","GameFontNormalSmall")
    wpTtaText:SetPoint("TOP",wpDistText,"BOTTOM",0,-1)
    wpTtaText:SetTextColor(0.6,0.6,0.6,1)
    wpTtaText:SetText("***")
end

function WP.Show(mapID,startX,startY,routeName)
    if not wpFrame then CreateWPArrow() end
    WP.mapID=mapID; WP.startX=startX; WP.startY=startY
    showDown=false; lastDist=0; ttaTimer=0; flashTimer=0; flashOn=false
    if wpTitleText then wpTitleText:SetText(routeName or "Start Point") end
    -- Set Arrow.blp as the active texture
    wpArrow:SetTexture(ARROW_PATH.."Arrow")
    wpArrow:SetSize(56,42); wpFrame:SetSize(56,42)
    wpArrow:SetVertexColor(0.2,1.0,0.2,1)
    wpFrame:Show()
end

function WP.Hide()
    if wpFrame then wpFrame:Hide() end
    WP.mapID = nil
end

function WP.Update(elapsed)
    if not wpFrame or not wpFrame:IsShown() then return end
    wpTimer=wpTimer+elapsed
    if wpTimer < WP_RATE then return end; wpTimer=0

    local px,py = GetPlayerMapPosition("player")
    if not px or (px==0 and py==0) then
        SafeSetMapToCurrentZone(); px,py=GetPlayerMapPosition("player")
        if not px or (px==0 and py==0) then return end
    end
    local mapID=GetCurrentMapAreaID()-1
    if mapID ~= WP.mapID then
        wpDistText:SetText("Wrong zone"); return
    end

    local zw=ZONE_WIDTHS[mapID] or 6000
    local zh=ZONE_HEIGHTS[mapID] or 4000
    local dx=(WP.startX-px)*zw
    local dy=(WP.startY-py)*zh
    local dist=math.sqrt(dx*dx+dy*dy)
    wpDistText:SetText(string.format("%d yds",math.floor(dist)))

    -- TTA
    ttaTimer=ttaTimer+WP_RATE
    if ttaTimer>=1.0 then
        local spd=(lastDist-dist)/ttaTimer
        if spd>0 then
            local eta=dist/spd
            wpTtaText:SetFormattedText("%01d:%02d",math.floor(eta/60),math.floor(eta%60))
        else
            wpTtaText:SetText("***")
        end
        lastDist=dist; ttaTimer=0
    end

    if dist <= ARRIVE_DIST then
        -- Arrival: switch to Arrow-UP.blp and animate
        if not showDown then
            wpArrow:SetTexture(ARROW_PATH.."Arrow-UP")  -- loads Arrow-UP.blp
            wpArrow:SetSize(53,70); wpFrame:SetSize(53,70)
            showDown=true
        end
        arrowCount=(arrowCount+1)%55
        wpArrow:SetTexCoord(GetArrowUPTexCoord(arrowCount))
        wpArrow:SetVertexColor(0.2,1.0,0.2,1)
        wpDistText:SetText("|cffff0000HERE!|r")
    else
        -- Direction: switch back to Arrow.blp if needed
        if showDown then
            wpArrow:SetTexture(ARROW_PATH.."Arrow")
            wpArrow:SetSize(56,42); wpFrame:SetSize(56,42)
            showDown=false
        end
        -- Astrolabe/TomTom exact angle formula
        local dir=math.atan2(dx,-dy)
        if dir>0 then dir=twopi-dir else dir=-dir end
        local angle=(dir-(GetPlayerFacing() or 0)+twopi)%twopi
        -- Color gradient: red=pointing away, green=pointing toward
        local perc=math.abs((math.pi-math.abs(angle-math.pi))/math.pi)
        wpArrow:SetVertexColor(1-perc*0.8, perc, 0, 1)
        wpArrow:SetTexCoord(GetArrowTexCoord(angle))
    end
end

-- ============================================================
-- [10] SYNC ROUTES TO OVERLAY
-- ============================================================
local function SyncToOverlay()
    local Overlay = _RealNibbaOverlay
    if not Overlay then return false end
    for mapID, mapData in pairs(Overlay.PathData) do
        for npcID in pairs(mapData) do
            if npcID >= 90000 then mapData[npcID]=nil end
        end
    end
    Overlay.NPCColors = Overlay.NPCColors or {}
    local id = 90000
    for _, route in pairs(DB.routes) do
        if DB.enabled[route.rtype] and route.data then
            Overlay.PathData[route.mapID] = Overlay.PathData[route.mapID] or {}
            Overlay.PathData[route.mapID][id] = route.data
            local col = TYPE_COLOR[route.rtype]
            Overlay.Colors[#Overlay.Colors+1] = col
            Overlay.NPCColors[id] = #Overlay.Colors
            Overlay.NPCAdd(id)
            id = id + 1
        end
    end
    if Overlay.Modules and Overlay.Modules.UpdateMap then Overlay.Modules.UpdateMap() end
    return true
end
-- ============================================================
-- [11] UI HELPERS
-- ============================================================
-- Forward declarations (used in closures before defined)
local RefreshFarmStats
local RefreshItemList
local UI_Routes = {}  -- populated in [20] ROUTES LIST MANAGEMENT

-- Shared UI variables (declared here so all builder functions can access them)
local farmFrame, routesFrame, configFrame
local farmScrollChild, farmItems
local sessDetailFrame, sessChild, sessRows
local cfgPriceBtns, tqCheckCount
local bagPopup, ScanBags, btnBagScan
local itemDropdown, ddCurrentLink
local ovBtn, backToFarm, rtDiv1, rtDiv2
local rtScroll, routesLbl, impBox, impBtn, impLbl, crBtn
local createPage

local WIN_W, WIN_H = 320, 400
local inputCount = 0

-- WoW native window backdrop
local BACKDROP_DIALOG = {
    bgFile   = "Interface/DialogFrame/UI-DialogBox-Background",
    edgeFile = "Interface/DialogFrame/UI-DialogBox-Border",
    tile=true, tileSize=32, edgeSize=32,
    insets={left=11,right=12,top=12,bottom=11}
}
local BACKDROP_DARK = {
    bgFile   = "Interface/ChatFrame/ChatFrameBackground",
    edgeFile = "Interface/Tooltips/UI-Tooltip-Border",
    tile=true, tileSize=16, edgeSize=16,
    insets={left=4,right=4,top=4,bottom=4}
}

local function MakeWindow(name, title, w, h)
    local f = CreateFrame("Frame", name, UIParent)
    f:SetSize(w, h)
    f:SetPoint("CENTER")
    f:SetMovable(true); f:EnableMouse(true)
    f:RegisterForDrag("LeftButton")
    f:SetScript("OnDragStart", function() f:StartMoving() end)
    f:SetScript("OnDragStop",  function() f:StopMovingOrSizing() end)
    f:SetClampedToScreen(true)
    f:SetBackdrop(BACKDROP_DIALOG)
    f:Hide()
    -- Title
    local bg = f:CreateTexture(nil,"BACKGROUND")
    bg:SetTexture("Interface/DialogFrame/UI-DialogBox-Header")
    bg:SetSize(math.min(w-10, 256), 64)
    bg:SetPoint("TOP", f, "TOP", 0, 30)
    local lbl = f:CreateFontString(nil,"OVERLAY","GameFontNormal")
    lbl:SetPoint("TOP", f, "TOP", 0, 22)
    lbl:SetText(title)
    -- Close button
    local x = CreateFrame("Button", nil, f, "UIPanelCloseButton")
    x:SetPoint("TOPRIGHT", f, "TOPRIGHT", -6, -6)
    x:SetScript("OnClick", function() f:Hide() end)
    return f, lbl
end

local function MakeBtn(label, parent, w, h)
    local b = CreateFrame("Button", nil, parent, "UIPanelButtonTemplate")
    b:SetSize(w or 80, h or 22)
    b:SetText(label)
    return b
end

local function MakeInput(parent, w, h, name)
    inputCount = inputCount + 1
    local f = CreateFrame("EditBox", name or ("RNPackIn"..inputCount), parent, "InputBoxTemplate")
    f:SetSize(w or 200, h or 20)
    f:SetAutoFocus(false)
    f:SetMaxLetters(256)
    f:SetScript("OnEscapePressed", function() f:ClearFocus() end)
    f:SetScript("OnEnterPressed",  function() f:ClearFocus() end)
    return f
end

local function MakeLbl(text, parent, size, r, g, b)
    local fs = parent:CreateFontString(nil,"OVERLAY","GameFontNormal")
    if size and size ~= 11 then
        fs:SetFont("Fonts\\FRIZQT__.TTF", size, "")
    end
    fs:SetText(text or "")
    if r then fs:SetTextColor(r,g,b,1) end
    return fs
end

local function MakeDivider(parent, yOff, padL, padR)
    local t = parent:CreateTexture(nil,"ARTWORK")
    t:SetHeight(1)
    t:SetPoint("TOPLEFT",  parent,"TOPLEFT",  padL or 10, yOff)
    t:SetPoint("TOPRIGHT", parent,"TOPRIGHT", -(padR or 10), yOff)
    t:SetTexture(0.5, 0.5, 0.5, 0.6)
    return t
end

local UI = {}

-- ============================================================
-- [12] FARMING WINDOW  (320x400, WoW native)
-- ============================================================
local function BuildFarmingWindow()
farmFrame, farmFrameLbl = MakeWindow("RNPackFarmFrame", "RealNibba.Pack", WIN_W, WIN_H)
farmFrame:SetToplevel(true)
farmFrame:SetScript("OnMouseDown", function() farmFrame:Raise() end)
UI.farmFrame = farmFrame

-- [Routes] button in title area
local routesTopBtn = MakeBtn("Routes", farmFrame, 60, 18)
routesTopBtn:SetPoint("TOP", farmFrame, "TOP", 30, -12)

-- [Config] button
local configTopBtn = MakeBtn("Config", farmFrame, 54, 18)
configTopBtn:SetPoint("RIGHT", routesTopBtn, "LEFT", -3, 0)

-- Content starts at y=-26 (below title)
local PAD = 14

-- Row 1: Zone | Status | Duration  y=-28
local lblZone = MakeLbl("Zone: -", farmFrame, 10)
lblZone:SetPoint("TOPLEFT", farmFrame,"TOPLEFT", PAD, -34)
lblZone:SetWidth(130); lblZone:SetJustifyH("LEFT")

local lblStatus = MakeLbl("STOPPED", farmFrame, 10, 0.5,0.5,0.5)
lblStatus:SetPoint("TOP", farmFrame,"TOP", 0, -34)

local lblDuration = MakeLbl("0m 00s", farmFrame, 10)
lblDuration:SetPoint("TOPRIGHT", farmFrame,"TOPRIGHT", -PAD, -34)

-- Row 2: Buttons  y=-48
local btnW = math.floor((WIN_W - PAD*2 - 9)/4)
local btnStart  = MakeBtn("Start",  farmFrame, btnW, 22)
local btnStop   = MakeBtn("Stop",   farmFrame, btnW, 22)
local btnPause  = MakeBtn("Pause",  farmFrame, btnW, 22)
local btnFinish = MakeBtn("Finish", farmFrame, btnW, 22)
btnStart:SetPoint("TOPLEFT", farmFrame,"TOPLEFT", PAD, -54)
btnStop:SetPoint("LEFT", btnStart,"RIGHT", 3, 0)
btnPause:SetPoint("LEFT", btnStop,"RIGHT", 3, 0)
btnFinish:SetPoint("LEFT", btnPause,"RIGHT", 3, 0)

MakeDivider(farmFrame, -82)

-- Row 3: Gold/hr  y=-82
local lblGPH = MakeLbl("Gold/hr: --", farmFrame, 10, 1,0.82,0)
lblGPH:SetPoint("TOPLEFT", farmFrame,"TOPLEFT", PAD, -88)

local lblTotalRight = MakeLbl("Total: --", farmFrame, 10)
lblTotalRight:SetPoint("TOPRIGHT", farmFrame,"TOPRIGHT", -PAD, -88)

-- Row 4: Cash/Items/Quests  y=-97
local lblCash = MakeLbl("Cash: 0  Items: 0  Quests: 0", farmFrame, 10)
lblCash:SetPoint("TOPLEFT", farmFrame,"TOPLEFT", PAD, -103)

-- Row 5: Mobs/Herbs/Mines  y=-112
local lblNodes = MakeLbl("Mobs: 0  Herbs: 0  Mines: 0  Items: 0", farmFrame, 10)
lblNodes:SetPoint("TOPLEFT", farmFrame,"TOPLEFT", PAD, -118)

MakeDivider(farmFrame, -133)

-- Items header row  y=-133
local lblItemsHdr = MakeLbl("LOOTED ITEMS", farmFrame, 10, 1, 0.82, 0)
lblItemsHdr:SetPoint("TOPLEFT", farmFrame,"TOPLEFT", PAD, -139)

local btnHideItems = MakeBtn("[-]", farmFrame, 28, 16)
btnHideItems:SetPoint("LEFT", lblItemsHdr,"RIGHT", 4, 0)

-- [P] moved to Config window
-- Add Bags Scan button next to Routes
btnBagScan = MakeBtn("Bags", farmFrame, 36, 18)

-- Item scroll  y=-152
local farmScroll = CreateFrame("ScrollFrame","RNPackFarmScroll",farmFrame,"UIPanelScrollFrameTemplate")
farmScroll:SetPoint("TOPLEFT",  farmFrame,"TOPLEFT",  PAD,   -160)
farmScroll:SetPoint("TOPRIGHT", farmFrame,"TOPRIGHT", -PAD-26,-160)
farmScroll:SetHeight(152)

farmScrollChild = CreateFrame("Frame",nil,farmScroll)
farmScrollChild:SetSize(farmScroll:GetWidth() or 260, 1)
farmScroll:SetScrollChild(farmScrollChild)

local itemsVisible = true
btnHideItems:SetScript("OnClick", function()
    itemsVisible = not itemsVisible
    if itemsVisible then farmScroll:Show(); btnHideItems:SetText("[-]")
    else farmScroll:Hide(); btnHideItems:SetText("[+]") end
end)

-- Bottom bar (AH Posted | AH Incoming | Mail)  at bottom
local bbH = 60
local bbY  = -(WIN_H - bbH - 20)

MakeDivider(farmFrame, bbY, 8, 8)

local bbW = math.floor((WIN_W - PAD*2 - 8)/3)

local function MakeGoldPanel(parent, title, x, y, w)
    local f = CreateFrame("Frame", nil, parent)
    f:SetSize(w, bbH-6); f:SetPoint("TOPLEFT", parent,"TOPLEFT", x, y)
    f:SetBackdrop(BACKDROP_DARK)
    f:SetBackdropColor(0.05,0.05,0.05,0.9)
    f:SetBackdropBorderColor(0.4,0.4,0.4,0.8)
    local top = f:CreateFontString(nil,"OVERLAY","GameFontNormalSmall")
    top:SetPoint("TOP",f,"TOP",0,-3)
    top:SetTextColor(0.7,0.7,0.7,1); top:SetText(title)
    local val = f:CreateFontString(nil,"OVERLAY","GameFontNormal")
    val:SetPoint("CENTER",f,"CENTER",0,-4)
    val:SetTextColor(1,0.82,0,1); val:SetText("0g")
    return f, val
end

local ahPostedPanel,  ahPostedVal  = MakeGoldPanel(farmFrame,"AH POSTED",  PAD,           bbY-6, bbW)
local ahIncomePanel,  ahIncomeVal  = MakeGoldPanel(farmFrame,"AH INCOMING",PAD+bbW+4,     bbY-6, bbW)
local mailPanel,      mailVal      = MakeGoldPanel(farmFrame,"MAIL GOLD",  PAD+bbW*2+8,   bbY-6, bbW)

-- Button scripts
btnStart:SetScript("OnClick",  function() SessionStart(); end)
btnStop:SetScript("OnClick",   function() SessionStop(); RefreshFarmStats() end)
btnPause:SetScript("OnClick",  function()
    if sessionPaused then SessionStart() else SessionPause() end
    RefreshFarmStats()
end)
btnFinish:SetScript("OnClick", function()
    SessionStop(); ResetSession(); RefreshFarmStats(); RefreshItemList()
end)

-- Refresh functions
farmItems = {}

RefreshFarmStats = function()
    local dur   = GetSessionDuration()
    local total = GetTotalMoney()
    local gph   = GetGoldPerHour()
    local z     = (session.zoneName ~= "" and session.zoneName) or (curZone ~= "" and curZone) or "Unknown"
    lblZone:SetText("Zone: "..z)
    if sessionActive and not sessionPaused then
        lblStatus:SetText("|cff00FF44ACTIVE|r")
    elseif sessionPaused then
        lblStatus:SetText("|cffFF8800PAUSED|r")
    else
        lblStatus:SetText("|cff888888STOPPED|r")
    end
    lblDuration:SetText(FormatTime(dur))
    lblGPH:SetText("Gold/hr: "..FormatMoneyShort(gph))
    lblTotalRight:SetText("Total: "..FormatMoneyShort(total))
    lblCash:SetText(
        "Cash: "..FormatMoneyShort(session.moneyCash or 0)..
        "  Items: "..FormatMoneyShort(session.moneyItems or 0)..
        "  Quests: "..FormatMoneyShort(session.moneyQuests or 0)
    )
    lblNodes:SetText(
        "Mobs: "..tostring(session.countMobs  or 0)..
        "  Herbs: "..tostring(session.countHerbs or 0)..
        "  Mines: "..tostring(session.countMines or 0)..
        "  Items: "..tostring(session.countItems or 0)
    )
end
UI.RefreshFarmStats = RefreshFarmStats

-- ============================================================
-- Right-click dropdown menu for item list
-- ============================================================
itemDropdown = CreateFrame("Frame","RNPackItemDropdown",UIParent)
itemDropdown:SetSize(160, 92)
itemDropdown:SetFrameStrata("TOOLTIP"); itemDropdown:SetFrameLevel(200)
itemDropdown:SetToplevel(true)
itemDropdown:SetBackdrop({bgFile="Interface/DialogFrame/UI-DialogBox-Background",
    edgeFile="Interface/Tooltips/UI-Tooltip-Border",tile=true,tileSize=16,edgeSize=16,
    insets={left=4,right=4,top=4,bottom=4}})
itemDropdown:Hide()
ddCurrentLink = nil

local function MakeDDBtn(label, y, onclick)
    local b=CreateFrame("Button",nil,itemDropdown)
    b:SetSize(148,20); b:SetPoint("TOPLEFT",itemDropdown,"TOPLEFT",6,y)
    local bg=b:CreateTexture(nil,"BACKGROUND"); bg:SetAllPoints(); bg:SetTexture(0,0,0,0)
    local t=b:CreateFontString(nil,"OVERLAY","GameFontNormalSmall")
    t:SetAllPoints(); t:SetJustifyH("LEFT"); t:SetText(label); t:SetTextColor(1,1,1,1)
    b:SetScript("OnEnter",function() bg:SetTexture(0.3,0.3,0.3,0.6) end)
    b:SetScript("OnLeave",function() bg:SetTexture(0,0,0,0) end)
    b:SetScript("OnClick",function() itemDropdown:Hide(); if ddCurrentLink then onclick(ddCurrentLink) end end)
end

local function DDSep(y)
    local s=itemDropdown:CreateTexture(nil,"ARTWORK"); s:SetHeight(1)
    s:SetPoint("TOPLEFT",itemDropdown,"TOPLEFT",6,y)
    s:SetPoint("TOPRIGHT",itemDropdown,"TOPRIGHT",-6,y); s:SetTexture(0.4,0.4,0.4,0.8)
end

MakeDDBtn("Set Custom Price", -8, function(l) UI.ShowPriceEdit(l) end)
MakeDDBtn("Remove Custom Price", -28, function(l)
    if DB.customPrices then DB.customPrices[l]=nil end; RefreshItemList(); prt("Custom price removed.")
end)
DDSep(-48)
MakeDDBtn("Delete from List", -52, function(l)
    if session.lootedItems then
        local d=session.lootedItems[l]
        if d then session.moneyItems=math.max(0,(session.moneyItems or 0)-d[1])
                   session.countItems=math.max(0,(session.countItems or 0)-d[2]) end
        session.lootedItems[l]=nil
    end; RefreshItemList(); RefreshFarmStats()
end)
DDSep(-68)
MakeDDBtn("Blacklist Item", -72, function(l)
    if DB.blacklist then
        DB.blacklist[l]=true
        if session.lootedItems then session.lootedItems[l]=nil end
        RefreshItemList(); RefreshFarmStats()
        local n=GetItemInfo(l); prt("Blacklisted: "..(n or l))
    end
end)

function UI.ShowItemDropdown(link)
    ddCurrentLink=link
    local x,y=GetCursorPosition(); local sc=UIParent:GetEffectiveScale()
    x,y=x/sc,y/sc
    itemDropdown:ClearAllPoints()
    itemDropdown:SetPoint("BOTTOMLEFT",UIParent,"BOTTOMLEFT",x,y)
    itemDropdown:Show(); itemDropdown:Raise()
end

local ddCloseF=CreateFrame("Frame")
ddCloseF:RegisterEvent("PLAYER_STARTED_MOVING")
ddCloseF:SetScript("OnEvent",function() itemDropdown:Hide() end)

-- ============================================================
RefreshItemList = function()
    if not farmScrollChild then return end
    local items = {}
    for link, data in pairs(session.lootedItems or {}) do
        items[#items+1] = {link=link, value=data[1], count=data[2]}
    end
    table.sort(items, function(a,b) return a.value > b.value end)
    local rowH = 20
    farmScrollChild:SetHeight(math.max(1, #items*rowH))
    for i, item in ipairs(items) do
        local row = farmItems[i]
        if not row then
            row = CreateFrame("Frame",nil,farmScrollChild)
            row:SetHeight(rowH)
            row:SetPoint("TOPLEFT",  farmScrollChild,"TOPLEFT",  0,-(i-1)*rowH)
            row:SetPoint("TOPRIGHT", farmScrollChild,"TOPRIGHT",  0,-(i-1)*rowH)
            local rbg = row:CreateTexture(nil,"BACKGROUND"); rbg:SetAllPoints()
            rbg:SetTexture(i%2==0 and 0.15 or 0.1, i%2==0 and 0.15 or 0.1, i%2==0 and 0.15 or 0.1, 0.4)
            local ico = row:CreateTexture(nil,"ARTWORK")
            ico:SetSize(16,16); ico:SetPoint("LEFT",row,"LEFT",2,0)
            row.icon = ico
            local nm = row:CreateFontString(nil,"OVERLAY","GameFontNormalSmall")
            nm:SetPoint("LEFT",ico,"RIGHT",2,0); nm:SetWidth(150); nm:SetJustifyH("LEFT")
            row.name = nm
            local cnt = row:CreateFontString(nil,"OVERLAY","GameFontNormalSmall")
            cnt:SetPoint("RIGHT",row,"RIGHT",-68,0); cnt:SetWidth(36); cnt:SetJustifyH("RIGHT")
            cnt:SetTextColor(0.7,0.7,0.7,1); row.cnt = cnt
            local val = row:CreateFontString(nil,"OVERLAY","GameFontNormalSmall")
            val:SetPoint("RIGHT",row,"RIGHT",-2,0); val:SetWidth(62); val:SetJustifyH("RIGHT")
            val:SetTextColor(1,0.82,0,1); row.val = val
            row:EnableMouse(true)
            farmItems[i] = row
        end
        local name, _, rarity, _, _, _, _, _, _, tex = GetItemInfo(item.link)
        if tex then row.icon:SetTexture(tex) else row.icon:SetTexture("Interface/Icons/INV_Misc_QuestionMark") end
        local qc = QUALITY_COLORS[rarity or 1] or QUALITY_COLORS[1]
        local displayName = name or "Unknown"
        if #displayName > 22 then displayName = string.sub(displayName,1,22).."..." end
        row.name:SetText(displayName)
        row.name:SetTextColor(qc[1],qc[2],qc[3],1)
        row.cnt:SetText("x"..item.count)
        local isCustom = DB.customPrices and DB.customPrices[item.link]
        row.val:SetText((isCustom and "|cffFFAA00" or "")..FormatMoneyShort(item.value)..(isCustom and "|r" or ""))
        local ilink = item.link
        row:SetScript("OnEnter", function()
            GameTooltip:SetOwner(row,"ANCHOR_RIGHT")
            GameTooltip:SetHyperlink(ilink)
            GameTooltip:AddLine(" ")
            GameTooltip:AddLine("Right-click for options",0.6,0.6,0.6)
            GameTooltip:Show()
        end)
        row:SetScript("OnLeave", function() GameTooltip:Hide() end)
        row:SetScript("OnMouseUp", function(self, btn)
            if btn=="RightButton" then UI.ShowItemDropdown(ilink, self) end
        end)
        row:SetPoint("TOPLEFT",  farmScrollChild,"TOPLEFT",  0,-(i-1)*rowH)
        row:Show()
    end
    for i=#items+1, #farmItems do farmItems[i]:Hide() end
end
UI.RefreshItemList = RefreshItemList

end
BuildFarmingWindow()

-- ============================================================
-- [13] AH / MAIL SCANNING (TSM method)
-- ============================================================
local function GetAHGold()
    local total, incoming = 0, 0
    local num = GetNumAuctionItems("owner")
    for i = 1, num do
        local count, _, _, _, _, _, buyout = select(3, GetAuctionItemInfo("owner", i))
        if buyout and buyout > 0 then
            total = total + buyout
            if count == 0 then  -- count==0 = sold/bid-won, awaiting collection
                incoming = incoming + buyout
            end
        end
    end
    return total, incoming
end

local function GetMailGold()
    local total = 0
    local num = GetInboxNumItems()
    for i = 1, num do
        local _, _, sender, subject, money = GetInboxHeaderInfo(i)
        if money and money > 0 and sender then
            local s = string.lower(sender)
            local sub = subject and string.lower(subject) or ""
            if string.find(s,"auction") or string.find(sub,"sold") or string.find(sub,"auction") then
                total = total + money
            end
        end
    end
    return total
end

local ahScanDelay, mailScanDelay = 0, 0
local ahScanFrame = CreateFrame("Frame")
ahScanFrame:RegisterEvent("AUCTION_HOUSE_SHOW")
ahScanFrame:RegisterEvent("MAIL_SHOW")
ahScanFrame:SetScript("OnUpdate", function(self, elapsed)
    if ahScanDelay > 0 then
        ahScanDelay = ahScanDelay - elapsed
        if ahScanDelay <= 0 then
            ahScanDelay = 0
            local posted, incoming = GetAHGold()
            ahPostedVal:SetText(posted > 0 and FormatMoney(posted) or "0g")
            ahIncomeVal:SetText(incoming > 0 and FormatMoney(incoming) or "0g")
        end
    end
    if mailScanDelay > 0 then
        mailScanDelay = mailScanDelay - elapsed
        if mailScanDelay <= 0 then
            mailScanDelay = 0
            local mail = GetMailGold()
            mailVal:SetText(mail > 0 and FormatMoney(mail) or "0g")
        end
    end
end)
ahScanFrame:SetScript("OnEvent", function(self, event)
    if event == "AUCTION_HOUSE_SHOW" then ahScanDelay = 1.5 end
    if event == "MAIL_SHOW" then mailScanDelay = 0.5 end
end)

-- ============================================================
local function BuildBagScanner()
-- [13b] BAG SCANNER
-- ============================================================
bagPopup = CreateFrame("Frame","RNPackBagPopup",UIParent)
bagPopup:SetSize(300,220)
bagPopup:SetFrameStrata("DIALOG"); bagPopup:SetFrameLevel(100)
bagPopup:SetToplevel(true)
bagPopup:SetBackdrop(BACKDROP_DIALOG); bagPopup:Hide()
local bagHdrTex = bagPopup:CreateTexture(nil,"BACKGROUND")
bagHdrTex:SetTexture("Interface/DialogFrame/UI-DialogBox-Header")
bagHdrTex:SetSize(200,64); bagHdrTex:SetPoint("TOP",bagPopup,"TOP",0,12)
local bagTitle = bagPopup:CreateFontString(nil,"OVERLAY","GameFontNormal")
bagTitle:SetPoint("TOP",bagPopup,"TOP",0,4); bagTitle:SetText("Bag Value")
local bagX = CreateFrame("Button",nil,bagPopup,"UIPanelCloseButton")
bagX:SetPoint("TOPRIGHT",bagPopup,"TOPRIGHT",-6,-6)
bagX:SetScript("OnClick",function() bagPopup:Hide() end)

local bagTotal = bagPopup:CreateFontString(nil,"OVERLAY","GameFontNormal")
bagTotal:SetPoint("TOPLEFT",bagPopup,"TOPLEFT",14,-30)
bagTotal:SetTextColor(1,0.82,0,1); bagTotal:SetText("Total: scanning...")

local bagBreak = bagPopup:CreateFontString(nil,"OVERLAY","GameFontNormalSmall")
bagBreak:SetPoint("TOPLEFT",bagPopup,"TOPLEFT",14,-50)
bagBreak:SetTextColor(0.8,0.8,0.8,1); bagBreak:SetText("")

local bagScroll = CreateFrame("ScrollFrame","RNPackBagScroll",bagPopup,"UIPanelScrollFrameTemplate")
bagScroll:SetPoint("TOPLEFT",bagPopup,"TOPLEFT",14,-72)
bagScroll:SetPoint("BOTTOMRIGHT",bagPopup,"BOTTOMRIGHT",-34,10)
local bagChild = CreateFrame("Frame",nil,bagScroll)
bagChild:SetSize(bagScroll:GetWidth() or 250,1)
bagScroll:SetScrollChild(bagChild)

local bagRows = {}

ScanBags = function()
    local total = 0
    local byQuality = {[0]=0,[1]=0,[2]=0,[3]=0,[4]=0,[5]=0}
    local items = {}

    for bag = 0, 4 do
        for slot = 1, GetContainerNumSlots(bag) do
            local link = GetContainerItemLink(bag, slot)
            if link then
                local _, count = GetContainerItemInfo(bag, slot)
                count = count or 1
                local price, rarity, name = GetItemPrice(link)
                if price and price > 0 then
                    local val = price * count
                    total = total + val
                    local q = rarity or 0
                    byQuality[q] = (byQuality[q] or 0) + val
                    items[#items+1] = {name=name or "?", count=count, value=val, rarity=q}
                end
            end
        end
    end

    table.sort(items, function(a,b) return a.value > b.value end)

    bagTotal:SetText("Total Bag Value: |cffFFD700"..FormatMoney(total).."|r")

    -- Quality breakdown
    local qNames = {[0]="Grey",[1]="White",[2]="Green",[3]="Blue",[4]="Purple",[5]="Orange"}
    local qColors = {[0]="ff999999",[1]="ffffffff",[2]="ff1eff00",[3]="ff0070dd",[4]="ffa335ee",[5]="ffff8000"}
    local parts = {}
    for q=0,5 do
        if byQuality[q] > 0 then
            parts[#parts+1] = string.format("|c%s%s|r:%s", qColors[q], qNames[q], FormatMoneyShort(byQuality[q]))
        end
    end
    bagBreak:SetText(table.concat(parts,"  "))

    -- Item rows
    for _,r in ipairs(bagRows) do r:Hide() end; bagRows={}
    local rowH=18
    bagChild:SetHeight(math.max(1,#items*rowH))
    for i,item in ipairs(items) do
        local row=CreateFrame("Frame",nil,bagChild)
        row:SetHeight(rowH)
        row:SetPoint("TOPLEFT",bagChild,"TOPLEFT",0,-(i-1)*rowH)
        row:SetPoint("TOPRIGHT",bagChild,"TOPRIGHT",0,-(i-1)*rowH)
        local rbg=row:CreateTexture(nil,"BACKGROUND"); rbg:SetAllPoints()
        rbg:SetTexture(i%2==0 and 0.12 or 0.08, i%2==0 and 0.12 or 0.08, i%2==0 and 0.12 or 0.08, 0.5)
        local qc=QUALITY_COLORS[item.rarity] or QUALITY_COLORS[1]
        local nm=row:CreateFontString(nil,"OVERLAY","GameFontNormalSmall")
        nm:SetPoint("LEFT",row,"LEFT",2,0); nm:SetWidth(150); nm:SetJustifyH("LEFT")
        local dn=item.name; if #dn>20 then dn=string.sub(dn,1,20).."..." end
        nm:SetText(dn); nm:SetTextColor(qc[1],qc[2],qc[3],1)
        local cnt=row:CreateFontString(nil,"OVERLAY","GameFontNormalSmall")
        cnt:SetPoint("RIGHT",row,"RIGHT",-58,0); cnt:SetWidth(30); cnt:SetJustifyH("RIGHT")
        cnt:SetText("x"..item.count); cnt:SetTextColor(0.7,0.7,0.7,1)
        local val=row:CreateFontString(nil,"OVERLAY","GameFontNormalSmall")
        val:SetPoint("RIGHT",row,"RIGHT",-2,0); val:SetWidth(54); val:SetJustifyH("RIGHT")
        val:SetText(FormatMoneyShort(item.value)); val:SetTextColor(1,0.82,0,1)
        bagRows[#bagRows+1]=row; row:Show()
    end

    bagPopup:Show(); bagPopup:Raise()
end

btnBagScan:SetScript("OnClick", function()
    if bagPopup:IsShown() then bagPopup:Hide()
    else
        bagTotal:SetText("Scanning bags...")
        bagBreak:SetText("")
        bagPopup:SetPoint("CENTER",UIParent,"CENTER",0,0)
        bagPopup:Show()
        ScanBags()
    end
end)

end
BuildBagScanner()

-- ============================================================
-- [14] PRICE CONFIG POPUP
-- ============================================================
local pricePopup = CreateFrame("Frame","RNPackPricePopup",UIParent)
pricePopup:SetSize(220, 200)
pricePopup:SetFrameStrata("DIALOG"); pricePopup:SetFrameLevel(50)
pricePopup:SetBackdrop(BACKDROP_DIALOG)
RNPack.ApplyBG(pricePopup)
pricePopup:Hide()
local ppLbl = MakeLbl("Price Sources by Quality", pricePopup, 11, 1,0.82,0)
ppLbl:SetPoint("TOP", pricePopup,"TOP", 0, -10)
local ppClose = CreateFrame("Button",nil,pricePopup,"UIPanelCloseButton")
ppClose:SetPoint("TOPRIGHT",pricePopup,"TOPRIGHT",-6,-6)
ppClose:SetScript("OnClick", function() pricePopup:Hide() end)

local qualDefs = {
    {i=0,n="Grey",  r=0.62,g=0.62,b=0.62},
    {i=1,n="White", r=1,   g=1,   b=1   },
    {i=2,n="Green", r=0.12,g=1,   b=0   },
    {i=3,n="Blue",  r=0,   g=0.44,b=0.87},
    {i=4,n="Purple",r=0.64,g=0.21,b=0.93},
    {i=5,n="Orange",r=1,   g=0.5, b=0   },
}
local ppBtns = {}
for qi, qd in ipairs(qualDefs) do
    local row = CreateFrame("Frame",nil,pricePopup)
    row:SetHeight(22); row:SetPoint("TOPLEFT",pricePopup,"TOPLEFT",10,-(qi-1)*24-28)
    row:SetPoint("TOPRIGHT",pricePopup,"TOPRIGHT",-10,-(qi-1)*24-28)
    local ql = MakeLbl(qd.n, row, 10, qd.r,qd.g,qd.b)
    ql:SetPoint("LEFT",row,"LEFT",0,0); ql:SetWidth(52)
    local sb = MakeBtn("Vendor", row, 72, 18)
    sb:SetPoint("LEFT",row,"LEFT",56,0)
    local qi_idx = qd.i
    sb:SetScript("OnClick", function()
        if not DB or not DB.priceByQuality then return end
        local cur = DB.priceByQuality[qi_idx] or "Vendor"
        local nxt = cur=="Vendor" and "Atr" or cur=="Atr" and "TSM" or "Vendor"
        DB.priceByQuality[qi_idx] = nxt
        sb:SetText(nxt)
    end)
    ppBtns[qi_idx] = sb
end

-- Price sources now in Config window (see [14] CONFIG WINDOW)

-- ============================================================
-- [15] PRICE EDIT POPUP
-- ============================================================
local priceEditPopup = CreateFrame("Frame","RNPackPriceEdit",UIParent)
priceEditPopup:SetSize(280, 86)
priceEditPopup:SetFrameStrata("DIALOG"); priceEditPopup:SetFrameLevel(100)
priceEditPopup:SetBackdrop(BACKDROP_DIALOG)
RNPack.ApplyBG(priceEditPopup)
priceEditPopup:SetToplevel(true)
priceEditPopup:Hide()
local peLbl = MakeLbl("Custom price (e.g. 5g 30s):", priceEditPopup, 10)
peLbl:SetPoint("TOPLEFT",priceEditPopup,"TOPLEFT",14,-12)
local peClose = CreateFrame("Button",nil,priceEditPopup,"UIPanelCloseButton")
peClose:SetPoint("TOPRIGHT",priceEditPopup,"TOPRIGHT",-6,-6)
peClose:SetScript("OnClick", function() priceEditPopup:Hide() end)
local peBox = MakeInput(priceEditPopup, 250, 20, "RNPackPriceEditBox")
peBox:SetPoint("TOPLEFT",priceEditPopup,"TOPLEFT",14,-30)
local peApply = MakeBtn("Apply",priceEditPopup,80,20)
peApply:SetPoint("TOPLEFT",priceEditPopup,"TOPLEFT",14,-56)
local peClear = MakeBtn("Clear",priceEditPopup,80,20)
peClear:SetPoint("LEFT",peApply,"RIGHT",6,0)

local peCurrentLink = nil
function UI.ShowPriceEdit(link)
    peCurrentLink = link
    local name = GetItemInfo(link)
    peLbl:SetText("Price for: "..(name or "?"))
    local cur = DB.customPrices and DB.customPrices[link]
    peBox:SetText(cur and FormatMoney(cur) or "")
    priceEditPopup:Show(); peBox:SetFocus()
end

local function ParseMoneyStr(s)
    s = string.lower(s or "")
    local g = tonumber(string.match(s,"(%d+)g")) or 0
    local sv= tonumber(string.match(s,"(%d+)s")) or 0
    local c = tonumber(string.match(s,"(%d+)c")) or 0
    return g*10000 + sv*100 + c
end
peApply:SetScript("OnClick", function()
    if not peCurrentLink then return end
    local copper = ParseMoneyStr(peBox:GetText())
    if copper > 0 then
        DB.customPrices[peCurrentLink] = copper
        priceEditPopup:Hide(); RefreshItemList()
        prt("Custom price set: "..FormatMoney(copper))
    end
end)
peClear:SetScript("OnClick", function()
    if peCurrentLink then DB.customPrices[peCurrentLink] = nil end
    priceEditPopup:Hide(); RefreshItemList()
end)

-- ============================================================
-- [16] EXPORT POPUP
-- ============================================================
local expPopup = CreateFrame("Frame","RNPackExportPopup",UIParent)
expPopup:SetSize(380,90)
expPopup:SetFrameStrata("TOOLTIP"); expPopup:SetFrameLevel(100)
expPopup:SetBackdrop(BACKDROP_DIALOG)
RNPack.ApplyBG(expPopup)
expPopup:SetToplevel(true)
expPopup:Hide()
local expLbl = MakeLbl("Share Route (Ctrl+A, Ctrl+C):", expPopup, 10, 1,0.82,0)
expLbl:SetPoint("TOPLEFT",expPopup,"TOPLEFT",14,-12)
local expX = CreateFrame("Button",nil,expPopup,"UIPanelCloseButton")
expX:SetPoint("TOPRIGHT",expPopup,"TOPRIGHT",-6,-6)
expX:SetScript("OnClick", function() expPopup:Hide() end)
local expEdit = CreateFrame("EditBox","RNPackExportBox",expPopup,"InputBoxTemplate")
expEdit:SetPoint("TOPLEFT",expPopup,"TOPLEFT",14,-30)
expEdit:SetPoint("BOTTOMRIGHT",expPopup,"BOTTOMRIGHT",-14,10)
expEdit:SetAutoFocus(false); expEdit:SetMaxLetters(0)
expEdit:SetScript("OnEscapePressed",function() expPopup:Hide() end)
expEdit:SetScript("OnEditFocusGained",function() expEdit:HighlightText() end)
function UI.ShowExport(str)
    if not str or str=="" then prt("Nothing to export."); return end
    expEdit:SetText(str); expEdit:HighlightText()
    expPopup:Show(); expEdit:SetFocus()
    prt("Ctrl+A, Ctrl+C to copy route string.")
end

local function BuildConfigWindow()
-- ============================================================
-- [17] CONFIG WINDOW  (sessions + price sources overview)
-- ============================================================
configFrame = CreateFrame("Frame","RNPackConfigFrame",UIParent)
configFrame:SetSize(360,440)
configFrame:SetPoint("CENTER",UIParent,"CENTER",200,0)
configFrame:SetMovable(true); configFrame:EnableMouse(true)
configFrame:RegisterForDrag("LeftButton")
configFrame:SetScript("OnDragStart", function() configFrame:StartMoving() end)
configFrame:SetScript("OnDragStop",  function() configFrame:StopMovingOrSizing() end)
configFrame:SetClampedToScreen(true)
configFrame:SetBackdrop(BACKDROP_DIALOG)
RNPack.ApplyBG(configFrame)
configFrame:SetToplevel(true)
configFrame:SetScript("OnMouseDown",function() configFrame:Raise() end)
configFrame:Hide()

local cfgHdrTex = configFrame:CreateTexture(nil,"BACKGROUND")
cfgHdrTex:SetTexture("Interface/DialogFrame/UI-DialogBox-Header")
cfgHdrTex:SetSize(256,64); cfgHdrTex:SetPoint("TOP",configFrame,"TOP",0,24)
local cfgTitle = configFrame:CreateFontString(nil,"OVERLAY","GameFontNormal")
cfgTitle:SetPoint("TOP",configFrame,"TOP",0,16); cfgTitle:SetText("Config")
local cfgX = CreateFrame("Button",nil,configFrame,"UIPanelCloseButton")
cfgX:SetPoint("TOPRIGHT",configFrame,"TOPRIGHT",-6,-6)
cfgX:SetScript("OnClick", function() configFrame:Hide() end)

-- Sessions list
-- ============ UPPER HALF: Price Sources + Quality Filter ============

local cfgPriceHdr = MakeLbl("PRICE SOURCES BY QUALITY", configFrame, 10, 1,0.82,0)
cfgPriceHdr:SetPoint("TOPLEFT",configFrame,"TOPLEFT",14,-28)

-- Embed price source rows from pricePopup into config window
local cfgQualDefs = {
    {i=0,n="Grey",  r=0.62,g=0.62,b=0.62},
    {i=1,n="White", r=1,   g=1,   b=1   },
    {i=2,n="Green", r=0.12,g=1,   b=0   },
    {i=3,n="Blue",  r=0,   g=0.44,b=0.87},
    {i=4,n="Purple",r=0.64,g=0.21,b=0.93},
    {i=5,n="Orange",r=1,   g=0.5, b=0   },
}

-- Two columns: 3 left, 3 right
cfgPriceBtns = {}
local cfgPriceRowH = 22
local colW = math.floor((configFrame:GetWidth()-42)/2)

for qi, qd in ipairs(cfgQualDefs) do
    local col = (qi <= 3) and 0 or 1
    local row = (qi <= 3) and (qi-1) or (qi-4)
    local xOff = 14 + col*(colW+8)
    local yOff = -44 - row*cfgPriceRowH

    local ql=MakeLbl(qd.n, configFrame, 10, qd.r,qd.g,qd.b)
    ql:SetPoint("TOPLEFT",configFrame,"TOPLEFT",xOff,yOff); ql:SetWidth(46)

    local sb=MakeBtn("Vendor", configFrame, 64, 18)
    sb:SetPoint("TOPLEFT",configFrame,"TOPLEFT",xOff+48,yOff)
    local qi_idx=qd.i
    sb:SetScript("OnClick",function()
        if not DB or not DB.priceByQuality then return end
        local cur=DB.priceByQuality[qi_idx] or "Vendor"
        local nxt=cur=="Vendor" and "Atr" or cur=="Atr" and "TSM" or "Vendor"
        DB.priceByQuality[qi_idx]=nxt; sb:SetText(nxt)
    end)
    cfgPriceBtns[qi_idx]=sb
end

MakeDivider(configFrame, -116)

-- Quality filter: which qualities to TRACK
local cfgTrackHdr = MakeLbl("TRACK QUALITIES", configFrame, 10, 1,0.82,0)
cfgTrackHdr:SetPoint("TOPLEFT",configFrame,"TOPLEFT",14,-122)

local tqDefs = {{i=0,n="Grey",r=0.62,g=0.62,b=0.62},{i=1,n="White",r=1,g=1,b=1},
                {i=2,n="Green",r=0.12,g=1,b=0},{i=3,n="Blue",r=0,g=0.44,b=0.87},
                {i=4,n="Purple",r=0.64,g=0.21,b=0.93},{i=5,n="Orange",r=1,g=0.5,b=0}}
tqCheckCount = 0
for _, qd in ipairs(tqDefs) do
    tqCheckCount = tqCheckCount + 1
    local cb = CreateFrame("CheckButton","RNPackTQ"..tqCheckCount,configFrame,"UICheckButtonTemplate")
    cb:SetSize(16,16)
    local col = (tqCheckCount <= 3) and (tqCheckCount-1) or (tqCheckCount-4)
    local row = (tqCheckCount <= 3) and 0 or 1
    cb:SetPoint("TOPLEFT",configFrame,"TOPLEFT", 14+col*112, -138-row*20)
    cb:SetChecked(true)
    local ql=MakeLbl(qd.n, configFrame, 10, qd.r,qd.g,qd.b)
    ql:SetPoint("LEFT",cb,"RIGHT",1,0)
    local qi_idx=qd.i
    cb:SetScript("OnClick",function()
        if DB and DB.trackQualities then
            DB.trackQualities[qi_idx] = cb:GetChecked()==1 or cb:GetChecked()==true
        end
    end)
end

MakeDivider(configFrame, -182)

-- ============ LOWER HALF: Sessions ============

local cfgSessHdr = MakeLbl("PREVIOUS SESSIONS", configFrame, 10, 1,0.82,0)
cfgSessHdr:SetPoint("TOPLEFT",configFrame,"TOPLEFT",14,-188)

local sessScroll = CreateFrame("ScrollFrame","RNPackSessScroll",configFrame,"UIPanelScrollFrameTemplate")
sessScroll:SetPoint("TOPLEFT",  configFrame,"TOPLEFT",  14, -206)
sessScroll:SetPoint("TOPRIGHT", configFrame,"TOPRIGHT", -34,-206)
sessScroll:SetHeight(200)
sessChild = CreateFrame("Frame",nil,sessScroll)
sessChild:SetSize(sessScroll:GetWidth() or 300, 1)
sessScroll:SetScrollChild(sessChild)

-- Session item detail popup
sessDetailFrame = CreateFrame("Frame","RNPackSessDetail",UIParent)
sessDetailFrame:SetSize(340,360)
sessDetailFrame:SetPoint("CENTER"); sessDetailFrame:SetMovable(true); sessDetailFrame:EnableMouse(true)
sessDetailFrame:RegisterForDrag("LeftButton")
sessDetailFrame:SetScript("OnDragStart",function() sessDetailFrame:StartMoving() end)
sessDetailFrame:SetScript("OnDragStop", function() sessDetailFrame:StopMovingOrSizing() end)
sessDetailFrame:SetClampedToScreen(true)
sessDetailFrame:SetBackdrop(BACKDROP_DIALOG)
RNPack.ApplyBG(sessDetailFrame, true)
sessDetailFrame:SetToplevel(true)
sessDetailFrame:SetScript("OnMouseDown",function() sessDetailFrame:Raise() end)
sessDetailFrame:SetFrameStrata("DIALOG")
sessDetailFrame:Hide()
local sdTex = sessDetailFrame:CreateTexture(nil,"BACKGROUND")
sdTex:SetTexture("Interface/DialogFrame/UI-DialogBox-Header")
sdTex:SetSize(512,64); sdTex:SetPoint("TOP",sessDetailFrame,"TOP",0,24)
local sdTitle = sessDetailFrame:CreateFontString(nil,"OVERLAY","GameFontNormal")
sdTitle:SetPoint("TOP",sessDetailFrame,"TOP",0,14); sdTitle:SetText("Session Items")
local sdX = CreateFrame("Button",nil,sessDetailFrame,"UIPanelCloseButton")
sdX:SetPoint("TOPRIGHT",sessDetailFrame,"TOPRIGHT",-6,-6)
sdX:SetScript("OnClick",function() sessDetailFrame:Hide() end)
local sdScroll = CreateFrame("ScrollFrame","RNPackSessDetailScroll",sessDetailFrame,"UIPanelScrollFrameTemplate")
sdScroll:SetPoint("TOPLEFT",sessDetailFrame,"TOPLEFT",14,-28)
sdScroll:SetPoint("BOTTOMRIGHT",sessDetailFrame,"BOTTOMRIGHT",-34,10)
local sdChild = CreateFrame("Frame",nil,sdScroll)
sdChild:SetSize(sdScroll:GetWidth() or 280,1); sdScroll:SetScrollChild(sdChild)

local sdRows = {}
local function ShowSessionDetail(sess)
    sdTitle:SetText(sess.zone.." | "..FormatTime(sess.duration))
    for _,r in ipairs(sdRows) do r:Hide() end; sdRows = {}
    local items = sess.lootedItems or {}
    sdChild:SetHeight(math.max(1, #items*20))
    for i,item in ipairs(items) do
        local row = CreateFrame("Frame",nil,sdChild)
        row:SetHeight(20)
        row:SetPoint("TOPLEFT",sdChild,"TOPLEFT",0,-(i-1)*20)
        row:SetPoint("TOPRIGHT",sdChild,"TOPRIGHT",0,-(i-1)*20)
        local rbg=row:CreateTexture(nil,"BACKGROUND"); rbg:SetAllPoints()
        rbg:SetTexture(i%2==0 and 0.15 or 0.1,i%2==0 and 0.15 or 0.1,i%2==0 and 0.15 or 0.1,0.4)
        local ico=row:CreateTexture(nil,"ARTWORK"); ico:SetSize(16,16); ico:SetPoint("LEFT",row,"LEFT",2,0)
        local nm=row:CreateFontString(nil,"OVERLAY","GameFontNormalSmall")
        nm:SetPoint("LEFT",ico,"RIGHT",2,0); nm:SetWidth(160); nm:SetJustifyH("LEFT")
        local cnt=row:CreateFontString(nil,"OVERLAY","GameFontNormalSmall")
        cnt:SetPoint("RIGHT",row,"RIGHT",-60,0); cnt:SetWidth(36); cnt:SetJustifyH("RIGHT")
        cnt:SetTextColor(0.7,0.7,0.7,1)
        local val=row:CreateFontString(nil,"OVERLAY","GameFontNormalSmall")
        val:SetPoint("RIGHT",row,"RIGHT",-2,0); val:SetWidth(54); val:SetJustifyH("RIGHT")
        val:SetTextColor(1,0.82,0,1)
        local name,_,rarity,_,_,_,_,_,_,tex = GetItemInfo(item.link or "")
        if tex then ico:SetTexture(tex) else ico:SetTexture("Interface/Icons/INV_Misc_QuestionMark") end
        local qc=QUALITY_COLORS[rarity or 1] or QUALITY_COLORS[1]
        nm:SetText(name or "Unknown"); nm:SetTextColor(qc[1],qc[2],qc[3],1)
        cnt:SetText("x"..(item.count or 0))
        val:SetText(FormatMoneyShort(item.value or 0))
        sdRows[#sdRows+1]=row; row:Show()
    end
    sessDetailFrame:Show()
end

sessRows = {}
local function RefreshSessions()
    for _,r in ipairs(sessRows) do r:Hide() end; sessRows={}
    local sessions = DB and DB.sessions or {}
    local rowH = 42
    sessChild:SetHeight(math.max(1, #sessions*rowH+4))
    for i, sess in ipairs(sessions) do
        local rowH2 = rowH
        local row = CreateFrame("Frame",nil,sessChild)
        row:SetHeight(rowH2)
        row:SetPoint("TOPLEFT",sessChild,"TOPLEFT",0,-(i-1)*(rowH2+3))
        row:SetPoint("TOPRIGHT",sessChild,"TOPRIGHT",0,-(i-1)*(rowH2+3))
        row:SetBackdrop(BACKDROP_DARK)
        row:SetBackdropColor(i%2==0 and 0.1 or 0.08, 0.06, 0.06, 0.9)
        row:SetBackdropBorderColor(0.35,0.05,0.05,0.8)

        local total=(sess.cash or 0)+(sess.items or 0)+(sess.quests or 0)
        local gph=sess.duration>0 and math.floor(total/sess.duration*3600) or 0
        local l1 = string.format("|cffFFD700%s|r  %s  |cffFFD700%s|r  (%s/hr)",
            sess.zone or "?", FormatTime(sess.duration), FormatMoneyShort(total), FormatMoneyShort(gph))
        local l2 = string.format("Mobs:%d  Herbs:%d  Mines:%d  Items:%d",
            sess.mobs or 0, sess.herbs or 0, sess.mines or 0, sess.itemCount or 0)

        local t1=row:CreateFontString(nil,"OVERLAY","GameFontNormalSmall")
        t1:SetPoint("TOPLEFT",row,"TOPLEFT",5,-4)
        t1:SetPoint("TOPRIGHT",row,"TOPRIGHT",-50,-4)
        t1:SetText(l1); t1:SetJustifyH("LEFT")

        local t2=row:CreateFontString(nil,"OVERLAY","GameFontNormalSmall")
        t2:SetPoint("TOPLEFT",row,"TOPLEFT",5,-18)
        t2:SetPoint("TOPRIGHT",row,"TOPRIGHT",-50,-18)
        t2:SetText(l2); t2:SetJustifyH("LEFT"); t2:SetTextColor(0.7,0.7,0.7,1)

        -- Small items button
        local itmBtn = MakeBtn("Items", row, 44, rowH2-4)
        itmBtn:SetPoint("RIGHT",row,"RIGHT",-3,0)
        local s = sess
        itmBtn:SetScript("OnClick", function() ShowSessionDetail(s) end)
        row:EnableMouse(true)

        sessRows[#sessRows+1]=row; row:Show()
    end
    if #sessions==0 then
        local nl=MakeLbl("No sessions yet. Start farming!",sessChild,10,0.5,0.5,0.5)
        nl:SetPoint("TOPLEFT",sessChild,"TOPLEFT",4,-4)
        sessRows[1]=nl
    end
end

configTopBtn:SetScript("OnClick", function()
    if configFrame:IsShown() then configFrame:Hide()
    else
        -- Sync price source button labels
        if DB and DB.priceByQuality then
            for qi,sb in pairs(cfgPriceBtns) do
                sb:SetText(DB.priceByQuality[qi] or "Vendor")
            end
        end
        -- Sync quality filter checkboxes
        if DB and DB.trackQualities then
            for i=1, tqCheckCount do
                local cb=_G["RNPackTQ"..i]
                if cb then
                    local qi = i-1; if i > 3 then qi=i-1 end
                    -- map: 1=0,2=1,3=2,4=3,5=4,6=5
                    local q = i-1
                    if DB.trackQualities[q] ~= nil then
                        cb:SetChecked(DB.trackQualities[q])
                    end
                end
            end
        end
        RefreshSessions(); configFrame:Show()
    end
end)
end  -- BuildConfigWindow

BuildConfigWindow()

local function BuildRoutesWindow()
-- ============================================================
-- [18] ROUTES WINDOW  (320x400, WoW native, separate)
-- ============================================================
routesFrame, routesFrameLbl = MakeWindow("RNPackRoutesFrame","RealNibba.Routes",WIN_W,WIN_H)
routesFrame:SetToplevel(true)
routesFrame:SetScript("OnMouseDown", function() routesFrame:Raise() end)
UI.routesFrame = routesFrame

routesTopBtn:SetScript("OnClick", function()
    if routesFrame:IsShown() then routesFrame:Hide()
    else routesFrame:Show(); UI_Routes.RefreshList() end
end)

-- Back to farm button inside routes window
backToFarm = MakeBtn("< Farming", routesFrame, 80, 20)
backToFarm:SetPoint("TOPLEFT",routesFrame,"TOPLEFT",14,-28)
backToFarm:SetScript("OnClick", function() routesFrame:Hide(); farmFrame:Show() end)

-- Overlay toggle
ovBtn = MakeBtn("Overlay: ON", routesFrame, 90, 20)
ovBtn:SetPoint("TOPRIGHT",routesFrame,"TOPRIGHT",-14,-28)
ovBtn:SetScript("OnClick", function()
    local Ov = _RealNibbaOverlay
    if Ov then
        Ov.Options.ShowAll = not Ov.Options.ShowAll
        Ov.Modules.UpdateMap()
        ovBtn:SetText(Ov.Options.ShowAll and "Overlay: ON" or "Overlay: OFF")
    end
end)

rtDiv1 = MakeDivider(routesFrame, -54)

-- Type toggles
rtElements = {}  -- all routes page elements to hide during create
local checkCount = 0
local typeX = 14
local function MakeTypeCheck(rtype, x)
    checkCount = checkCount + 1
    local cb = CreateFrame("CheckButton","RNPackRTC"..checkCount,routesFrame,"UICheckButtonTemplate")
    cb:SetSize(18,18)
    cb:SetPoint("TOPLEFT",routesFrame,"TOPLEFT",x,-58)
    cb:SetChecked(true)
    local col = TYPE_COLOR[rtype]
    local lbl = MakeLbl(rtype, routesFrame, 10, col.r,col.g,col.b)
    lbl:SetPoint("LEFT",cb,"RIGHT",1,0)
    local rt = rtype
    cb:SetScript("OnClick", function()
        if DB then
            DB.enabled[rt] = cb:GetChecked()==1 or cb:GetChecked()==true
            SyncToOverlay()
        end
    end)
    return x + 86
end
typeX = MakeTypeCheck("Herbs", typeX)
typeX = MakeTypeCheck("Mines", typeX)
typeX = MakeTypeCheck("Mixed", typeX)

-- Routes label
routesLbl = MakeLbl("SAVED ROUTES", routesFrame, 10, 1,0.82,0)
routesLbl:SetPoint("TOPLEFT",routesFrame,"TOPLEFT",14,-82)

-- Routes scroll
rtScroll = CreateFrame("ScrollFrame","RNPackRtScroll",routesFrame,"UIPanelScrollFrameTemplate")
rtScroll:SetPoint("TOPLEFT",  routesFrame,"TOPLEFT",  14,-96)
rtScroll:SetPoint("TOPRIGHT", routesFrame,"TOPRIGHT", -34,-96)
rtScroll:SetHeight(170)
rtChild = CreateFrame("Frame",nil,rtScroll)
rtChild:SetSize(rtScroll:GetWidth() or 260,1); rtScroll:SetScrollChild(rtChild)

-- Import
rtDiv2 = MakeDivider(routesFrame, -272)
impLbl = MakeLbl("Import Route:", routesFrame, 10)
impLbl:SetPoint("TOPLEFT",routesFrame,"TOPLEFT",14,-278)
impBox = MakeInput(routesFrame, WIN_W-94, 20, "RNPackImportBox")
impBox:SetPoint("TOPLEFT",routesFrame,"TOPLEFT",14,-296)
impBox:SetMaxLetters(0)
impBtn = MakeBtn("Import", routesFrame, 60, 20)
impBtn:SetPoint("LEFT",impBox,"RIGHT",4,0)
impBtn:SetScript("OnClick", function()
    local str = impBox:GetText()
    local route, err = ImportString(str)
    if not route then prt("Import failed: "..(err or "?")); return end
    DB.routes[route.name] = route
    SyncToOverlay(); UI_Routes.RefreshList()
    impBox:SetText(""); prt("Imported: "..route.name)
end)

-- Create route button
crBtn = MakeBtn("+ Create New Route", routesFrame, WIN_W-28, 24)
crBtn:SetPoint("TOPLEFT",routesFrame,"TOPLEFT",14,-324)

-- ============================================================
-- [19] CREATE ROUTE SUB-PAGE (inside routes window)
-- ============================================================
createPage = CreateFrame("Frame",nil,routesFrame)
createPage:SetPoint("TOPLEFT",routesFrame,"TOPLEFT",14,-28)
createPage:SetPoint("BOTTOMRIGHT",routesFrame,"BOTTOMRIGHT",-14,10)
createPage:Hide()

local crBack = MakeBtn("< Routes", createPage, 70, 20)
crBack:SetPoint("TOPLEFT",createPage,"TOPLEFT",0,0)
crBack:SetScript("OnClick", function()
    if rec.active then rec.active=false; WP.Hide() end
    createPage:Hide()
    -- Restore ALL routes page elements
    backToFarm:Show(); ovBtn:Show()
    rtDiv1:Show(); rtDiv2:Show()
    crBtn:Show(); impBox:Show(); impBtn:Show(); impLbl:Show()
    rtScroll:Show(); routesLbl:Show()
    for i=1,9 do
        local f=_G["RNPackRTC"..i]
        if f then f:Show() end
    end
    UI_Routes.RefreshList()
end)

local crTitle = MakeLbl("Create New Route", createPage, 11, 1,0.82,0)
crTitle:SetPoint("LEFT",crBack,"RIGHT",8,0)

local nameLbl2 = MakeLbl("Name:", createPage, 10); nameLbl2:SetPoint("TOPLEFT",createPage,"TOPLEFT",0,-28)
local nameInput = MakeInput(createPage, WIN_W-42, 20, "RNPackRouteNameInput")
nameInput:SetPoint("TOPLEFT",createPage,"TOPLEFT",0,-44)
nameInput:SetText("My Route")
nameInput:SetScript("OnEditFocusGained", function()
    if nameInput:GetText()=="My Route" then nameInput:SetText("") end
end)
nameInput:SetScript("OnEditFocusLost", function()
    if nameInput:GetText()=="" then nameInput:SetText("My Route") end
end)
UI.recNameInput = nameInput

local typeLbl2 = MakeLbl("Type:", createPage, 10); typeLbl2:SetPoint("TOPLEFT",createPage,"TOPLEFT",0,-72)
local typeOpts2 = {"Herbs","Mines","Mixed"}; local typeIdx2=1
local typeBtn2 = MakeBtn(typeOpts2[1], createPage, 100, 20)
typeBtn2:SetPoint("TOPLEFT",createPage,"TOPLEFT",0,-88)
typeBtn2:SetScript("OnClick",function()
    typeIdx2=(typeIdx2 % #typeOpts2)+1; typeBtn2:SetText(typeOpts2[typeIdx2])
end)

local thickLbl2 = MakeLbl("Thickness:", createPage, 10); thickLbl2:SetPoint("TOPLEFT",createPage,"TOPLEFT",0,-116)
local thickBtns2={}; local curThick=3
local thickW2=math.floor((WIN_W-46)/3)
for i,lbl in ipairs({"Thin","Medium","Thick"}) do
    local tb=MakeBtn(lbl.." ("..i..")", createPage, thickW2, 20)
    tb:SetPoint("TOPLEFT",createPage,"TOPLEFT",(i-1)*(thickW2+2),-132)
    local li=i
    tb:SetScript("OnClick",function()
        curThick=li; RIBBON=RIBBON_LEVELS[li]
        for j,b in ipairs(thickBtns2) do
            b:GetNormalTexture():SetVertexColor(j==li and 0.6 or 0.3, j==li and 0.6 or 0.3, j==li and 0.6 or 0.3)
        end
    end)
    thickBtns2[i]=tb
end
thickBtns2[3]:GetNormalTexture():SetVertexColor(0.6,0.6,0.6)

local statusBox = CreateFrame("Frame",nil,createPage)
statusBox:SetSize(WIN_W-42,40); statusBox:SetPoint("TOPLEFT",createPage,"TOPLEFT",0,-162)
statusBox:SetBackdrop(BACKDROP_DARK)
statusBox:SetBackdropColor(0.05,0.05,0.05,0.9)
statusBox:SetBackdropBorderColor(0.4,0.4,0.4,0.8)
-- Parent to statusBox so it renders above the backdrop
local statusLbl = statusBox:CreateFontString(nil,"OVERLAY","GameFontNormalSmall")
statusLbl:SetPoint("CENTER",statusBox,"CENTER",0,0)
statusLbl:SetText("Ready to record.")
statusLbl:SetTextColor(0.9,0.9,0.9,1)
UI.recStatusLbl = statusLbl

local recBtnW = math.floor((WIN_W-46)/2)
local startRecBtn = MakeBtn("Start Recording", createPage, recBtnW, 22)
startRecBtn:SetPoint("TOPLEFT",createPage,"TOPLEFT",0,-210)
local stopRecBtn  = MakeBtn("Stop Recording",  createPage, recBtnW, 22)
stopRecBtn:SetPoint("LEFT",startRecBtn,"RIGHT",2,0)

local sendLbl2 = MakeLbl("Send to Overlay?", createPage, 10, 0.7,0.7,0.7)
sendLbl2:SetPoint("TOPLEFT",createPage,"TOPLEFT",0,-242); sendLbl2:Hide()
local sendBtn2 = MakeBtn("SEND TO OVERLAY", createPage, WIN_W-42, 24)
sendBtn2:SetPoint("TOPLEFT",createPage,"TOPLEFT",0,-258); sendBtn2:Hide()

local lastRoute2 = nil

startRecBtn:SetScript("OnClick", function()
    local name = nameInput:GetText()
    if name=="" or name=="My Route" then prt("Enter a route name!"); return end
    StartRecording(name, typeOpts2[typeIdx2])
    statusLbl:SetText("|cff00FF44Recording...|r  0 pts")
    startRecBtn:SetText("Recording...")
    startRecBtn:GetNormalTexture():SetVertexColor(0.4,0.4,0.4)
    sendLbl2:Hide(); sendBtn2:Hide(); lastRoute2=nil
    prt("Recording: |cffffff00"..name.."|r  ("..typeOpts2[typeIdx2]..")")
end)

stopRecBtn:SetScript("OnClick", function()
    if not rec.active then prt("Not recording."); return end
    local route, err = StopRecording(curThick)
    if not route then prt("Error: "..(err or "?")); return end
    lastRoute2 = route
    statusLbl:SetText(string.format("|cffffff00%s|r  %d pts -> %d tris", route.name, #rec.pts, route.tris))
    startRecBtn:SetText("Start Recording")
    startRecBtn:GetNormalTexture():SetVertexColor(1,1,1)
    sendLbl2:Show(); sendBtn2:Show()
    DB.routes[route.name] = route
    prt("Recorded: "..route.name.."  "..route.tris.." tris")
end)

sendBtn2:SetScript("OnClick", function()
    if not lastRoute2 then return end
    if SyncToOverlay() then
        if _RealNibbaOverlay then
            local Ov=_RealNibbaOverlay
            if Ov.Modules and Ov.Modules.UpdateMap then Ov.Modules.UpdateMap() end
            if Ov.Synchronize then Ov.Synchronize(RealNibbaOverlayOptions) end
        end
        prt("|cff00FF44Sent!|r  "..lastRoute2.name.." is on your map.")
    else prt("Overlay not loaded — saved to DB.") end
    createPage:Hide()
    -- Restore ALL routes page elements
    backToFarm:Show(); ovBtn:Show()
    rtDiv1:Show(); rtDiv2:Show()
    crBtn:Show(); impBox:Show(); impBtn:Show(); impLbl:Show()
    rtScroll:Show(); routesLbl:Show()
    for i=1,9 do
        local f=_G["RNPackRTC"..i]
        if f then f:Show() end
    end
    UI_Routes.RefreshList()
end)

crBtn:SetScript("OnClick", function()
    createPage:Show()
    -- Hide ALL routes page elements
    backToFarm:Hide(); ovBtn:Hide()
    rtDiv1:Hide(); rtDiv2:Hide()
    crBtn:Hide(); impBox:Hide(); impBtn:Hide(); impLbl:Hide()
    rtScroll:Hide(); routesLbl:Hide()
    -- Hide type checkboxes
    for i=1,9 do
        local f=_G["RNPackRTC"..i]
        if f then f:Hide() end
    end
    statusLbl:SetText("Ready to record.")
    startRecBtn:SetText("Start Recording")
    startRecBtn:GetNormalTexture():SetVertexColor(1,1,1)
    sendLbl2:Hide(); sendBtn2:Hide(); lastRoute2=nil
end)

-- ============================================================
-- [20] ROUTES LIST MANAGEMENT
-- ============================================================
-- UI_Routes forward declared above
routeRows = {}

function UI_Routes.RefreshList()
    for _,r in ipairs(routeRows) do r:Hide() end; routeRows={}
    if not DB then return end
    local rowH=26; local y=0; local W=rtChild:GetWidth() or 260
    rtChild:SetHeight(1)
    local hasAny=false
    for name, route in pairs(DB.routes) do
        hasAny=true
        local row=CreateFrame("Frame",nil,rtChild)
        row:SetHeight(rowH); row:SetPoint("TOPLEFT",rtChild,"TOPLEFT",0,y)
        row:SetPoint("TOPRIGHT",rtChild,"TOPRIGHT",0,y)
        local rbg=row:CreateTexture(nil,"BACKGROUND"); rbg:SetAllPoints()
        rbg:SetTexture(0.15,0.15,0.15,0.4)
        local col=TYPE_COLOR[route.rtype] or {r=1,g=1,b=1}
        local nl=row:CreateFontString(nil,"OVERLAY","GameFontNormalSmall")
        nl:SetPoint("LEFT",row,"LEFT",2,0); nl:SetWidth(W-120)
        nl:SetText(name); nl:SetJustifyH("LEFT")
        nl:SetTextColor(col.r,col.g,col.b,1)
        local tl=row:CreateFontString(nil,"OVERLAY","GameFontNormalSmall")
        tl:SetPoint("LEFT",row,"LEFT",W-116,0); tl:SetWidth(60)
        tl:SetText(route.rtype); tl:SetTextColor(0.6,0.6,0.6,1)
        local shareBtn=MakeBtn("Share",row,46,20)
        shareBtn:SetPoint("RIGHT",row,"RIGHT",-24,0)
        shareBtn:SetScript("OnClick",function()
            local str=ExportString(route)
            if str then UI.ShowExport(str)
            else prt("Re-record this route to enable sharing.") end
        end)
        local delBtn=MakeBtn("X",row,20,20)
        delBtn:SetPoint("RIGHT",row,"RIGHT",-2,0)
        local rn=name
        delBtn:SetScript("OnClick",function()
            DB.routes[rn]=nil; SyncToOverlay(); UI_Routes.RefreshList()
        end)
        routeRows[#routeRows+1]=row; row:Show()
        y=y-rowH-2
    end
    rtChild:SetHeight(math.max(1,math.abs(y)))
    if not hasAny then
        local el=MakeLbl("No routes saved.",rtChild,10,0.5,0.5,0.5)
        el:SetPoint("TOPLEFT",rtChild,"TOPLEFT",4,-4)
        routeRows[1]=el
    end
end

end
BuildRoutesWindow()

-- ============================================================
-- [21] MINIMAP BUTTON
-- ============================================================
local function CreateMinimapButton()
    local btn=CreateFrame("Button","RNPackMinimapBtn",Minimap)
    btn:SetSize(32,32); btn:SetFrameStrata("MEDIUM"); btn:SetFrameLevel(8)
    btn:SetClampedToScreen(true)
    local bg=btn:CreateTexture(nil,"BACKGROUND")
    bg:SetTexture("Interface/Minimap/UI-Minimap-ZoomButton-Highlight"); bg:SetAllPoints()
    local icon=btn:CreateTexture(nil,"ARTWORK")
    icon:SetTexture("Interface/Icons/Trade_Herbalism")
    icon:SetSize(20,20); icon:SetPoint("CENTER",btn,"CENTER",0,0)
    local mask=btn:CreateTexture(nil,"OVERLAY")
    mask:SetTexture("Interface/Minimap/UI-Minimap-ZoomButton-Highlight"); mask:SetAllPoints()
    mask:SetBlendMode("ADD"); mask:SetAlpha(0)
    local border=btn:CreateTexture(nil,"OVERLAY")
    border:SetTexture("Interface/Minimap/MiniMap-TrackingBorder")
    border:SetSize(54,54); border:SetPoint("TOPLEFT",btn,"TOPLEFT",-13,13)
    btn:SetScript("OnEnter",function()
        mask:SetAlpha(0.4)
        GameTooltip:SetOwner(btn,"ANCHOR_LEFT")
        GameTooltip:AddLine("|cffFFD700RealNibba|r.Pack")
        GameTooltip:AddLine("Left-click: toggle UI",1,1,1)
        GameTooltip:Show()
    end)
    btn:SetScript("OnLeave",function() mask:SetAlpha(0); GameTooltip:Hide() end)
    btn:RegisterForClicks("LeftButtonUp")
    btn:SetScript("OnClick",function()
        if farmFrame:IsShown() then farmFrame:Hide()
        else farmFrame:Show(); RefreshFarmStats(); RefreshItemList() end
    end)
    local angle=math.rad(225); local radius=80
    btn:SetPoint("CENTER",Minimap,"CENTER",radius*math.cos(angle),radius*math.sin(angle))
    btn:SetMovable(true); btn:RegisterForDrag("LeftButton")
    btn:SetScript("OnDragStart",function() btn:StartMoving() end)
    btn:SetScript("OnDragStop",function()
        btn:StopMovingOrSizing()
        local cx,cy=Minimap:GetCenter(); local bx,by=btn:GetCenter()
        if cx and cy and bx and by then
            local a=math.atan2(by-cy,bx-cx)
            btn:ClearAllPoints()
            btn:SetPoint("CENTER",Minimap,"CENTER",radius*math.cos(a),radius*math.sin(a))
        end
    end)
end

-- ============================================================
-- [22] TICKER
-- ============================================================
local ticker=CreateFrame("Frame")
local updateT=0
ticker:SetScript("OnUpdate",function(self,elapsed)
    WP.Update(elapsed)
    updateT=updateT+elapsed
    if updateT>=1.0 then
        updateT=0
        if farmFrame:IsShown() then RefreshFarmStats() end
    end
    if rec.active then
        rec.timer=rec.timer+elapsed
        if rec.timer>=SAMPLE_RATE then
            rec.timer=0
            local px,py=GetPlayerMapPosition("player")
            if not px or (px==0 and py==0) then
                SafeSetMapToCurrentZone(); px,py=GetPlayerMapPosition("player")
            end
            if px and not(px==0 and py==0) and px>=0 and px<=1 and py>=0 and py<=1 then
                local curMap=GetCurrentMapAreaID()-1
                if curMap==rec.mapID then
                    if rec.lastX==nil then
                        rec.lastX,rec.lastY=px,py; rec.pts[1]={px,py}
                        if not rec.wpShown then rec.wpShown=true; WP.Show(rec.mapID,px,py,rec.name) end
                    else
                        local dx=px-rec.lastX; local dy=py-rec.lastY
                        if math.sqrt(dx*dx+dy*dy)>=MIN_DIST_MAP then
                            rec.pts[#rec.pts+1]={px,py}; rec.lastX,rec.lastY=px,py
                        end
                    end
                    if UI.recStatusLbl and rec.active then
                        UI.recStatusLbl:SetText("|cff00FF44Recording...|r  "..#rec.pts.." pts")
                    end
                end
            end
        end
    end
end)

-- ============================================================
-- [23] EVENTS
-- ============================================================
eventFrame = CreateFrame("Frame","RealNibbaPack_EventFrame",UIParent)
eventFrame:RegisterEvent("PLAYER_LOGIN")
eventFrame:RegisterEvent("PLAYER_ENTERING_WORLD")
eventFrame:RegisterEvent("ZONE_CHANGED_NEW_AREA")
eventFrame:RegisterEvent("ZONE_CHANGED")
eventFrame:RegisterEvent("PLAYER_STARTED_MOVING")

eventFrame:SetScript("OnEvent",function(self,event,...)
    if event=="PLAYER_LOGIN" then
        InitDB()
        CreateMinimapButton()
        if _RealNibbaOverlay then
            _RealNibbaOverlay.Init()
            if next(DB.routes) then SyncToOverlay() end
        end
        curZone=GetZoneText() or ""
        RefreshFarmStats(); UI_Routes.RefreshList()
        -- Zone retry
        local zr=CreateFrame("Frame"); local zw=0
        zr:SetScript("OnUpdate",function(s,e)
            zw=zw+e
            if zw>=2 then s:SetScript("OnUpdate",nil)
                local z=GetZoneText() or ""
                if z~="" then curZone=z; RefreshFarmStats() end
            end
        end)
        prt("Loaded v"..ADDON_VER.." — /rnpack to toggle")

    elseif event=="PLAYER_STARTED_MOVING" then
        -- Clear all edit box focus so player movement isn't blocked
        for _, name in ipairs({"RNPackRouteNameInput","RNPackImportBox","RNPackPriceEditBox","RNPackExportBox"}) do
            local box = _G[name]
            if box and box.ClearFocus then box:ClearFocus() end
        end

    elseif event=="PLAYER_ENTERING_WORLD" or event=="ZONE_CHANGED_NEW_AREA" or event=="ZONE_CHANGED" then
        SafeSetMapToCurrentZone()
        local z=GetZoneText() or ""
        if z~="" then curZone=z end
        RefreshFarmStats()
        if event=="PLAYER_ENTERING_WORLD" then
            local _,instType=IsInInstance()
            if instType=="raid" or instType=="party" then
                local name=GetInstanceInfo() or curZone
                instanceLog[name]=(instanceLog[name] or 0)+1
            end
        end

    elseif event=="CHAT_MSG_LOOT" then
        local msg=...
        OnChatLoot(msg)
        if farmFrame:IsShown() then RefreshItemList() end

    elseif event=="CHAT_MSG_MONEY" then
        OnChatMoney(...)

    elseif event=="QUEST_TURNED_IN" then
        local _,_,money=...
        OnQuestTurnedIn(money)

    elseif event=="COMBAT_LOG_EVENT_UNFILTERED" then
        OnCombatLog(...)
    end
end)

-- ============================================================
-- [24] SLASH COMMANDS
-- ============================================================
SLASH_RNPACK1="/rnpack"
SLASH_RNPACK2="/realnibbapack"
SlashCmdList["RNPACK"]=function()
    if farmFrame:IsShown() then farmFrame:Hide()
    else farmFrame:Show(); RefreshFarmStats(); RefreshItemList() end
end
