package com.tiedup.remake.core;

import com.tiedup.remake.entities.skins.Gender;
import com.tiedup.remake.util.ModGameRules;
import java.util.Map;
import java.util.function.Supplier;
import net.minecraft.world.level.GameRules;
import net.minecraftforge.common.ForgeConfigSpec;
import org.jetbrains.annotations.Nullable;

/**
 * Centralized accessor for all TiedUp! mod settings.
 *
 * <h2>Three-Tier Resolution</h2>
 * <ul>
 *   <li><b>Tier 1 — GameRules</b> (per-world, overridable via {@code /gamerule}): ~24 settings.
 *       These take priority when a world is loaded and {@code GameRules} is available.</li>
 *   <li><b>Tier 2 — ModConfig</b> (server {@code .toml} defaults): ~78+ settings.
 *       Read via {@link #safeGet} to tolerate early access before the config spec is loaded.</li>
 *   <li><b>Tier 3 — Hardcoded fallbacks</b>: safe defaults returned when neither Tier 1
 *       nor Tier 2 is available (e.g., during mod construction).</li>
 * </ul>
 *
 * <h2>Rule</h2>
 * <p><b>All game code should read settings through this class, never directly from
 * {@link ModConfig} or {@link ModGameRules}.</b> This ensures a single choke-point
 * for defaults, null-safety, and future GameRule promotion of any setting.</p>
 *
 * <p>Settings that only exist in ModConfig (no GameRule equivalent) use parameterless
 * getters. Settings that have a GameRule override accept a {@code @Nullable GameRules}
 * parameter.</p>
 *
 * <p>Client-side config ({@code ModConfig.CLIENT.*}) is excluded — it is read-only
 * and single-source, so no accessor is needed.</p>
 *
 * @see ModConfig.ServerConfig
 * @see ModGameRules
 */
public class SettingsAccessor {

    // ==================== Spawn Toggles ====================

    /**
     * Check if damsel spawning is enabled.
     *
     * @param rules GameRules from the current world, or null if unavailable
     * @return true if damsels should spawn
     */
    public static boolean doDamselsSpawn(@Nullable GameRules rules) {
        if (rules != null) return rules.getBoolean(ModGameRules.DAMSELS_SPAWN);
        return safeGet(() -> ModConfig.SERVER.damselsSpawn.get(), true);
    }

    /**
     * Check if kidnapper spawning is enabled.
     *
     * @param rules GameRules from the current world, or null if unavailable
     * @return true if kidnappers should spawn
     */
    public static boolean doKidnappersSpawn(@Nullable GameRules rules) {
        if (rules != null) return rules.getBoolean(
            ModGameRules.KIDNAPPERS_SPAWN
        );
        return safeGet(() -> ModConfig.SERVER.kidnappersSpawn.get(), true);
    }

    // ==================== Spawn Rates ====================

    /**
     * Get damsel spawn rate (0-100).
     * ModConfig default: 60, GameRule registration default was 100 (now fixed to 60).
     *
     * @param rules GameRules from the current world, or null if unavailable
     * @return spawn rate percentage
     */
    public static int getDamselSpawnRate(@Nullable GameRules rules) {
        if (rules != null) return rules.getInt(ModGameRules.DAMSEL_SPAWN_RATE);
        return safeGet(() -> ModConfig.SERVER.damselSpawnRate.get(), 60);
    }

    /**
     * Get base kidnapper spawn rate (0-100).
     * ModConfig default: 70, GameRule registration default was 100 (now fixed to 70).
     *
     * @param rules GameRules from the current world, or null if unavailable
     * @return spawn rate percentage
     */
    public static int getKidnapperSpawnRate(@Nullable GameRules rules) {
        if (rules != null) return rules.getInt(
            ModGameRules.KIDNAPPER_SPAWN_RATE
        );
        return safeGet(() -> ModConfig.SERVER.kidnapperSpawnRate.get(), 70);
    }

