// Assets/_Project/Scripts/Gameplay/WaveManager.cs
using System.Collections;
using Unity.Netcode;
using UnityEngine;
using TD.Core;
using TD.Gameplay.Draft;
using TD.Levels;
using TD.UI;

namespace TD.Gameplay
{
    /// <summary>
    /// Server-authoritative wave controller. Spawns enemies across all player zones,
    /// tracks wave completion, awards kill gold, and manages the shared lives pool.
    /// </summary>
    /// <remarks>
    /// <b>Wave lifecycle:</b>
    /// <list type="bullet">
    ///   <item>When <see cref="MatchPhase.Playing"/> is entered,
    ///         <see cref="StartNextWave"/> advances <see cref="MatchState.CurrentWave"/>
    ///         immediately (so the HUD shows the wave number during prep), then waits
    ///         <see cref="WaveDefinition.PrepTime"/> before spawning.</item>
    ///   <item>Each <see cref="WaveEntry"/> spawns <c>Count</c> enemies per player zone,
    ///         one zone per frame-group, with <c>SpawnInterval</c> seconds between
    ///         individual enemies in the group.</item>
    ///   <item>After all entries are spawned, the wave is considered complete only when
    ///         every active enemy is either killed or has reached the goal.</item>
    ///   <item>All waves exhausted → <see cref="MatchPhase.Victory"/>.</item>
    ///   <item>Lives drop to 0 → <see cref="MatchPhase.Defeat"/>.</item>
    /// </list>
    ///
    /// <b>Kill gold:</b> When an enemy dies, <see cref="EnemyHealth.LastHitOwner"/> names
    /// the tower's player. <see cref="PlayerMatchState.GetForSlot"/> resolves the
    /// <c>OwnerClientId</c>, and <see cref="PlayerGoldManager.GetForClient"/> awards
    /// the gold.
    ///
    /// <b>Zone leak counts:</b> <see cref="zoneLeakCounts"/> is a <c>NetworkList</c>
    /// indexed by <c>(int)PlayerSlot</c> (indices 0–8). It is incremented when an enemy
    /// crosses from one player zone into another, giving the HUD a per-player leak score.
    /// Index 0 corresponds to <see cref="PlayerSlot.None"/> and is unused.
    ///
    /// <b>Inspector setup:</b>
    /// <list type="bullet">
    ///   <item>Assign <see cref="waveDefinitions"/> in order (Wave 1 at index 0).</item>
    ///   <item>Set <see cref="startingLives"/> to match your level design intent.</item>
    /// </list>
    /// </remarks>
    public class WaveManager : NetworkBehaviour
    {
        // ----- Singleton --------------------------------------------------

        public static WaveManager Instance { get; private set; }

        // ----- Inspector --------------------------------------------------

        [Tooltip("Wave definitions in order. Index 0 = Wave 1.")]
        [SerializeField] private WaveDefinition[] waveDefinitions;

        [Tooltip("Shared lives pool at the start of a match.")]
        [SerializeField] private int startingLives = 20;

        [Tooltip("Single source of truth for every gold tunable: starting gold, per-wave " +
                 "kill rewards, completion bonus, no-leak bonus. Required for a real match; " +
                 "if unset the game falls back to per-player startingGold defaults and grants " +
                 "no kill/wave rewards (designer-error indicator, not a supported runtime mode).")]
        [SerializeField] private GoldConfig goldConfig;

        // ----- Networked state --------------------------------------------

        // Per-slot total leak counters across the whole match. Index = (int)PlayerSlot;
        // size = 10 (0-9). Index 0 (PlayerSlot.None) is allocated but never written.
        // Replicated so the HUD scoreboard can show total leaks per player.
        //
        // Semantics: zoneLeakCounts[P] is incremented exactly once per enemy that
        // ORIGINATED in player P's spawn AND escaped P's zone (crossed P's leak
        // volume). Transit through other zones (e.g. a P1 enemy passing through
        // P4 on its way to the goal) does NOT increment any counter — this is the
        // "enemies I failed to stop in my own maze" metric, not a transit count.
        private readonly NetworkList<int> zoneLeakCounts = new NetworkList<int>();

