package com.livingworld.bootstrap;

import com.livingworld.climate.ClimateEvent;
import com.livingworld.climate.ClimateEventType;
import com.livingworld.commands.LivingWorldCommandRoot;
import com.livingworld.config.DefaultConfigService;
import com.livingworld.config.EcosystemTuning;
import com.livingworld.config.SimulationConfig;
import com.livingworld.core.LivingWorldConstants;
import com.livingworld.core.services.CoreServices;
import com.livingworld.core.services.FileRegionPersistenceService;
import com.livingworld.core.services.ServiceRegistry;
import com.livingworld.core.simulation.DefaultTimeService;
import com.livingworld.core.simulation.SimulationManager;
import com.livingworld.core.simulation.SimulationScheduler;
import com.livingworld.core.simulation.UpdateReason;
import com.livingworld.debug.SimulationProfiler;
import com.livingworld.debug.DiagnosticCategory;
import com.livingworld.debug.LivingWorldLogger;
import com.livingworld.events.LivingWorldEventBus;
import com.livingworld.modules.ModuleContext;
import com.livingworld.modules.ModuleRegistry;
import com.livingworld.modules.ServerContext;
import com.livingworld.modules.ecosystem.EcosystemModule;
import com.livingworld.modules.pollution.PollutionModule;
import com.livingworld.modules.ecosystem.EcosystemRegionData;
import java.util.function.BooleanSupplier;
import com.livingworld.modules.pollution.PollutionRegionData;
import com.livingworld.modules.recovery.RecoveryModule;
import com.livingworld.modules.recovery.RecoveryRegionData;
import com.livingworld.modules.recovery.SuccessionStage;
import com.livingworld.modules.resources.ResourceDepletionModule;
import com.livingworld.modules.resources.ResourceRegionData;
import com.livingworld.modules.soil.SoilModule;
import com.livingworld.modules.soil.SoilRegionData;
import com.livingworld.modules.vegetation.VegetationModule;
import com.livingworld.modules.vegetation.VegetationRegionData;
import com.livingworld.modules.water.WaterModule;
import com.livingworld.modules.water.WaterRegionData;
import com.livingworld.climate.GlobalClimateTracker;
import com.livingworld.modules.atmosphere.AtmosphereModule;
import com.livingworld.modules.atmosphere.AtmosphereRegionData;
import com.livingworld.modules.atmosphere.Season;
import com.livingworld.modules.worldeffects.WorldEffectRequest;
import com.livingworld.modules.worldeffects.WorldEffectType;
import com.livingworld.modules.worldeffects.WorldEffectsModule;
import com.livingworld.platform.BlockBreakInfo;
import com.livingworld.platform.PlatformAdapter;
import com.livingworld.regions.Region;
import com.livingworld.regions.RegionCoordinate;
import com.livingworld.regions.RegionFactory;
import com.livingworld.regions.RegionLifecycleController;
import com.livingworld.regions.RegionManager;
import com.livingworld.regions.RegionStorage;
import com.livingworld.regions.cache.RegionCache;
import com.livingworld.regions.query.RegionQueryEngine;
import com.mojang.brigadier.CommandDispatcher;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.function.LongSupplier;
import java.util.Map;
import java.util.Optional;
import java.util.Random;
import java.util.Set;
import java.util.UUID;
import com.livingworld.regions.RegionMetrics;
import net.minecraft.commands.CommandSourceStack;

/**
 * Bootstrap class for Living World mod startup lifecycle.
 */
public final class LivingWorldBootstrap {

    private static final double WIND_DRIFT_MAX = 0.05; // radians per sim cycle
    /** Number of health samples retained per region for trend calculation. */
    private static final int TREND_WINDOW = 5;
    /** River erosion: accumulated runoff intensity before a RIVER_CARVE effect fires. */
    private static final double RIVER_CARVE_THRESHOLD = 80.0;
    /** Climate event checks run every N post-sim cycles to avoid per-tick overhead. */
    private static final int CLIMATE_CHECK_INTERVAL = 8;
    /** Groundwater level required for a valley spring to emerge. */
    private static final double GROUNDWATER_THRESHOLD = 50.0;
    /** Average surface elevation (Y) above which a region is treated as potentially volcanic. */
    private static final double VOLCANIC_ELEVATION = 85.0;
    /** Volcano state machine advances every N post-sim cycles. */
    private static final int VOLCANO_TICK_INTERVAL = 16;

    /** Lifecycle states for a volcanic region. */
    private enum VolcanoPhase { DORMANT, BUILDING, ERUPTING, COOLING, FERTILE }

    private double windAngle = 0.0;
    private final Random windRandom = new Random();
    /** Ecosystem tuning constants — loaded from TOML config at server start. */
    private EcosystemTuning ecosystemTuning = new EcosystemTuning();
    /**
     * Per-region accumulated incoming seed strength from healthy neighbours.
     * Populated each post-sim cycle by {@link #applySeedDispersal()}.
     * Transient — not persisted; re-accumulates within a few sim cycles on restart.
     */
    private final Map<RegionCoordinate, Double> accumulatedSeedRain = new HashMap<>();
    private final Set<UUID> hudEnabledPlayers = new HashSet<>();

    // --- Trend tracking (transient, resets on restart) ---
    private final Map<RegionCoordinate, double[]> healthHistory  = new HashMap<>();
    private final Map<RegionCoordinate, Integer>  historyIndex   = new HashMap<>();

    // --- River erosion (transient) ---
    private final Map<RegionCoordinate, Double> riverFlowIntensity = new HashMap<>();

    // --- Climate events ---
    private final List<ClimateEvent> activeClimateEvents   = new ArrayList<>();
    private final List<String>       pendingEventMessages  = new ArrayList<>();
    private int climateEventTick = 0;

    // --- Groundwater aquifer (transient) ---
    private final Map<RegionCoordinate, Double> groundwaterLevel = new HashMap<>();

    // --- Volcano lifecycle (transient — re-derives from elevation on restart) ---
    private final Map<RegionCoordinate, VolcanoPhase> volcanoPhase  = new HashMap<>();
    private final Map<RegionCoordinate, Integer>      volcanoTicks  = new HashMap<>();
    private final Set<RegionCoordinate>               volcanicRegions = new HashSet<>();
    private int volcanicActivityTick = 0;

    private PlatformAdapter platformAdapter;
    private Path worldSaveDirectory;
    private ServiceRegistry services;
    private RegionManager regionManager;
    private ModuleRegistry moduleRegistry;
    private SimulationManager simulationManager;
    private WorldEffectsModule worldEffectsModule;
    private AtmosphereModule atmosphereModule;
    private final GlobalClimateTracker climateTracker = new GlobalClimateTracker();
    private BooleanSupplier overworldRaining = () -> false;
    private LongSupplier absoluteDaySupplier = () -> 0L;
    private boolean initialized;
    private boolean serverReady;
    /** Average surface elevation (block Y) per region, sampled once by the platform layer. */
    private final Map<RegionCoordinate, Double> regionElevations = new HashMap<>();
    /** Extra sim cycles run per normal tick interval (1 = real-time, >1 = accelerated). */
    private int simSpeedMultiplier = 1;

    /**
     * Called once during mod construction.
     */
    public void initialize(PlatformAdapter platformAdapter) {
        if (initialized) {
            throw new IllegalStateException("bootstrap is already initialized");
        }
        if (platformAdapter == null) {
            throw new IllegalArgumentException("platformAdapter must not be null");
        }
        this.platformAdapter = platformAdapter;
        this.platformAdapter.registerCommands();
        this.platformAdapter.registerServerTickHook();
        this.platformAdapter.registerPlayerEventHooks();
        this.initialized = true;

        LivingWorldLogger.info(
                DiagnosticCategory.BOOTSTRAP,
                "Platform=" + platformAdapter.getPlatformName()
                        + ", minecraft=" + platformAdapter.getMinecraftVersion()
                        + ", loader=" + platformAdapter.getLoaderVersion()
                        + ", dedicatedServer=" + platformAdapter.isDedicatedServer());
        LivingWorldLogger.info(
                DiagnosticCategory.BOOTSTRAP,
                "initialize - mod bootstrap initialized.");
    }

    /**
     * Called during common (dedicated & integrated server) setup phase.
     */
    public void onCommonSetup() {
        requireInitialized();
        LivingWorldLogger.info(
                DiagnosticCategory.BOOTSTRAP,
                "onCommonSetup - common setup phase reached.");
    }

    /**
     * Called when the server is starting.
     */
    public void onServerStarting(Path worldSaveDirectory) {
        requireInitialized();
        if (worldSaveDirectory == null) {
            throw new IllegalArgumentException("worldSaveDirectory must not be null");
        }
        if (serverReady) {
            throw new IllegalStateException("server services are already initialized");
        }
        this.worldSaveDirectory = worldSaveDirectory;
        createServerServices(worldSaveDirectory);
        climateTracker.load(worldSaveDirectory.resolve("living_world/global_climate.dat"));
        this.serverReady = true;
        LivingWorldLogger.info(
                DiagnosticCategory.BOOTSTRAP,
                "onServerStarting - services ready at " + worldSaveDirectory);
    }

    /**
     * Called when the server has finished starting.
     */
    public void onServerStarted() {
        requireServerReady();
        ServerContext context = new ServerContext();
        moduleRegistry.getEnabledModules().forEach(module -> module.onServerStarted(context));
        LivingWorldLogger.info(
                DiagnosticCategory.BOOTSTRAP,
                "onServerStarted - server started.");
    }

    /**
     * Handles a block-break event forwarded from the platform adapter.
     *
     * <p>Determines whether the block represents mining, logging, or farming activity
     * and records the corresponding depletion on the region's
     * {@link ResourceRegionData}. Safe to call before the server is ready (returns
     * immediately in that case).</p>
     *
     * @param info platform-neutral description of the broken block
     */
    public void handleBlockBreak(BlockBreakInfo info) {
        if (!serverReady) {
            return;
        }
        RegionCoordinate coord = RegionCoordinate.fromBlock(
                info.dimensionId(),
                info.blockX(),
                info.blockZ(),
                LivingWorldConstants.DEFAULT_REGION_SIZE_CHUNKS);

        Region region = regionManager.getOrCreateRegion(coord);
        ResourceRegionData resources = region.getModuleData()
                .get(ResourceDepletionModule.MODULE_ID, ResourceRegionData.class)
                .orElseGet(ResourceRegionData::defaults);

        String block = info.blockRegistryName();
        if (isLog(block) || isLeaves(block) || isWood(block)) {
            resources.recordLogging(1.0);
        } else if (isOre(block) || isStone(block)) {
            resources.recordMining(1.0);
        } else if (isCrop(block) || isFarmland(block)) {
            resources.recordFarming(1.0);
        } else {
            return; // not a tracked resource — don't dirty the region
        }

        region.getModuleData().put(ResourceDepletionModule.MODULE_ID, resources);
        regionManager.markDirty(region);
    }