    /**
     * Get kidnapper archer spawn rate (0-100).
     * ModConfig default: 70, GameRule registration default was 100 (now fixed to 70).
     *
     * @param rules GameRules from the current world, or null if unavailable
     * @return spawn rate percentage
     */
    public static int getKidnapperArcherSpawnRate(@Nullable GameRules rules) {
        if (rules != null) return rules.getInt(
            ModGameRules.KIDNAPPER_ARCHER_SPAWN_RATE
        );
        return safeGet(
            () -> ModConfig.SERVER.kidnapperArcherSpawnRate.get(),
            70
        );
    }

    /**
     * Get kidnapper elite spawn rate (0-100).
     * ModConfig default: 70, GameRule registration default was 100 (now fixed to 70).
     *
     * @param rules GameRules from the current world, or null if unavailable
     * @return spawn rate percentage
     */
    public static int getKidnapperEliteSpawnRate(@Nullable GameRules rules) {
        if (rules != null) return rules.getInt(
            ModGameRules.KIDNAPPER_ELITE_SPAWN_RATE
        );
        return safeGet(
            () -> ModConfig.SERVER.kidnapperEliteSpawnRate.get(),
            70
        );
    }

    /**
     * Get kidnapper merchant spawn rate (0-100).
     * ModConfig default: 70, GameRule registration default was 100 (now fixed to 70).
     *
     * @param rules GameRules from the current world, or null if unavailable
     * @return spawn rate percentage
     */
    public static int getKidnapperMerchantSpawnRate(@Nullable GameRules rules) {
        if (rules != null) return rules.getInt(
            ModGameRules.KIDNAPPER_MERCHANT_SPAWN_RATE
        );
        return safeGet(
            () -> ModConfig.SERVER.kidnapperMerchantSpawnRate.get(),
            70
        );
    }

    /**
     * Get master spawn rate (0-100).
     * ModConfig default: 100, GameRule default: 100 (these already matched).
     *
     * @param rules GameRules from the current world, or null if unavailable
     * @return spawn rate percentage
     */
    public static int getMasterSpawnRate(@Nullable GameRules rules) {
        if (rules != null) return rules.getInt(ModGameRules.MASTER_SPAWN_RATE);
        return safeGet(() -> ModConfig.SERVER.masterSpawnRate.get(), 100);
    }

    // ==================== Bind Resistance ====================

    /**
     * Get the configured base resistance for a bind type.
     *
     * <p>Normalizes the bind key so callers can pass raw item names
     * (e.g., "ropes", "vine_seed") or config keys (e.g., "rope", "vine").
     *
     * <p>The normalization mapping:
     * <ul>
     *   <li>"ropes" / "shibari" -&gt; "rope"</li>
     *   <li>"vine_seed" -&gt; "vine"</li>
     *   <li>"web_bind" -&gt; "web"</li>
     *   <li>"duct_tape" -&gt; "tape"</li>
     *   <li>"leather_straps" / "medical_straps" / "dogbinder" -&gt; "armbinder"</li>
     *   <li>"beam_cuffs" -&gt; "chain"</li>
     * </ul>
     *
     * <p>Single source of truth for bind resistance — both the display
     * layer and the struggle minigame resolve here so they can't drift.</p>
     *
     * @param bindType The raw item name or config key
     * @return Resistance value from config, or 100 as fallback
     */
    public static int getBindResistance(String bindType) {
        String key = normalizeBindKey(bindType);
        return safeGet(
            () -> {
                if (ModConfig.SERVER == null) {
                    return 100;
                }
                Map<String, ForgeConfigSpec.IntValue> resistances =
                    ModConfig.SERVER.bindResistances;
                if (resistances != null && resistances.containsKey(key)) {
                    return resistances.get(key).get();
                }
                return 100;
            },
            100
        );
    }

    /**
     * Get the configured resistance added by a padlock.
     *
     * @param rules GameRules from the current world, or null if unavailable
     * @return Padlock resistance, or 250 as fallback
     */
    public static int getPadlockResistance(@Nullable GameRules rules) {
        if (rules != null) return rules.getInt(ModGameRules.PADLOCK_RESISTANCE);
        return safeGet(
            () -> {
                if (ModConfig.SERVER == null) {
                    return 250;
                }
                return ModConfig.SERVER.padlockResistance.get();
            },
            250
        );
    }

