local _, ns = ...

ns.Atlas = {}
local Atlas = ns.Atlas

local fallbackData = {
    zones = {
        ["11"] = {
            id = 11,
            name = "Wetlands",
            fishingLevel = 55,
            fishingPools = {
                { id = "6358", count = 50 },
                { id = "6359", count = 69 },
            },
            fishStats = {
                ["6361"] = { catchChance = 0.49 },
                ["6358"] = { catchChance = 0.24 },
                ["6359"] = { catchChance = 0.17 },
                ["6308"] = { catchChance = 0.02 },
            },
        },
        ["17"] = {
            id = 17,
            name = "The Barrens",
            fishingLevel = 1,
            fishingPools = {
                { id = "6522", count = 32 },
                { id = "6358", count = 28 },
                { id = "6359", count = 5 },
            },
            fishStats = {
                ["6522"] = { catchChance = 0.27 },
                ["6289"] = { catchChance = 0.19 },
                ["6358"] = { catchChance = 0.18 },
                ["6308"] = { catchChance = 0.07 },
            },
        },
        ["40"] = {
            id = 40,
            name = "Westfall",
            fishingLevel = 1,
            fishingPools = {
                { id = "6358", count = 38 },
            },
            fishStats = {
                ["6358"] = { catchChance = 0.41 },
                ["6361"] = { catchChance = 0.21 },
                ["6303"] = { catchChance = 0.14 },
                ["6289"] = { catchChance = 0.04 },
            },
        },
    },
    fish = {
        ["6358"] = { name = "Oily Blackmouth", requirements = { "Common in coastal pools and open water." } },
        ["6359"] = { name = "Firefin Snapper", requirements = { "Best used for alchemy oils." } },
        ["6522"] = { name = "Deviate Fish", requirements = { "Wailing Caverns has a strong catch rate." } },
        ["6361"] = { name = "Raw Rainbow Fin Albacore", requirements = {} },
        ["6308"] = { name = "Raw Bristle Whisker Catfish", requirements = {} },
        ["6289"] = { name = "Raw Longjaw Mud Snapper", requirements = {} },
    },
    recipes = {
        ["6358"] = {
            { productId = "6370", recipeName = "Blackmouth Oil" },
        },
        ["6359"] = {
            { productId = "6371", recipeName = "Fire Oil" },
        },
        ["6522"] = {
            { productId = "6657", recipeName = "Savory Deviate Delight" },
        },
    },
    equipment = {
        lures = {
            { id = 6529, order = 1, desc = "Shiny Bauble from fishing vendors." },
            { id = 6530, order = 2, desc = "Nightcrawlers from fishing vendors." },
            { id = 6532, order = 3, desc = "Bright Baubles from fishing vendors." },
            { id = 6533, order = 4, desc = "Aquadynamic fish lens style lure options." },
            { id = 6811, order = 5, desc = "Aquadynamic Fish Attractor." },
        },
        rods = {
            { id = 6256, order = 1, desc = "Basic fishing pole from vendors." },
            { id = 6365, order = 2, desc = "Strong Fishing Pole, limited vendor stock." },
        },
    },
    mapToZoneID = {
        [1436] = "40",
        [1437] = "11",
        [1413] = "17",
    },
}

local function trimLower(text)
    return string.lower(tostring(text or ""):gsub("^%s+", ""):gsub("%s+$", ""))
end

local function normalizeZoneToken(text)
    return trimLower(text):gsub("[%s%p_%-]+", "")
end

local function getData()
    if ns.EmbeddedAtlasData and ns.EmbeddedAtlasData.zones and ns.EmbeddedAtlasData.fish then
        return ns.EmbeddedAtlasData, "Azeroth Angler Atlas (Embedded)"
    end
    return fallbackData, "Azeroth Angler Atlas (Fallback)"
end

local function getCurrentMapID()
    if C_Map and C_Map.GetBestMapForUnit then
        local mapID = C_Map.GetBestMapForUnit("player")
        if mapID and tonumber(mapID) then
            return tonumber(mapID)
        end
    end
    return nil
end