    /**
     * Called when the server is stopping.
     */
    /** Sets the ecosystem tuning loaded from the server config. Must be called before onServerStarting(). */
    public void setEcosystemTuning(EcosystemTuning tuning) {
        if (tuning != null) ecosystemTuning = tuning;
    }

    public void onServerStopping() {
        if (!serverReady) {
            return;
        }
        regionManager.flushAll();
        moduleRegistry.shutdownAll();
        climateTracker.save(worldSaveDirectory.resolve("living_world/global_climate.dat"));
        hudEnabledPlayers.clear();
        regionElevations.clear();
        accumulatedSeedRain.clear();
        healthHistory.clear();
        historyIndex.clear();
        riverFlowIntensity.clear();
        activeClimateEvents.clear();
        pendingEventMessages.clear();
        climateEventTick = 0;
        groundwaterLevel.clear();
        volcanoPhase.clear();
        volcanoTicks.clear();
        volcanicRegions.clear();
        volcanicActivityTick = 0;
        simSpeedMultiplier = 1;
        serverReady = false;
        LivingWorldLogger.info(
                DiagnosticCategory.BOOTSTRAP,
                "onServerStopping - persistence flushed and modules stopped.");
    }

    /**
     * Sets the supplier that reports whether it is currently raining in the overworld.
     * Called from the platform layer after the server starts and cleared on stop.
     */
    /** Returns all currently cached (active) regions. Safe to call from the platform layer. */
    public Collection<Region> getActiveRegions() {
        if (!serverReady) return List.of();
        return regionManager.getActiveRegions();
    }

    /**
     * Records pollution produced by burning activity (e.g. furnaces) in a region.
     * Called from the platform layer once per simulation interval.
     *
     * @param waterAmount acid-rain component — SO₂ from coal dissolves in atmospheric moisture
     */
    public void handleFurnaceActivity(Region region, double airAmount, double groundAmount, double waterAmount) {
        if (!serverReady || region == null) return;
        PollutionRegionData data = region.getModuleData()
                .get(PollutionModule.MODULE_ID, PollutionRegionData.class)
                .orElseGet(PollutionRegionData::defaults);
        data.addPollution(airAmount, groundAmount, waterAmount);
        region.getModuleData().put(PollutionModule.MODULE_ID, data);
        regionManager.markDirty(region);
    }

    /** Returns the live metrics for the region containing the given world position. */
    public Optional<RegionMetrics> getMetricsAt(String dimensionId, double x, double z) {
        if (!serverReady) return Optional.empty();
        RegionCoordinate coord = RegionCoordinate.fromBlock(
                dimensionId, (int) x, (int) z, LivingWorldConstants.DEFAULT_REGION_SIZE_CHUNKS);
        return regionManager.resolve(coord).map(Region::getMetrics);
    }

    /** Called when a sapling grows into a tree — reduces logging depletion in that region. */
    public void handleTreeGrowth(RegionCoordinate coord) {
        if (!serverReady || coord == null) return;
        regionManager.resolve(coord).ifPresent(region -> {
            ResourceRegionData res = region.getModuleData()
                    .get(ResourceDepletionModule.MODULE_ID, ResourceRegionData.class)
                    .orElse(null);
            if (res == null) return;
            res.setLoggingDepletion(Math.max(0, res.getLoggingDepletion() - 2.0));
            region.getModuleData().put(ResourceDepletionModule.MODULE_ID, res);
            regionManager.markDirty(region);
        });
    }

    /** Called when a player applies bone meal — boosts soil fertility. */
    public void handleBoneMeal(String dimensionId, int x, int z) {
        if (!serverReady) return;
        RegionCoordinate coord = RegionCoordinate.fromBlock(
                dimensionId, x, z, LivingWorldConstants.DEFAULT_REGION_SIZE_CHUNKS);
        regionManager.resolve(coord).ifPresent(region -> {
            SoilRegionData soil = region.getModuleData()
                    .get(SoilModule.MODULE_ID, SoilRegionData.class)
                    .orElse(null);
            if (soil == null) return;
            soil.setFertility(Math.min(100, soil.getFertility() + 2.0));
            region.getModuleData().put(SoilModule.MODULE_ID, soil);
            regionManager.markDirty(region);
        });
    }

    /** Called when a player harvests a fully-grown crop — drains soil fertility. */
    public void handleCropHarvest(String dimensionId, int x, int z) {
        if (!serverReady) return;
        RegionCoordinate coord = RegionCoordinate.fromBlock(
                dimensionId, x, z, LivingWorldConstants.DEFAULT_REGION_SIZE_CHUNKS);
        regionManager.resolve(coord).ifPresent(region -> {
            SoilRegionData soil = region.getModuleData()
                    .get(SoilModule.MODULE_ID, SoilRegionData.class)
                    .orElse(null);
            if (soil == null) return;
            soil.setFertility(Math.max(0, soil.getFertility() - 0.5));
            ResourceRegionData res = region.getModuleData()
                    .get(ResourceDepletionModule.MODULE_ID, ResourceRegionData.class)
                    .orElse(null);
            if (res != null) {
                res.recordFarming(0.3);
                region.getModuleData().put(ResourceDepletionModule.MODULE_ID, res);
            }
            region.getModuleData().put(SoilModule.MODULE_ID, soil);
            regionManager.markDirty(region);
        });
    }

    /** Sets the biome-derived succession ceiling for a region. */
    public void setRegionBiomeCap(RegionCoordinate coord, SuccessionStage maxStage) {
        if (!serverReady || coord == null || maxStage == null) return;
        regionManager.resolve(coord).ifPresent(region -> {
            RecoveryRegionData recovery = region.getModuleData()
                    .get(RecoveryModule.MODULE_ID, RecoveryRegionData.class)
                    .orElseGet(RecoveryRegionData::defaults);
            recovery.setMaxSuccessionStage(maxStage);
            region.getModuleData().put(RecoveryModule.MODULE_ID, recovery);
            regionManager.markDirty(region);
        });
    }

    public void notifyPlayerInRegion(RegionCoordinate coordinate) {
        if (!serverReady || coordinate == null) return;
        regionManager.getOrCreateRegion(coordinate);
        simulationManager.queueRegionForUpdate(coordinate, 10, com.livingworld.core.simulation.UpdateReason.PLAYER_NEARBY);
    }

    public void setOverworldRaining(BooleanSupplier supplier) {
        this.overworldRaining = supplier != null ? supplier : () -> false;
    }

    public void setAbsoluteDaySupplier(LongSupplier supplier) {
        this.absoluteDaySupplier = supplier != null ? supplier : () -> 0L;
    }

    public Season getCurrentSeason() {
        return Season.fromAbsoluteDay(absoluteDaySupplier.getAsLong());
    }

    public void onServerTick() {
        if (!serverReady) {
            return;
        }
        long previousSimulationTick = simulationManager.getSimulationTickCounter();
        simulationManager.onMinecraftServerTick();
        if (simulationManager.getSimulationTickCounter() != previousSimulationTick) {
            runPostSimHooks();
            for (int extra = 1; extra < simSpeedMultiplier; extra++) {
                for (Region r : regionManager.getActiveRegions()) {
                    simulationManager.queueRegionForUpdate(
                            r.getCoordinate(), 0, UpdateReason.NORMAL_ROLLING_UPDATE);
                }
                simulationManager.runSimulationCycle();
                runPostSimHooks();
            }
            regionManager.saveDirtyRegions();
        }
    }

    private void runPostSimHooks() {
        spreadPollutionAcrossRegions();
        applySeasonalEffects();
        climateTracker.update(regionManager.getActiveRegions());
        applyClimateWarmingEffects();
        applyWaterRunoff();
        applyGroundwaterAndSprings();
        applyDynamicCapUpdate();
        applySeedDispersal();
        recordHealthTrend();
        if (++climateEventTick % CLIMATE_CHECK_INTERVAL == 0) {
            applyClimateEvents();
        }
        if (++volcanicActivityTick % VOLCANO_TICK_INTERVAL == 0) {
            applyVolcanicActivity();
        }
    }

    /** Sets the simulation speed multiplier (1 = real-time, max 100). */
    public int setSimSpeedMultiplier(int multiplier) {
        this.simSpeedMultiplier = Math.max(1, Math.min(100, multiplier));
        LivingWorldLogger.info(DiagnosticCategory.BOOTSTRAP,
                "Simulation speed set to " + simSpeedMultiplier + "x");
        return this.simSpeedMultiplier;
    }

    public int getSimSpeedMultiplier() { return simSpeedMultiplier; }

    private void spreadPollutionAcrossRegions() {
        Collection<Region> active = regionManager.getActiveRegions();
        if (active.size() < 2) return;

        // Wind drifts slowly each sim cycle.
        windAngle += (windRandom.nextDouble() * 2.0 - 1.0) * WIND_DRIFT_MAX;

        Map<RegionCoordinate, Region> byCoord = new HashMap<>();
        for (Region r : active) byCoord.put(r.getCoordinate(), r);

        // dx, dz, and the angle that offset points in (atan2(dz, dx))
        int[][] offsets = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
        for (Region region : active) {
            RegionCoordinate coord = region.getCoordinate();
            PollutionRegionData data = region.getModuleData()
                    .get(PollutionModule.MODULE_ID, PollutionRegionData.class)
                    .orElse(null);
            if (data == null) continue;
            for (int[] off : offsets) {
                RegionCoordinate neighbourCoord = new RegionCoordinate(
                        coord.dimensionId(), coord.x() + off[0], coord.z() + off[1]);
                Region neighbour = byCoord.get(neighbourCoord);
                if (neighbour == null) continue;
                PollutionRegionData neighbourData = neighbour.getModuleData()
                        .get(PollutionModule.MODULE_ID, PollutionRegionData.class)
                        .orElse(null);
                if (neighbourData == null) continue;
                double diff = data.getAirPollution() - neighbourData.getAirPollution();
                if (diff <= 0) continue;
                // Wind alignment: positive = downwind (faster spread), negative = upwind (slower).
                double offsetAngle = Math.atan2(off[1], off[0]);
                double alignment = Math.cos(windAngle - offsetAngle);
                double rate = ecosystemTuning.getPollutionSpreadRate()
                        * (1.0 + alignment * ecosystemTuning.getWindBoost());
                double transfer = diff * rate;
                data.addPollution(-transfer, 0, 0);
                neighbourData.addPollution(transfer, 0, 0);
                neighbour.getModuleData().put(PollutionModule.MODULE_ID, neighbourData);
                regionManager.markDirty(neighbour);
            }
            region.getModuleData().put(PollutionModule.MODULE_ID, data);
            regionManager.markDirty(region);
        }
    }