    /**
     * Normalize a raw bind item name to its config key.
     *
     * <p>Normalizes raw item names to config keys so that
     * every call site resolves to the same config entry.
     *
     * @param bindType Raw item name (e.g., "ropes", "vine_seed", "collar")
     * @return Normalized config key (e.g., "rope", "vine", "collar")
     */
    static String normalizeBindKey(String bindType) {
        if (bindType == null) {
            return "rope";
        }
        return switch (bindType.toLowerCase()) {
            // Plural -> singular
            case "ropes" -> "rope";
            // Aliases that share another type's resistance
            case "shibari" -> "rope";
            case "leather_straps", "medical_straps", "dogbinder" -> "armbinder";
            case "beam_cuffs" -> "chain";
            case "choke_collar" -> "collar";
            // Registry name -> config key rename
            case "vine_seed" -> "vine";
            case "web_bind" -> "web";
            case "duct_tape" -> "tape";
            // Already a valid config key (rope, chain, armbinder, wrap,
            // straitjacket, latex_sack, ribbon, vine, web, slime, tape,
            // gag, blindfold, collar)
            default -> bindType.toLowerCase();
        };
    }

    // ==================== Struggle System ====================

    /**
     * Check if struggle system is enabled.
     *
     * @param rules GameRules from the current world, or null if unavailable
     * @return true if struggle is enabled
     */
    public static boolean isStruggleEnabled(@Nullable GameRules rules) {
        if (rules != null) return rules.getBoolean(ModGameRules.STRUGGLE);
        return safeGet(() -> ModConfig.SERVER.struggleEnabled.get(), true);
    }

    /**
     * Get the struggle success probability (0-100).
     *
     * @param rules GameRules from the current world, or null if unavailable
     * @return Probability percentage
     */
    public static int getProbabilityStruggle(@Nullable GameRules rules) {
        if (rules != null) return rules.getInt(
            ModGameRules.PROBABILITY_STRUGGLE
        );
        return safeGet(() -> ModConfig.SERVER.struggleProbability.get(), 40);
    }

    /**
     * Get the minimum resistance decrease on successful struggle.
     *
     * @param rules GameRules from the current world, or null if unavailable
     * @return Minimum decrease amount (default 1)
     */
    public static int getStruggleMinDecrease(@Nullable GameRules rules) {
        if (rules != null) return rules.getInt(
            ModGameRules.STRUGGLE_MIN_DECREASE
        );
        return safeGet(() -> ModConfig.SERVER.struggleMinDecrease.get(), 1);
    }

    /**
     * Get the maximum resistance decrease on successful struggle.
     *
     * @param rules GameRules from the current world, or null if unavailable
     * @return Maximum decrease amount (default 10)
     */
    public static int getStruggleMaxDecrease(@Nullable GameRules rules) {
        if (rules != null) return rules.getInt(
            ModGameRules.STRUGGLE_MAX_DECREASE
        );
        return safeGet(() -> ModConfig.SERVER.struggleMaxDecrease.get(), 10);
    }

    /**
     * Get the struggle cooldown timer in ticks.
     *
     * @param rules GameRules from the current world, or null if unavailable
     * @return Cooldown in ticks (default 80 = 4 seconds)
     */
    public static int getStruggleTimer(@Nullable GameRules rules) {
        if (rules != null) return rules.getInt(ModGameRules.STRUGGLE_TIMER);
        return safeGet(() -> ModConfig.SERVER.struggleTimer.get(), 80);
    }

    /**
     * Get the probability of random shock during collar struggle (0-100).
     *
     * @param rules GameRules from the current world, or null if unavailable
     * @return Shock probability percentage (default 20)
     */
    public static int getStruggleCollarRandomShock(@Nullable GameRules rules) {
        if (rules != null) return rules.getInt(
            ModGameRules.STRUGGLE_COLLAR_RANDOM_SHOCK
        );
        return safeGet(
            () -> ModConfig.SERVER.struggleCollarRandomShock.get(),
            20
        );
    }

