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 mod settings that resolves the GameRules vs ModConfig priority.
 *
 * Priority order:
 * 1. GameRules (if a world is loaded and rules are available) - these are per-world overrides
 * 2. ModConfig (if config is loaded) - these are the user's intended defaults
 * 3. Hardcoded fallback - safe defaults if neither system is available yet
 *
 * This class exists because GameRules defaults (set at registration time before config loads)
 * can diverge from ModConfig defaults. Without this accessor, ModConfig spawn rate values
 * were completely ignored (BUG-001).
 */
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><b>BUG-003 fix:</b> Previously, {@code IHasResistance.getBaseResistance()}
     * called {@code ModGameRules.getResistance()} which only knew 4 types (rope, gag,
     * blindfold, collar) and returned hardcoded 100 for the other 10 types. Meanwhile
     * {@code BindVariant.getResistance()} read from ModConfig which had all 14 types.
     * This caused a display-vs-struggle desync (display: 250, struggle: 100).
     * Now both paths use this method.
     *
     * @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>Replicates the mapping from
     * {@link com.tiedup.remake.items.base.BindVariant#getResistance()} 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
        );
    }

    // ==================== 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;
        }
    }
}