        // Per-slot leak counter for the CURRENT wave only. Same shape as zoneLeakCounts;
        // server resets every entry to 0 at the start of each wave. Used to determine
        // who earns the NoLeaksBonus on wave completion. Not currently surfaced in UI
        // separately from zoneLeakCounts — could be exposed if a "this wave: 0 leaks"
        // indicator becomes desirable.
        private readonly NetworkList<int> waveLeakCounts = new NetworkList<int>();

        // Networked prep-phase countdown. Counts down from WaveDefinition.PrepTime to
        // zero during prep; 0 while the wave is active or being mopped up. Read by the
        // HUD (next-wave-label) to render "next: 0:12". Server is the only writer.
        private readonly NetworkVariable<float> prepCountdown = new NetworkVariable<float>(
            value: 0f,
            readPerm: NetworkVariableReadPermission.Everyone,
            writePerm: NetworkVariableWritePermission.Server);

        // ----- Server-local runtime state ---------------------------------

        private int       remainingLives;
        private int       activeEnemyCount;
        private bool      spawningComplete;
        private int       currentWaveIndex = -1;  // -1 = not yet started
        private Coroutine activeWaveCoroutine;

        private readonly System.Collections.Generic.List<EnemyHealth> heldEnemies
            = new System.Collections.Generic.List<EnemyHealth>();

        // ----- NGO lifecycle ----------------------------------------------

        public override void OnNetworkSpawn()
        {
            if (Instance != null && Instance != this)
            {
                Debug.LogError("[WaveManager] Duplicate WaveManager detected. " +
                               "Only one may exist per scene.");
                return;
            }
            Instance = this;

            if (!IsServer) return;

            // Populate the NetworkLists with 10 zeros (indices 0-9 for PlayerSlot.None..Player9).
            for (int i = 0; i < 10; i++)
            {
                zoneLeakCounts.Add(0);
                waveLeakCounts.Add(0);
            }

            remainingLives = startingLives;

            // NGO's scene-object spawn sweep calls all OnNetworkSpawn methods
            // synchronously in a single call stack. Yielding one frame guarantees
            // every sibling NetworkBehaviour (including MatchState) has finished
            // its own OnNetworkSpawn before we try to read MatchState.Instance.
            StartCoroutine(InitAfterSpawn());
        }

        private System.Collections.IEnumerator InitAfterSpawn()
        {
            yield return null; // wait one frame

            var ms = MatchState.Instance;
            if (ms == null)
            {
                Debug.LogWarning("[WaveManager] MatchState not found after spawn. " +
                                 "Waves will not start automatically.");
                yield break;
            }

            ms.SetLives(remainingLives);
            ms.OnPhaseChanged += HandlePhaseChanged;

            // Apply StartingGold from the config to every connected player, overwriting
            // whatever fallback the PlayerGoldManager set during its own OnNetworkSpawn.
            // PlayerGoldManager spawns once per client connection (in MainMenu/Lobby),
            // before the Match scene exists — so this is where the config-driven init
            // actually lands. Skipped silently if no goldConfig is assigned; the per-
            // player fallback startingGold stays.
            if (goldConfig != null)
            {
                foreach (var pms in PlayerMatchState.AllPlayers)
                {
                    var gm = PlayerGoldManager.GetForClient(pms.OwnerClientId);
                    if (gm != null) gm.ServerSetGold(goldConfig.StartingGold);
                }
            }

            // Seed each player's tower deck with the starter set. Runs here (after the
            // one-frame yield) so the TowerPlacementManager catalog is guaranteed spawned
            // and its starting-deck assets resolve to valid TypeIds. Clearing-and-setting
            // means Retry / return-to-lobby cycles reset the deck cleanly.
            var placement = TowerPlacementManager.Instance;
            if (placement != null)
            {
                var startingTypeIds = placement.GetStartingDeckTypeIds();
                foreach (var pms in PlayerMatchState.AllPlayers)
                {
                    var deck = PlayerTowerDeck.GetForClient(pms.OwnerClientId);
                    if (deck != null) deck.ServerInitialize(startingTypeIds);
                }
            }
            else
            {
                Debug.LogWarning("[WaveManager] TowerPlacementManager not found at match start. " +
                                 "Player decks were not seeded.");
            }

            if (ms.Phase == MatchPhase.Playing)
                StartNextWave();
        }