    /**
     * Get ticks per 1 resistance point for continuous struggle.
     * GameRule-only — no ModConfig equivalent exists.
     *
     * @param rules GameRules from the current world, or null if unavailable
     * @return Ticks per resistance (default 20 = 1 per second)
     */
    public static int getStruggleContinuousRate(@Nullable GameRules rules) {
        if (rules != null) return rules.getInt(
            ModGameRules.STRUGGLE_CONTINUOUS_RATE
        );
        return 20;
    }

    // ==================== Tying/Untying ====================

    /**
     * Get the tying duration in seconds.
     *
     * @param rules GameRules from the current world, or null if unavailable
     * @return Duration in seconds (default 5)
     */
    public static int getTyingPlayerTime(@Nullable GameRules rules) {
        if (rules != null) return rules.getInt(ModGameRules.TYING_PLAYER_TIME);
        return safeGet(() -> ModConfig.SERVER.tyingTime.get(), 5);
    }

    /**
     * Get the untying duration in seconds.
     *
     * @param rules GameRules from the current world, or null if unavailable
     * @return Duration in seconds (default 10)
     */
    public static int getUntyingPlayerTime(@Nullable GameRules rules) {
        if (rules != null) return rules.getInt(
            ModGameRules.UNTYING_PLAYER_TIME
        );
        return safeGet(() -> ModConfig.SERVER.untyingTime.get(), 10);
    }

    // ==================== Enslavement ====================

    /**
     * Check if the enslavement system is enabled.
     *
     * @param rules GameRules from the current world, or null if unavailable
     * @return true if enslavement is enabled
     */
    public static boolean isEnslavementEnabled(@Nullable GameRules rules) {
        if (rules != null) return rules.getBoolean(
            ModGameRules.ENSLAVEMENT_ENABLED
        );
        return safeGet(() -> ModConfig.SERVER.enslavementEnabled.get(), true);
    }

    // ==================== Bounty System ====================

    /**
     * Get maximum bounties per player.
     *
     * @param rules GameRules from the current world, or null if unavailable
     * @return Max bounties (default 5)
     */
    public static int getMaxBounties(@Nullable GameRules rules) {
        if (rules != null) return rules.getInt(ModGameRules.MAX_BOUNTIES);
        return safeGet(() -> ModConfig.SERVER.maxBountiesPerPlayer.get(), 5);
    }

    /**
     * Get bounty duration in seconds.
     *
     * @param rules GameRules from the current world, or null if unavailable
     * @return Duration in seconds (default 14400 = 4 hours)
     */
    public static int getBountyDuration(@Nullable GameRules rules) {
        if (rules != null) return rules.getInt(ModGameRules.BOUNTY_DURATION);
        return safeGet(() -> ModConfig.SERVER.bountyDuration.get(), 14400);
    }

    /**
     * Get bounty delivery detection radius.
     *
     * @param rules GameRules from the current world, or null if unavailable
     * @return Radius in blocks (default 5)
     */
    public static int getBountyDeliveryRadius(@Nullable GameRules rules) {
        if (rules != null) return rules.getInt(
            ModGameRules.BOUNTY_DELIVERY_RADIUS
        );
        return safeGet(() -> ModConfig.SERVER.bountyDeliveryRadius.get(), 5);
    }

    // ==================== Kidnap Bomb ====================

    /**
     * Get the kidnap bomb explosion radius.
     *
     * @param rules GameRules from the current world, or null if unavailable
     * @return Radius in blocks (default 5)
     */
    public static int getKidnapBombRadius(@Nullable GameRules rules) {
        if (rules != null) return rules.getInt(ModGameRules.KIDNAP_BOMB_RADIUS);
        return safeGet(() -> ModConfig.SERVER.kidnapBombRadius.get(), 5);
    }