    private void applyClimateWarmingEffects() {
        double warming = climateTracker.getWarmingLevel();
        if (warming < 0.05) return; // no measurable effect below 5 % warming
        for (Region region : regionManager.getActiveRegions()) {
            WaterRegionData water = region.getModuleData()
                    .get(WaterModule.MODULE_ID, WaterRegionData.class)
                    .orElse(null);
            if (water == null) continue;
            // Warming raises drought pressure everywhere; effect is tiny per cycle
            // but accumulates into persistent drought at high warming levels.
            water.setDroughtRisk(Math.min(100, water.getDroughtRisk() + warming * 0.02));
            region.getModuleData().put(WaterModule.MODULE_ID, water);
            regionManager.markDirty(region);
        }
    }

    private void applySeasonalEffects() {
        Season season = getCurrentSeason();
        for (Region region : regionManager.getActiveRegions()) {
            SoilRegionData soil = region.getModuleData()
                    .get(SoilModule.MODULE_ID, SoilRegionData.class).orElse(null);
            WaterRegionData water = region.getModuleData()
                    .get(WaterModule.MODULE_ID, WaterRegionData.class).orElse(null);
            VegetationRegionData veg = region.getModuleData()
                    .get(VegetationModule.MODULE_ID, VegetationRegionData.class).orElse(null);
            if (soil == null || water == null || veg == null) continue;

            double droughtDelta = season.droughtMod();
            double vegDelta     = season.vegGrowthMod();

            switch (season) {
                case SPRING -> {
                    // Snowmelt: fertility boost, moisture recovery, drought eases, growth surge
                    soil.setFertility(Math.min(100, soil.getFertility() + 0.005));
                    soil.setMoisture(Math.min(100, soil.getMoisture() + 0.08));
                    veg.setGrassPressure(Math.min(100, veg.getGrassPressure() + vegDelta));
                    veg.setFlowerPressure(Math.min(100, veg.getFlowerPressure() + vegDelta * 0.8));
                }
                case SUMMER -> {
                    // Heat: evaporation reduces moisture and water availability
                    soil.setMoisture(Math.max(0, soil.getMoisture() - 0.05));
                    water.setWaterAvailability(Math.max(0, water.getWaterAvailability() - 0.04));
                    // Mild growth boost from warmth, only if moisture is adequate
                    if (soil.getMoisture() > 30) {
                        veg.setGrassPressure(Math.min(100, veg.getGrassPressure() + vegDelta));
                    }
                }
                case AUTUMN -> {
                    // Senescence: dead litter accumulates, soil gains a little from decomposition
                    veg.setDeadVegetation(Math.min(100, veg.getDeadVegetation() + 0.08));
                    soil.setFertility(Math.min(100, soil.getFertility() + 0.002));
                    veg.setFlowerPressure(Math.max(0, veg.getFlowerPressure() + vegDelta));
                    veg.setGrassPressure(Math.max(0, veg.getGrassPressure() + vegDelta));
                }
                case WINTER -> {
                    // Frost: moisture freezes, drought risk creeps up, vegetation retreats
                    soil.setMoisture(Math.max(0, soil.getMoisture() - 0.004));
                    water.setWaterAvailability(Math.max(0, water.getWaterAvailability() - 0.06));
                    veg.setGrassPressure(Math.max(0, veg.getGrassPressure() + vegDelta));
                    veg.setFlowerPressure(Math.max(0, veg.getFlowerPressure() + vegDelta));
                    veg.setShrubPressure(Math.max(0, veg.getShrubPressure() + vegDelta * 0.5));
                }
            }

            // Universal drought drift based on season
            water.setDroughtRisk(Math.min(100, Math.max(0, water.getDroughtRisk() + droughtDelta)));

            region.getModuleData().put(SoilModule.MODULE_ID, soil);
            region.getModuleData().put(WaterModule.MODULE_ID, water);
            region.getModuleData().put(VegetationModule.MODULE_ID, veg);
            regionManager.markDirty(region);
        }
    }

    /**
     * Tracks subsurface groundwater per region: recharges from rainfall and lateral seepage
     * from uphill neighbours. When the aquifer level is high in a valley (lower elevation
     * than ≥2 neighbours), a GROUND_SPRING_EMERGES effect is queued — placing a permanent
     * water source that flows naturally downhill under Minecraft's own fluid physics.
     */
    private void applyGroundwaterAndSprings() {
        Collection<Region> active = regionManager.getActiveRegions();
        if (active.isEmpty()) return;
        int[][] offsets = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};

