--[[
    READABLE CONFIGURATION FRONT END

    This module loads loot_config.lua and checks common editing mistakes before
    any Unreal Engine objects are changed. Keeping validation here makes both
    main.lua and the difficult game-facing code easier to follow.
]]

local Logger = require("logger")

local ConfigLoader = {}

local MAX_IDENTIFIER_LENGTH = 128
local MAX_QUANTITY = 2147483647


local function isFiniteNumber(value)
    return type(value) == "number"
        and value == value
        and value ~= math.huge
        and value ~= -math.huge
end


local function isNonNegativeInteger(value)
    return isFiniteNumber(value)
        and value >= 0
        and value <= MAX_QUANTITY
        and value == math.floor(value)
end


local function isIdentifier(value)
    return type(value) == "string"
        and #value > 0
        and #value <= MAX_IDENTIFIER_LENGTH
        and value:match("^[%w_-]+$") ~= nil
end


local function validateLootEntry(bagId, position, loot)
    local location = bagId .. ".Loot[" .. tostring(position) .. "]"
    local valid = true

    if type(loot) ~= "table" then
        Logger.Warning("CONFIG", location .. " is not a { ... } block; ignored")
        return nil
    end

    if not isIdentifier(loot.Item) then
        Logger.Warning("CONFIG", location
            .. ".Item is not a safe ITEM_ID; drop ignored")
        valid = false
    end

    if not isFiniteNumber(loot.DropRate)
        or loot.DropRate < 0
        or loot.DropRate > 1 then
        Logger.Warning("CONFIG", location
            .. ".DropRate must be a finite number from 0 to 1; drop ignored")
        valid = false
    end

    if not isNonNegativeInteger(loot.Min) then
        Logger.Warning("CONFIG", location .. ".Min must be a whole number from 0 to "
            .. tostring(MAX_QUANTITY) .. "; drop ignored")
        valid = false
    end

    if not isNonNegativeInteger(loot.Max) then
        Logger.Warning("CONFIG", location .. ".Max must be a whole number from 0 to "
            .. tostring(MAX_QUANTITY) .. "; drop ignored")
        valid = false
    end

    if isNonNegativeInteger(loot.Min)
        and isNonNegativeInteger(loot.Max)
        and loot.Min > loot.Max then
        Logger.Warning("CONFIG", location
            .. ".Min cannot be greater than .Max; drop ignored")
        valid = false
    end

    if not valid then
        return nil
    end

    -- Copy only the four supported fields. This prevents accidental helper
    -- values or metatables in the user file from reaching Unreal-facing code.
    return {
        Item = loot.Item,
        DropRate = loot.DropRate,
        Min = loot.Min,
        Max = loot.Max,
    }
end


local function sortedKeys(source)
    local keys = {}

    for key in pairs(source) do
        keys[#keys + 1] = key
    end

    table.sort(keys, function(left, right)
        return tostring(left) < tostring(right)
    end)

    return keys
end


local function sanitizeLootList(bagId, lootList)
    local positions = {}
    local skipped = 0
    local hadEntries = next(lootList) ~= nil

    for key in pairs(lootList) do
        if type(key) == "number"
            and key >= 1
            and key == math.floor(key) then
            positions[#positions + 1] = key
        else
            skipped = skipped + 1
            Logger.Warning("CONFIG", bagId .. ".Loot contains unsupported key "
                .. tostring(key) .. "; entry ignored")
        end
    end

    table.sort(positions)

    local cleanLoot = {}
    for _, position in ipairs(positions) do
        local loot = validateLootEntry(bagId, position, lootList[position])

        if loot then
            cleanLoot[#cleanLoot + 1] = loot
        else
            skipped = skipped + 1
        end
    end

    return cleanLoot, hadEntries, skipped
end


local function sanitizeConfig(config)
    if type(config) ~= "table" then
        Logger.Error("CONFIG", "loot_config.lua must return one table")
        return nil
    end

    local cleanConfig = {}
    local stats = {
        Bags = 0,
        Drops = 0,
        SkippedBags = 0,
        SkippedDrops = 0,
    }

    for _, bagId in ipairs(sortedKeys(config)) do
        local bagSettings = config[bagId]

        if not isIdentifier(bagId) then
            stats.SkippedBags = stats.SkippedBags + 1
            Logger.Warning("CONFIG", "Unsafe loot-bag key " .. tostring(bagId)
                .. " ignored")
        elseif type(bagSettings) ~= "table" then
            stats.SkippedBags = stats.SkippedBags + 1
            Logger.Warning("CONFIG", bagId
                .. " does not contain a { Loot = { ... } } table; bag ignored")
        elseif type(bagSettings.Loot) ~= "table" then
            stats.SkippedBags = stats.SkippedBags + 1
            Logger.Warning("CONFIG", bagId .. ".Loot is not a table; bag ignored")
        else
            local cleanLoot, hadEntries, skipped = sanitizeLootList(
                bagId,
                bagSettings.Loot
            )

            stats.SkippedDrops = stats.SkippedDrops + skipped

            -- Loot = {} is an intentional request to clear a bag. A non-empty
            -- list whose entries were all invalid must not clear it by accident.
            if hadEntries and #cleanLoot == 0 then
                stats.SkippedBags = stats.SkippedBags + 1
                Logger.Warning("CONFIG", bagId
                    .. " has no valid drops; bag configuration ignored")
            else
                cleanConfig[bagId] = { Loot = cleanLoot }
                stats.Bags = stats.Bags + 1
                stats.Drops = stats.Drops + #cleanLoot
            end
        end
    end

    return cleanConfig, stats
end


function ConfigLoader.Load(path)
    local loaded, configOrError = pcall(dofile, path)

    if not loaded then
        Logger.Error("CONFIG", "Could not load " .. tostring(path))
        Logger.Error("CONFIG", configOrError)
        return nil
    end

    local config, stats = sanitizeConfig(configOrError)
    if not config then
        return nil
    end

    Logger.Info("Configuration loaded: " .. tostring(stats.Bags)
        .. " bag(s), " .. tostring(stats.Drops) .. " drop(s); skipped "
        .. tostring(stats.SkippedBags) .. " bag(s) and "
        .. tostring(stats.SkippedDrops) .. " drop(s)")

    return config, stats
end

return ConfigLoader