    // ==================== NPC Struggle ====================

    /**
     * Check if NPC struggle system is enabled.
     * GameRule-only — no ModConfig equivalent exists.
     *
     * @param rules GameRules from the current world, or null if unavailable
     * @return true if NPCs can struggle against restraints
     */
    public static boolean isNpcStruggleEnabled(@Nullable GameRules rules) {
        if (rules != null) return rules.getBoolean(
            ModGameRules.NPC_STRUGGLE_ENABLED
        );
        return true;
    }

    /**
     * Get the base interval in ticks between NPC struggle attempts.
     * GameRule-only — no ModConfig equivalent exists.
     * Modified by personality type (FIERCE x0.3, SUBMISSIVE x3.0, etc.).
     *
     * @param rules GameRules from the current world, or null if unavailable
     * @return Base interval in ticks (default 6000 = 5 minutes)
     */
    public static int getNpcStruggleInterval(@Nullable GameRules rules) {
        if (rules != null) return rules.getInt(
            ModGameRules.NPC_STRUGGLE_INTERVAL
        );
        return 6000;
    }

    // ==================== Equipment Settings ====================

    /**
     * Get the shocker controller detection radius.
     *
     * @param rules GameRules from the current world, or null if unavailable
     * @return Radius in blocks (default 50, range 1-200)
     */
    public static int getShockerControllerRadius(@Nullable GameRules rules) {
        if (rules != null) return rules.getInt(
            ModGameRules.SHOCKER_CONTROLLER_BASE_RADIUS
        );
        return safeGet(
            () -> ModConfig.SERVER.shockerControllerRadius.get(),
            50
        );
    }

    // ==================== Gameplay Settings ====================

    /**
     * Check if gag talk proximity chat is enabled.
     * When enabled, gagged players' muffled speech is only heard by nearby players.
     *
     * @param rules GameRules from the current world, or null if unavailable
     * @return true if proximity chat is enabled for gagged players
     */
    public static boolean isGagTalkProximityEnabled(@Nullable GameRules rules) {
        if (rules != null) return rules.getBoolean(
            ModGameRules.GAG_TALK_PROXIMITY
        );
        return safeGet(() -> ModConfig.SERVER.gagTalkProximity.get(), true);
    }

    // ==================== Gender Mode ====================

    /**
     * Resolve preferred spawn gender. GameRule overrides ModConfig.
     *
     * <p>GameRule values: 0 = fall through to ModConfig, 1 = Female only, 2 = Male only.
     * ModConfig values: BOTH, FEMALE_ONLY, MALE_ONLY.
     *
     * @param rules GameRules from the current world, or null if unavailable
     * @return null for both genders, Gender.FEMALE, or Gender.MALE
     */
    public static @Nullable Gender getPreferredSpawnGender(
        @Nullable GameRules rules
    ) {
        if (rules != null) {
            int mode = rules.getInt(ModGameRules.SPAWN_GENDER_MODE);
            if (mode == 1) return Gender.FEMALE;
            if (mode == 2) return Gender.MALE;
            // mode == 0 or invalid: fall through to ModConfig
        }
        ModConfig.ServerConfig.SpawnGenderMode configMode = safeGet(
            () -> ModConfig.SERVER.spawnGenderMode.get(),
            ModConfig.ServerConfig.SpawnGenderMode.BOTH
        );
        return switch (configMode) {
            case FEMALE_ONLY -> Gender.FEMALE;
            case MALE_ONLY -> Gender.MALE;
            default -> null; // both
        };
    }

    // ==================== Chloroform ====================

    /**
     * Get the chloroform effect duration in ticks.
     * Config-only — no real GameRule exists (the old ModGameRules getter just read ModConfig).
     *
     * @return Duration in ticks (default 200 = 10 seconds)
     */
    public static int getChloroformDuration() {
        return safeGet(
            () -> ModConfig.SERVER.chloroformEffectDuration.get(),
            200
        );
    }

    // ==================== Minigame Toggles ====================