        public override void OnNetworkDespawn()
        {
            if (Instance == this) Instance = null;

            if (MatchState.Instance != null)
                MatchState.Instance.OnPhaseChanged -= HandlePhaseChanged;
        }

        // ----- Public accessors -------------------------------------------

        /// <summary>
        /// Total number of waves in this match. Same on every peer because
        /// <see cref="waveDefinitions"/> is a serialized prefab field, identical
        /// on host and clients. Returns 0 if the array is unassigned.
        /// </summary>
        public int TotalWaves => waveDefinitions?.Length ?? 0;

        /// <summary>
        /// Number of times enemies have leaked out of the given player's zone over the
        /// entire match. Replicated — safe to call on any peer.
        /// </summary>
        public int GetZoneLeakCount(PlayerSlot slot)
        {
            int idx = (int)slot;
            return (idx >= 0 && idx < zoneLeakCounts.Count) ? zoneLeakCounts[idx] : 0;
        }

        /// <summary>
        /// The <see cref="GoldConfig"/> assigned to this wave manager, or null if unset.
        /// Exposed so other systems (HUD, future income panels) can read its values.
        /// </summary>
        public GoldConfig GoldConfig => goldConfig;

        /// <summary>
        /// Seconds remaining in the prep phase before the next wave starts spawning.
        /// Zero outside of the prep phase. Replicated; safe to call on any peer.
        /// HUD reads this each frame to render the countdown label.
        /// </summary>
        public float PrepCountdown => prepCountdown.Value;


        // ----- Phase handling ---------------------------------------------

        private void HandlePhaseChanged(MatchPhase previous, MatchPhase next)
        {
            if (!IsServer) return;
            if (next == MatchPhase.Playing && currentWaveIndex < 0)
                StartNextWave();
        }

        // ----- Wave coroutine ---------------------------------------------

        private void StartNextWave(bool skipPrep = false)
        {
            currentWaveIndex++;

            if (waveDefinitions == null || currentWaveIndex >= waveDefinitions.Length)
            {
                Debug.Log("[WaveManager] All waves complete. Victory.");
                MatchState.Instance?.SetPhase(MatchPhase.Victory);
                return;
            }

            // Advance the replicated wave counter at the START of prep so the HUD
            // shows the upcoming wave number during the countdown.
            MatchState.Instance?.SetCurrentWave(currentWaveIndex + 1); // 1-based

            // Reset per-wave bookkeeping for the wave that's about to begin:
            //  - waveLeakCounts: per-slot leaks this wave, used for the no-leak bonus.
            //  - PlayerGoldManager.goldEarnedThisWave: HUD top-bar resets so the
            //    "+N g/wave" counter starts at 0.
            for (int i = 0; i < waveLeakCounts.Count; i++)
                waveLeakCounts[i] = 0;
            foreach (var pms in PlayerMatchState.AllPlayers)
            {
                var gm = PlayerGoldManager.GetForClient(pms.OwnerClientId);
                if (gm != null) gm.ServerResetWaveEarnings();
            }

            activeEnemyCount = 0;
            spawningComplete = false;

            activeWaveCoroutine = StartCoroutine(
                RunWave(waveDefinitions[currentWaveIndex], skipPrep));
        }

        // ----- Dev / cheats -----------------------------------------------