        for (Region region : active) {
            RegionCoordinate coord = region.getCoordinate();
            AtmosphereRegionData atm = region.getModuleData()
                    .get(AtmosphereModule.MODULE_ID, AtmosphereRegionData.class).orElse(null);
            Double myElev = regionElevations.get(coord);

            // Recharge from rainfall
            if (atm != null && atm.getRainLevel() > 0.1) {
                groundwaterLevel.merge(coord, atm.getRainLevel() * 0.008, Double::sum);
            }

            // Lateral underground seepage from higher neighbours
            if (myElev != null) {
                for (int[] off : offsets) {
                    RegionCoordinate nCoord = new RegionCoordinate(
                            coord.dimensionId(), coord.x() + off[0], coord.z() + off[1]);
                    Double nElev = regionElevations.get(nCoord);
                    if (nElev != null && nElev > myElev + 3) {
                        double seepage = 0.002 * Math.min((nElev - myElev) / 50.0, 1.0);
                        groundwaterLevel.merge(coord, seepage, Double::sum);
                    }
                }
            }

            // Slow evaporation / natural discharge
            double level = Math.max(0, groundwaterLevel.getOrDefault(coord, 0.0) - 0.001);
            groundwaterLevel.put(coord, level);

            // Spring emergence: valley floor with saturated aquifer
            if (level >= GROUNDWATER_THRESHOLD && myElev != null) {
                int higherNeighbours = 0;
                for (int[] off : offsets) {
                    RegionCoordinate nCoord = new RegionCoordinate(
                            coord.dimensionId(), coord.x() + off[0], coord.z() + off[1]);
                    Double nElev = regionElevations.get(nCoord);
                    if (nElev != null && nElev > myElev + 5) higherNeighbours++;
                }
                if (higherNeighbours >= 2 && worldEffectsModule != null) {
                    worldEffectsModule.queueEffect(new WorldEffectRequest(
                            WorldEffectType.GROUND_SPRING_EMERGES, coord,
                            Math.min(1.0, (level - GROUNDWATER_THRESHOLD) / 50.0 + 0.3)));
                    // Discharge: spring reduces aquifer level
                    groundwaterLevel.put(coord, level - 15.0);
                    WaterRegionData water = region.getModuleData()
                            .get(WaterModule.MODULE_ID, WaterRegionData.class).orElse(null);
                    if (water != null) {
                        water.setWaterAvailability(Math.min(100, water.getWaterAvailability() + 5.0));
                        water.setDroughtRisk(Math.max(0, water.getDroughtRisk() - 3.0));
                        region.getModuleData().put(WaterModule.MODULE_ID, water);
                    }
                    // Active spring contributes to river erosion even without rain
                    riverFlowIntensity.merge(coord, 10.0, Double::sum);
                    regionManager.markDirty(region);
                }
            }

            // Hydraulic erosion fires when the region has meaningful flow intensity
            // from any source (rainfall runoff OR active spring), independent of rain
            double flowHere = riverFlowIntensity.getOrDefault(coord, 0.0);
            if (flowHere > 20.0 && worldEffectsModule != null) {
                worldEffectsModule.queueEffect(new WorldEffectRequest(
                        WorldEffectType.HYDRAULIC_EROSION, coord,
                        Math.min(1.0, flowHere / (RIVER_CARVE_THRESHOLD * 2))));
            }
        }
    }

    /**
     * Advances the volcanic lifecycle for all high-elevation regions:
     * DORMANT → BUILDING → ERUPTING → COOLING → FERTILE → DORMANT.
     * Each phase applies physical world effects and ecosystem data changes.
     */
    private void applyVolcanicActivity() {
        Collection<Region> active = regionManager.getActiveRegions();
        if (active.isEmpty()) return;
        Map<RegionCoordinate, Region> byCoord = new HashMap<>();
        for (Region r : active) byCoord.put(r.getCoordinate(), r);
        int[][] offsets = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};

        for (Region region : active) {
            RegionCoordinate coord = region.getCoordinate();
            Double elev = regionElevations.get(coord);
            if (elev == null) continue;

            // Register high-elevation regions as potentially volcanic on first encounter
            if (!volcanoPhase.containsKey(coord)) {
                if (elev < VOLCANIC_ELEVATION) continue;
                volcanicRegions.add(coord);
                volcanoPhase.put(coord, VolcanoPhase.DORMANT);
                volcanoTicks.put(coord, 0);
            }

            VolcanoPhase phase = volcanoPhase.get(coord);
            int ticks = volcanoTicks.getOrDefault(coord, 0);
            volcanoTicks.put(coord, ticks + 1);

            switch (phase) {
                case DORMANT -> {
                    // Rare stochastic chance to awaken (~0.2% per check)
                    if (windRandom.nextDouble() < 0.002) {
                        volcanoPhase.put(coord, VolcanoPhase.BUILDING);
                        volcanoTicks.put(coord, 0);
                        pendingEventMessages.add("[LW] 🌋 Seismic activity at mountain region ("
                                + coord.x() + "," + coord.z() + ") — a volcano may be awakening...");
                    }
                }
                case BUILDING -> {
                    // Venting gases: mild pollution increase; 30 cycles before eruption
                    PollutionRegionData poll = region.getModuleData()
                            .get(PollutionModule.MODULE_ID, PollutionRegionData.class).orElse(null);
                    if (poll != null) {
                        poll.addPollution(0.3, 0.1, 0);
                        region.getModuleData().put(PollutionModule.MODULE_ID, poll);
                        regionManager.markDirty(region);
                    }
                    if (ticks >= 30) {
                        volcanoPhase.put(coord, VolcanoPhase.ERUPTING);
                        volcanoTicks.put(coord, 0);
                        pendingEventMessages.add("[LW] 🌋 VOLCANIC ERUPTION at region ("
                                + coord.x() + "," + coord.z() + ")! Lava and ash spreading now!");
                    }
                }
                case ERUPTING -> {
                    // Lava flows and ash — physical block changes
                    if (worldEffectsModule != null) {
                        worldEffectsModule.queueEffect(new WorldEffectRequest(
                                WorldEffectType.LAVA_FLOW, coord,
                                Math.min(1.0, 0.4 + (ticks / 20.0) * 0.6)));
                        if (windRandom.nextDouble() < 0.40) {
                            worldEffectsModule.queueEffect(new WorldEffectRequest(
                                    WorldEffectType.ASH_DEPOSIT, coord, 0.7));
                        }
                        if (windRandom.nextDouble() < 0.25) {
                            worldEffectsModule.queueEffect(new WorldEffectRequest(
                                    WorldEffectType.VEGETATION_DIES, coord, 0.9));
                        }
                    }
                    // Ash cloud spreads to adjacent regions
                    for (int[] off : offsets) {
                        RegionCoordinate nCoord = new RegionCoordinate(
                                coord.dimensionId(), coord.x() + off[0], coord.z() + off[1]);
                        Region nb = byCoord.get(nCoord);
                        if (nb == null) continue;
                        if (worldEffectsModule != null && windRandom.nextDouble() < 0.3) {
                            worldEffectsModule.queueEffect(new WorldEffectRequest(
                                    WorldEffectType.ASH_DEPOSIT, nCoord, 0.3));
                        }
                        PollutionRegionData nbPoll = nb.getModuleData()
                                .get(PollutionModule.MODULE_ID, PollutionRegionData.class).orElse(null);
                        if (nbPoll != null) {
                            nbPoll.addPollution(0.8, 0.2, 0);
                            nb.getModuleData().put(PollutionModule.MODULE_ID, nbPoll);
                            regionManager.markDirty(nb);
                        }
                    }
                    // Eruption zone: spike air pollution, drain vegetation
                    PollutionRegionData poll = region.getModuleData()
                            .get(PollutionModule.MODULE_ID, PollutionRegionData.class).orElse(null);
                    if (poll != null) {
                        poll.addPollution(2.0, 1.0, 0.5);
                        region.getModuleData().put(PollutionModule.MODULE_ID, poll);
                    }
                    VegetationRegionData veg = region.getModuleData()
                            .get(VegetationModule.MODULE_ID, VegetationRegionData.class).orElse(null);
                    if (veg != null) {
                        veg.setGrassPressure(Math.max(0, veg.getGrassPressure() - 5));
                        veg.setTreePressure(Math.max(0, veg.getTreePressure() - 3));
                        region.getModuleData().put(VegetationModule.MODULE_ID, veg);
                    }
                    regionManager.markDirty(region);
                    // Eruption lasts ~40 cycles
                    if (ticks >= 40) {
                        volcanoPhase.put(coord, VolcanoPhase.COOLING);
                        volcanoTicks.put(coord, 0);
                        pendingEventMessages.add("[LW] 🌋 Eruption subsiding at ("
                                + coord.x() + "," + coord.z() + ") — lava cooling and solidifying...");
                    }
                }
                case COOLING -> {
                    // Lava solidifies — new land forms from cooling rock
                    if (worldEffectsModule != null && windRandom.nextDouble() < 0.5) {
                        worldEffectsModule.queueEffect(new WorldEffectRequest(
                                WorldEffectType.COBBLESTONE_FORMS, coord, 0.6));
                    }
                    if (ticks >= 30) {
                        volcanoPhase.put(coord, VolcanoPhase.FERTILE);
                        volcanoTicks.put(coord, 0);
                        pendingEventMessages.add("[LW] 🌱 Volcanic soil enriching region ("
                                + coord.x() + "," + coord.z() + ") — fertile basalt plain emerging!");
                    }
                }
                case FERTILE -> {
                    // One-time volcanic mineral fertility boost in the first 10 cycles
                    if (ticks < 10) {
                        SoilRegionData soil = region.getModuleData()
                                .get(SoilModule.MODULE_ID, SoilRegionData.class).orElse(null);
                        if (soil != null) {
                            soil.setFertility(Math.min(100, soil.getFertility() + 30));
                            soil.setContamination(Math.max(0, soil.getContamination() - 20));
                            region.getModuleData().put(SoilModule.MODULE_ID, soil);
                        }
                    }
                    // Pollution clears naturally as fresh minerals neutralise it
                    if (ticks < 20) {
                        PollutionRegionData poll = region.getModuleData()
                                .get(PollutionModule.MODULE_ID, PollutionRegionData.class).orElse(null);
                        if (poll != null) {
                            poll.addPollution(-1.0, -0.5, -0.3);
                            region.getModuleData().put(PollutionModule.MODULE_ID, poll);
                        }
                    }
                    regionManager.markDirty(region);
                    if (ticks >= 50) {
                        volcanoPhase.put(coord, VolcanoPhase.DORMANT);
                        volcanoTicks.put(coord, 0);
                    }
                }
            }
        }
    }

    /** Returns the current volcano phase name for the given region, or null if not volcanic. */
    public String getVolcanoPhaseAt(RegionCoordinate coord) {
        if (coord == null) return null;
        VolcanoPhase phase = volcanoPhase.get(coord);
        return phase != null ? phase.name() : null;
    }

    /**
     * Returns a formatted string listing all tracked volcanic regions and their current phase.
     * Used by {@code /lw volcanoes}.
     */
    public String getVolcanoStatusString() {
        if (volcanicRegions.isEmpty()) return "[LW] No volcanic regions detected yet (requires high-elevation terrain).";
        StringBuilder sb = new StringBuilder("[LW] Volcanic regions: ").append(volcanicRegions.size()).append("\n");
        for (RegionCoordinate coord : volcanicRegions) {
            VolcanoPhase phase = volcanoPhase.getOrDefault(coord, VolcanoPhase.DORMANT);
            int ticks = volcanoTicks.getOrDefault(coord, 0);
            Double elev = regionElevations.get(coord);
            sb.append("  (").append(coord.x()).append(",").append(coord.z()).append(") ")
              .append(phase.name())
              .append(" — ticks: ").append(ticks);
            if (elev != null) sb.append(", elev: ").append(String.format("%.0f", elev));
            sb.append("\n");
        }
        return sb.toString().trim();
    }

    /**
     * Returns a snapshot of all active climate events for the {@code /lw events} command.
     */
    public List<ClimateEvent> getActiveClimateEventSnapshot() {
        return List.copyOf(activeClimateEvents);
    }

    /**
     * Stores the sampled average surface elevation for a region so that
     * {@link #applyWaterRunoff()} can model water flowing downhill between neighbours.
     * Called once per region by the platform layer when a player first enters it.
     */
    public void setRegionElevation(RegionCoordinate coord, double elevation) {
        if (coord != null) regionElevations.put(coord, elevation);
    }

    /**
     * Transfers water availability from higher-elevation regions to lower neighbours.
     * Simulates rainfall runoff: valleys collect water, hilltops drain. The transfer
     * only occurs while regional rain is falling (rain level > 0.1) so dry seasons
     * don't create permanent hydration differences from a single past rain event.
     */
    private void applyWaterRunoff() {
        Collection<Region> active = regionManager.getActiveRegions();
        if (active.size() < 2) return;

        Map<RegionCoordinate, Region> byCoord = new HashMap<>();
        for (Region r : active) byCoord.put(r.getCoordinate(), r);

        int[][] offsets = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
        for (Region region : active) {
            RegionCoordinate coord = region.getCoordinate();
            Double myElevation = regionElevations.get(coord);
            if (myElevation == null) continue;

            AtmosphereRegionData myAtm = region.getModuleData()
                    .get(AtmosphereModule.MODULE_ID, AtmosphereRegionData.class)
                    .orElse(null);
            if (myAtm == null || myAtm.getRainLevel() < 0.1) continue;

            WaterRegionData myWater = region.getModuleData()
                    .get(WaterModule.MODULE_ID, WaterRegionData.class)
                    .orElse(null);
            if (myWater == null) continue;

            for (int[] off : offsets) {
                RegionCoordinate nCoord = new RegionCoordinate(
                        coord.dimensionId(), coord.x() + off[0], coord.z() + off[1]);
                Region neighbour = byCoord.get(nCoord);
                if (neighbour == null) continue;
                Double nElevation = regionElevations.get(nCoord);
                if (nElevation == null) continue;

                double heightDiff = myElevation - nElevation;
                if (heightDiff <= 2.0) continue; // only meaningful slopes trigger runoff

                // Transfer rate: proportional to slope and rain, capped at a small value
                // so the simulation doesn't drain a region in one cycle.
                double runoff = Math.min(heightDiff / 200.0, 0.2) * myAtm.getRainLevel();
                myWater.setWaterAvailability(Math.max(0, myWater.getWaterAvailability() - runoff * 0.4));

                // Accumulate river flow intensity toward the downhill neighbour.
                // Over time this triggers RIVER_CARVE effects that slowly carve channels.
                if (runoff > 0.04 && heightDiff > 5.0) {
                    double flowContrib = runoff * heightDiff / 50.0;
                    riverFlowIntensity.merge(nCoord, flowContrib, Double::sum);
                }

                WaterRegionData nWater = neighbour.getModuleData()
                        .get(WaterModule.MODULE_ID, WaterRegionData.class)
                        .orElse(null);
                if (nWater == null) continue;
                nWater.setWaterAvailability(Math.min(100, nWater.getWaterAvailability() + runoff));
                nWater.setDroughtRisk(Math.max(0, nWater.getDroughtRisk() - runoff * 0.5));
                neighbour.getModuleData().put(WaterModule.MODULE_ID, nWater);

                // Runoff also carries ground pollution downstream — contaminants travel with water.
                PollutionRegionData srcPoll = region.getModuleData()
                        .get(PollutionModule.MODULE_ID, PollutionRegionData.class).orElse(null);
                PollutionRegionData dstPoll = neighbour.getModuleData()
                        .get(PollutionModule.MODULE_ID, PollutionRegionData.class).orElse(null);
                if (srcPoll != null && dstPoll != null && srcPoll.getGroundPollution() > 0.5) {
                    double pollTransfer = runoff * 0.15 * srcPoll.getGroundPollution();
                    srcPoll.addPollution(0, -pollTransfer, 0);
                    dstPoll.addPollution(0, pollTransfer, pollTransfer * 0.1); // small bleed to water
                    region.getModuleData().put(PollutionModule.MODULE_ID, srcPoll);
                    neighbour.getModuleData().put(PollutionModule.MODULE_ID, dstPoll);
                }

                regionManager.markDirty(neighbour);
            }
            region.getModuleData().put(WaterModule.MODULE_ID, myWater);
            regionManager.markDirty(region);
        }

        // Dispatch RIVER_CARVE effects for regions with high accumulated flow.
        if (worldEffectsModule != null) {
            for (var entry : riverFlowIntensity.entrySet()) {
                if (entry.getValue() >= RIVER_CARVE_THRESHOLD) {
                    worldEffectsModule.queueEffect(new WorldEffectRequest(
                            WorldEffectType.RIVER_CARVE, entry.getKey(),
                            Math.min(1.0, entry.getValue() / (RIVER_CARVE_THRESHOLD * 2))));
                    riverFlowIntensity.put(entry.getKey(),
                            entry.getValue() - RIVER_CARVE_THRESHOLD);
                }
            }
        }
    }

    /**
     * Raises the succession cap for regions where physical conditions have improved enough
     * to support more advanced ecosystems. The cap can only increase — regression is handled
     * separately by damage accumulation within {@link com.livingworld.modules.recovery.RecoveryModule}.
     *
     * <p>This is the mechanism that allows a desert (initial cap = SPARSE_GRASS) to eventually
     * support a MATURE_FOREST if the player (or natural runoff) sustains good water and soil
     * conditions for long enough.
     */
    private void applyDynamicCapUpdate() {
        for (Region region : regionManager.getActiveRegions()) {
            RecoveryRegionData recovery = region.getModuleData()
                    .get(RecoveryModule.MODULE_ID, RecoveryRegionData.class)
                    .orElse(null);
            if (recovery == null) continue;

            WaterRegionData water = region.getModuleData()
                    .get(WaterModule.MODULE_ID, WaterRegionData.class)
                    .orElse(null);
            SoilRegionData soil = region.getModuleData()
                    .get(SoilModule.MODULE_ID, SoilRegionData.class)
                    .orElse(null);
            if (water == null || soil == null) continue;

            SuccessionStage current  = recovery.getMaxSuccessionStage();
            SuccessionStage computed = computeDynamicCap(water, soil);

            // Cap can rise when conditions genuinely support a higher stage.
            if (computed.ordinal() > current.ordinal()) {
                recovery.setMaxSuccessionStage(computed);
                region.getModuleData().put(RecoveryModule.MODULE_ID, recovery);
                regionManager.markDirty(region);
                LivingWorldLogger.info(DiagnosticCategory.SIMULATION,
                        "Region " + region.getCoordinate() + " succession cap raised to " + computed);

            // Desertification permanence: prolonged damage under very bad conditions
            // lowers the cap by one stage so the region can no longer recover as high
            // without player intervention (re-watering, removing pollution, etc.).
            } else if (current.ordinal() > computed.ordinal() + 1
                    && current.hasPrev()
                    && recovery.getDamageAccumulation() > 65
                    && region.getMetrics().getEcosystemHealth() < 20) {
                SuccessionStage lowered = current.prev();
                recovery.setMaxSuccessionStage(lowered);
                region.getModuleData().put(RecoveryModule.MODULE_ID, recovery);
                regionManager.markDirty(region);
                LivingWorldLogger.info(DiagnosticCategory.SIMULATION,
                        "Region " + region.getCoordinate()
                        + " DESERTIFICATION: cap lowered to " + lowered);
                pendingEventMessages.add("[LW] Desertification: region ("
                        + region.getCoordinate().x() + "," + region.getCoordinate().z()
                        + ") degraded — cap now " + lowered.name());
            }
        }
    }

    private static SuccessionStage computeDynamicCap(WaterRegionData water, SoilRegionData soil) {
        // Net water score: availability minus half the drought penalty.
        double w = water.getWaterAvailability() - water.getDroughtRisk() * 0.5;
        // Net soil score: fertility minus half the contamination penalty.
        double s = soil.getFertility() - soil.getContamination() * 0.5;
        if (w >= 60 && s >= 60) return SuccessionStage.MATURE_FOREST;
        if (w >= 42 && s >= 42) return SuccessionStage.YOUNG_WOODLAND;
        if (w >= 28 && s >= 28) return SuccessionStage.GRASSLAND;
        if (w >= 15 || s >= 15) return SuccessionStage.SCRUBLAND;
        return SuccessionStage.SPARSE_GRASS;
    }

    /**
     * Spreads seeds from healthy regions (YOUNG_WOODLAND+) to their neighbours, allowing
     * degraded regions to recover faster — especially when flanked by multiple healthy regions
     * (corridor effect).
     *
     * <p>Seeds are blocked by pollution in the target region. When a target region receives
     * sufficient seeds it can have its succession cap temporarily pioneered one stage ahead of
     * what physical conditions would normally allow, representing natural recolonisation.
     */
    private void applySeedDispersal() {
        Collection<Region> active = regionManager.getActiveRegions();
        if (active.size() < 2) return;

        Map<RegionCoordinate, Region> byCoord = new HashMap<>();
        for (Region r : active) byCoord.put(r.getCoordinate(), r);

        // First pass: collect seed emissions from healthy source regions.
        Map<RegionCoordinate, Double> incomingSeeds = new HashMap<>();
        int[][] offsets = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};

        for (Region region : active) {
            RecoveryRegionData recovery = region.getModuleData()
                    .get(RecoveryModule.MODULE_ID, RecoveryRegionData.class).orElse(null);
            if (recovery == null) continue;
            if (recovery.getSuccessionStage().ordinal() < SuccessionStage.YOUNG_WOODLAND.ordinal()) continue;

            double vegPressure = region.getMetrics().getVegetationPressure();
            double emissionChance = vegPressure * ecosystemTuning.getSeedEmissionRate();
            if (windRandom.nextDouble() > emissionChance) continue; // stochastic emission

            double seedStrength = vegPressure * 0.1;
            RegionCoordinate coord = region.getCoordinate();
            for (int[] off : offsets) {
                RegionCoordinate nCoord = new RegionCoordinate(
                        coord.dimensionId(), coord.x() + off[0], coord.z() + off[1]);
                if (byCoord.containsKey(nCoord)) {
                    incomingSeeds.merge(nCoord, seedStrength, Double::sum);
                }
            }
        }

        if (incomingSeeds.isEmpty()) return;

        // Second pass: apply incoming seeds to target regions.
        for (var entry : incomingSeeds.entrySet()) {
            RegionCoordinate targetCoord = entry.getKey();
            Region target = byCoord.get(targetCoord);
            if (target == null) continue;

            // Pollution in the target region blocks seed germination.
            double pollution = target.getMetrics().getPollutionScore();
            double pollBlock = Math.min(1.0, pollution * ecosystemTuning.getSeedPollutionBlock());
            double effectiveSeeds = entry.getValue() * (1.0 - pollBlock);
            if (effectiveSeeds <= 0) continue;

            // Corridor boost: count how many neighbours are themselves healthy.
            int healthyNeighbours = 0;
            for (int[] off : offsets) {
                RegionCoordinate nCoord = new RegionCoordinate(
                        targetCoord.dimensionId(), targetCoord.x() + off[0], targetCoord.z() + off[1]);
                Region neighbour = byCoord.get(nCoord);
                if (neighbour == null) continue;
                RecoveryRegionData nRec = neighbour.getModuleData()
                        .get(RecoveryModule.MODULE_ID, RecoveryRegionData.class).orElse(null);
                if (nRec != null && nRec.getSuccessionStage().ordinal()
                        >= SuccessionStage.YOUNG_WOODLAND.ordinal()) {
                    healthyNeighbours++;
                }
            }
            if (healthyNeighbours >= 2) {
                effectiveSeeds *= ecosystemTuning.getCorridorBoostMultiplier();
            }

            accumulatedSeedRain.merge(targetCoord, effectiveSeeds, Double::sum);

            // Attempt to pioneer the succession cap one stage ahead of what conditions currently
            // support, representing natural recolonisation via seed rain.
            RecoveryRegionData targetRec = target.getModuleData()
                    .get(RecoveryModule.MODULE_ID, RecoveryRegionData.class).orElse(null);
            WaterRegionData water = target.getModuleData()
                    .get(WaterModule.MODULE_ID, WaterRegionData.class).orElse(null);
            SoilRegionData soil = target.getModuleData()
                    .get(SoilModule.MODULE_ID, SoilRegionData.class).orElse(null);
            if (targetRec == null || water == null || soil == null) continue;

            SuccessionStage conditionCap = computeDynamicCap(water, soil);
            // Seeds pioneer only one stage ahead and only if accumulated rain is significant.
            double totalRain = accumulatedSeedRain.getOrDefault(targetCoord, 0.0);
            if (conditionCap.hasNext() && totalRain >= 5.0) {
                SuccessionStage pioneered = conditionCap.next();
                if (pioneered.ordinal() > targetRec.getMaxSuccessionStage().ordinal()) {
                    targetRec.setMaxSuccessionStage(pioneered);
                    target.getModuleData().put(RecoveryModule.MODULE_ID, targetRec);
                    regionManager.markDirty(target);
                    LivingWorldLogger.info(DiagnosticCategory.SIMULATION,
                            "Region " + targetCoord + " seed-rain pioneered cap to " + pioneered
                            + (healthyNeighbours >= 2 ? " [corridor x" + String.format("%.1f",
                                    ecosystemTuning.getCorridorBoostMultiplier()) + "]" : ""));
                }
            }
        }
    }

    // ---------------------------------------------------------------
    // Trend tracking
    // ---------------------------------------------------------------

    private void recordHealthTrend() {
        for (Region region : regionManager.getActiveRegions()) {
            RegionCoordinate coord = region.getCoordinate();
            double health = region.getMetrics().getEcosystemHealth();
            double[] history = healthHistory.computeIfAbsent(coord, k -> new double[TREND_WINDOW]);
            int idx = historyIndex.getOrDefault(coord, 0);
            history[idx % TREND_WINDOW] = health;
            historyIndex.put(coord, idx + 1);
        }
    }

    public double getHealthTrend(RegionCoordinate coord) {
        double[] history = healthHistory.get(coord);
        int count = historyIndex.getOrDefault(coord, 0);
        if (history == null || count < TREND_WINDOW) return 0.0;
        // Compare the two most-recent values against the three oldest in the window
        double recent = (history[(count - 1) % TREND_WINDOW] + history[(count - 2) % TREND_WINDOW]) / 2.0;
        double older  = (history[(count - 3) % TREND_WINDOW]
                       + history[(count - 4) % TREND_WINDOW]
                       + history[(count - 5) % TREND_WINDOW]) / 3.0;
        return recent - older;
    }

    public String getTrendArrow(RegionCoordinate coord) {
        double trend = getHealthTrend(coord);
        if (trend >  2.0) return "↑";
        if (trend < -2.0) return "↓";
        return "→";
    }

    public double getSeedRainAt(RegionCoordinate coord) {
        return accumulatedSeedRain.getOrDefault(coord, 0.0);
    }

    /** Returns the sampled average surface elevation for the region, or null if not yet sampled. */
    public Double getRegionElevation(RegionCoordinate coord) {
        return coord != null ? regionElevations.get(coord) : null;
    }

    // ---------------------------------------------------------------
    // Climate events
    // ---------------------------------------------------------------

    /**
     * Checks for new multi-region climate events (drought, wildfire, flood) and
     * advances existing active events each {@link #CLIMATE_CHECK_INTERVAL} cycles.
     */
    private void applyClimateEvents() {
        Collection<Region> active = regionManager.getActiveRegions();
        if (active.size() < 2) return;

        Map<RegionCoordinate, Region> byCoord = new HashMap<>();
        for (Region r : active) byCoord.put(r.getCoordinate(), r);
        int[][] offsets = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
        long simTick = simulationManager.getSimulationTickCounter();

        // Advance existing events
        Iterator<ClimateEvent> iter = activeClimateEvents.iterator();
        while (iter.hasNext()) {
            ClimateEvent ev = iter.next();
            ev.incrementTick();
            advanceClimateEvent(ev, byCoord, offsets);
            if (ev.isResolved()) {
                pendingEventMessages.add("[LW] " + ev.getType().displayName()
                        + " resolved after " + ev.getTicksActive() + " cycles.");
                iter.remove();
            }
        }

        // Check if any active event already covers these regions (avoid duplicates)
        Set<RegionCoordinate> coveredByEvent = new HashSet<>();
        for (ClimateEvent ev : activeClimateEvents) coveredByEvent.addAll(ev.getAffectedRegions());

        // Try to trigger new events
        for (Region region : active) {
            if (coveredByEvent.contains(region.getCoordinate())) continue;
            RegionCoordinate coord = region.getCoordinate();
            WaterRegionData water = region.getModuleData().get(WaterModule.MODULE_ID, WaterRegionData.class).orElse(null);
            AtmosphereRegionData atm = region.getModuleData().get(AtmosphereModule.MODULE_ID, AtmosphereRegionData.class).orElse(null);
            RecoveryRegionData recovery = region.getModuleData().get(RecoveryModule.MODULE_ID, RecoveryRegionData.class).orElse(null);
            PollutionRegionData poll = region.getModuleData().get(PollutionModule.MODULE_ID, PollutionRegionData.class).orElse(null);
            if (water == null || atm == null || recovery == null) continue;

            // --- DROUGHT trigger ---
            if (water.getWaterAvailability() < 20 && water.getDroughtRisk() > 65) {
                int droughtNeighbours = 0;
                for (int[] off : offsets) {
                    Region nb = byCoord.get(new RegionCoordinate(coord.dimensionId(), coord.x() + off[0], coord.z() + off[1]));
                    if (nb == null) continue;
                    WaterRegionData nbW = nb.getModuleData().get(WaterModule.MODULE_ID, WaterRegionData.class).orElse(null);
                    if (nbW != null && nbW.getWaterAvailability() < 25 && nbW.getDroughtRisk() > 55) droughtNeighbours++;
                }
                if (droughtNeighbours >= 2) {
                    double severity = (65.0 - water.getWaterAvailability()) / 65.0;
                    ClimateEvent drought = new ClimateEvent(ClimateEventType.DROUGHT, coord, simTick, severity);
                    activeClimateEvents.add(drought);
                    coveredByEvent.add(coord);
                    pendingEventMessages.add("[LW] ⚠ Drought spreading! Epicentre region ("
                            + coord.x() + "," + coord.z() + ") — " + droughtNeighbours + " dry neighbours.");
                }
            }

            // --- WILDFIRE trigger ---
            if (recovery.getSuccessionStage().ordinal() >= SuccessionStage.YOUNG_WOODLAND.ordinal()
                    && water.getDroughtRisk() > 65
                    && atm.getRainLevel() < 0.1
                    && poll != null && poll.getAirPollution() > 25
                    && windRandom.nextDouble() < 0.12) {
                double severity = Math.min(1.0, (water.getDroughtRisk() - 65) / 35.0 + 0.2);
                ClimateEvent fire = new ClimateEvent(ClimateEventType.WILDFIRE, coord, simTick, severity);
                activeClimateEvents.add(fire);
                coveredByEvent.add(coord);
                pendingEventMessages.add("[LW] 🔥 Wildfire ignited at region ("
                        + coord.x() + "," + coord.z() + ")! Drought risk: "
                        + String.format("%.0f%%", water.getDroughtRisk()));
            }

            // --- FLOOD trigger ---
            if (water.getWaterAvailability() > 92 && atm.getRainLevel() > 0.72) {
                boolean hasLowNeighbour = false;
                for (int[] off : offsets) {
                    RegionCoordinate nCoord = new RegionCoordinate(coord.dimensionId(), coord.x() + off[0], coord.z() + off[1]);
                    Double myElev = regionElevations.get(coord);
                    Double nElev  = regionElevations.get(nCoord);
                    if (myElev != null && nElev != null && myElev > nElev + 3) { hasLowNeighbour = true; break; }
                }
                if (hasLowNeighbour) {
                    ClimateEvent flood = new ClimateEvent(ClimateEventType.FLOOD, coord, simTick, 0.6);
                    activeClimateEvents.add(flood);
                    coveredByEvent.add(coord);
                    pendingEventMessages.add("[LW] 🌊 Flood event! Region ("
                            + coord.x() + "," + coord.z() + ") overflowing into lowlands.");
                }
            }
        }
    }

    private void advanceClimateEvent(ClimateEvent ev, Map<RegionCoordinate, Region> byCoord, int[][] offsets) {
        boolean shouldResolve = false;

        for (RegionCoordinate coord : new HashSet<>(ev.getAffectedRegions())) {
            Region region = byCoord.get(coord);
            if (region == null) continue;

            switch (ev.getType()) {
                case DROUGHT -> {
                    WaterRegionData water = region.getModuleData().get(WaterModule.MODULE_ID, WaterRegionData.class).orElse(null);
                    if (water == null) break;
                    water.setDroughtRisk(Math.min(100, water.getDroughtRisk() + 0.02));
                    water.setWaterAvailability(Math.max(0, water.getWaterAvailability() - 0.03));
                    region.getModuleData().put(WaterModule.MODULE_ID, water);
                    regionManager.markDirty(region);
                    // Resolution: epicentre water availability recovers
                    if (coord.equals(ev.getEpicenter()) && water.getWaterAvailability() > 45) shouldResolve = true;
                    // Spread to dry neighbours
                    if (ev.getTicksActive() % 5 == 0) {
                        for (int[] off : offsets) {
                            RegionCoordinate nCoord = new RegionCoordinate(coord.dimensionId(), coord.x() + off[0], coord.z() + off[1]);
                            Region nb = byCoord.get(nCoord);
                            if (nb == null) continue;
                            WaterRegionData nbW = nb.getModuleData().get(WaterModule.MODULE_ID, WaterRegionData.class).orElse(null);
                            if (nbW != null && nbW.getWaterAvailability() < 35) ev.addAffectedRegion(nCoord);
                        }
                    }
                }
                case WILDFIRE -> {
                    WaterRegionData water = region.getModuleData().get(WaterModule.MODULE_ID, WaterRegionData.class).orElse(null);
                    AtmosphereRegionData atm = region.getModuleData().get(AtmosphereModule.MODULE_ID, AtmosphereRegionData.class).orElse(null);
                    PollutionRegionData poll = region.getModuleData().get(PollutionModule.MODULE_ID, PollutionRegionData.class).orElse(null);
                    if (poll != null) {
                        poll.addPollution(0.5 * ev.getSeverity(), 0.2 * ev.getSeverity(), 0);
                        region.getModuleData().put(PollutionModule.MODULE_ID, poll);
                    }
                    if (worldEffectsModule != null) {
                        worldEffectsModule.queueEffect(new WorldEffectRequest(WorldEffectType.VEGETATION_DIES, coord, ev.getSeverity()));
                        if (windRandom.nextDouble() < 0.3) {
                            worldEffectsModule.queueEffect(new WorldEffectRequest(WorldEffectType.WILDFIRE, coord, ev.getSeverity()));
                        }
                    }
                    regionManager.markDirty(region);
                    // Resolution: rain extinguishes fire
                    if (coord.equals(ev.getEpicenter()) && atm != null && atm.getRainLevel() > 0.4) shouldResolve = true;
                    // Spread to adjacent forest regions
                    if (ev.getTicksActive() % 3 == 0) {
                        for (int[] off : offsets) {
                            RegionCoordinate nCoord = new RegionCoordinate(coord.dimensionId(), coord.x() + off[0], coord.z() + off[1]);
                            Region nb = byCoord.get(nCoord);
                            if (nb == null) continue;
                            RecoveryRegionData nRec = nb.getModuleData().get(RecoveryModule.MODULE_ID, RecoveryRegionData.class).orElse(null);
                            WaterRegionData nW = nb.getModuleData().get(WaterModule.MODULE_ID, WaterRegionData.class).orElse(null);
                            if (nRec != null && nW != null
                                    && nRec.getSuccessionStage().ordinal() >= SuccessionStage.SCRUBLAND.ordinal()
                                    && nW.getDroughtRisk() > 50) {
                                ev.addAffectedRegion(nCoord);
                            }
                        }
                    }
                }
                case FLOOD -> {
                    SoilRegionData soil = region.getModuleData().get(SoilModule.MODULE_ID, SoilRegionData.class).orElse(null);
                    WaterRegionData water = region.getModuleData().get(WaterModule.MODULE_ID, WaterRegionData.class).orElse(null);
                    AtmosphereRegionData atm = region.getModuleData().get(AtmosphereModule.MODULE_ID, AtmosphereRegionData.class).orElse(null);
                    if (soil != null) {
                        soil.setContamination(Math.min(100, soil.getContamination() + 0.05));
                        region.getModuleData().put(SoilModule.MODULE_ID, soil);
                    }
                    if (worldEffectsModule != null) {
                        worldEffectsModule.queueEffect(new WorldEffectRequest(WorldEffectType.WATER_POOL_FORMS, coord, 0.8));
                    }
                    regionManager.markDirty(region);
                    if (coord.equals(ev.getEpicenter()) && water != null && atm != null
                            && (water.getWaterAvailability() < 70 || atm.getRainLevel() < 0.25)) {
                        shouldResolve = true;
                    }
                }
            }
        }

        if (shouldResolve) ev.resolve();

        // Safety timeout: resolve after 200 ticks regardless
        if (ev.getTicksActive() > 200) ev.resolve();
    }

    /**
     * Drains and returns any pending climate event notification messages.
     * Called by the platform layer to broadcast these to all players.
     */
    public List<String> pollClimateEventMessages() {
        if (pendingEventMessages.isEmpty()) return List.of();
        List<String> copy = new ArrayList<>(pendingEventMessages);
        pendingEventMessages.clear();
        return copy;
    }

    // ---------------------------------------------------------------
    // Tidal effect + biome initialisation
    // ---------------------------------------------------------------

    /**
     * Applies a tidal water-availability delta to a coastal region.
     * Positive delta = rising tide (more water), negative = falling tide.
     */
    public void applyTidalEffect(RegionCoordinate coord, double waterDelta) {
        if (!serverReady || coord == null) return;
        regionManager.resolve(coord).ifPresent(region -> {
            WaterRegionData water = region.getModuleData()
                    .get(WaterModule.MODULE_ID, WaterRegionData.class).orElse(null);
            if (water == null) return;
            water.setWaterAvailability(Math.min(100, Math.max(0,
                    water.getWaterAvailability() + waterDelta)));
            if (waterDelta > 0) {
                water.setDroughtRisk(Math.max(0, water.getDroughtRisk() - waterDelta * 0.4));
            }
            region.getModuleData().put(WaterModule.MODULE_ID, water);
            regionManager.markDirty(region);
        });
    }

    /**
     * Initialises soil and water values for a newly loaded region based on biome
     * temperature and downfall, giving each biome-type region a realistic starting state.
     */
    public void initializeRegionBiomeValues(RegionCoordinate coord, float temp, float downfall) {
        if (!serverReady || coord == null) return;
        regionManager.resolve(coord).ifPresent(region -> {
            SoilRegionData soil = region.getModuleData()
                    .get(SoilModule.MODULE_ID, SoilRegionData.class).orElse(null);
            WaterRegionData water = region.getModuleData()
                    .get(WaterModule.MODULE_ID, WaterRegionData.class).orElse(null);
            if (soil == null || water == null) return;

            // downfall: 0=arid, 1=wet  |  temp: 0.15=cold, 2.0=desert hot
            double wetness = Math.min(1.0, Math.max(0.0, downfall));
            double heat    = Math.min(1.0, Math.max(0.0, (temp - 0.15) / 1.85));

            double soilFertility  = clampD(25 + wetness * 55 - heat * 10);
            double soilMoisture   = clampD(20 + wetness * 60 - heat * 15);
            double droughtRisk    = clampD(70 - wetness * 55 + heat * 20);
            double waterAvail     = clampD(20 + wetness * 60 - heat * 15);

            soil.setFertility(soilFertility);
            soil.setMoisture(soilMoisture);
            water.setWaterAvailability(waterAvail);
            water.setDroughtRisk(droughtRisk);

            region.getModuleData().put(SoilModule.MODULE_ID, soil);
            region.getModuleData().put(WaterModule.MODULE_ID, water);
            regionManager.markDirty(region);
        });
    }

    private static double clampD(double v) { return Math.min(100, Math.max(0, v)); }

    /** Returns a formatted wind status string for the {@code /lw wind} command. */
    public String getWindInfo() {
        // Map angle to 8-point compass (N=0°, E=90°, S=180°, W=270°).
        String[] compass = {"N", "NE", "E", "SE", "S", "SW", "W", "NW"};
        double normalised = ((windAngle % (2 * Math.PI)) + 2 * Math.PI) % (2 * Math.PI);
        int idx = (int) Math.round(normalised / (Math.PI / 4)) % 8;
        return String.format("[LW] Wind: %s (%.0f°) | Spread rate: %.3f | Speed: %dx",
                compass[idx], Math.toDegrees(normalised),
                ecosystemTuning.getPollutionSpreadRate(), simSpeedMultiplier);
    }

    /** Returns the succession stage for the region at the given world position, if loaded. */
    public Optional<SuccessionStage> getSuccessionStageAt(String dimId, double x, double z) {
        if (!serverReady) return Optional.empty();
        RegionCoordinate coord = RegionCoordinate.fromBlock(
                dimId, (int) x, (int) z, LivingWorldConstants.DEFAULT_REGION_SIZE_CHUNKS);
        return regionManager.resolve(coord)
                .flatMap(r -> r.getModuleData().get(RecoveryModule.MODULE_ID, RecoveryRegionData.class))
                .map(RecoveryRegionData::getSuccessionStage);
    }

    /** Returns the current atmospheric state for a region, if it has been initialised. */
    public Optional<AtmosphereRegionData> getRegionalWeather(RegionCoordinate coord) {
        if (!serverReady || coord == null) return Optional.empty();
        return regionManager.resolve(coord)
                .flatMap(r -> r.getModuleData().get(AtmosphereModule.MODULE_ID, AtmosphereRegionData.class));
    }

    /** Applies a water quality and purification bonus to regions adjacent to or containing water bodies. */
    public void applyWaterBodyBoost(RegionCoordinate coord) {
        if (!serverReady || coord == null) return;
        regionManager.resolve(coord).ifPresent(region -> {
            WaterRegionData water = region.getModuleData()
                    .get(WaterModule.MODULE_ID, WaterRegionData.class)
                    .orElse(null);
            if (water == null) return;
            water.setPurificationCapacity(Math.min(100, water.getPurificationCapacity() + 15.0));
            water.setWaterAvailability(Math.min(100, water.getWaterAvailability() + 10.0));
            water.setDroughtRisk(Math.max(0, water.getDroughtRisk() - 10.0));
            region.getModuleData().put(WaterModule.MODULE_ID, water);
            regionManager.markDirty(region);
        });
    }

    /** Returns the global climate tracker (read-only access for tests and commands). */
    public GlobalClimateTracker getClimateTracker() { return climateTracker; }

    /** Returns a formatted global climate status string for /lw climate. */
    public String getClimateStatusFor(CommandSourceStack css) {
        return String.format(
                "Carbon: %.0f ppm (+%.0f ppm) | Warming: +%.2f°C | Biodiversity: %.0f%% | Rain penalty: -%.0f%%",
                climateTracker.getCarbonPpm(),
                climateTracker.getCarbonPpm() - 280.0,
                climateTracker.getTemperatureCelsius(),
                climateTracker.getBiodiversityIndex(),
                climateTracker.getRainPenalty() * 100);
    }

    /** Returns a formatted atmosphere status string for the region at the command source's position. */
    public String getAtmosphereStatusFor(CommandSourceStack css) {
        if (!serverReady) return "Server not ready.";
        String dimId = css.getLevel().dimension().location().toString();
        var pos = css.getPosition();
        RegionCoordinate coord = RegionCoordinate.fromBlock(
                dimId, (int) pos.x, (int) pos.z, LivingWorldConstants.DEFAULT_REGION_SIZE_CHUNKS);
        Season season = getCurrentSeason();
        Optional<Region> regionOpt = regionManager.resolve(coord);
        String successionLine = regionOpt.flatMap(r -> r.getModuleData()
                .get(RecoveryModule.MODULE_ID, RecoveryRegionData.class))
                .map(rec -> String.format(" | Stage: %s (cap: %s)",
                        rec.getSuccessionStage().name(), rec.getMaxSuccessionStage().name()))
                .orElse("");
        Double elev = regionElevations.get(coord);
        String elevLine = elev != null ? String.format(" | Elev: %.0f", elev) : "";
        String trend    = getTrendArrow(coord);
        double seedRain = getSeedRainAt(coord);
        String seedLine = seedRain > 0.1 ? String.format(" | Seeds: %.1f", seedRain) : "";
        String trendLine = String.format(" | Trend: %s (%.1f)", trend, getHealthTrend(coord));
        return regionOpt.flatMap(r -> r.getModuleData().get(AtmosphereModule.MODULE_ID, AtmosphereRegionData.class))
                .map(atm -> String.format(
                        "Region (%d,%d) | Season: %s | Rain: %.0f%% | Storm: %.0f%%%s%s%s%s",
                        coord.x(), coord.z(), season.displayName(),
                        atm.getRainLevel() * 100, atm.getThunderLevel() * 100,
                        successionLine, elevLine, trendLine, seedLine))
                .orElse(String.format("Region (%d,%d) — atmosphere not yet computed. Season: %s%s",
                        coord.x(), coord.z(), season.displayName(), trendLine));
    }

    /** Toggles the debug HUD for the given player. Returns true if HUD is now enabled. */
    public boolean toggleHud(UUID playerId) {
        if (hudEnabledPlayers.remove(playerId)) return false;
        hudEnabledPlayers.add(playerId);
        return true;
    }

    public boolean isHudEnabled(UUID playerId) {
        return hudEnabledPlayers.contains(playerId);
    }

    public void registerCommands(CommandDispatcher<CommandSourceStack> dispatcher) {
        requireInitialized();
        LivingWorldCommandRoot.registerDeferred(
                dispatcher,
                () -> requireService(regionManager, "regionManager"),
                () -> requireService(moduleRegistry, "moduleRegistry"),
                () -> requireService(simulationManager, "simulationManager"),
                this::toggleHud,
                this::getAtmosphereStatusFor,
                this::getClimateStatusFor,
                this::setSimSpeedMultiplier,
                this::getWindInfo,
                css -> getEventsStatusFor(),
                this::getVolcanoStatusString);
    }

    /** Returns a formatted string of all active climate events for {@code /lw events}. */
    public String getEventsStatusFor() {
        List<ClimateEvent> events = getActiveClimateEventSnapshot();
        if (events.isEmpty()) return "[LW] No active climate events.";
        StringBuilder sb = new StringBuilder("[LW] Active Climate Events: ")
                .append(events.size()).append("\n");
        for (ClimateEvent ev : events) {
            sb.append("  ").append(ev.getType().displayName())
              .append(" at (").append(ev.getEpicenter().x()).append(",").append(ev.getEpicenter().z()).append(")")
              .append(" | ticks: ").append(ev.getTicksActive())
              .append(" | severity: ").append(String.format("%.0f%%", ev.getSeverity() * 100))
              .append(" | regions: ").append(ev.getAffectedRegions().size())
              .append("\n");
        }
        return sb.toString().trim();
    }

    public Path getWorldSaveDirectory() {
        return worldSaveDirectory;
    }

    public boolean isServerReady() {
        return serverReady;
    }

    public ServiceRegistry getServices() {
        requireServerReady();
        return services;
    }

    private void createServerServices(Path saveDirectory) {
        DefaultConfigService configService = new DefaultConfigService();
        SimulationConfig config = configService.getSimulationConfig();
        FileRegionPersistenceService persistenceService =
                new FileRegionPersistenceService(
                        saveDirectory.resolve("living_world"),
                        LivingWorldConstants.MOD_VERSION);
        registerModuleCodecs(persistenceService);
        RegionCache regionCache = new RegionCache();
        RegionQueryEngine queryEngine = new RegionQueryEngine(regionCache);
        regionManager = new RegionManager(
                new RegionFactory(),
                new RegionStorage(persistenceService),
                regionCache,
                queryEngine,
                new RegionLifecycleController(),
                config);
        moduleRegistry = new ModuleRegistry();
        registerEcosystemModules(moduleRegistry);
        LivingWorldEventBus eventBus = new LivingWorldEventBus();
        DefaultTimeService timeService = new DefaultTimeService();
        SimulationScheduler scheduler = new SimulationScheduler(config);
        SimulationProfiler profiler = new SimulationProfiler();
        simulationManager = new SimulationManager(
                scheduler,
                regionManager,
                moduleRegistry,
                eventBus,
                timeService,
                persistenceService,
                profiler);

        services = new ServiceRegistry();
        services.register(CoreServices.CONFIG, configService);
        services.register(CoreServices.TUNING, ecosystemTuning);
        services.register(CoreServices.REGIONS, regionManager);
        services.register(CoreServices.SIMULATION, simulationManager);
        services.register(CoreServices.PERSISTENCE, persistenceService);
        services.register(CoreServices.EVENTS, eventBus);
        services.register(CoreServices.MODULES, moduleRegistry);
        services.register(CoreServices.TIME, timeService);
        services.register(CoreServices.DEBUG, profiler);
        services.lock();

        moduleRegistry.initializeAll(new ModuleContext(services));
    }

    // ------------------------------------------------------------------
    // Block categorisation helpers (registry-name based, no Minecraft imports)
    // ------------------------------------------------------------------

    private static boolean isLog(String block) {
        return block.contains("_log") || block.contains("_stem");
    }

    private static boolean isWood(String block) {
        return block.contains("_wood") || block.contains("_hyphae");
    }

    private static boolean isLeaves(String block) {
        return block.contains("_leaves");
    }

    private static boolean isOre(String block) {
        return block.contains("_ore");
    }

    private static boolean isStone(String block) {
        return block.equals("minecraft:stone")
                || block.equals("minecraft:deepslate")
                || block.equals("minecraft:cobblestone")
                || block.equals("minecraft:gravel")
                || block.equals("minecraft:sand");
    }

    private static boolean isCrop(String block) {
        return block.contains("wheat") || block.contains("carrot")
                || block.contains("potato") || block.contains("beetroot")
                || block.contains("sugar_cane") || block.contains("pumpkin")
                || block.contains("melon");
    }

    private static boolean isFarmland(String block) {
        return block.contains("farmland");
    }

    /**
     * Returns the {@link WorldEffectsModule} so the platform adapter can register a
     * block-change consumer after bootstrap.
     *
     * <p>Call only after {@link #onServerStarting(Path)} has returned.</p>
     */
    public WorldEffectsModule getWorldEffectsModule() {
        requireServerReady();
        return worldEffectsModule;
    }

    /**
     * Registers per-region data codecs for all 7 data-bearing ecosystem modules.
     *
     * <p>WorldEffects has no persistent per-region data and is intentionally omitted.</p>
     */
    private static void registerModuleCodecs(FileRegionPersistenceService service) {
        service.registerModuleCodec(
                PollutionModule.MODULE_ID,
                (data, w) -> {
                    PollutionRegionData d = data.get(PollutionModule.MODULE_ID, PollutionRegionData.class)
                            .orElseGet(PollutionRegionData::defaults);
                    w.writeDouble("airPollution",    d.getAirPollution());
                    w.writeDouble("groundPollution", d.getGroundPollution());
                    w.writeDouble("waterPollution",  d.getWaterPollution());
                    w.writeDouble("decayResistance", d.getDecayResistance());
                },
                (r, data) -> data.put(PollutionModule.MODULE_ID, new PollutionRegionData(
                        r.readDouble("airPollution",    0.0),
                        r.readDouble("groundPollution", 0.0),
                        r.readDouble("waterPollution",  0.0),
                        r.readDouble("decayResistance", 20.0))));

        service.registerModuleCodec(
                SoilModule.MODULE_ID,
                (data, w) -> {
                    SoilRegionData d = data.get(SoilModule.MODULE_ID, SoilRegionData.class)
                            .orElseGet(SoilRegionData::defaults);
                    w.writeDouble("fertility",     d.getFertility());
                    w.writeDouble("moisture",      d.getMoisture());
                    w.writeDouble("contamination", d.getContamination());
                    w.writeDouble("compaction",    d.getCompaction());
                    w.writeDouble("erosion",       d.getErosion());
                },
                (r, data) -> data.put(SoilModule.MODULE_ID, new SoilRegionData(
                        r.readDouble("fertility",     60.0),
                        r.readDouble("moisture",      50.0),
                        r.readDouble("contamination",  0.0),
                        r.readDouble("compaction",    10.0),
                        r.readDouble("erosion",        0.0))));

        service.registerModuleCodec(
                WaterModule.MODULE_ID,
                (data, w) -> {
                    WaterRegionData d = data.get(WaterModule.MODULE_ID, WaterRegionData.class)
                            .orElseGet(WaterRegionData::defaults);
                    w.writeDouble("waterAvailability",   d.getWaterAvailability());
                    w.writeDouble("purificationCapacity", d.getPurificationCapacity());
                    w.writeDouble("droughtRisk",          d.getDroughtRisk());
                    w.writeDouble("floodRisk",            d.getFloodRisk());
                },
                (r, data) -> data.put(WaterModule.MODULE_ID, new WaterRegionData(
                        r.readDouble("waterAvailability",   60.0),
                        r.readDouble("purificationCapacity", 50.0),
                        r.readDouble("droughtRisk",         10.0),
                        r.readDouble("floodRisk",           10.0))));

        service.registerModuleCodec(
                VegetationModule.MODULE_ID,
                (data, w) -> {
                    VegetationRegionData d = data.get(VegetationModule.MODULE_ID, VegetationRegionData.class)
                            .orElseGet(VegetationRegionData::defaults);
                    w.writeDouble("grassPressure",  d.getGrassPressure());
                    w.writeDouble("flowerPressure", d.getFlowerPressure());
                    w.writeDouble("shrubPressure",  d.getShrubPressure());
                    w.writeDouble("treePressure",   d.getTreePressure());
                    w.writeDouble("deadVegetation", d.getDeadVegetation());
                },
                (r, data) -> data.put(VegetationModule.MODULE_ID, new VegetationRegionData(
                        r.readDouble("grassPressure",  50.0),
                        r.readDouble("flowerPressure", 30.0),
                        r.readDouble("shrubPressure",  30.0),
                        r.readDouble("treePressure",   40.0),
                        r.readDouble("deadVegetation",  5.0))));

        service.registerModuleCodec(
                ResourceDepletionModule.MODULE_ID,
                (data, w) -> {
                    ResourceRegionData d = data.get(ResourceDepletionModule.MODULE_ID, ResourceRegionData.class)
                            .orElseGet(ResourceRegionData::defaults);
                    w.writeDouble("miningDepletion",  d.getMiningDepletion());
                    w.writeDouble("loggingDepletion", d.getLoggingDepletion());
                    w.writeDouble("farmingDepletion", d.getFarmingDepletion());
                },
                (r, data) -> data.put(ResourceDepletionModule.MODULE_ID, new ResourceRegionData(
                        r.readDouble("miningDepletion",  0.0),
                        r.readDouble("loggingDepletion", 0.0),
                        r.readDouble("farmingDepletion", 0.0))));

        service.registerModuleCodec(
                RecoveryModule.MODULE_ID,
                (data, w) -> {
                    RecoveryRegionData d = data.get(RecoveryModule.MODULE_ID, RecoveryRegionData.class)
                            .orElseGet(RecoveryRegionData::defaults);
                    w.writeString("successionStage",    d.getSuccessionStage().name());
                    w.writeDouble("recoveryProgress",   d.getRecoveryProgress());
                    w.writeDouble("damageAccumulation", d.getDamageAccumulation());
                    w.writeString("maxSuccessionStage", d.getMaxSuccessionStage().name());
                },
                (r, data) -> data.put(RecoveryModule.MODULE_ID, new RecoveryRegionData(
                        SuccessionStage.valueOf(r.readString("successionStage", SuccessionStage.GRASSLAND.name())),
                        r.readDouble("recoveryProgress",   0.0),
                        r.readDouble("damageAccumulation", 0.0),
                        SuccessionStage.valueOf(r.readString("maxSuccessionStage", SuccessionStage.MATURE_FOREST.name())))));

        service.registerModuleCodec(
                EcosystemModule.MODULE_ID,
                (data, w) -> {
                    EcosystemRegionData d = data.get(EcosystemModule.MODULE_ID, EcosystemRegionData.class)
                            .orElseGet(EcosystemRegionData::defaults);
                    w.writeDouble("ecosystemHealth", d.getEcosystemHealth());
                    w.writeDouble("stress",          d.getStress());
                    w.writeDouble("resilience",      d.getResilience());
                    w.writeDouble("recoveryRate",    d.getRecoveryRate());
                },
                (r, data) -> data.put(EcosystemModule.MODULE_ID, new EcosystemRegionData(
                        r.readDouble("ecosystemHealth", 60.0),
                        r.readDouble("stress",          20.0),
                        r.readDouble("resilience",      50.0),
                        r.readDouble("recoveryRate",     5.0))));

        service.registerModuleCodec(
                AtmosphereModule.MODULE_ID,
                (data, w) -> {
                    AtmosphereRegionData d = data.get(AtmosphereModule.MODULE_ID, AtmosphereRegionData.class)
                            .orElseGet(AtmosphereRegionData::defaults);
                    w.writeDouble("rainLevel",    d.getRainLevel());
                    w.writeDouble("thunderLevel", d.getThunderLevel());
                },
                (r, data) -> data.put(AtmosphereModule.MODULE_ID, new AtmosphereRegionData(
                        r.readDouble("rainLevel",    0.4),
                        r.readDouble("thunderLevel", 0.0))));
    }

    /**
     * Registers all ecosystem simulation modules with the registry in pipeline order.
     *
     * <p>Order matters: each module may read metrics written by earlier modules in the
     * same tick. The declared order here is the execution order inside
     * {@link SimulationManager}.</p>
     */
    private void registerEcosystemModules(ModuleRegistry registry) {
        registry.register(new PollutionModule());
        registry.register(new SoilModule());
        registry.register(new WaterModule());
        registry.register(new VegetationModule());
        registry.register(new ResourceDepletionModule());
        registry.register(new RecoveryModule());
        registry.register(new EcosystemModule());
        worldEffectsModule = new WorldEffectsModule();
        registry.register(worldEffectsModule);
        atmosphereModule = new AtmosphereModule(
                () -> overworldRaining.getAsBoolean(),
                this::getCurrentSeason,
                climateTracker::getRainPenalty);
        registry.register(atmosphereModule);
        LivingWorldLogger.info(
                DiagnosticCategory.BOOTSTRAP,
                "Registered 9 ecosystem modules (pollution → soil → water → vegetation"
                        + " → resources → recovery → ecosystem → worldeffects → atmosphere).");
    }

    private void requireInitialized() {
        if (!initialized) {
            throw new IllegalStateException("bootstrap has not been initialized");
        }
    }

    private void requireServerReady() {
        requireInitialized();
        if (!serverReady) {
            throw new IllegalStateException("server services are not ready");
        }
    }

    private static <T> T requireService(T service, String name) {
        if (service == null) {
            throw new IllegalStateException(name + " is not ready");
        }
        return service;
    }
}
