package com.livingworld;

import net.neoforged.fml.ModContainer;
import net.neoforged.fml.common.Mod;
import net.neoforged.fml.config.ModConfig;
import net.neoforged.fml.event.lifecycle.FMLCommonSetupEvent;
import net.neoforged.bus.api.IEventBus;
import net.neoforged.neoforge.common.NeoForge;
import net.neoforged.neoforge.event.entity.living.FinalizeSpawnEvent;
import net.neoforged.neoforge.event.entity.player.PlayerInteractEvent;
import net.neoforged.neoforge.event.level.BlockEvent;
import net.neoforged.neoforge.event.server.ServerStartedEvent;
import net.neoforged.neoforge.event.server.ServerStartingEvent;
import net.neoforged.neoforge.event.server.ServerStoppingEvent;
import net.minecraft.ChatFormatting;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Holder;
import net.minecraft.core.registries.Registries;
import net.minecraft.network.chat.Component;
import net.minecraft.resources.ResourceKey;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.tags.BiomeTags;
import net.minecraft.tags.BlockTags;
import net.minecraft.world.entity.animal.Animal;
import net.minecraft.world.entity.monster.Monster;
import net.minecraft.world.item.Items;
import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.block.entity.AbstractFurnaceBlockEntity;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.entity.CampfireBlockEntity;
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
import net.minecraft.world.level.chunk.LevelChunk;
import net.minecraft.world.level.levelgen.Heightmap;
import net.minecraft.world.level.storage.LevelResource;
import net.minecraft.core.Holder;
import net.minecraft.core.particles.ParticleTypes;
import net.minecraft.network.protocol.game.ClientboundGameEventPacket;
import net.minecraft.network.protocol.game.ClientboundSoundPacket;
import net.minecraft.sounds.SoundEvent;
import net.minecraft.sounds.SoundEvents;
import net.minecraft.sounds.SoundSource;
import net.minecraft.tags.FluidTags;
import net.minecraft.world.effect.MobEffectInstance;
import net.minecraft.world.effect.MobEffects;
import net.neoforged.neoforge.event.tick.ServerTickEvent;

import com.livingworld.bootstrap.LivingWorldBootstrap;
import com.livingworld.modules.atmosphere.AtmosphereRegionData;
import com.livingworld.core.LivingWorldConstants;
import com.livingworld.debug.DiagnosticCategory;
import com.livingworld.debug.LivingWorldLogger;
import com.livingworld.modules.recovery.SuccessionStage;
import com.livingworld.platform.neoforge.NeoForgeModConfig;
import com.livingworld.platform.neoforge.NeoForgePlatformAdapter;
import com.livingworld.platform.neoforge.NeoForgeWorldEffectExecutor;
import com.livingworld.regions.Region;
import com.livingworld.regions.RegionCoordinate;
import com.livingworld.regions.RegionMetrics;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Optional;
import java.util.Random;
import java.util.Set;
import java.util.UUID;

/**
 * Mod entrypoint for Living World.
 * <p>
 * This class contains only mod loader wiring. All simulation logic is
 * delegated to the bootstrap system and core services.
 */
@Mod(LivingWorldConstants.MOD_ID)
public class LivingWorldMod {

    public static final String MOD_ID = LivingWorldConstants.MOD_ID;

    private static final int    FURNACE_SCAN_INTERVAL          = 50;
    private static final double AIR_POLLUTION_PER_FURNACE     = 0.5;
    private static final double GROUND_POLLUTION_PER_FURNACE  = 0.1;
    private static final double WATER_POLLUTION_PER_FURNACE   = 0.1;
    private static final double AIR_POLLUTION_PER_CAMPFIRE    = 0.2;
    private static final double GROUND_POLLUTION_PER_CAMPFIRE = 0.05;

    private static final int PLAYER_CHECK_INTERVAL = 20;
    /** Re-scan water bodies every ~10 min so player-built pools are detected promptly. */
    private static final int WATER_BODY_RESCAN_INTERVAL = 600; // × PLAYER_CHECK_INTERVAL ticks

    /** Passive mobs suppressed in regions with ecosystem health below this. */
    private static final double PASSIVE_SUPPRESS_HEALTH = 30.0;
    /** Hostile mobs suppressed in regions with ecosystem health above this. */
    private static final double HOSTILE_SUPPRESS_HEALTH = 60.0;

    private static final int TIDE_CHECK_INTERVAL = 1200; // ~1 real minute
    /** Apply river current forces to players every N ticks. */
    private static final int RIVER_CURRENT_INTERVAL = 4;
    /** Additional horizontal push (m/tick²) per unit of normalised flow speed. */
    private static final double CURRENT_STRENGTH = 0.045;