        /// <summary>
        /// Dev cheat: skip the rest of the current wave (despawn any remaining
        /// enemies, kill the prep timer) and start the next wave immediately.
        /// Server-only — silently no-ops on clients. Safe to call during prep,
        /// mid-spawn, or while enemies are alive.
        /// </summary>
        public void ForceAdvanceToNextWave()
        {
            if (!IsServer) return;

            // Stop the current wave's coroutine (cancels prep timer + remaining spawns).
            if (activeWaveCoroutine != null)
            {
                StopCoroutine(activeWaveCoroutine);
                activeWaveCoroutine = null;
            }

            // Despawn anything still running around. Iterate a snapshot since
            // EnemyHealth.HandleDeath will despawn the NetworkObject, mutating
            // the live collection.
            var spawnManager = NetworkManager.Singleton?.SpawnManager;
            if (spawnManager != null)
            {
                var snapshot = new System.Collections.Generic.List<NetworkObject>(
                    spawnManager.SpawnedObjectsList);
                foreach (var no in snapshot)
                {
                    if (no == null || !no.IsSpawned) continue;
                    var movement = no.GetComponent<EnemyMovement>();
                    if (movement == null) continue;

                    // Unsubscribe so the despawn doesn't deduct lives or fire side effects.
                    var h = no.GetComponent<EnemyHealth>();
                    UnsubscribeEnemy(h);
                    no.Despawn();
                }
            }

            // Reset bookkeeping and start the next wave's RunWave coroutine,
            // skipping the prep timer so spawning starts immediately.
            activeEnemyCount = 0;
            spawningComplete = false;
            heldEnemies.Clear();

            // Force-advancing interrupts the prep phase, so resolve any drafts the players
            // had open before skipping ahead.
            DraftService.Instance?.ServerAutoResolveAll();

            StartNextWave(skipPrep: true);
        }

        private IEnumerator RunWave(WaveDefinition def, bool skipPrep = false)
        {
            // Prep phase — players build while the countdown ticks. Skipped when
            // a dev cheat forces the wave to start immediately. We tick the
            // replicated prepCountdown each frame so every HUD can render the
            // remaining time consistently. Server is the only writer; clients
            // observe the value via NetworkVariable replication.
            if (!skipPrep)
            {
                // Open a draft for every player at the start of the build phase. They pick
                // during prep; any unpicked draft is auto-resolved when the timer expires.
                DraftService.Instance?.ServerOfferToAll();

                prepCountdown.Value = def.PrepTime;
                float remaining = def.PrepTime;
                // Throttle network sync to ~10 Hz. NetworkVariable replicates on every
                // mutation; at 60 fps we'd send ~600 deltas per 10s prep purely to
                // animate text that only changes once per second on the HUD. 0.1s
                // gives a smooth-enough fall while keeping bandwidth minimal.
                const float NetworkSyncInterval = 0.1f;
                float nextSync = def.PrepTime - NetworkSyncInterval;
                while (remaining > 0f)
                {
                    yield return null;
                    remaining = Mathf.Max(0f, remaining - Time.deltaTime);
                    if (remaining <= nextSync || remaining <= 0f)
                    {
                        prepCountdown.Value = remaining;
                        nextSync = remaining - NetworkSyncInterval;
                    }
                }

                // Prep timer expired — auto-resolve any draft the player didn't pick so
                // the free reward isn't wasted.
                DraftService.Instance?.ServerAutoResolveAll();
            }
            // Ensure the countdown reads zero entering the spawn phase, regardless of
            // whether prep was skipped or just expired.
            prepCountdown.Value = 0f;

            // Spawn all enemies at once in a held (untargetable, immobile) state.
            if (def.Entries != null)
            {
                foreach (var entry in def.Entries)
                {
                    if (entry.EnemyType == null || entry.Count <= 0) continue;
                    for (int i = 0; i < entry.Count; i++)
                        SpawnEnemyInAllZones(entry.EnemyType, held: true);
                }
            }

            // Release in chunks of 10% of the total held count.
            int total     = heldEnemies.Count;
            int chunkSize = Mathf.Max(1, Mathf.RoundToInt(total * 0.1f));
            while (heldEnemies.Count > 0)
            {
                int burst = Mathf.Min(chunkSize, heldEnemies.Count);
                for (int i = 0; i < burst; i++)
                {
                    var h = heldEnemies[0];
                    heldEnemies.RemoveAt(0);
                    if (h != null && !h.IsDead)
                        h.SetHeld(false);
                }

                if (heldEnemies.Count > 0)
                    yield return new WaitForSeconds(def.ReleaseInterval);
            }

            spawningComplete = true;

            // If every spawned enemy was already resolved before this coroutine finished
            // (edge case: SpawnInterval = 0 and enemies die instantly), complete the wave now.
            CheckWaveComplete();
        }