    /**
     * Check if the struggle mini-game is enabled.
     * When disabled, struggle actions are rejected or use instant logic.
     *
     * @return true if the struggle mini-game is enabled (default true)
     */
    public static boolean isStruggleMiniGameEnabled() {
        return safeGet(() -> ModConfig.SERVER.struggleMiniGameEnabled.get(), true);
    }

    /**
     * Check if the lockpick mini-game is enabled.
     * When disabled, lockpick actions are rejected or use instant logic.
     *
     * @return true if the lockpick mini-game is enabled (default true)
     */
    public static boolean isLockpickMiniGameEnabled() {
        return safeGet(() -> ModConfig.SERVER.lockpickMiniGameEnabled.get(), true);
    }

    // ==================== Combat & Weapons ====================

    /**
     * Get the whip damage amount.
     *
     * @return Damage as float (default 2.0)
     */
    public static float getWhipDamage() {
        return safeGet(() -> ModConfig.SERVER.whipDamage.get(), 2.0).floatValue();
    }

    /**
     * Get the resistance decrease per whip hit.
     *
     * @return Resistance decrease amount (default 15)
     */
    public static int getWhipResistanceDecrease() {
        return safeGet(() -> ModConfig.SERVER.whipResistanceDecrease.get(), 15);
    }

    /**
     * Get the taser stun effect duration in ticks.
     *
     * @return Duration in ticks (default 100 = 5 seconds)
     */
    public static int getTaserStunDuration() {
        return safeGet(() -> ModConfig.SERVER.taserStunDuration.get(), 100);
    }

    /**
     * Get the swim speed multiplier applied to tied players.
     *
     * @return Multiplier (0.0-1.0, default 0.5)
     */
    public static double getTiedSwimSpeedMultiplier() {
        return safeGet(() -> ModConfig.SERVER.tiedSwimSpeedMultiplier.get(), 0.5);
    }

    // ==================== Lockpick ====================

    /**
     * Get the lockpick success chance (0-100).
     *
     * @return Success chance percentage (default 25)
     */
    public static int getLockpickSuccessChance() {
        return safeGet(() -> ModConfig.SERVER.lockpickSuccessChance.get(), 25);
    }

    /**
     * Get the lockpick jam chance (0.0-100.0).
     *
     * @return Jam chance percentage as double (default 2.5)
     */
    public static double getLockpickJamChance() {
        return safeGet(() -> ModConfig.SERVER.lockpickJamChance.get(), 2.5);
    }

    /**
     * Get the lockpick break chance (0-100).
     *
     * @return Break chance percentage (default 15)
     */
    public static int getLockpickBreakChance() {
        return safeGet(() -> ModConfig.SERVER.lockpickBreakChance.get(), 15);
    }

    // ==================== Rag ====================

    /**
     * Get the default rag wet time in ticks.
     *
     * @return Wet time in ticks (default 6000 = 5 minutes)
     */
    public static int getRagWetTime() {
        return safeGet(() -> ModConfig.SERVER.ragWetTime.get(), 6000);
    }

    // ==================== Arrows ====================

    /**
     * Get the rope arrow bind chance for non-archer shooters (0-100).
     *
     * @return Bind chance percentage (default 50)
     */
    public static int getRopeArrowBindChance() {
        return safeGet(() -> ModConfig.SERVER.ropeArrowBindChance.get(), 50);
    }

    /**
     * Get the archer's base bind chance per arrow hit (0-100).
     *
     * @return Base bind chance percentage (default 10)
     */
    public static int getArcherArrowBindChanceBase() {
        return safeGet(() -> ModConfig.SERVER.archerArrowBindChanceBase.get(), 10);
    }

    /**
     * Get the additional bind chance per previous hit for archers (0-100).
     *
     * @return Per-hit chance increase percentage (default 10)
     */
    public static int getArcherArrowBindChancePerHit() {
        return safeGet(() -> ModConfig.SERVER.archerArrowBindChancePerHit.get(), 10);
    }

    // ==================== Kidnap Bomb ====================