    private final LivingWorldBootstrap bootstrap;
    private final Random random = new Random();
    private MinecraftServer minecraftServer;
    private int furnaceScanTick = 0;
    private int playerCheckTick = 0;
    private int tideTick = 0;
    private double lastTideLevel = 0.0;
    private int riverCurrentTick = 0;
    private final Map<UUID, RegionCoordinate> playerRegionCache = new HashMap<>();
    private final Map<UUID, Boolean> playerRainState = new HashMap<>();
    private final Set<RegionCoordinate> biomeInitialized = new HashSet<>();
    /** Stores playerCheckTick value when a region was last water-body-scanned. */
    private final Map<RegionCoordinate, Integer> waterBodyLastScan = new HashMap<>();
    private final Set<RegionCoordinate> elevationInitialized = new HashSet<>();
    /** Stores playerCheckTick when a region last played an ambient sound (per-region throttle). */
    private final Map<RegionCoordinate, Integer> regionSoundLastTick = new HashMap<>();

    public LivingWorldMod(IEventBus eventBus, ModContainer modContainer) {
        LivingWorldLogger.info(DiagnosticCategory.BOOTSTRAP, "Living World mod starting...");

        // Register server-side TOML config (written to world/serverconfig/livingworld-server.toml).
        modContainer.registerConfig(ModConfig.Type.SERVER, NeoForgeModConfig.SPEC);

        this.bootstrap = new LivingWorldBootstrap();
        NeoForgePlatformAdapter platformAdapter = new NeoForgePlatformAdapter(
                bootstrap::getWorldSaveDirectory,
                bootstrap::registerCommands,
                bootstrap::onServerTick,
                bootstrap::handleBlockBreak);
        this.bootstrap.initialize(platformAdapter);

        eventBus.addListener(FMLCommonSetupEvent.class, event -> bootstrap.onCommonSetup());
        NeoForge.EVENT_BUS.addListener(
                ServerStartingEvent.class,
                event -> {
                    // Config is loaded before ServerStartingEvent; extract tuning values now.
                    bootstrap.setEcosystemTuning(NeoForgeModConfig.createTuning());
                    bootstrap.onServerStarting(event.getServer().getWorldPath(LevelResource.ROOT));
                });
        NeoForge.EVENT_BUS.addListener(ServerStartedEvent.class, event -> {
            this.minecraftServer = event.getServer();
            bootstrap.onServerStarted();
            bootstrap.getWorldEffectsModule().registerConsumer(
                    new NeoForgeWorldEffectExecutor(() -> minecraftServer));
            bootstrap.setOverworldRaining(
                    () -> minecraftServer != null && minecraftServer.overworld().isRaining());
            bootstrap.setAbsoluteDaySupplier(
                    () -> minecraftServer != null ? minecraftServer.overworld().getGameTime() / 24000L : 0L);
        });
        NeoForge.EVENT_BUS.addListener(ServerStoppingEvent.class, event -> {
            bootstrap.onServerStopping();
            bootstrap.setOverworldRaining(null);
            bootstrap.setAbsoluteDaySupplier(null);
            playerRegionCache.clear();
            playerRainState.clear();
            biomeInitialized.clear();
            waterBodyLastScan.clear();
            elevationInitialized.clear();
            regionSoundLastTick.clear();
            tideTick = 0;
            lastTideLevel = 0.0;
            riverCurrentTick = 0;
            this.minecraftServer = null;
        });

        NeoForge.EVENT_BUS.addListener(ServerTickEvent.Post.class, event -> {
            if (minecraftServer == null || !bootstrap.isServerReady()) return;
            if (++furnaceScanTick % FURNACE_SCAN_INTERVAL == 0) {
                scanAndRecordFurnaceActivity();
            }
            if (++playerCheckTick % PLAYER_CHECK_INTERVAL == 0) {
                checkPlayerRegions();
            }
            if (++tideTick >= TIDE_CHECK_INTERVAL) {
                tideTick = 0;
                updateTidalEffects();
            }
            if (++riverCurrentTick % RIVER_CURRENT_INTERVAL == 0) {
                applyRiverCurrents();
            }
            // Broadcast any climate event messages queued by the bootstrap
            java.util.List<String> eventMsgs = bootstrap.pollClimateEventMessages();
            if (!eventMsgs.isEmpty()) {
                for (String msg : eventMsgs) {
                    minecraftServer.getPlayerList().broadcastSystemMessage(
                            net.minecraft.network.chat.Component.literal(msg)
                                    .withStyle(ChatFormatting.YELLOW), false);
                }
            }
        });

        // Step 1: Mob spawn feedback — passive mobs suppressed in degraded regions,
        // hostile mobs suppressed in healthy ones.
        NeoForge.EVENT_BUS.addListener(FinalizeSpawnEvent.class, event -> {
            if (!bootstrap.isServerReady()) return;
            if (!(event.getLevel() instanceof ServerLevel level)) return;
            String dimId = level.dimension().location().toString();
            Optional<RegionMetrics> metricsOpt =
                    bootstrap.getMetricsAt(dimId, event.getX(), event.getZ());
            if (metricsOpt.isEmpty()) return;
            double health = metricsOpt.get().getEcosystemHealth();
            if (event.getEntity() instanceof Animal) {
                if (health < PASSIVE_SUPPRESS_HEALTH) {
                    double chance = (PASSIVE_SUPPRESS_HEALTH - health) / PASSIVE_SUPPRESS_HEALTH * 0.7;
                    if (random.nextDouble() < chance) event.setSpawnCancelled(true);
                }
            } else if (event.getEntity() instanceof Monster) {
                if (health > HOSTILE_SUPPRESS_HEALTH) {
                    double chance = (health - HOSTILE_SUPPRESS_HEALTH) / (100.0 - HOSTILE_SUPPRESS_HEALTH) * 0.5;
                    if (random.nextDouble() < chance) event.setSpawnCancelled(true);
                }
            }
        });

        // Step 4: Agriculture — bone meal boosts soil fertility.
        NeoForge.EVENT_BUS.addListener(PlayerInteractEvent.RightClickBlock.class, event -> {
            if (!bootstrap.isServerReady()) return;
            if (!(event.getLevel() instanceof ServerLevel level)) return;
            BlockPos pos = event.getPos();
            if (event.getItemStack().is(Items.BONE_MEAL)) {
                bootstrap.handleBoneMeal(level.dimension().location().toString(), pos.getX(), pos.getZ());
            } else if (event.getItemStack().is(Items.WATER_BUCKET)) {
                // Player placed a water source — invalidate the water-body scan for this region
                // so the next player-check cycle detects the new water body.
                RegionCoordinate coord = RegionCoordinate.fromBlock(
                        level.dimension().location().toString(), pos.getX(), pos.getZ(),
                        LivingWorldConstants.DEFAULT_REGION_SIZE_CHUNKS);
                waterBodyLastScan.remove(coord);
            }
        });

        // Step 4: Agriculture — harvesting fully-grown crops drains soil fertility.
        NeoForge.EVENT_BUS.addListener(BlockEvent.BreakEvent.class, event -> {
            if (!bootstrap.isServerReady()) return;
            if (!(event.getLevel() instanceof ServerLevel level)) return;
            var state = event.getState();
            if (!state.is(BlockTags.CROPS)) return;
            if (!state.hasProperty(BlockStateProperties.AGE_7)) return;
            if (state.getValue(BlockStateProperties.AGE_7) < 7) return;
            BlockPos pos = event.getPos();
            bootstrap.handleCropHarvest(level.dimension().location().toString(), pos.getX(), pos.getZ());
        });

        LivingWorldLogger.info(DiagnosticCategory.BOOTSTRAP, "Living World Bootstrap initialized successfully.");
    }