        // ----- Spawn helpers ----------------------------------------------

        private void SpawnEnemyInAllZones(EnemyDefinition def, bool held = false)
        {
            var loader = LevelLoader.Instance;
            if (loader?.LevelData?.PlayerZones == null) return;

            foreach (var zone in loader.LevelData.PlayerZones)
            {
                if (zone.Spawners == null || zone.Spawners.Length == 0) continue;

                // Skip zones whose owning slot is empty. Until a lobby exists,
                // this means a 1-player test session only spawns enemies in
                // Player 1's zone; Player 2/3/... zones stay quiet until those
                // slots are actually filled. PlayerMatchState.GetForSlot returns
                // null for unoccupied slots (and for PlayerSlot.None).
                if (PlayerMatchState.GetForSlot(zone.Owner) == null) continue;

                // Use the first spawner in the zone. Future: round-robin through Spawners.
                // Pass zone.Owner explicitly so EnemyMovement knows which player owns
                // this enemy for leak attribution — can't be derived from the spawner
                // tile's owner-grid entry because SpawnerVolume sits outside
                // PlayerZoneVolume (so OwnerGrid[spawnerTile] = None).
                var spawner = zone.Spawners[0];
                (float xHalf, float zHalf) = ComputeSpawnerHalfExtents(spawner.TileArea);
                SpawnEnemy(def, spawner.TilePosition, zone.Owner, xHalf, zHalf, held, spawner.Facing);
            }
        }

        // Computes half-extents in world units from a spawner's tile footprint.
        // X maps to world X; grid-Y maps to world Z (the grid lives on the XZ plane).
        private static (float x, float z) ComputeSpawnerHalfExtents(Vector2Int[] tileArea)
        {
            if (tileArea == null || tileArea.Length == 0) return (0f, 0f);

            int minX = int.MaxValue, maxX = int.MinValue;
            int minY = int.MaxValue, maxY = int.MinValue;
            foreach (var tile in tileArea)
            {
                if (tile.x < minX) minX = tile.x;
                if (tile.x > maxX) maxX = tile.x;
                if (tile.y < minY) minY = tile.y;
                if (tile.y > maxY) maxY = tile.y;
            }

            float xHalf = (maxX - minX + 1) * GridCoordinates.TILE_SIZE * 0.5f;
            float zHalf = (maxY - minY + 1) * GridCoordinates.TILE_SIZE * 0.5f;
            return (xHalf, zHalf);
        }

        private void SpawnEnemy(EnemyDefinition def, Vector2Int spawnerTile, PlayerSlot ownerSlot,
                                float xHalfExtent = 0f, float zHalfExtent = 0f, bool held = false,
                                Direction facing = Direction.South)
        {
            if (def.EnemyPrefab == null)
            {
                Debug.LogWarning($"[WaveManager] EnemyDefinition '{def.name}' has no EnemyPrefab assigned.");
                return;
            }

            Vector3 spawnPos = GridCoordinates.GridToWorld(spawnerTile);
            if (xHalfExtent > 0f)
                spawnPos.x += Random.Range(-xHalfExtent, xHalfExtent);
            if (zHalfExtent > 0f)
                spawnPos.z += Random.Range(-zHalfExtent, zHalfExtent);

            // Flying enemies spawn elevated so they visually soar over towers. The
            // movement code preserves Y per-frame, so this height persists for the
            // enemy's whole flight. NetworkTransform replicates the raised position.
            if (def.IsFlying)
                spawnPos.y += def.FlightHeight;

            float yaw = facing switch
            {
                Direction.North => 0f,
                Direction.East  => 90f,
                Direction.South => 180f,
                Direction.West  => 270f,
                _               => 0f,
            };

            var go = Instantiate(
                def.EnemyPrefab,
                spawnPos,
                Quaternion.Euler(0f, yaw, 0f));

            var health   = go.GetComponent<EnemyHealth>();
            var movement = go.GetComponent<EnemyMovement>();

            if (health == null || movement == null)
            {
                Debug.LogError($"[WaveManager] Enemy prefab '{def.EnemyPrefab.name}' is missing " +
                               $"EnemyHealth or EnemyMovement. Enemy will not be spawned.");
                Destroy(go);
                return;
            }

            health.InitializeServer(def.MaxHp, def.LivesCost, def.IsFlying, held);
            movement.InitializeServer(def.MoveSpeed, spawnerTile, ownerSlot, def.IsFlying);
            if (held) heldEnemies.Add(health);

            health.OnDied         += HandleEnemyKilled;
            movement.OnZoneLeaked += HandleZoneLeak;
            movement.OnReachedGoal += HandleEnemyReachedGoal;

            activeEnemyCount++;

            go.GetComponent<NetworkObject>().Spawn();
        }

