-- sv_sdf_system_api.lua — API publique serveur.
-- Fonctions appelables par d'autres addons.
-- NE PAS AddCSLuaFile ce fichier.

SDFSystem = SDFSystem or {}

-- ─── Dépendance DBALevel (douce) ─────────────────────────────────────────
local function TryAddXP(ply, xpType, amount, source)
    if not DBALevel or not DBALevel.AddXP then return end
    local ok, _, leveled = DBALevel.AddXP(ply, xpType, amount, source or "sdf_search")
    if not ok then return end
    if leveled and SDFSystem.SvConfig.Debug then
        print("[SDFSystem] " .. ply:Nick() .. " a leveled up (" .. xpType .. ") suite à une fouille.")
    end
end

local function GetLegalLevel(ply)
    if DBALevel and DBALevel.GetLevel then
        return math.floor(tonumber(DBALevel.GetLevel(ply, "legal")) or 0)
    end
    return 0
end

-- =========================================================================
-- A. Tirage d'argent pondéré vers le bas
-- =========================================================================
-- Formule : amount = floor( min + (max - min) * (rand() ^ bias) )
--   bias > 1 → courbe de probabilité décroissante (petits montants plus fréquents).
--   bias = 1 → uniforme.
local function BiasedMoneyRoll(minV, maxV, bias)
    minV = math.floor(tonumber(minV) or 0)
    maxV = math.floor(tonumber(maxV) or minV)
    bias = tonumber(bias) or 1.0
    if minV >= maxV then return minV end
    return math.floor(minV + (maxV - minV) * (math.random() ^ bias))
end

-- Calcule min/max effectifs en tenant compte du niveau légal.
local function EffectiveMoneyRange(lot, lvl)
    local cfg     = SDFSystem.Config
    local mRange  = cfg.MoneyRange
    local scaling = cfg.LevelScaling

    -- Base : lot peut surcharger les valeurs globales.
    local baseMin  = tonumber(lot.min)  or tonumber(mRange.min)  or 100
    local baseMax  = tonumber(lot.max)  or tonumber(mRange.max)  or 500
    local baseBias = tonumber(lot.bias) or tonumber(mRange.bias) or 2.0

    -- Scaling niveau.
    local factorMax = tonumber(scaling.MoneyLevelFactor)    or 0.02
    local factorMin = tonumber(scaling.MoneyLevelMinFactor) or 0.005
    local effMin = math.floor(baseMin * (1 + lvl * factorMin))
    local effMax = math.floor(baseMax * (1 + lvl * factorMax))
    if effMin > effMax then effMin = effMax end

    return effMin, effMax, baseBias
end