    private void scanAndRecordFurnaceActivity() {
        for (Region region : bootstrap.getActiveRegions()) {
            String dimensionId = region.getCoordinate().dimensionId();
            ServerLevel level = minecraftServer.getLevel(
                    ResourceKey.create(Registries.DIMENSION, ResourceLocation.parse(dimensionId)));
            if (level == null) continue;

            int baseChunkX = region.getCoordinate().x() * LivingWorldConstants.DEFAULT_REGION_SIZE_CHUNKS;
            int baseChunkZ = region.getCoordinate().z() * LivingWorldConstants.DEFAULT_REGION_SIZE_CHUNKS;

            int furnaces = 0;
            int campfires = 0;
            for (int cx = baseChunkX; cx < baseChunkX + LivingWorldConstants.DEFAULT_REGION_SIZE_CHUNKS; cx++) {
                for (int cz = baseChunkZ; cz < baseChunkZ + LivingWorldConstants.DEFAULT_REGION_SIZE_CHUNKS; cz++) {
                    LevelChunk chunk = level.getChunkSource().getChunkNow(cx, cz);
                    if (chunk == null) continue;
                    for (BlockEntity be : chunk.getBlockEntities().values()) {
                        if (be instanceof AbstractFurnaceBlockEntity
                                && be.getBlockState().hasProperty(BlockStateProperties.LIT)
                                && Boolean.TRUE.equals(be.getBlockState().getValue(BlockStateProperties.LIT))) {
                            furnaces++;
                        } else if (be instanceof CampfireBlockEntity
                                && be.getBlockState().hasProperty(BlockStateProperties.LIT)
                                && Boolean.TRUE.equals(be.getBlockState().getValue(BlockStateProperties.LIT))) {
                            campfires++;
                        }
                    }
                }
            }

            double air    = furnaces * AIR_POLLUTION_PER_FURNACE    + campfires * AIR_POLLUTION_PER_CAMPFIRE;
            double ground = furnaces * GROUND_POLLUTION_PER_FURNACE  + campfires * GROUND_POLLUTION_PER_CAMPFIRE;
            double water  = furnaces * WATER_POLLUTION_PER_FURNACE;
            if (air > 0 || ground > 0) {
                bootstrap.handleFurnaceActivity(region, air, ground, water);
            }
        }
    }