        // ----- Enemy event handlers (server-only) -------------------------

        private void HandleEnemyKilled(EnemyHealth health)
        {
            // Kill reward comes from GoldConfig for the current wave — same value for
            // every enemy in the wave regardless of EnemyDefinition type. Missing config
            // or out-of-range wave → 0 reward (gold flow disabled, designer-error mode).
            int killReward = 0;
            var goldEntry = goldConfig?.GetWaveEntry(currentWaveIndex + 1);
            if (goldEntry != null) killReward = goldEntry.GoldPerEnemy;

            // Award kill gold to the tower owner that landed the killing blow.
            PlayerSlot killerSlot = health.LastHitOwner;
            if (killerSlot != PlayerSlot.None && killReward > 0)
            {
                var pms = PlayerMatchState.GetForSlot(killerSlot);
                if (pms != null)
                    PlayerGoldManager.GetForClient(pms.OwnerClientId)
                                     ?.AwardGold(killReward);
            }

            // Show a "+N" gold popup above the corpse on every peer. Capture the
            // position here on the server — by the time the RPC fires on clients
            // the death sequence will be moving the corpse, but the spawn point
            // is good enough and we want the popup to anchor where the kill happened.
            if (killReward > 0)
                ShowGoldRewardClientRpc(health.transform.position, killReward);

            UnsubscribeEnemy(health);
            DecrementAndCheckComplete();
        }

        private void HandleZoneLeak(PlayerSlot leavingZone)
        {
            // EnemyMovement fires this exactly once per enemy, when it escapes its
            // origin zone. We increment both the match-total and the per-wave counter
            // for the originating player. Per-wave count drives the no-leak bonus
            // eligibility check at wave completion.
            int idx = (int)leavingZone;
            if (idx >= 0 && idx < zoneLeakCounts.Count)
                zoneLeakCounts[idx]++;
            if (idx >= 0 && idx < waveLeakCounts.Count)
                waveLeakCounts[idx]++;
        }

        private void HandleEnemyReachedGoal(EnemyMovement movement, int livesCost)
        {
            // Capture the leak position BEFORE the enemy NetworkObject despawns
            // (HandleGoalReached on the enemy calls Despawn right after firing the
            // event we're handling here). Show the "-N" popup on every peer.
            Vector3 leakPos = movement.transform.position;
            if (livesCost > 0)
                ShowLifeLossClientRpc(leakPos, livesCost);

            UnsubscribeEnemy(movement.GetComponent<EnemyHealth>());

            remainingLives = Mathf.Max(0, remainingLives - livesCost);
            MatchState.Instance?.SetLives(remainingLives);

            if (remainingLives <= 0)
            {
                Debug.Log("[WaveManager] Lives depleted. Defeat.");
                MatchState.Instance?.SetPhase(MatchPhase.Defeat);
                return;
            }

            DecrementAndCheckComplete();
        }

        // ----- Floating-text ClientRpcs -----------------------------------