-- =========================================================================
-- E. Système de rareté
-- =========================================================================
-- Calcule les poids effectifs par tier, en tenant compte du niveau légal.
local function CalcTierWeights(lvl)
    local rarities = SDFSystem.Config.Rarities
    local scaling  = SDFSystem.Config.LevelScaling

    -- Niveau effectif plafonné (évite l'emballement des raretés à haut niveau).
    local maxLvl = tonumber(scaling.MaxBonusLevel) or 50
    local effLvl = math.Clamp(math.floor(lvl or 0), 0, maxLvl)

    local weights = {}
    for _, r in ipairs(rarities) do
        local w = tonumber(r.baseWeight) or 0

        -- Bonus de niveau légal (doux : décale légèrement vers les raretés hautes).
        local lvlBonus = tonumber(scaling.RarityBonusPerLevel[r.id]) or 0
        w = w + effLvl * lvlBonus

        -- Poids ne peut pas être négatif.
        weights[r.id] = math.max(0, w)
    end
    return weights
end

-- Tire un tier de rareté au hasard selon les poids effectifs.
-- Retourne l'id du tier (string) ou "commun" en fallback.
local function DrawRarityTier(tierWeights)
    local rarities = SDFSystem.Config.Rarities
    local total    = 0
    for _, r in ipairs(rarities) do
        total = total + (tierWeights[r.id] or 0)
    end
    if total <= 0 then return "commun" end

    local roll = math.random() * total
    local cumul = 0
    for _, r in ipairs(rarities) do
        cumul = cumul + (tierWeights[r.id] or 0)
        if roll <= cumul then return r.id end
    end
    return rarities[#rarities].id
end

-- =========================================================================
-- H. Tirage de loot par modèle
-- =========================================================================
-- Récupère la table de loot du modèle (ou "default").
local function GetLootTable(model)
    local byModel = SDFSystem.Config.LootByModel
    local tbl = byModel[model]
    if not tbl then
        tbl = byModel["default"]
    end
    return tbl or {}
end

-- Tire un lot dans le tier donné depuis la table fournie.
-- Si aucun lot de ce tier, retombe sur le tier inférieur (index dans Rarities).
local function DrawLootFromTier(lootTable, tierId)
    local rarities = SDFSystem.Config.Rarities

    -- Ordre des tiers (index dans la config = ordre croissant de puissance).
    local tierOrder = {}
    for i, r in ipairs(rarities) do tierOrder[r.id] = i end

    local targetIdx = tierOrder[tierId] or 1

    -- Chercher un lot du tier exact, puis descendre si aucun n'existe.
    for attempt = targetIdx, 1, -1 do
        local wantedTier = rarities[attempt] and rarities[attempt].id or "commun"

        -- Lots du tier voulu dans la table.
        local candidates = {}
        local totalW     = 0
        for _, lot in ipairs(lootTable) do
            if lot.rarity == wantedTier then
                local w = tonumber(lot.weight) or 0
                totalW  = totalW + w
                candidates[#candidates + 1] = lot
            end
        end

        if totalW > 0 then
            -- Tirage pondéré parmi les candidats.
            local roll  = math.random() * totalW
            local cumul = 0
            for _, lot in ipairs(candidates) do
                cumul = cumul + (tonumber(lot.weight) or 0)
                if roll <= cumul then return lot, wantedTier end
            end
            -- Fallback dernier candidat.
            return candidates[#candidates], wantedTier
        end
        -- Aucun lot dans ce tier → essaie le tier inférieur.
    end

    -- Aucun lot du tout → nil.
    return nil, "commun"
end

-- =========================================================================
-- Livraison d'un lot
-- =========================================================================
local function DeliverLoot(ply, lot, lvl)
    if not IsValid(ply) or not lot then return "rien" end
    local cfg = SDFSystem.Config

    if lot.type == "money" then
        local effMin, effMax, bias = EffectiveMoneyRange(lot, lvl)
        local amount = BiasedMoneyRoll(effMin, effMax, bias)
        ply:addMoney(amount)
        return DarkRP.formatMoney(amount)

    elseif lot.type == "weapon" then
        local class = tostring(lot.class or "")
        if class == "" then return "arme invalide" end
        ply:Give(class)
        return class

    elseif lot.type == "entity" then
        local class = tostring(lot.class or "")
        if class == "" then return "entité invalide" end
        local spawnDist = math.floor(tonumber(cfg.EntitySpawnDistance) or 60)
        local spawnPos  = ply:GetPos() + ply:GetForward() * spawnDist

        if class == "spawned_money" then
            local effMin, effMax, bias = EffectiveMoneyRange(lot, lvl)
            local amount = BiasedMoneyRoll(effMin, effMax, bias)
            if DarkRP and DarkRP.createMoneyBag then
                local bag = DarkRP.createMoneyBag(spawnPos + Vector(0, 0, 16), amount)
                if IsValid(bag) and bag.CPPISetOwner then bag:CPPISetOwner(ply) end
                return DarkRP.formatMoney(amount) .. " (sac)"
            end
            ply:addMoney(amount)
            return DarkRP.formatMoney(amount)
        end

        local ent = ents.Create(class)
        if IsValid(ent) then
            ent:SetPos(spawnPos)
            ent:Spawn()
            ent:DropToFloor()
            if ent.CPPISetOwner then ent:CPPISetOwner(ply) end
            return class
        else
            ErrorNoHalt("[SDFSystem] DeliverLoot : entité " .. class .. " invalide — fallback money\n")
            local fallback = BiasedMoneyRoll(50, 200, 2.0)
            ply:addMoney(fallback)
            return DarkRP.formatMoney(fallback) .. " (fallback)"
        end

    else
        return "lot inconnu"
    end
end

-- =========================================================================
-- G. Tirage d'issue (risque/récompense)
-- =========================================================================
local function DrawOutcome()
    local weights = SDFSystem.Config.Outcomes.Weights
    local total   = 0
    for _, w in pairs(weights) do total = total + (tonumber(w) or 0) end
    if total <= 0 then return "success" end

    local roll  = math.random() * total
    local cumul = 0
    -- Ordre défini pour la reproductibilité.
    local order = { "success", "nothing", "rat", "contraband" }
    for _, key in ipairs(order) do
        cumul = cumul + (tonumber(weights[key]) or 0)
        if roll <= cumul then return key end
    end
    return "success"
end

-- =========================================================================
-- API publique : complétion de fouille
-- =========================================================================
function SDFSystem.CompletSearch(ply, trashEnt)
    if not IsValid(ply)       then return end
    if not IsValid(trashEnt)  then return end
    if trashEnt.searchCompleted then return end
    trashEnt.searchCompleted = true

    -- Immersion : stopper son et geste.
    trashEnt:StopSearchSound()
    trashEnt:StopGestureLoop()

    -- ── Niveau légal (scaling D) ──────────────────────────────────────────
    local lvl = GetLegalLevel(ply)

    -- ── G. Tirage d'issue ─────────────────────────────────────────────────
    local outcome = DrawOutcome()

    -- ── Cooldown (dans tous les cas) ──────────────────────────────────────
    trashEnt.cooldownEnd = CurTime() + SDFSystem.Config.CooldownDuration
    trashEnt:SetCooldownEnd(trashEnt.cooldownEnd)

    -- Reset état fouille.
    trashEnt.searcher  = nil
    trashEnt.searchEnd = nil
    trashEnt:SetSearchEnd(0)
    trashEnt:SetSearcher(NULL)
    trashEnt:SetSearchStart(0)
    if IsValid(ply) then ply.sdfSearching = nil end

    -- ── Traitement par issue ──────────────────────────────────────────────
    if outcome == "nothing" then
        -- Rien dans la poubelle.
        local xpNothing = math.floor(tonumber(SDFSystem.Config.Outcomes.XpNothing) or 2)
        if xpNothing > 0 then TryAddXP(ply, "legal", xpNothing, "sdf_nothing") end

        net.Start("SDFSystem.SearchComplete")
            net.WriteBool(false)         -- pas de loot
            net.WriteString("Poubelle vide...")
            net.WriteUInt(xpNothing, 8)
            net.WriteUInt(0, SDFSystem.RARITY_BITS)  -- rareté irrelevante
            net.WriteBool(false)         -- pas de jackpot
            net.WriteString("nothing")
        net.Send(ply)

        if SDFSystem.SvConfig.Debug then
            print(string.format("[SDFSystem] %s : issue=nothing", ply:Nick()))
        end
        return

    elseif outcome == "rat" then
        -- Morsure de rat.
        local dmg    = math.floor(tonumber(SDFSystem.Config.Outcomes.RatDamage) or 15)
        local ratSnd = SDFSystem.Config.Outcomes.RatSound or ""
        local di     = DamageInfo()
        di:SetDamage(dmg)
        di:SetAttacker(ply)   -- l'attaquant = lui-même (accident)
        di:SetInflictor(trashEnt)
        di:SetDamageType(DMG_SLASH)
        ply:TakeDamageInfo(di)

        if ratSnd ~= "" then
            ply:EmitSound(ratSnd, 80, 100, 1.0)
        end

        local xpRat = math.floor(tonumber(SDFSystem.Config.Outcomes.XpRat) or 0)
        if xpRat > 0 then TryAddXP(ply, "legal", xpRat, "sdf_rat") end

        net.Start("SDFSystem.SearchComplete")
            net.WriteBool(false)
            net.WriteString("Un rat vous a mordu ! (-" .. dmg .. " HP)")
            net.WriteUInt(xpRat, 8)
            net.WriteUInt(0, SDFSystem.RARITY_BITS)
            net.WriteBool(false)
            net.WriteString("rat")
        net.Send(ply)

        if SDFSystem.SvConfig.Debug then
            print(string.format("[SDFSystem] %s : issue=rat (%d dmg)", ply:Nick(), dmg))
        end
        return

    elseif outcome == "contraband" then
        -- Contrebande : XP illégal + petit montant.
        local xpContraband = math.floor(tonumber(SDFSystem.Config.Outcomes.XpContraband) or 30)
        TryAddXP(ply, "illegal", xpContraband, "sdf_contraband")

        local cbMoney   = SDFSystem.Config.Outcomes.ContrabandMoney
        local cbAmount  = BiasedMoneyRoll(
            tonumber(cbMoney.Min) or 50,
            tonumber(cbMoney.Max) or 200,
            2.0
        )
        ply:addMoney(cbAmount)

        local desc = "Contrebande trouvée ! (" .. DarkRP.formatMoney(cbAmount) .. ")"

        net.Start("SDFSystem.SearchComplete")
            net.WriteBool(true)
            net.WriteString(desc)
            net.WriteUInt(0, 8)   -- XP illégal, pas affiché comme XP légal
            net.WriteUInt(0, SDFSystem.RARITY_BITS)
            net.WriteBool(false)
            net.WriteString("contraband")
        net.Send(ply)

        if SDFSystem.SvConfig.Debug then
            print(string.format("[SDFSystem] %s : issue=contraband (+%d XP illégal)", ply:Nick(), xpContraband))
        end
        return
    end

    -- ── outcome == "success" : loot normal ────────────────────────────────

    -- Calcul des poids de tier (rareté + scaling niveau légal).
    local tierWeights = CalcTierWeights(lvl)

    -- Tirage du tier.
    local tierId = DrawRarityTier(tierWeights)

    -- Tirage du lot dans la table du modèle (H).
    local model    = IsValid(trashEnt) and trashEnt:GetModel() or ""
    local lootTbl  = GetLootTable(model)
    local lot, actualTier = DrawLootFromTier(lootTbl, tierId)

    -- Livraison du lot.
    local lootDesc = DeliverLoot(ply, lot, lvl)

    -- XP légal (fouille classique : montant aléatoire dans la fourchette).
    local outcomeXP = SDFSystem.Config.Outcomes.XpSuccess
    local xpMin = math.floor(tonumber(outcomeXP.Min) or 5)
    local xpMax = math.floor(tonumber(outcomeXP.Max) or 25)
    local xpGain = math.min(math.random(xpMin, xpMax), 255)  -- cap 255 pour 8 bits réseau
    TryAddXP(ply, "legal", xpGain, "sdf_search")

    -- Index numérique du tier (pour net.WriteUInt).
    local rarities = SDFSystem.Config.Rarities
    local rarityIdx = 0
    for i, r in ipairs(rarities) do
        if r.id == actualTier then rarityIdx = i - 1; break end  -- 0-based
    end

    -- NetworkVar de flash 3D2D.
    if IsValid(trashEnt) then
        trashEnt:SetLastRarity(rarityIdx)
        trashEnt:SetRarityFlashTime(CurTime())
    end

    -- F. Jackpot si rareté légendaire.
    local isJackpot = (actualTier == "legendaire") and SDFSystem.Config.Jackpot.Enabled
    if isJackpot then
        net.Start("SDFSystem.Jackpot")
            net.WriteFloat(tonumber(SDFSystem.Config.Jackpot.Duration) or 2.5)
        net.Send(ply)
    end

    -- Notifier le résultat.
    net.Start("SDFSystem.SearchComplete")
        net.WriteBool(true)
        net.WriteString(lootDesc)
        net.WriteUInt(math.min(xpGain, 255), 8)
        net.WriteUInt(rarityIdx, SDFSystem.RARITY_BITS)
        net.WriteBool(isJackpot)
        net.WriteString("success")
    net.Send(ply)

    if SDFSystem.SvConfig.Debug then
        print(string.format(
            "[SDFSystem] %s → %s [%s] (+%d XP légal, lvl=%d)",
            ply:Nick(), lootDesc, actualTier, xpGain, lvl))
    end
end

-- =========================================================================
-- API publique : annulation de fouille
-- =========================================================================
function SDFSystem.CancelSearch(trashEnt, reason)
    if not IsValid(trashEnt) then return end

    trashEnt:StopSearchSound()
    trashEnt:StopGestureLoop()

    local ply = trashEnt.searcher
    trashEnt.searcher        = nil
    trashEnt.searchEnd       = nil
    trashEnt.searchCompleted = nil
    trashEnt:SetSearchEnd(0)
    trashEnt:SetSearcher(NULL)
    trashEnt:SetSearchStart(0)
    if IsValid(ply) then ply.sdfSearching = nil end

    if IsValid(ply) then
        net.Start("SDFSystem.SearchCancelled")
            net.WriteString(reason or "")
        net.Send(ply)
    end

    if SDFSystem.SvConfig.Debug then
        print(string.format("[SDFSystem] Fouille annulée (%s) sur %s", tostring(reason), tostring(trashEnt)))
    end
end

-- =========================================================================
-- API publique : spawn d'une poubelle
-- =========================================================================
function SDFSystem.SpawnTrash(modelIdx, pos, ang)
    modelIdx = math.floor(tonumber(modelIdx) or 0)
    local model = SDFSystem.Config.Models[modelIdx]
    if not model then
        ErrorNoHalt("[SDFSystem] SpawnTrash : index model invalide : " .. tostring(modelIdx) .. "\n")
        return nil
    end

    local ent = ents.Create("sdf_trash")
    if not IsValid(ent) then
        ErrorNoHalt("[SDFSystem] SpawnTrash : impossible de créer sdf_trash\n")
        return nil
    end

    ent:SetModel(model)
    ent:SetPos(pos)
    ent:SetAngles(ang)
    ent:Spawn()
    ent:DropToFloor()

    local dbId = SDFSystem.DB.Insert(model, ent:GetPos(), ent:GetAngles())
    if not dbId then
        ent:Remove()
        return nil
    end
    ent.dbId = dbId
    ent:SetDbId(dbId)

    return ent
end

-- =========================================================================
-- API publique : suppression d'une poubelle
-- =========================================================================
function SDFSystem.RemoveTrash(dbId)
    dbId = math.floor(tonumber(dbId) or 0)
    if dbId <= 0 then return false end

    for _, ent in ipairs(ents.FindByClass("sdf_trash")) do
        if IsValid(ent) and ent.dbId == dbId then
            if IsValid(ent.searcher) then
                SDFSystem.CancelSearch(ent, "suppression")
            end
            ent:Remove()
            break
        end
    end

    return SDFSystem.DB.Delete(dbId)
end

-- =========================================================================
-- Validation des classes de lot au chargement
-- =========================================================================
hook.Add("InitPostEntity", "SDFSystem.ValidateLoot", function()
    -- Valide toutes les tables de loot (LootByModel) au démarrage.
    for key, tbl in pairs(SDFSystem.Config.LootByModel) do
        for _, lot in ipairs(tbl) do
            if lot.type == "weapon" or lot.type == "entity" then
                local class = tostring(lot.class or "")
                if class ~= "" and class ~= "spawned_money" then
                    local test = ents.Create(class)
                    if IsValid(test) then
                        test:Remove()
                    else
                        print("[SDFSystem] WARN : loot " .. lot.type .. " '" .. class ..
                            "' dans LootByModel['" .. tostring(key) .. "'] introuvable.")
                    end
                end
            end
        end
    end

    -- Précache des sons HL2.
    util.PrecacheSound(SDFSystem.Config.Immersion.SearchSound or "")
    util.PrecacheSound(SDFSystem.Config.Outcomes.RatSound or "")
    util.PrecacheSound(SDFSystem.Config.Jackpot.Sound or "")
end)

-- ─── Concommand admin (alias console) ────────────────────────────────────
concommand.Add("sdf_addmenu", function(ply, _, _)
    if not IsValid(ply) then return end
    if not SDFSystem.SvConfig.CanManage(ply) then
        DarkRP.notify(ply, 1, 4, "Accès refusé.")
        return
    end
    net.Start("SDFSystem.OpenMenu")
    net.Send(ply)
end)