    private void checkPlayerRegions() {
        for (ServerPlayer player : minecraftServer.getPlayerList().getPlayers()) {
            String dimId = player.level().dimension().location().toString();
            int regionX = (int) Math.floor(player.getX() / (LivingWorldConstants.DEFAULT_REGION_SIZE_CHUNKS * 16.0));
            int regionZ = (int) Math.floor(player.getZ() / (LivingWorldConstants.DEFAULT_REGION_SIZE_CHUNKS * 16.0));
            RegionCoordinate coord = new RegionCoordinate(dimId, regionX, regionZ);
            RegionCoordinate previous = playerRegionCache.put(player.getUUID(), coord);

            if (!coord.equals(previous)) {
                bootstrap.notifyPlayerInRegion(coord);

                if (player.level() instanceof ServerLevel serverLevel) {
                    // Biome-aware succession cap and initial soil/water values —
                    // derived once per region on first player entry.
                    if (!biomeInitialized.contains(coord)) {
                        SuccessionStage cap = deriveBiomeCap(serverLevel, coord);
                        bootstrap.setRegionBiomeCap(coord, cap);
                        initializeRegionFromBiome(serverLevel, coord);
                        biomeInitialized.add(coord);
                    }
                    // Elevation sampling — derived once per region; feeds water runoff physics.
                    if (!elevationInitialized.contains(coord)) {
                        double elev = sampleRegionElevation(serverLevel, coord);
                        bootstrap.setRegionElevation(coord, elev);
                        elevationInitialized.add(coord);
                    }
                }
            }

            // Water body scan — periodic so player-built pools are detected within ~10 min.
            if (player.level() instanceof ServerLevel serverLevel) {
                Integer lastScan = waterBodyLastScan.get(coord);
                boolean needsScan = lastScan == null
                        || (playerCheckTick - lastScan) >= WATER_BODY_RESCAN_INTERVAL;
                if (needsScan) {
                    if (hasWaterBody(serverLevel, coord)) {
                        bootstrap.applyWaterBodyBoost(coord);
                    }
                    waterBodyLastScan.put(coord, playerCheckTick);
                }
            }

            // Regional weather — send per-player rain/thunder packets so each region
            // has its own sky independently of global Minecraft weather.
            AtmosphereRegionData atm = bootstrap.getRegionalWeather(coord).orElse(null);
            if (atm != null) {
                float rainLevel    = (float) atm.getRainLevel();
                float thunderLevel = (float) atm.getThunderLevel();
                boolean wasRaining = playerRainState.getOrDefault(player.getUUID(), false);
                boolean nowRaining = rainLevel > 0.1f;
                if (nowRaining != wasRaining) {
                    player.connection.send(nowRaining
                            ? new ClientboundGameEventPacket(ClientboundGameEventPacket.START_RAINING, 0f)
                            : new ClientboundGameEventPacket(ClientboundGameEventPacket.STOP_RAINING, 0f));
                    playerRainState.put(player.getUUID(), nowRaining);
                }
                player.connection.send(new ClientboundGameEventPacket(
                        ClientboundGameEventPacket.RAIN_LEVEL_CHANGE, rainLevel));
                player.connection.send(new ClientboundGameEventPacket(
                        ClientboundGameEventPacket.THUNDER_LEVEL_CHANGE, thunderLevel));
            }

            // Player effects from pollution — applied/refreshed each check interval.
            Optional<RegionMetrics> metricsOpt = bootstrap.getMetricsAt(dimId, player.getX(), player.getZ());
            metricsOpt.ifPresent(m -> {
                double poll = m.getPollutionScore();
                if (poll > 40) {
                    // CONFUSION = nausea in 1.21.1 (registry name "nausea", Java constant CONFUSION)
                    player.addEffect(new MobEffectInstance(MobEffects.CONFUSION, 100, 0, true, false));
                }
                if (poll > 60) {
                    player.addEffect(new MobEffectInstance(MobEffects.MOVEMENT_SLOWDOWN, 100, 0, true, false));
                }
                if (poll > 80) {
                    player.addEffect(new MobEffectInstance(MobEffects.WEAKNESS, 100, 0, true, false));
                }
                // Fog particles — sent only to this player via the targeted sendParticles overload.
                if (poll > 40 && player.level() instanceof ServerLevel sl) {
                    int fogCount = Math.min(3, (int) ((poll - 40) / 20.0) + 1);
                    for (int f = 0; f < fogCount; f++) {
                        double px = player.getX() + (random.nextDouble() - 0.5) * 6;
                        double py = player.getEyeY() + (random.nextDouble() - 0.5);
                        double pz = player.getZ() + (random.nextDouble() - 0.5) * 6;
                        sl.sendParticles(player, ParticleTypes.SMOKE, false, px, py, pz, 1, 0.4, 0.2, 0.4, 0.01);
                    }
                }
            });

            // Compass HUD — display region health while holding a compass, or debug HUD is on.
            boolean showHud = player.getMainHandItem().is(Items.COMPASS)
                    || player.getOffhandItem().is(Items.COMPASS)
                    || bootstrap.isHudEnabled(player.getUUID());
            if (showHud) {
                metricsOpt.ifPresent(m -> player.displayClientMessage(buildHud(coord, m, atm), true));
            }

            // Sound ambience — throttled per region so only one sound fires per region per
            // ~8 seconds regardless of how many players are standing in it.
            if (player.level() instanceof ServerLevel ambLevel) {
                Integer lastSound = regionSoundLastTick.get(coord);
                if (lastSound == null || (playerCheckTick - lastSound) >= 8) {
                    SuccessionStage stage = bootstrap.getSuccessionStageAt(dimId, player.getX(), player.getZ())
                            .orElse(null);
                    double health = metricsOpt.map(RegionMetrics::getEcosystemHealth).orElse(0.0);
                    double poll   = metricsOpt.map(RegionMetrics::getPollutionScore).orElse(0.0);
                    if (tryPlayRegionAmbience(player, stage, health, poll)) {
                        regionSoundLastTick.put(coord, playerCheckTick);
                    }
                }
            }
        }
    }

