-- Settings tab of the PHMenu. Inline scrolling settings rows in the
-- vanilla FS25 style: label on the left, MultiTextOption / BinaryOption
-- control on the right. All controls share a single onClickSetting
-- callback that routes by element id.

PHMenuSettingsFrame = {}
local PHMenuSettingsFrame_mt = Class(PHMenuSettingsFrame, TabbedMenuFrameElement)

local modDirectory = g_currentModDirectory

local function buildHourTexts()
    local t = {}
    for h = 0, 23 do
        t[#t + 1] = string.format("%02d:00", h)
    end
    return t
end

local function buildOnOffTexts()
    return { g_i18n:getText("ui_off"), g_i18n:getText("ui_on") }
end

-- Meadow regrowth presets. Each entry is the fraction of capacity that's
-- allowed to grow per in-game day. Lower = slower recovery, more realistic.
-- Picker state index maps directly to this list.
PHMenuSettingsFrame.REGROWTH_VALUES = {
    0.02, 0.05, 0.08, 0.12, 0.20, 0.30, 0.40, 0.50, 0.75, 1.00,
}

-- Rotate-at-grass-percent presets. State 1 = "Rotate every day" (legacy
-- behavior, value 0). Subsequent states pick a stay-until-grass-drops
-- threshold. Picker state index maps directly to this list.
PHMenuSettingsFrame.ROTATE_AT_VALUES = {
    0, 0.10, 0.20, 0.30, 0.40, 0.50, 0.60,
}

-- Quality decay (per grazing day) presets.
PHMenuSettingsFrame.QUALITY_DECAY_VALUES = {
    0.05, 0.10, 0.15, 0.20, 0.25, 0.35, 0.50,
}
local function buildQualityDecayTexts()
    local out = {}
    for i, v in ipairs(PHMenuSettingsFrame.QUALITY_DECAY_VALUES) do
        out[i] = string.format("-%d%% per grazing day", math.floor(v * 100 + 0.5))
    end
    return out
end

-- Quality recovery (per rest day) presets.
PHMenuSettingsFrame.QUALITY_RECOVERY_VALUES = {
    0.02, 0.05, 0.08, 0.10, 0.15, 0.20, 0.30, 0.50,
}

-- Fraction of a cow's daily food intake that comes from grass when on
-- pasture. Drives meadow drain math + the "grass lasts Xh" and "fits ~N
-- cows" menu predictions. Dairy production breeds (Holstein) sit around
-- 55-60% roughage; beef-grazing breeds (Angus) can be 80-90%.
PHMenuSettingsFrame.GRASS_FRACTION_VALUES = {
    0.50, 0.55, 0.60, 0.65, 0.70, 0.75, 0.80, 0.85, 0.90,
}
local function buildGrassFractionTexts()
    local out = {}
    for i, v in ipairs(PHMenuSettingsFrame.GRASS_FRACTION_VALUES) do
        out[i] = string.format("%d%% from grass", math.floor(v * 100 + 0.5))
    end
    return out
end

-- Herdsman hourly wage presets ($/hour). Monthly cost shown to the player
-- is hourly × 39 hr/wk × 52 wk / 12 months.
PHMenuSettingsFrame.HERDSMAN_WAGE_VALUES = {
    0, 7, 10, 12, 14, 17, 20, 25, 30, 40, 50,
}
local function buildHerdsmanWageTexts()
    local function fmt(amount)
        if g_i18n ~= nil and type(g_i18n.formatMoney) == "function" then
            return g_i18n:formatMoney(amount, 0, true, true)
        end
        return string.format("%d", amount)
    end
    local out = {}
    for i, v in ipairs(PHMenuSettingsFrame.HERDSMAN_WAGE_VALUES) do
        if v <= 0 then
            out[i] = "Free (no charge)"
        else
            local monthly = math.floor(v * 39 * 52 / 12)
            out[i] = string.format("%s/hr  \194\183  %s/month", fmt(v), fmt(monthly))
        end
    end
    return out
end
local function buildQualityRecoveryTexts()
    local out = {}
    for i, v in ipairs(PHMenuSettingsFrame.QUALITY_RECOVERY_VALUES) do
        out[i] = string.format("+%d%% per rest day", math.floor(v * 100 + 0.5))
    end
    return out
end
local function buildRotateAtTexts()
    local out = {}
    for i, v in ipairs(PHMenuSettingsFrame.ROTATE_AT_VALUES) do
        if v <= 0 then
            out[i] = "Rotate every day"
        else
            out[i] = string.format("Rotate when grass < %d%%", math.floor(v * 100 + 0.5))
        end
    end
    return out
end

-- Starvation trigger thresholds (herd is "out of food"). Both must be crossed:
-- pasture grass below the grass threshold AND the trough supplement below the
-- supplement threshold. Then the herdsman moves the herd (Auto Pasture Select
-- on) or warns the player (off).
PHMenuSettingsFrame.STARVATION_GRASS_VALUES = {
    0.05, 0.10, 0.15, 0.20, 0.25, 0.30,
}
local function buildStarvationGrassTexts()
    local out = {}
    for i, v in ipairs(PHMenuSettingsFrame.STARVATION_GRASS_VALUES) do
        out[i] = string.format("Grass below %d%%", math.floor(v * 100 + 0.5))
    end
    return out
end
PHMenuSettingsFrame.STARVATION_SUPP_VALUES = {
    0, 0.05, 0.10, 0.15, 0.20,
}
local function buildStarvationSuppTexts()
    local out = {}
    for i, v in ipairs(PHMenuSettingsFrame.STARVATION_SUPP_VALUES) do
        if v <= 0 then
            out[i] = "and trough empty"
        else
            out[i] = string.format("and trough below %d%%", math.floor(v * 100 + 0.5))
        end
    end
    return out
end

-- Grazing output boost: how much extra beef weight / dairy milk a herd earns at
-- full grazing condition (grazing OFTEN on GOOD grass). 0 disables that side.
PHMenuSettingsFrame.GRAZE_WEIGHT_VALUES = {
    0, 0.05, 0.10, 0.15, 0.20, 0.25,
}
PHMenuSettingsFrame.GRAZE_MILK_VALUES = {
    0, 0.05, 0.10, 0.15, 0.20, 0.25, 0.30,
}
local function buildGrazeWeightTexts()
    local out = {}
    for i, v in ipairs(PHMenuSettingsFrame.GRAZE_WEIGHT_VALUES) do
        out[i] = (v <= 0) and "Off" or string.format("+%d%% weight", math.floor(v * 100 + 0.5))
    end
    return out
end
local function buildGrazeMilkTexts()
    local out = {}
    for i, v in ipairs(PHMenuSettingsFrame.GRAZE_MILK_VALUES) do
        out[i] = (v <= 0) and "Off" or string.format("+%d%% milk", math.floor(v * 100 + 0.5))
    end
    return out
end
-- Condition ramp speed: how fast the grazing condition builds while out on good
-- grass and fades while housed. One picker sets BOTH build and decay rates
-- (symmetric). Values are the per-in-game-hour easing rate; labels show the
-- rough time to build/fade.
PHMenuSettingsFrame.GRAZE_RAMP_VALUES = {
    0.02, 0.03, 0.05,
}
local function buildGrazeRampTexts()
    return { "Slow (~5 days)", "Normal (~3 days)", "Fast (~2 days)" }
end

-- Auto-gate mode (3-way): open gates on every walk, only on herdsman-driven
-- (automatic) walks, or never.
PHMenuSettingsFrame.GATE_MODE_VALUES = { "all", "herdsman", "off" }
local function buildGateModeTexts()
    return { "On for all walks", "Herdsman walks only", "Off" }
end
-- Walk-path recording sample spacing presets (meters between waypoints).
PHMenuSettingsFrame.PATH_SAMPLE_VALUES = {
    0.5, 1.0, 2.0, 3.0, 5.0,
}
local function buildPathSampleTexts()
    local out = {}
    for i, v in ipairs(PHMenuSettingsFrame.PATH_SAMPLE_VALUES) do
        out[i] = (v == math.floor(v)) and string.format("%d m", v) or string.format("%.1f m", v)
    end
    return out
end
local function buildRegrowthTexts()
    local out = {}
    for i, v in ipairs(PHMenuSettingsFrame.REGROWTH_VALUES) do
        local pct = math.floor(v * 100 + 0.5)
        local days = (v > 0) and math.ceil(1 / v) or 999
        if v >= 1.0 then
            out[i] = string.format("%d%%/day (vanilla, instant)", pct)
        else
            out[i] = string.format("%d%%/day (~%d days to full)", pct, days)
        end
    end
    return out
end

local function buildMonthTexts()
    -- Use FS25's localized period names where they exist (ui_periodName1..12).
    -- If those keys aren't present, fall back to English short names.
    local fallback = { "Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec" }
    local out = {}
    for i = 1, 12 do
        local key = "ui_periodName" .. i
        local s = fallback[i]
        if type(g_i18n.hasText) == "function" and g_i18n:hasText(key) then
            s = g_i18n:getText(key)
        end
        out[#out + 1] = s
    end
    return out
end

function PHMenuSettingsFrame.new(target, mt)
    local self = PHMenuSettingsFrame:superClass().new(target, mt or PHMenuSettingsFrame_mt)
    self.name = "PHMenuSettingsFrame"
    self.hasCustomMenuButtons = true
    self.backButtonInfo = { inputAction = InputAction.MENU_BACK }
    self.menuButtonInfo = { self.backButtonInfo }
    return self
end

function PHMenuSettingsFrame.setupGui()
    if g_gui == nil then return end
    local xmlPath = Utils.getFilename("gui/frames/PHMenuSettingsFrame.xml", modDirectory)
    g_gui:loadGui(xmlPath, "PHMenuSettingsFrame", PHMenuSettingsFrame.new(), true)
end

function PHMenuSettingsFrame:initialize() end

local function scheduler()
    local f = rawget(_G, "g_pastureHerding")
    return f and f.grazingScheduler or nil
end

function PHMenuSettingsFrame:_seedTexts()
    if self.set_sunrise and type(self.set_sunrise.setTexts) == "function" then
        self.set_sunrise:setTexts(buildHourTexts())
    end
    if self.set_sunset and type(self.set_sunset.setTexts) == "function" then
        self.set_sunset:setTexts(buildHourTexts())
    end
    if self.set_auto and type(self.set_auto.setTexts) == "function" then
        self.set_auto:setTexts(buildOnOffTexts())
    end
    if self.set_debugContours and type(self.set_debugContours.setTexts) == "function" then
        self.set_debugContours:setTexts(buildOnOffTexts())
    end
    if self.set_debugPaths and type(self.set_debugPaths.setTexts) == "function" then
        self.set_debugPaths:setTexts(buildOnOffTexts())
    end
    if self.set_debugLog and type(self.set_debugLog.setTexts) == "function" then
        self.set_debugLog:setTexts(buildOnOffTexts())
    end
    if self.set_debugSprayCoverage and type(self.set_debugSprayCoverage.setTexts) == "function" then
        self.set_debugSprayCoverage:setTexts(buildOnOffTexts())
    end
    local months = buildMonthTexts()
    if self.set_seasonStart and type(self.set_seasonStart.setTexts) == "function" then
        self.set_seasonStart:setTexts(months)
    end
    if self.set_seasonEnd and type(self.set_seasonEnd.setTexts) == "function" then
        self.set_seasonEnd:setTexts(months)
    end
    if self.set_restDays and type(self.set_restDays.setTexts) == "function" then
        local restLabels = {}
        for d = 0, 7 do
            restLabels[#restLabels + 1] = (d == 0) and g_i18n:getText("ui_off")
                                        or string.format("%d day%s", d, d == 1 and "" or "s")
        end
        self.set_restDays:setTexts(restLabels)
    end
    if self.set_weatherGate and type(self.set_weatherGate.setTexts) == "function" then
        self.set_weatherGate:setTexts(buildOnOffTexts())
    end
    if self.set_pfSoil and type(self.set_pfSoil.setTexts) == "function" then
        self.set_pfSoil:setTexts(buildOnOffTexts())
    end
    if self.set_regrowth and type(self.set_regrowth.setTexts) == "function" then
        self.set_regrowth:setTexts(buildRegrowthTexts())
    end
    if self.set_rotateAt and type(self.set_rotateAt.setTexts) == "function" then
        self.set_rotateAt:setTexts(buildRotateAtTexts())
    end
    if self.set_starvationGrass and type(self.set_starvationGrass.setTexts) == "function" then
        self.set_starvationGrass:setTexts(buildStarvationGrassTexts())
    end
    if self.set_starvationSupp and type(self.set_starvationSupp.setTexts) == "function" then
        self.set_starvationSupp:setTexts(buildStarvationSuppTexts())
    end
    if self.set_gateMode and type(self.set_gateMode.setTexts) == "function" then
        self.set_gateMode:setTexts(buildGateModeTexts())
    end
    if self.set_grazeWeight and type(self.set_grazeWeight.setTexts) == "function" then
        self.set_grazeWeight:setTexts(buildGrazeWeightTexts())
    end
    if self.set_grazeMilk and type(self.set_grazeMilk.setTexts) == "function" then
        self.set_grazeMilk:setTexts(buildGrazeMilkTexts())
    end
    if self.set_grazeRamp and type(self.set_grazeRamp.setTexts) == "function" then
        self.set_grazeRamp:setTexts(buildGrazeRampTexts())
    end
    if self.set_qualityDecay and type(self.set_qualityDecay.setTexts) == "function" then
        self.set_qualityDecay:setTexts(buildQualityDecayTexts())
    end
    if self.set_qualityRecovery and type(self.set_qualityRecovery.setTexts) == "function" then
        self.set_qualityRecovery:setTexts(buildQualityRecoveryTexts())
    end
    if self.set_grassFraction and type(self.set_grassFraction.setTexts) == "function" then
        self.set_grassFraction:setTexts(buildGrassFractionTexts())
    end
    if self.set_herdsmanWage and type(self.set_herdsmanWage.setTexts) == "function" then
        self.set_herdsmanWage:setTexts(buildHerdsmanWageTexts())
    end
    if self.set_pathSample and type(self.set_pathSample.setTexts) == "function" then
        self.set_pathSample:setTexts(buildPathSampleTexts())
    end
end

function PHMenuSettingsFrame:refresh()
    local s = scheduler()
    if s == nil then return end
    if self.set_sunrise and type(self.set_sunrise.setState) == "function" then
        self.set_sunrise:setState((s.SUNRISE_HOUR or 7) + 1, true)
    end
    if self.set_sunset and type(self.set_sunset.setState) == "function" then
        self.set_sunset:setState((s.SUNSET_HOUR or 19) + 1, true)
    end
    if self.set_auto and type(self.set_auto.setState) == "function" then
        self.set_auto:setState(s.autoEnabled and 2 or 1, true)
    end
    local f = rawget(_G, "g_pastureHerding")
    if self.set_debugContours and type(self.set_debugContours.setState) == "function" then
        self.set_debugContours:setState((f and f.debugContours) and 2 or 1, true)
    end
    if self.set_debugPaths and type(self.set_debugPaths.setState) == "function" then
        self.set_debugPaths:setState((f and f.debugPaths) and 2 or 1, true)
    end
    if self.set_debugLog and type(self.set_debugLog.setState) == "function" then
        self.set_debugLog:setState(PastureHerdingLog.devMode and 2 or 1, true)
    end
    if self.set_debugSprayCoverage and type(self.set_debugSprayCoverage.setState) == "function" then
        self.set_debugSprayCoverage:setState((f and f.debugSprayCoverage) and 2 or 1, true)
    end
    if self.set_seasonStart and type(self.set_seasonStart.setState) == "function" then
        local m = (f and f.grazingSeasonStart) or 3
        self.set_seasonStart:setState(math.max(1, math.min(12, m)), true)
    end
    if self.set_seasonEnd and type(self.set_seasonEnd.setState) == "function" then
        local m = (f and f.grazingSeasonEnd) or 10
        self.set_seasonEnd:setState(math.max(1, math.min(12, m)), true)
    end
    if self.set_restDays and type(self.set_restDays.setState) == "function" then
        local d = (f and f.grazingMinRestDays) or 1
        self.set_restDays:setState(math.max(1, math.min(8, d + 1)), true)
    end
    if self.set_weatherGate and type(self.set_weatherGate.setState) == "function" then
        local on = f and f.gateOnWeather == true
        self.set_weatherGate:setState(on and 2 or 1, true)
    end
    if self.set_pfSoil and type(self.set_pfSoil.setState) == "function" then
        -- Default ON: only OFF when the player has explicitly disabled it.
        local on = (f == nil) or (f.pfSoilEnabled ~= false)
        self.set_pfSoil:setState(on and 2 or 1, true)
    end
    if self.set_regrowth and type(self.set_regrowth.setState) == "function" then
        local v = (f and f.maxMeadowRegrowthPerDay) or 0.08
        local best, bestDiff = 1, math.huge
        for i, presetV in ipairs(PHMenuSettingsFrame.REGROWTH_VALUES) do
            local diff = math.abs(presetV - v)
            if diff < bestDiff then bestDiff = diff; best = i end
        end
        self.set_regrowth:setState(best, true)
    end
    if self.set_rotateAt and type(self.set_rotateAt.setState) == "function" then
        local v = (f and f.rotateAtGrassPercent) or 0
        local best, bestDiff = 1, math.huge
        for i, presetV in ipairs(PHMenuSettingsFrame.ROTATE_AT_VALUES) do
            local diff = math.abs(presetV - v)
            if diff < bestDiff then bestDiff = diff; best = i end
        end
        self.set_rotateAt:setState(best, true)
    end
    if self.set_starvationGrass and type(self.set_starvationGrass.setState) == "function" then
        local v = (f and f.starvationGrassPct) or 0.10
        local best, bestDiff = 1, math.huge
        for i, presetV in ipairs(PHMenuSettingsFrame.STARVATION_GRASS_VALUES) do
            local diff = math.abs(presetV - v)
            if diff < bestDiff then bestDiff = diff; best = i end
        end
        self.set_starvationGrass:setState(best, true)
    end
    if self.set_starvationSupp and type(self.set_starvationSupp.setState) == "function" then
        local v = (f and f.starvationSupplementPct) or 0.05
        local best, bestDiff = 1, math.huge
        for i, presetV in ipairs(PHMenuSettingsFrame.STARVATION_SUPP_VALUES) do
            local diff = math.abs(presetV - v)
            if diff < bestDiff then bestDiff = diff; best = i end
        end
        self.set_starvationSupp:setState(best, true)
    end
    if self.set_gateMode and type(self.set_gateMode.setState) == "function" then
        local mode = (f and f.gateAutoMode) or "all"
        local idx = 1
        for i, presetV in ipairs(PHMenuSettingsFrame.GATE_MODE_VALUES) do
            if presetV == mode then idx = i; break end
        end
        self.set_gateMode:setState(idx, true)
    end
    if self.set_grazeWeight and type(self.set_grazeWeight.setState) == "function" then
        local v = (f and f.grazeWeightBonus) or 0.10
        local best, bestDiff = 1, math.huge
        for i, presetV in ipairs(PHMenuSettingsFrame.GRAZE_WEIGHT_VALUES) do
            local diff = math.abs(presetV - v)
            if diff < bestDiff then bestDiff = diff; best = i end
        end
        self.set_grazeWeight:setState(best, true)
    end
    if self.set_grazeMilk and type(self.set_grazeMilk.setState) == "function" then
        local v = (f and f.grazeMilkBonus) or 0.15
        local best, bestDiff = 1, math.huge
        for i, presetV in ipairs(PHMenuSettingsFrame.GRAZE_MILK_VALUES) do
            local diff = math.abs(presetV - v)
            if diff < bestDiff then bestDiff = diff; best = i end
        end
        self.set_grazeMilk:setState(best, true)
    end
    if self.set_grazeRamp and type(self.set_grazeRamp.setState) == "function" then
        local v = (f and f.grazeConditionBuildRate) or 0.03
        local best, bestDiff = 1, math.huge
        for i, presetV in ipairs(PHMenuSettingsFrame.GRAZE_RAMP_VALUES) do
            local diff = math.abs(presetV - v)
            if diff < bestDiff then bestDiff = diff; best = i end
        end
        self.set_grazeRamp:setState(best, true)
    end
    if self.set_qualityDecay and type(self.set_qualityDecay.setState) == "function" then
        local v = (f and f.grazingQualityDecayPerGrazingDay) or 0.15
        local best, bestDiff = 1, math.huge
        for i, presetV in ipairs(PHMenuSettingsFrame.QUALITY_DECAY_VALUES) do
            local diff = math.abs(presetV - v)
            if diff < bestDiff then bestDiff = diff; best = i end
        end
        self.set_qualityDecay:setState(best, true)
    end
    if self.set_qualityRecovery and type(self.set_qualityRecovery.setState) == "function" then
        local v = (f and f.grazingQualityRecoveryPerRestDay) or 0.10
        local best, bestDiff = 1, math.huge
        for i, presetV in ipairs(PHMenuSettingsFrame.QUALITY_RECOVERY_VALUES) do
            local diff = math.abs(presetV - v)
            if diff < bestDiff then bestDiff = diff; best = i end
        end
        self.set_qualityRecovery:setState(best, true)
    end
    if self.set_grassFraction and type(self.set_grassFraction.setState) == "function" then
        local v = (f and f.pastureGrassFoodFraction) or 0.65
        local best, bestDiff = 1, math.huge
        for i, presetV in ipairs(PHMenuSettingsFrame.GRASS_FRACTION_VALUES) do
            local diff = math.abs(presetV - v)
            if diff < bestDiff then bestDiff = diff; best = i end
        end
        self.set_grassFraction:setState(best, true)
    end
    if self.set_herdsmanWage and type(self.set_herdsmanWage.setState) == "function" then
        local v = (f and f.herdsmanHourlyWage) or 14
        local best, bestDiff = 1, math.huge
        for i, presetV in ipairs(PHMenuSettingsFrame.HERDSMAN_WAGE_VALUES) do
            local diff = math.abs(presetV - v)
            if diff < bestDiff then bestDiff = diff; best = i end
        end
        self.set_herdsmanWage:setState(best, true)
    end
    if self.set_pathSample and type(self.set_pathSample.setState) == "function" then
        local v = (f and f._pathSampleSpacing) or 1.0
        local best, bestDiff = 1, math.huge
        for i, presetV in ipairs(PHMenuSettingsFrame.PATH_SAMPLE_VALUES) do
            local diff = math.abs(presetV - v)
            if diff < bestDiff then bestDiff = diff; best = i end
        end
        self.set_pathSample:setState(best, true)
    end
end

function PHMenuSettingsFrame:onFrameOpen()
    PHMenuSettingsFrame:superClass().onFrameOpen(self)
    -- Seed the sub-tab paging element exactly once per open. Without this the
    -- MultiTextOption renders nothing and recurses on layout — the cause of
    -- the stack overflows we kept hitting when we used the BoxLayout + tab
    -- Button structure without this initialisation step.
    if self.subCategoryPaging ~= nil and type(self.subCategoryPaging.setTexts) == "function" then
        self.subCategoryPaging:setTexts({ g_i18n:getText("ph_menu_settings_tab_general") })
        if type(self.subCategoryPaging.setState) == "function" then
            self.subCategoryPaging:setState(1, true)
        end
    end
    self:_seedTexts()
    self:refresh()
end

function PHMenuSettingsFrame:onFrameClose()
    PHMenuSettingsFrame:superClass().onFrameClose(self)
end

-- Sub-category tab callbacks. We only have one tab ("General") so these
-- are no-ops, but the XML wires them and they must exist on self.
function PHMenuSettingsFrame:onClickGeneralTab() end
function PHMenuSettingsFrame:updateSubCategoryPages() end

-- Shared handler for every row in the settings layout. Routes by element id.
function PHMenuSettingsFrame:onClickSetting(stateIndex, element)
    local s = scheduler()
    if s == nil then return end
    local id = element and element.id or "?"
    if id == "set_sunrise" then
        s:setSchedule((stateIndex or 1) - 1, nil)
    elseif id == "set_sunset" then
        s:setSchedule(nil, (stateIndex or 1) - 1)
    elseif id == "set_auto" then
        s:setAutoEnabled(stateIndex == 2)
    elseif id == "set_debugContours" then
        local f = rawget(_G, "g_pastureHerding")
        if f ~= nil then f.debugContours = (stateIndex == 2) end
    elseif id == "set_debugPaths" then
        local f = rawget(_G, "g_pastureHerding")
        if f ~= nil then f.debugPaths = (stateIndex == 2) end
    elseif id == "set_debugLog" then
        PastureHerdingLog.devMode = (stateIndex == 2)
    elseif id == "set_debugSprayCoverage" then
        local f = rawget(_G, "g_pastureHerding")
        if f ~= nil then f.debugSprayCoverage = (stateIndex == 2) end
    elseif id == "set_seasonStart" then
        local f = rawget(_G, "g_pastureHerding")
        if f ~= nil then f.grazingSeasonStart = math.max(1, math.min(12, stateIndex or 1)) end
    elseif id == "set_seasonEnd" then
        local f = rawget(_G, "g_pastureHerding")
        if f ~= nil then f.grazingSeasonEnd = math.max(1, math.min(12, stateIndex or 12)) end
    elseif id == "set_restDays" then
        local f = rawget(_G, "g_pastureHerding")
        if f ~= nil then f.grazingMinRestDays = math.max(0, math.min(7, (stateIndex or 1) - 1)) end
    elseif id == "set_weatherGate" then
        local f = rawget(_G, "g_pastureHerding")
        if f ~= nil then f.gateOnWeather = (stateIndex == 2) end
    elseif id == "set_pfSoil" then
        -- One switch for the whole Precision Farming soil layer: starting soil
        -- quality AND the ongoing pH modifier.
        local f = rawget(_G, "g_pastureHerding")
        if f ~= nil then
            local on = (stateIndex == 2)
            f.pfSoilEnabled = on
            f.pfPHEnabled = on
        end
    elseif id == "set_regrowth" then
        local f = rawget(_G, "g_pastureHerding")
        local v = PHMenuSettingsFrame.REGROWTH_VALUES[stateIndex or 3] or 0.08
        if f ~= nil then f.maxMeadowRegrowthPerDay = v end
    elseif id == "set_rotateAt" then
        local f = rawget(_G, "g_pastureHerding")
        local v = PHMenuSettingsFrame.ROTATE_AT_VALUES[stateIndex or 1] or 0
        if f ~= nil then f.rotateAtGrassPercent = v end
    elseif id == "set_starvationGrass" then
        local f = rawget(_G, "g_pastureHerding")
        local v = PHMenuSettingsFrame.STARVATION_GRASS_VALUES[stateIndex or 2] or 0.10
        if f ~= nil then f.starvationGrassPct = v end
    elseif id == "set_starvationSupp" then
        local f = rawget(_G, "g_pastureHerding")
        local v = PHMenuSettingsFrame.STARVATION_SUPP_VALUES[stateIndex or 2] or 0.05
        if f ~= nil then f.starvationSupplementPct = v end
    elseif id == "set_gateMode" then
        local f = rawget(_G, "g_pastureHerding")
        local v = PHMenuSettingsFrame.GATE_MODE_VALUES[stateIndex or 1] or "all"
        if f ~= nil then f.gateAutoMode = v end
    elseif id == "set_grazeWeight" then
        local f = rawget(_G, "g_pastureHerding")
        local v = PHMenuSettingsFrame.GRAZE_WEIGHT_VALUES[stateIndex or 3] or 0.10
        if f ~= nil then f.grazeWeightBonus = v end
    elseif id == "set_grazeMilk" then
        local f = rawget(_G, "g_pastureHerding")
        local v = PHMenuSettingsFrame.GRAZE_MILK_VALUES[stateIndex or 4] or 0.15
        if f ~= nil then f.grazeMilkBonus = v end
    elseif id == "set_grazeRamp" then
        local f = rawget(_G, "g_pastureHerding")
        local v = PHMenuSettingsFrame.GRAZE_RAMP_VALUES[stateIndex or 2] or 0.03
        if f ~= nil then
            -- One picker drives both directions (symmetric ramp).
            f.grazeConditionBuildRate = v
            f.grazeConditionDecayRate = v
        end
    elseif id == "set_qualityDecay" then
        local f = rawget(_G, "g_pastureHerding")
        local v = PHMenuSettingsFrame.QUALITY_DECAY_VALUES[stateIndex or 3] or 0.15
        if f ~= nil then f.grazingQualityDecayPerGrazingDay = v end
    elseif id == "set_qualityRecovery" then
        local f = rawget(_G, "g_pastureHerding")
        local v = PHMenuSettingsFrame.QUALITY_RECOVERY_VALUES[stateIndex or 4] or 0.10
        if f ~= nil then f.grazingQualityRecoveryPerRestDay = v end
    elseif id == "set_grassFraction" then
        local f = rawget(_G, "g_pastureHerding")
        local v = PHMenuSettingsFrame.GRASS_FRACTION_VALUES[stateIndex or 4] or 0.65
        if f ~= nil then f.pastureGrassFoodFraction = v end
    elseif id == "set_herdsmanWage" then
        local f = rawget(_G, "g_pastureHerding")
        local v = PHMenuSettingsFrame.HERDSMAN_WAGE_VALUES[stateIndex or 5] or 14
        if f ~= nil then
            f.herdsmanHourlyWage = v
            f.herdsmanMonthlyCost = math.floor(v * (f.herdsmanHoursPerWeek or 39) * 52 / 12)
        end
    elseif id == "set_pathSample" then
        local f = rawget(_G, "g_pastureHerding")
        local v = PHMenuSettingsFrame.PATH_SAMPLE_VALUES[stateIndex or 2] or 1.0
        if f ~= nil then f._pathSampleSpacing = v end
    else
        PastureHerdingLog.dev("[PHMenuSettings] onClickSetting: unknown id %s", tostring(id))
    end
end