local function resolveMapToZone(data, mapID)
    local currentMapID = tonumber(mapID)
    if not currentMapID then return nil, nil end

    local seen = {}
    local depth = 0
    while currentMapID and not seen[currentMapID] and depth < 10 do
        seen[currentMapID] = true
        local mappedZoneID = (data.mapToZoneID and (data.mapToZoneID[currentMapID] or data.mapToZoneID[tostring(currentMapID)])) or nil
        if mappedZoneID then
            local zoneID = tostring(mappedZoneID)
            local zone = data.zones and data.zones[zoneID] or nil
            if zone then
                return zone, zoneID
            end
        end
        if C_Map and C_Map.GetMapInfo then
            local mapInfo = C_Map.GetMapInfo(currentMapID)
            currentMapID = mapInfo and tonumber(mapInfo.parentMapID) or nil
        else
            currentMapID = nil
        end
        depth = depth + 1
    end

    return nil, nil
end

local function resolveNameToZone(data, zoneName)
    local wanted = trimLower(zoneName)
    local wantedToken = normalizeZoneToken(zoneName)
    if wanted == "" and wantedToken == "" then
        return nil, nil
    end

    local partialRow, partialZoneID
    for zoneID, row in pairs(data.zones or {}) do
        local rowName = tostring(row.name or "")
        local rowLower = trimLower(rowName)
        local rowToken = normalizeZoneToken(rowName)

        if rowLower == wanted or rowToken == wantedToken then
            return row, tostring(zoneID)
        end

        if not partialRow and wanted ~= "" then
            if string.find(rowLower, wanted, 1, true) or string.find(wanted, rowLower, 1, true) then
                partialRow = row
                partialZoneID = tostring(zoneID)
            elseif wantedToken ~= "" and (string.find(rowToken, wantedToken, 1, true) or string.find(wantedToken, rowToken, 1, true)) then
                partialRow = row
                partialZoneID = tostring(zoneID)
            end
        end
    end

    return partialRow, partialZoneID
end

local function resolveMapNameChainToZone(data, mapID)
    local currentMapID = tonumber(mapID)
    if not currentMapID or not (C_Map and C_Map.GetMapInfo) then
        return nil, nil
    end

    local seen = {}
    local depth = 0
    while currentMapID and not seen[currentMapID] and depth < 10 do
        seen[currentMapID] = true
        local mapInfo = C_Map.GetMapInfo(currentMapID)
        if mapInfo and mapInfo.name and mapInfo.name ~= "" then
            local byNameZone, byNameZoneID = resolveNameToZone(data, mapInfo.name)
            if byNameZone then
                return byNameZone, byNameZoneID
            end
        end
        currentMapID = mapInfo and tonumber(mapInfo.parentMapID) or nil
        depth = depth + 1
    end

    return nil, nil
end

local function rememberLastKnownZone(zoneID)
    if not zoneID then return end
    if not (ns.DB and ns.DB.profile and ns.DB.profile.atlas and ns.DB.profile.atlas.rememberLastKnownZone) then
        return
    end
    ns.DB.profile.atlas.lastKnownZoneID = tostring(zoneID)
end

local function resolveRememberedZone(data)
    local remembered = ns.DB and ns.DB.profile and ns.DB.profile.atlas and ns.DB.profile.atlas.lastKnownZoneID or nil
    local zoneID = tostring(remembered or "")
    if zoneID == "" then return nil, nil end
    local zone = data.zones and data.zones[zoneID] or nil
    if zone then
        return zone, zoneID
    end
    return nil, nil
end

local function resolveHistoryZone(data)
    local zoneStats = ns.DB and ns.DB.v2 and ns.DB.v2.zoneStats or nil
    if type(zoneStats) ~= "table" then return nil, nil end

    local bestName, bestCatches = nil, -1
    for zoneName, stats in pairs(zoneStats) do
        local catches = tonumber(stats and stats.catches) or 0
        if catches > bestCatches then
            bestCatches = catches
            bestName = zoneName
        end
    end
    if not bestName or bestCatches <= 0 then
        return nil, nil
    end

    return resolveNameToZone(data, bestName)
end

local function pickFallbackZone(data)
    local bestRow, bestZoneID, bestFishCount = nil, nil, -1
    for zoneID, row in pairs(data.zones or {}) do
        local fishCount = 0
        for _ in pairs(row.fishStats or {}) do
            fishCount = fishCount + 1
        end
        if fishCount > bestFishCount then
            bestFishCount = fishCount
            bestRow = row
            bestZoneID = tostring(zoneID)
        end
    end
    return bestRow, bestZoneID
end

local function fishName(data, fishID)
    local row = data.fish and data.fish[tostring(fishID)]
    if row and row.name then
        return row.name
    end
    local name = GetItemInfo and GetItemInfo(tonumber(fishID)) or nil
    return name or ("Item " .. tostring(fishID))
end