    /**
     * Applies realistic river current forces to players and non-player entities standing
     * in flowing water. Uses {@link net.minecraft.world.level.material.FluidState#getFlow}
     * — the same directional vector Minecraft computes internally — so currents always
     * follow the actual water flow path, not a simulation approximation.
     * Mountain rivers (level 7, close to source) push hard; lowland channels (level 1-2)
     * are gentle enough to wade through. Heavy armour players notice the difference.
     */
    private void applyRiverCurrents() {
        if (minecraftServer == null) return;
        for (ServerPlayer player : minecraftServer.getPlayerList().getPlayers()) {
            if (!(player.level() instanceof ServerLevel level)) continue;
            BlockPos pos = player.blockPosition();
            var fluid = level.getFluidState(pos);
            // Only flowing water — source blocks carry no directional current
            if (!fluid.is(FluidTags.WATER) || fluid.isSource()) continue;
            net.minecraft.world.phys.Vec3 flowVec = fluid.getFlow(level, pos);
            if (flowVec.lengthSqr() < 0.0001) continue;
            // Fluid amount: 8 = source, decreases with distance.
            // Amount 7 = one block from source (fastest); amount 1 = farthest (slowest).
            double flowStrength = fluid.getAmount() / 8.0;
            double force = CURRENT_STRENGTH * flowStrength;
            var movement = player.getDeltaMovement();
            player.setDeltaMovement(
                    movement.x + flowVec.x * force,
                    movement.y,
                    movement.z + flowVec.z * force);
        }
    }