        // Fired on every peer (server + clients) so each one spawns its own local
        // FloatingText. The spawned GameObjects are not networked — purely visual.
        [ClientRpc]
        private void ShowGoldRewardClientRpc(Vector3 worldPos, int amount)
        {
            FloatingTextSpawner.Instance?.SpawnGoldReward(worldPos, amount);
        }

        [ClientRpc]
        private void ShowLifeLossClientRpc(Vector3 worldPos, int amount)
        {
            FloatingTextSpawner.Instance?.SpawnLifeLoss(worldPos, amount);
            OnLifeLost?.Invoke(amount);
        }

        // ----- Local-only notification events -----------------------------

        /// <summary>
        /// Fired on every peer immediately after a life-loss popup spawns.
        /// HUD subscribes to flash a centered banner; gameplay code can also
        /// hook this for audio cues, screen-shake, etc. Argument is the number
        /// of lives lost (usually 1, but boss enemies with LivesCost &gt; 1
        /// fire a single event carrying the larger value).
        /// </summary>
        public static event System.Action<int> OnLifeLost;

        // ----- Helpers ----------------------------------------------------

        private void UnsubscribeEnemy(EnemyHealth health)
        {
            if (health == null) return;
            health.OnDied -= HandleEnemyKilled;

            var movement = health.GetComponent<EnemyMovement>();
            if (movement != null)
            {
                movement.OnZoneLeaked  -= HandleZoneLeak;
                movement.OnReachedGoal -= HandleEnemyReachedGoal;
            }
        }

        private void DecrementAndCheckComplete()
        {
            activeEnemyCount--;
            CheckWaveComplete();
        }

        private void CheckWaveComplete()
        {
            if (!spawningComplete) return;
            if (activeEnemyCount > 0) return;

            // Guard: don't start the next wave if the match is already decided.
            var ms = MatchState.Instance;
            if (ms == null || ms.Phase == MatchPhase.Defeat || ms.Phase == MatchPhase.Victory)
                return;

            // Award per-wave bonuses BEFORE advancing the wave (so waveLeakCounts still
            // reflects this wave's leaks, and goldEarnedThisWave still accumulates this
            // wave's bonus on top of kill gold). Completion bonus is unconditional;
            // no-leak bonus only if the player's waveLeakCounts entry is exactly 0.
            AwardWaveCompletionBonuses();

            Debug.Log($"[WaveManager] Wave {currentWaveIndex + 1} complete. Starting next wave.");
            StartNextWave();
        }

        // Server-only. Iterates active players, awards CompletionBonus to each, plus
        // NoLeaksBonus to those whose per-wave leak counter is zero. Floating-text popups
        // are spawned at each player's builder position so the reward is visible in-world.
        // Skipped silently if no goldConfig or no entry for this wave.
        private void AwardWaveCompletionBonuses()
        {
            var entry = goldConfig?.GetWaveEntry(currentWaveIndex + 1);
            if (entry == null) return;

            int completionBonus = entry.CompletionBonus;
            int noLeaksBonus    = entry.NoLeaksBonus;
            if (completionBonus <= 0 && noLeaksBonus <= 0) return;

            foreach (var pms in PlayerMatchState.AllPlayers)
            {
                var gm = PlayerGoldManager.GetForClient(pms.OwnerClientId);
                if (gm == null) continue;

                int award = 0;
                if (completionBonus > 0) award += completionBonus;

                int slotIdx = (int)pms.Slot;
                bool zeroLeaks = slotIdx >= 0 && slotIdx < waveLeakCounts.Count
                                && waveLeakCounts[slotIdx] == 0;
                if (zeroLeaks && noLeaksBonus > 0) award += noLeaksBonus;

                if (award > 0)
                {
                    gm.AwardGold(award);

                    // Surface the bonus in-world so players see it land. Position the
                    // popup at the player's builder if we can find one; otherwise the
                    // origin (popups still spawn, just centered on world origin).
                    Vector3 popupPos = Vector3.zero;
                    var builder = Builder.GetForClient(pms.OwnerClientId);
                    if (builder != null) popupPos = builder.CurrentPosition;
                    ShowGoldRewardClientRpc(popupPos, award);
                }
            }
        }
    }
}
