MS_NaturesCallCompat = MS_NaturesCallCompat or {}

local NC = MS_NaturesCallCompat
local _isNaturesCallActive = nil
local _quickCache = {}
local _detailCache = {}

local function nowMs()
    return getTimestampMs and getTimestampMs() or 0
end

local function clamp(value, minValue, maxValue)
    value = tonumber(value) or 0
    if value < minValue then return minValue end
    if value > maxValue then return maxValue end
    return value
end

local function getPlayerKey(player)
    if player and player.getPlayerNum then
        return tostring(player:getPlayerNum())
    end
    return "0"
end

local function getNaturesCallGlobal()
    if NaturesCall ~= nil then
        return NaturesCall
    end
    if _G and _G.NaturesCall ~= nil then
        return _G.NaturesCall
    end
    return nil
end

function NC.isActive()
    if _isNaturesCallActive ~= nil then
        return _isNaturesCallActive
    end

    local ncGlobal = getNaturesCallGlobal()
    if ncGlobal ~= nil then
        _isNaturesCallActive = true
        return true
    end

    local activatedMods = getActivatedMods and getActivatedMods() or nil
    if activatedMods then
        for i = 0, activatedMods:size() - 1 do
            local modId = tostring(activatedMods:get(i) or "")
            local lowerId = string.lower(modId)
            if modId == "NC_NaturesCall" or lowerId == "naturescall" or lowerId == "nature's call" or lowerId == "natures_call" then
                _isNaturesCallActive = true
                return true
            end
        end
    end

    _isNaturesCallActive = false
    return false
end

local function getRawPlayerData(player)
    if not player or not player.getModData then return nil end
    local modData = player:getModData()
    return modData and modData.NaturesCall or nil
end

local function copyQuickFromRaw(raw)
    if not raw then return nil end
    return {
        bladderFill = tonumber(raw.bladderFill) or 0,
        bowelFill = tonumber(raw.bowelFill) or 0,
        control = tonumber(raw.control) or 0,
        gutHealth = tonumber(raw.gutHealth) or 5,
        gutTrendPerHour = tonumber(raw.gutTrendPerHour) or 0,
        effects = raw.effects or {},
        bodySoilage = raw.bodySoilage or {},
        bodySoilageEffects = raw.bodySoilageEffects or {},
        wipeRequired = raw.wipeRequired == true,
        wipeKind = raw.wipeKind,
        wipeNeed = tonumber(raw.wipeNeed) or 0,
        clothing = {},
    }
end

function NC.getQuickSnapshot(player, maxAgeMs)
    if not NC.isActive() or not player or player:isDead() then
        return nil
    end

    maxAgeMs = tonumber(maxAgeMs) or 1500
    local key = getPlayerKey(player)
    local current = nowMs()
    local cache = _quickCache[key]
    if cache and (current - (cache.time or 0)) < maxAgeMs then
        return cache.data
    end

    local data = nil
    local ncGlobal = getNaturesCallGlobal()
    local state = ncGlobal and ncGlobal.State or nil
    if state and state.getSnapshot then
        local ok, result = pcall(function()
            return state.getSnapshot(player)
        end)
        if ok and type(result) == "table" then
            data = result
        end
    end

    if not data then
        data = copyQuickFromRaw(getRawPlayerData(player))
    end

    _quickCache[key] = { time = current, data = data }
    return data
end

function NC.getDetailedSnapshot(player, maxAgeMs)
    if not NC.isActive() or not player or player:isDead() then
        return nil
    end

    maxAgeMs = tonumber(maxAgeMs) or 2500
    local key = getPlayerKey(player)
    local current = nowMs()
    local cache = _detailCache[key]
    if cache and (current - (cache.time or 0)) < maxAgeMs then
        return cache.data
    end

    local data = nil
    local ncGlobal = getNaturesCallGlobal()
    local state = ncGlobal and ncGlobal.State or nil
    if state and state.getDetailedSnapshot then
        local ok, result = pcall(function()
            return state.getDetailedSnapshot(player)
        end)
        if ok and type(result) == "table" then
            data = result
        end
    end

    if not data then
        data = copyQuickFromRaw(getRawPlayerData(player))
    end

    _detailCache[key] = { time = current, data = data }
    return data