    /**
     * Plays a single ambient sound appropriate for the region's ecological state.
     * Each sound fires at low volume and random pitch so it blends naturally.
     * Returns true if a sound was played (so the per-region cooldown can be set).
     */
    private boolean tryPlayRegionAmbience(
            ServerPlayer player, SuccessionStage stage, double health, double pollution) {
        String dimId = player.level().dimension().location().toString();
        int regionX = (int) Math.floor(player.getX() / (LivingWorldConstants.DEFAULT_REGION_SIZE_CHUNKS * 16.0));
        int regionZ = (int) Math.floor(player.getZ() / (LivingWorldConstants.DEFAULT_REGION_SIZE_CHUNKS * 16.0));
        RegionCoordinate coord = new RegionCoordinate(dimId, regionX, regionZ);

        // Volcanic ambience — highest priority when eruption is active
        String volcanoPhase = bootstrap.getVolcanoPhaseAt(coord);
        if (volcanoPhase != null) {
            if ("ERUPTING".equals(volcanoPhase) && random.nextInt(4) == 0) {
                playAmbientSound(player, Holder.direct(SoundEvents.FIRE_AMBIENT), 0.55f, 0.8f + random.nextFloat() * 0.4f);
                return true;
            }
            if ("BUILDING".equals(volcanoPhase) && random.nextInt(20) == 0) {
                playAmbientSound(player, SoundEvents.AMBIENT_BASALT_DELTAS_MOOD, 0.25f, 0.7f + random.nextFloat() * 0.2f);
                return true;
            }
        }

        float pitch = 0.75f + random.nextFloat() * 0.5f;
        if (stage != null && stage.ordinal() >= SuccessionStage.YOUNG_WOODLAND.ordinal()
                && health > 50.0 && random.nextInt(10) == 0) {
            // Rustling leaves — healthy forest ambience.
            playAmbientSound(player, Holder.direct(SoundEvents.AZALEA_LEAVES_STEP), 0.18f, pitch);
            return true;
        }
        if (stage != null && stage.ordinal() <= SuccessionStage.SPARSE_GRASS.ordinal()
                && random.nextInt(15) == 0) {
            // Dry, shifting sand — barren/arid ambience.
            playAmbientSound(player, Holder.direct(SoundEvents.SAND_STEP), 0.10f, 0.55f + random.nextFloat() * 0.2f);
            return true;
        }
        if (pollution > 50.0 && random.nextInt(18) == 0) {
            // Industrial mood — heavy-pollution ambience (already a Holder.Reference).
            playAmbientSound(player, SoundEvents.AMBIENT_BASALT_DELTAS_MOOD, 0.12f, 1.0f + random.nextFloat() * 0.15f);
            return true;
        }
        return false;
    }

    /** Sends an ambient sound packet directly to one player so others nearby don't hear it. */
    private static void playAmbientSound(ServerPlayer player, Holder<SoundEvent> sound, float vol, float pitch) {
        player.connection.send(new ClientboundSoundPacket(
                sound,
                SoundSource.AMBIENT,
                player.getX(), player.getEyeY(), player.getZ(),
                vol, pitch, player.getRandom().nextLong()));
    }

    // -----------------------------------------------------------------------
    // Tidal simulation
    // -----------------------------------------------------------------------

    /**
     * Runs every {@link #TIDE_CHECK_INTERVAL} ticks. Computes the current tidal level
     * from the in-game time (two full tidal cycles per Minecraft day) and:
     * <ol>
     *   <li>Adjusts coastal region water-availability scores so droughts ease during high tide.</li>
     *   <li>Physically places/removes water source blocks at the shoreline so the tidal
     *       boundary moves visibly up and down by one block over the cycle.</li>
     * </ol>
     */
    private void updateTidalEffects() {
        if (minecraftServer == null) return;
        ServerLevel overworld = minecraftServer.overworld();
        long dayTime = overworld.getDayTime();
        // Two complete tidal cycles per Minecraft day (24 000 ticks)
        double tideLevel = Math.sin(dayTime / 12000.0 * Math.PI); // -1 low … +1 high
        double delta = tideLevel - lastTideLevel;
        lastTideLevel = tideLevel;

        int seaLevel = overworld.getSeaLevel();
        boolean risingTide = delta > 0.05;
        boolean fallingTide = delta < -0.05;

        for (Region region : bootstrap.getActiveRegions()) {
            if (!region.getCoordinate().dimensionId().equals("minecraft:overworld")) continue;
            Double elev = bootstrap.getRegionElevation(region.getCoordinate());
            if (elev == null || elev > seaLevel + 6) continue; // only coastal/ocean

            // Update data model (water availability)
            bootstrap.applyTidalEffect(region.getCoordinate(), delta * 7.0);

            // Physical block changes at the shoreline
            if ((risingTide || fallingTide) && Math.abs(delta) > 0.15) {
                applyTideBlocks(overworld, region.getCoordinate(), seaLevel, risingTide);
            }
        }
    }