    /**
     * Get the kidnap bomb fuse duration in ticks.
     *
     * @return Fuse in ticks (default 80 = 4 seconds)
     */
    public static int getKidnapBombFuse() {
        return safeGet(() -> ModConfig.SERVER.kidnapBombFuse.get(), 80);
    }

    // ==================== Merchant ====================

    /**
     * Get the merchant hostile cooldown duration in ticks.
     *
     * @return Duration in ticks (default 6000 = 5 minutes)
     */
    public static int getMerchantHostileDuration() {
        return safeGet(() -> ModConfig.SERVER.merchantHostileDuration.get(), 6000);
    }

    /**
     * Get the minimum number of random trades a merchant generates.
     *
     * @return Minimum trade count (default 8)
     */
    public static int getMerchantMinTrades() {
        return safeGet(() -> ModConfig.SERVER.merchantMinTrades.get(), 8);
    }

    /**
     * Get the maximum number of random trades a merchant generates.
     *
     * @return Maximum trade count (default 12)
     */
    public static int getMerchantMaxTrades() {
        return safeGet(() -> ModConfig.SERVER.merchantMaxTrades.get(), 12);
    }

    /**
     * Get the tier 1 minimum price in nuggets.
     * @return Min price (default 9)
     */
    public static int getTier1PriceMin() {
        return safeGet(() -> ModConfig.SERVER.tier1PriceMin.get(), 9);
    }

    /**
     * Get the tier 1 maximum price in nuggets.
     * @return Max price (default 18)
     */
    public static int getTier1PriceMax() {
        return safeGet(() -> ModConfig.SERVER.tier1PriceMax.get(), 18);
    }

    /**
     * Get the tier 2 minimum price in nuggets.
     * @return Min price (default 27)
     */
    public static int getTier2PriceMin() {
        return safeGet(() -> ModConfig.SERVER.tier2PriceMin.get(), 27);
    }

    /**
     * Get the tier 2 maximum price in nuggets.
     * @return Max price (default 45)
     */
    public static int getTier2PriceMax() {
        return safeGet(() -> ModConfig.SERVER.tier2PriceMax.get(), 45);
    }

    /**
     * Get the tier 3 minimum price in nuggets.
     * @return Min price (default 54)
     */
    public static int getTier3PriceMin() {
        return safeGet(() -> ModConfig.SERVER.tier3PriceMin.get(), 54);
    }

    /**
     * Get the tier 3 maximum price in nuggets.
     * @return Max price (default 90)
     */
    public static int getTier3PriceMax() {
        return safeGet(() -> ModConfig.SERVER.tier3PriceMax.get(), 90);
    }

    /**
     * Get the tier 4 minimum price in nuggets.
     * @return Min price (default 90)
     */
    public static int getTier4PriceMin() {
        return safeGet(() -> ModConfig.SERVER.tier4PriceMin.get(), 90);
    }

    /**
     * Get the tier 4 maximum price in nuggets.
     * @return Max price (default 180)
     */
    public static int getTier4PriceMax() {
        return safeGet(() -> ModConfig.SERVER.tier4PriceMax.get(), 180);
    }

    // ==================== Dialogue ====================

    /**
     * Get the dialogue broadcast radius in blocks.
     *
     * @return Radius in blocks (default 20)
     */
    public static int getDialogueRadius() {
        return safeGet(() -> ModConfig.SERVER.dialogueRadius.get(), 20);
    }

    /**
     * Get the dialogue cooldown in ticks.
     *
     * @return Cooldown in ticks (default 100 = 5 seconds)
     */
    public static int getDialogueCooldown() {
        return safeGet(() -> ModConfig.SERVER.dialogueCooldown.get(), 100);
    }

    // ==================== Labor ====================

    /**
     * Get the labor rest duration in seconds.
     *
     * @return Rest duration in seconds (default 120)
     */
    public static int getLaborRestSeconds() {
        return safeGet(() -> ModConfig.SERVER.laborRestSeconds.get(), 120);
    }

    // ==================== Solo Mode / Master Spawn ====================