end

function NC.getFillMax()
    local ncGlobal = getNaturesCallGlobal()
    local defs = ncGlobal and ncGlobal.Definitions or nil
    local fill = defs and defs.Fill or nil
    return tonumber(fill and fill.Max) or 120
end

function NC.getFillThreshold(name, fallback)
    local ncGlobal = getNaturesCallGlobal()
    local defs = ncGlobal and ncGlobal.Definitions or nil
    local fill = defs and defs.Fill or nil
    return tonumber(fill and fill[name]) or fallback
end

function NC.getGutNeutral()
    local ncGlobal = getNaturesCallGlobal()
    local defs = ncGlobal and ncGlobal.Definitions or nil
    local gut = defs and defs.GutHealth or nil
    return tonumber(gut and gut.Neutral) or 5
end

function NC.getGutMin()
    local ncGlobal = getNaturesCallGlobal()
    local defs = ncGlobal and ncGlobal.Definitions or nil
    local gut = defs and defs.GutHealth or nil
    return tonumber(gut and gut.Min) or 1
end

function NC.getGutMax()
    local ncGlobal = getNaturesCallGlobal()
    local defs = ncGlobal and ncGlobal.Definitions or nil
    local gut = defs and defs.GutHealth or nil
    return tonumber(gut and gut.Max) or 10
end

function NC.getBladderFill(player, detailed)
    local snap = detailed and NC.getDetailedSnapshot(player) or NC.getQuickSnapshot(player)
    return snap and tonumber(snap.bladderFill) or 0
end

function NC.getBowelFill(player, detailed)
    local snap = detailed and NC.getDetailedSnapshot(player) or NC.getQuickSnapshot(player)
    return snap and tonumber(snap.bowelFill) or 0
end

function NC.getGutHealth(player, detailed)
    local snap = detailed and NC.getDetailedSnapshot(player) or NC.getQuickSnapshot(player, 5000)
    return snap and (tonumber(snap.gutHealth) or NC.getGutNeutral()) or NC.getGutNeutral()
end

function NC.isMeterHidden(player, kind)
    if kind == "bladder" then
        return (NC.getBladderFill(player, true) or 0) < 0
    elseif kind == "bowel" then
        return (NC.getBowelFill(player, true) or 0) < 0
    end
    return false
end

local function maxNumber(current, value)
    value = tonumber(value) or 0
    if value > current then return value end
    return current
end

function NC.getSoilageData(player)
    local snapshot = NC.getDetailedSnapshot(player, 3000)
    local clothing = snapshot and snapshot.clothing or {}
    local body = snapshot and snapshot.bodySoilage or {}
    local effects = snapshot and snapshot.bodySoilageEffects or {}

    local worst = 0
    worst = maxNumber(worst, clothing.urine)
    worst = maxNumber(worst, clothing.feces)
    worst = maxNumber(worst, clothing.driedUrine)
    worst = maxNumber(worst, body.urine)
    worst = maxNumber(worst, body.feces)
    worst = maxNumber(worst, snapshot and snapshot.wipeNeed)

    return {
        value = clamp(worst, 0, 100),
        clothingUrine = clamp(clothing.urine, 0, 100),
        clothingFeces = clamp(clothing.feces, 0, 100),
        driedUrine = clamp(clothing.driedUrine, 0, 100),
        driedFeces = clothing.driedFeces == true,
        unknownSoilage = clothing.unknownSoilage == true,
        bodyUrine = clamp(body.urine, 0, 100),
        bodyFeces = clamp(body.feces, 0, 100),
        wipeRequired = snapshot and snapshot.wipeRequired == true,
        wipeKind = snapshot and snapshot.wipeKind or nil,
        wipeNeed = clamp(snapshot and snapshot.wipeNeed, 0, 100),
        urineRash = effects.urineRash == true,
        fecesRash = effects.fecesRash == true,
        urineInfection = effects.urineInfection == true,
        fecesInfection = effects.fecesInfection == true,
    }
end