    /**
     * Scans sample positions in the region at sea level and places/removes water blocks
     * to simulate the tidal boundary moving by one block.
     */
    private void applyTideBlocks(ServerLevel level, RegionCoordinate coord, int seaLevel, boolean rising) {
        int baseX = coord.x() * REGION_BLOCKS;
        int baseZ = coord.z() * REGION_BLOCKS;
        int scans = 12;
        for (int i = 0; i < scans; i++) {
            int x = baseX + random.nextInt(REGION_BLOCKS);
            int z = baseZ + random.nextInt(REGION_BLOCKS);
            BlockPos tidePos = new BlockPos(x, seaLevel, z);
            if (!level.isLoaded(tidePos)) continue;

            var stateAtTide = level.getBlockState(tidePos);
            var stateBelow  = level.getBlockState(tidePos.below());

            if (rising) {
                // Rising tide: fill air at sea-level above natural shoreline blocks with water
                if (stateAtTide.isAir()
                        && (stateBelow.is(net.minecraft.world.level.block.Blocks.SAND)
                         || stateBelow.is(net.minecraft.world.level.block.Blocks.GRAVEL)
                         || stateBelow.is(net.minecraft.world.level.block.Blocks.STONE)
                         || stateBelow.is(net.minecraft.world.level.block.Blocks.SANDSTONE))) {
                    level.setBlock(tidePos, net.minecraft.world.level.block.Blocks.WATER.defaultBlockState(),
                            net.minecraft.world.level.block.Block.UPDATE_ALL);
                }
            } else {
                // Falling tide: remove water source blocks sitting on natural terrain at sea-level
                if (stateAtTide.is(net.minecraft.world.level.block.Blocks.WATER)
                        && stateAtTide.getFluidState().isSource()
                        && (stateBelow.is(net.minecraft.world.level.block.Blocks.SAND)
                         || stateBelow.is(net.minecraft.world.level.block.Blocks.GRAVEL)
                         || stateBelow.is(net.minecraft.world.level.block.Blocks.STONE))) {
                    level.setBlock(tidePos, net.minecraft.world.level.block.Blocks.AIR.defaultBlockState(),
                            net.minecraft.world.level.block.Block.UPDATE_ALL);
                }
            }
        }
    }

    // -----------------------------------------------------------------------
    // Biome-aware region initialisation
    // -----------------------------------------------------------------------

    /**
     * Reads biome temperature and downfall from the region centre block and passes them to
     * the bootstrap so it can set realistic starting soil/water values for the region.
     */
    private void initializeRegionFromBiome(ServerLevel level, RegionCoordinate coord) {
        int cx = coord.x() * REGION_BLOCKS + REGION_BLOCKS / 2;
        int cz = coord.z() * REGION_BLOCKS + REGION_BLOCKS / 2;
        int cy = level.getHeight(net.minecraft.world.level.levelgen.Heightmap.Types.WORLD_SURFACE, cx, cz);
        net.minecraft.core.Holder<net.minecraft.world.level.biome.Biome> biome =
                level.getBiome(new BlockPos(cx, cy, cz));
        float temp     = biome.value().getBaseTemperature();
        float downfall = biome.value().getModifiedClimateSettings().downfall();
        bootstrap.initializeRegionBiomeValues(coord, temp, downfall);
    }

    private static final int REGION_BLOCKS =
            LivingWorldConstants.DEFAULT_REGION_SIZE_CHUNKS * 16;

    /** Samples 25 random surface heights in the region and returns the average Y. */
    private double sampleRegionElevation(ServerLevel level, RegionCoordinate coord) {
        int baseX = coord.x() * REGION_BLOCKS;
        int baseZ = coord.z() * REGION_BLOCKS;
        double total = 0;
        int count = 0;
        for (int i = 0; i < 25; i++) {
            int x = baseX + random.nextInt(REGION_BLOCKS);
            int z = baseZ + random.nextInt(REGION_BLOCKS);
            total += level.getHeight(Heightmap.Types.WORLD_SURFACE, x, z);
            count++;
        }
        return count > 0 ? total / count : 64.0;
    }

    /** Scans 30 random surface positions in the region; returns true if ≥5 are water. */
    private boolean hasWaterBody(ServerLevel level, RegionCoordinate coord) {
        int baseX = coord.x() * REGION_BLOCKS;
        int baseZ = coord.z() * REGION_BLOCKS;
        int waterCount = 0;
        for (int i = 0; i < 30; i++) {
            int x = baseX + random.nextInt(REGION_BLOCKS);
            int z = baseZ + random.nextInt(REGION_BLOCKS);
            int y = level.getHeight(Heightmap.Types.WORLD_SURFACE, x, z) - 1;
            if (y >= level.getMinBuildHeight()
                    && level.getFluidState(new BlockPos(x, y, z)).is(FluidTags.WATER)) {
                waterCount++;
            }
        }
        return waterCount >= 5;
    }