local function clampList(rows, limit)
    if not limit or #rows <= limit then return rows end
    while #rows > limit do
        table.remove(rows)
    end
    return rows
end

function Atlas:GetDataSourceName()
    local _, source = getData()
    return source
end

function Atlas:GetCurrentMapID()
    return getCurrentMapID()
end

function Atlas:GetZoneInfo(zoneName, mapID)
    local data = getData()

    local numericZoneID = tonumber(zoneName)
    if numericZoneID and data.zones and data.zones[tostring(numericZoneID)] then
        return data.zones[tostring(numericZoneID)], tostring(numericZoneID)
    end

    local mapZone, mapZoneID = resolveMapToZone(data, mapID or getCurrentMapID())
    if mapZone then
        return mapZone, mapZoneID
    end

    local nameZone, nameZoneID = resolveNameToZone(data, zoneName)
    if nameZone then
        return nameZone, nameZoneID
    end

    return nil, nil
end

local function buildFishRateRows(self, data, zone, limit)
    local rows = {}
    for fishID, stat in pairs(zone.fishStats or {}) do
        rows[#rows + 1] = {
            itemID = tostring(fishID),
            name = fishName(data, fishID),
            catchChance = tonumber(stat.catchChance) or 0,
            auction = self:GetAuctionPrice(tostring(fishID)),
        }
    end
    table.sort(rows, function(a, b)
        if a.catchChance == b.catchChance then
            return a.name < b.name
        end
        return a.catchChance > b.catchChance
    end)
    return clampList(rows, limit)
end

local function buildPoolRows(data, zone, limit)
    local rows = {}
    for _, pool in ipairs(zone.fishingPools or {}) do
        rows[#rows + 1] = {
            itemID = tostring(pool.id),
            name = fishName(data, pool.id),
            count = tonumber(pool.count) or 0,
        }
    end
    table.sort(rows, function(a, b)
        if a.count == b.count then
            return a.name < b.name
        end
        return a.count > b.count
    end)
    return clampList(rows, limit)
end

local function pickRecipeFishID(data, fishRateRows)
    for i = 1, #fishRateRows do
        local fishID = tostring(fishRateRows[i].itemID or "")
        local recipes = data.recipes and data.recipes[fishID] or nil
        if recipes and #recipes > 0 then
            return fishID
        end
    end
    return nil
end

function Atlas:GetZoneFishRates(zoneName, limit, mapID)
    local data = getData()
    local zone = self:GetZoneInfo(zoneName, mapID)
    if not zone then return {} end
    return buildFishRateRows(self, data, zone, limit)
end

function Atlas:GetZonePoolCounts(zoneName, limit, mapID)
    local data = getData()
    local zone = self:GetZoneInfo(zoneName, mapID)
    if not zone then return {} end
    return buildPoolRows(data, zone, limit)
end