local function getEffectDefinitions()
    local ncGlobal = getNaturesCallGlobal()
    local defs = ncGlobal and ncGlobal.Definitions or nil
    return defs and defs.Effects or nil
end

local function getEffectScale(effectsDefs, key, fallback)
    local value = effectsDefs and tonumber(effectsDefs[key]) or nil
    if value and value > 0 then
        return value
    end
    return fallback
end

local function addDigestiveEffect(list, key, labelKey, fallback, value, valueType, scale)
    value = tonumber(value) or 0
    if value <= 0.01 then return 0 end

    local severity = 0
    if valueType == "hours" then
        severity = clamp((value / math.max(scale or 4, 0.01)) * 100, 0, 100)
    elseif valueType == "intensity" then
        severity = clamp((value / math.max(scale or 1, 0.01)) * 100, 0, 100)
    elseif valueType == "strain" then
        -- Digestive strain is mostly relevant once it crosses the Nature's Call threshold.
        if value < (scale or 0.6) then return 0 end
        severity = clamp((value / math.max(scale or 0.6, 0.01)) * 60, 0, 100)
    else
        severity = clamp(value, 0, 100)
    end

    list[#list + 1] = {
        key = key,
        labelKey = labelKey,
        fallback = fallback,
        value = value,
        valueType = valueType,
        severity = severity,
    }
    return severity
end

function NC.getDigestiveEffectsData(player)
    local snapshot = NC.getDetailedSnapshot(player, 3500)
    local effects = snapshot and snapshot.effects or nil
    if not effects then
        return { value = 0, activeCount = 0, effects = {} }
    end

    local defs = getEffectDefinitions()
    local active = {}
    local maxSeverity = 0

    -- Use a fixed 4 h visual scale for timed digestive effects so the bar fill
    -- maps consistently to the visible hour value (for example 2 h = 50%).
    local hourScale = 4
    maxSeverity = math.max(maxSeverity, addDigestiveEffect(active, "cramps", "IGUI_ModernStatus_NCDigestiveCramps", "Cramps", effects.crampsHours, "hours", hourScale))
    maxSeverity = math.max(maxSeverity, addDigestiveEffect(active, "heartburn", "IGUI_ModernStatus_NCDigestiveHeartburn", "Heartburn", effects.heartburnHours, "hours", hourScale))
    maxSeverity = math.max(maxSeverity, addDigestiveEffect(active, "diarrhea", "IGUI_ModernStatus_NCDigestiveDiarrhea", "Diarrhea", effects.diarrheaHours, "hours", hourScale))
    maxSeverity = math.max(maxSeverity, addDigestiveEffect(active, "constipation", "IGUI_ModernStatus_NCDigestiveConstipation", "Constipation", effects.constipationHours, "hours", hourScale))
    maxSeverity = math.max(maxSeverity, addDigestiveEffect(active, "laxative", "IGUI_ModernStatus_NCDigestiveLaxative", "Laxative", effects.laxativeHours, "hours", hourScale))
    maxSeverity = math.max(maxSeverity, addDigestiveEffect(active, "smell", "IGUI_ModernStatus_NCDigestiveSmell", "Smell", effects.smellHours, "hours", hourScale))
    maxSeverity = math.max(maxSeverity, addDigestiveEffect(active, "binge", "IGUI_ModernStatus_NCDigestiveBinge", "Binge", effects.bingeIntensity, "intensity", getEffectScale(defs, "BingeIntensityCap", 3)))
    maxSeverity = math.max(maxSeverity, addDigestiveEffect(active, "strain", "IGUI_ModernStatus_NCDigestiveStrain", "Digestive strain", effects.digestiveStrain, "strain", getEffectScale(defs, "DigestiveStrainThreshold", 0.6)))

    table.sort(active, function(a, b)
        return (tonumber(a.severity) or 0) > (tonumber(b.severity) or 0)
    end)

    return {
        value = clamp(maxSeverity, 0, 100),
        activeCount = #active,
        effects = active,
        primary = active[1],
    }
end

function NC.formatFill(value)
    value = tonumber(value) or 0
    if value < 0 then
        return nil
    end
    return string.format("%.0f / %.0f", value, NC.getFillMax())
end

return NC