    /** Step 6: Samples the dominant biome at the region centre and returns a succession ceiling. */
    private SuccessionStage deriveBiomeCap(ServerLevel level, RegionCoordinate coord) {
        int cx = coord.x() * LivingWorldConstants.DEFAULT_REGION_SIZE_CHUNKS * 16 + 64;
        int cz = coord.z() * LivingWorldConstants.DEFAULT_REGION_SIZE_CHUNKS * 16 + 64;
        int cy = level.getHeight(Heightmap.Types.WORLD_SURFACE, cx, cz);
        Holder<Biome> biome = level.getBiome(new BlockPos(cx, cy, cz));

        // Temperature ≥ 2.0 = desert or badlands (no IS_DESERT tag in MC 1.21).
        float temp = biome.value().getBaseTemperature();
        if (biome.is(BiomeTags.IS_BADLANDS) || temp >= 2.0f) {
            return SuccessionStage.SPARSE_GRASS;
        } else if (biome.is(BiomeTags.IS_SAVANNA)) {
            return SuccessionStage.SCRUBLAND;
        } else if (biome.is(BiomeTags.IS_MOUNTAIN) || temp < 0.15f) {
            return SuccessionStage.SCRUBLAND;
        } else if (biome.is(BiomeTags.IS_BEACH) || biome.is(BiomeTags.IS_OCEAN)
                || biome.is(BiomeTags.IS_DEEP_OCEAN)) {
            return SuccessionStage.GRASSLAND;
        } else if (biome.is(BiomeTags.IS_FOREST) || biome.is(BiomeTags.IS_TAIGA)
                || biome.is(BiomeTags.IS_JUNGLE)) {
            return SuccessionStage.MATURE_FOREST;
        } else {
            return SuccessionStage.YOUNG_WOODLAND;
        }
    }

    /** Builds the action-bar HUD component for a player holding a compass or with /lw hud on. */
    private Component buildHud(RegionCoordinate coord, RegionMetrics m, AtmosphereRegionData atm) {
        double eco  = m.getEcosystemHealth();
        double poll = m.getPollutionScore();
        double soil = m.getSoilQuality();
        double wat  = m.getWaterQuality();

        ChatFormatting ecoCol  = eco  > 60 ? ChatFormatting.GREEN  : eco  > 30 ? ChatFormatting.YELLOW : ChatFormatting.RED;
        ChatFormatting pollCol = poll < 15 ? ChatFormatting.GREEN  : poll < 40 ? ChatFormatting.YELLOW : ChatFormatting.RED;
        ChatFormatting soilCol = soil > 50 ? ChatFormatting.GREEN  : soil > 25 ? ChatFormatting.YELLOW : ChatFormatting.RED;
        ChatFormatting watCol  = wat  > 50 ? ChatFormatting.GREEN  : wat  > 25 ? ChatFormatting.YELLOW : ChatFormatting.RED;

        String trendArrow = bootstrap.getTrendArrow(coord);
        ChatFormatting trendCol = "↑".equals(trendArrow) ? ChatFormatting.GREEN
                                : "↓".equals(trendArrow) ? ChatFormatting.RED
                                : ChatFormatting.GRAY;

        var line = Component.empty()
                .append(Component.literal("[LW] ").withStyle(ChatFormatting.GOLD))
                .append(Component.literal(String.format("(%d,%d)", coord.x(), coord.z())).withStyle(ChatFormatting.GRAY))
                .append(Component.literal(trendArrow + " ").withStyle(trendCol))
                .append(Component.literal("Eco:").withStyle(ChatFormatting.WHITE))
                .append(Component.literal(String.format("%.0f ", eco)).withStyle(ecoCol))
                .append(Component.literal("Poll:").withStyle(ChatFormatting.WHITE))
                .append(Component.literal(String.format("%.1f ", poll)).withStyle(pollCol))
                .append(Component.literal("Soil:").withStyle(ChatFormatting.WHITE))
                .append(Component.literal(String.format("%.0f ", soil)).withStyle(soilCol))
                .append(Component.literal("Wat:").withStyle(ChatFormatting.WHITE))
                .append(Component.literal(String.format("%.0f", wat)).withStyle(watCol));

        if (atm != null) {
            double rain    = atm.getRainLevel()    * 100;
            double thunder = atm.getThunderLevel() * 100;
            ChatFormatting rainCol = rain > 50 ? ChatFormatting.AQUA : rain > 20 ? ChatFormatting.YELLOW : ChatFormatting.RED;
            line.append(Component.literal(" Rain:").withStyle(ChatFormatting.WHITE))
                .append(Component.literal(String.format("%.0f%%", rain)).withStyle(rainCol));
            if (thunder > 15) {
                ChatFormatting stormCol = thunder > 50 ? ChatFormatting.RED : ChatFormatting.YELLOW;
                line.append(Component.literal(" Storm:").withStyle(ChatFormatting.WHITE))
                    .append(Component.literal(String.format("%.0f%%", thunder)).withStyle(stormCol));
            }
        }

        double seedRain = bootstrap.getSeedRainAt(coord);
        if (seedRain > 0.5) {
            line.append(Component.literal(" Seeds:").withStyle(ChatFormatting.WHITE))
                .append(Component.literal(String.format("%.1f", seedRain)).withStyle(ChatFormatting.GREEN));
        }

        return line;
    }
}