function Atlas:GetRecipesForFish(fishID, limit, fishRateRows)
    local data = getData()
    local rows = {}
    for _, recipe in ipairs((data.recipes and data.recipes[tostring(fishID)]) or {}) do
        rows[#rows + 1] = {
            productID = tostring(recipe.productId or ""),
            recipeName = tostring(recipe.recipeName or "Unknown recipe"),
            auction = self:GetAuctionPrice(tostring(recipe.productId or "")),
        }
    end

    -- Some zones (especially newly added ones) may not have embedded recipe mappings yet.
    -- In that case, show top fish market rows so the Recipes/Prices panel is still useful.
    if #rows == 0 then
        if fishRateRows and #fishRateRows > 0 then
            for i = 1, math.min(#fishRateRows, tonumber(limit) or #fishRateRows) do
                local fishRow = fishRateRows[i]
                local fishIDValue = tostring(fishRow and fishRow.itemID or "")
                rows[#rows + 1] = {
                    productID = fishIDValue,
                    recipeName = tostring((fishRow and fishRow.name) or fishName(data, fishIDValue)) .. " (market)",
                    auction = self:GetAuctionPrice(fishIDValue),
                    isMarket = true,
                }
            end
        elseif fishID then
            local fishIDValue = tostring(fishID)
            rows[#rows + 1] = {
                productID = fishIDValue,
                recipeName = fishName(data, fishIDValue) .. " (market)",
                auction = self:GetAuctionPrice(fishIDValue),
                isMarket = true,
            }
        end
    end

    return clampList(rows, limit)
end

function Atlas:GetFishRequirements(fishID, limit)
    local data = getData()
    local req = (data.fish and data.fish[tostring(fishID)] and data.fish[tostring(fishID)].requirements) or {}
    local rows = {}
    for i = 1, #req do
        rows[#rows + 1] = tostring(req[i])
    end
    return clampList(rows, limit)
end

function Atlas:GetEquipmentRows(limit)
    local data = getData()
    local rows = {}
    for categoryName, categoryRows in pairs((data.equipment or {})) do
        for _, row in ipairs(categoryRows) do
            rows[#rows + 1] = {
                category = tostring(categoryName),
                itemID = tostring(row.id or ""),
                desc = tostring(row.desc or ""),
            }
        end
    end
    table.sort(rows, function(a, b)
        if a.category == b.category then
            return tonumber(a.itemID) < tonumber(b.itemID)
        end
        return a.category < b.category
    end)
    return clampList(rows, limit)
end

function Atlas:GetAuctionPrice(itemID)
    if not (_G.Auctionator and Auctionator.Database and Auctionator.Database.GetPrice) then
        return nil
    end
    local ok, price = pcall(function()
        return Auctionator.Database:GetPrice(tostring(itemID))
    end)
    if ok and price and tonumber(price) and tonumber(price) > 0 then
        return tonumber(price)
    end
    return nil
end

function Atlas:FormatAuctionPrice(itemID)
    local price = self:GetAuctionPrice(itemID)
    if not price then return "n/a" end
    local g = math.floor(price / 10000)
    local s = math.floor((price % 10000) / 100)
    local c = math.floor(price % 100)
    return string.format("%dg %ds %dc", g, s, c)
end

function Atlas:GetMapZoneIDs(zoneID)
    local data = getData()
    local ids = {}
    local wanted = tostring(zoneID or "")
    for mapID, zone in pairs(data.mapToZoneID or {}) do
        if tostring(zone) == wanted then
            ids[#ids + 1] = tonumber(mapID)
        end
    end
    table.sort(ids)
    return ids
end

function Atlas:BuildZoneSummary(zoneName, mapID)
    local data, source = getData()
    local resolvedMapID = tonumber(mapID) or getCurrentMapID()
    local requestedZoneName = tostring(zoneName or "")
    local zone, zoneID = self:GetZoneInfo(requestedZoneName, resolvedMapID)
    local usedFallbackZone = false
    local fallbackSource = nil
    local canUseGuessedFallback = (requestedZoneName == "" and not resolvedMapID)

    if not zone then
        zone, zoneID = resolveMapNameChainToZone(data, resolvedMapID)
        if zone then
            usedFallbackZone = true
            fallbackSource = "parent-map"
        end
    end

    if not zone and canUseGuessedFallback then
        zone, zoneID = resolveHistoryZone(data)
        if zone then
            usedFallbackZone = true
            fallbackSource = "history"
        end
    end

    if not zone and canUseGuessedFallback then
        zone, zoneID = resolveRememberedZone(data)
        if zone then
            usedFallbackZone = true
            fallbackSource = "last-known"
        end
    end

    if not zone then
        return {
            zoneName = requestedZoneName,
            zoneID = nil,
            source = source,
            fishRates = {},
            pools = {},
            recipes = {},
            requirements = {},
            equipment = self:GetEquipmentRows(6),
            mapIDs = {},
            fishingLevel = nil,
            mapID = resolvedMapID,
            usedFallbackZone = usedFallbackZone,
            fallbackSource = fallbackSource,
            requestedZoneName = requestedZoneName,
        }
    end

    if not usedFallbackZone or fallbackSource == "parent-map" then
        rememberLastKnownZone(zoneID)
    end

    local fishRates = buildFishRateRows(self, data, zone, 8)
    local pools = buildPoolRows(data, zone, 6)
    local primaryFish = fishRates[1] and fishRates[1].itemID or nil
    local recipeFish = pickRecipeFishID(data, fishRates) or primaryFish
    return {
        zoneName = zone.name,
        zoneID = zoneID,
        source = source,
        fishRates = fishRates,
        pools = pools,
        recipes = self:GetRecipesForFish(recipeFish, 6, fishRates),
        requirements = primaryFish and self:GetFishRequirements(primaryFish, 4) or {},
        equipment = self:GetEquipmentRows(6),
        mapIDs = self:GetMapZoneIDs(zoneID),
        fishingLevel = zone.fishingLevel,
        mapID = resolvedMapID,
        usedFallbackZone = usedFallbackZone,
        fallbackSource = fallbackSource,
        requestedZoneName = requestedZoneName,
        recipeFishID = recipeFish,
    }
end