    /**
     * Check if Master NPC spawning is enabled.
     *
     * @return true if Master can spawn (default true)
     */
    public static boolean isEnableMasterSpawn() {
        return safeGet(() -> ModConfig.SERVER.enableMasterSpawn.get(), true);
    }

    /**
     * Check if solo mode fallback behavior is enabled.
     * When enabled, kidnappers in solo worlds use alternative captive handling.
     *
     * @return true if solo fallback is enabled (default true)
     */
    public static boolean isEnableSoloFallback() {
        return safeGet(() -> ModConfig.SERVER.enableSoloFallback.get(), true);
    }

    /**
     * Get the solo mode timeout in seconds before the kidnapper gives up waiting.
     *
     * @return Timeout in seconds (default 120)
     */
    public static int getSoloTimeoutSeconds() {
        return safeGet(() -> ModConfig.SERVER.soloTimeoutSeconds.get(), 120);
    }

    /**
     * Get the probability (0.0-1.0) that a kidnapper keeps the captive in solo mode.
     *
     * @return Keep chance (default 0.6)
     */
    public static double getSoloKeepChance() {
        return safeGet(() -> ModConfig.SERVER.soloKeepChance.get(), 0.6);
    }

    // ==================== Abandon Behavior ====================

    /**
     * Check if kidnapper abandonment keeps the blindfold on the captive.
     *
     * @return true if blindfold is kept (default true)
     */
    public static boolean isAbandonKeepsBlindfold() {
        return safeGet(() -> ModConfig.SERVER.abandonKeepsBlindfold.get(), true);
    }

    /**
     * Check if kidnapper abandonment keeps binds on the captive.
     *
     * @return true if binds are kept (default true)
     */
    public static boolean isAbandonKeepsBinds() {
        return safeGet(() -> ModConfig.SERVER.abandonKeepsBinds.get(), true);
    }

    // ==================== Gag Settings ====================

    /**
     * Get the configured comprehension factor for a gag material.
     * Falls back to the material's hardcoded default if the config key is missing.
     *
     * @param materialKey Lowercase gag material name (e.g., "cloth", "ball")
     * @param defaultValue The material's built-in default comprehension
     * @return Comprehension factor as float
     */
    public static float getGagComprehension(String materialKey, float defaultValue) {
        return safeGet(
            () -> {
                if (ModConfig.SERVER == null) return defaultValue;
                Map<String, ForgeConfigSpec.DoubleValue> map =
                    ModConfig.SERVER.gagComprehension;
                if (map != null && map.containsKey(materialKey)) {
                    return map.get(materialKey).get().floatValue();
                }
                return defaultValue;
            },
            defaultValue
        );
    }

    /**
     * Get the configured talk range for a gag material.
     * Falls back to the material's hardcoded default if the config key is missing.
     *
     * @param materialKey Lowercase gag material name (e.g., "cloth", "ball")
     * @param defaultValue The material's built-in default talk range
     * @return Talk range as double
     */
    public static double getGagTalkRange(String materialKey, double defaultValue) {
        return safeGet(
            () -> {
                if (ModConfig.SERVER == null) return defaultValue;
                Map<String, ForgeConfigSpec.DoubleValue> map =
                    ModConfig.SERVER.gagRange;
                if (map != null && map.containsKey(materialKey)) {
                    return map.get(materialKey).get();
                }
                return defaultValue;
            },
            defaultValue
        );
    }

    // ==================== Helpers ====================

    /**
     * Safely get a config value, returning fallback if config isn't loaded yet.
     * ModConfig values throw IllegalStateException when accessed before the config
     * spec is loaded (e.g., during mod construction or very early startup).
     *
     * @param supplier the config value supplier
     * @param fallback value to return if config isn't available
     * @param <T> value type
     * @return the config value or fallback
     */
    private static <T> T safeGet(Supplier<T> supplier, T fallback) {
        try {
            return supplier.get();
        } catch (IllegalStateException e) {
            return fallback;
        }
    }
}
