// Assets/_Project/Scripts/Gameplay/EnemyMovement.cs
using System.Collections.Generic;
using Unity.Netcode;
using UnityEngine;
using TD.Core;
using TD.Levels;

namespace TD.Gameplay
{
    /// <summary>
    /// Server-authoritative enemy movement along a dynamically computed A* path.
    /// </summary>
    /// <remarks>
    /// <b>Initialization:</b> Call <see cref="InitializeServer"/> after <c>Instantiate</c>
    /// and before <c>NetworkObject.Spawn()</c>. Provides the base move speed (from
    /// <see cref="EnemyDefinition.MoveSpeed"/>) and the tile the enemy spawned on.
    ///
    /// <b>Path lifecycle:</b>
    /// <list type="bullet">
    ///   <item><see cref="OnNetworkSpawn"/> (server): queries <see cref="PathfindingService"/>
    ///         and stores the tile waypoint list.</item>
    ///   <item>Each frame: moves toward the world center of <c>remainingPath[0]</c>.
    ///         When within snap distance, pops the waypoint and checks for zone transitions.</item>
    ///   <item>When <see cref="PathfindingService.OnPathsInvalidated"/> fires (tower placed /
    ///         sold), <see cref="RecomputePath"/> reruns A* from the current tile.</item>
    ///   <item>When <c>remainingPath</c> is empty after a pop, the enemy has reached the
    ///         goal — <see cref="OnReachedGoal"/> fires and the enemy is despawned.</item>
    /// </list>
    ///
    /// <b>Zone leak tracking:</b> Each time a waypoint is popped, <see cref="LevelLoader.GetOwner"/>
    /// is compared against <c>currentZone</c>. A change means the enemy has crossed a zone
    /// boundary. <see cref="OnZoneLeaked"/> fires with the zone being LEFT so <c>WaveManager</c>
    /// can debit the correct player's life pool.
    ///
    /// <b>Speed:</b> Effective speed = <c>moveSpeed * EnemyStatus.GetSpeedMultiplier()</c>.
    /// <c>EnemyStatus</c> replicates the multiplier as a NetworkVariable so movement looks
    /// correct on all peers.
    ///
    /// <b>Movement replication:</b> Requires a <c>NetworkTransform</c> on the prefab.
    /// Position is written by the server; <c>NetworkTransform</c> interpolates on clients.
    /// </remarks>
    [RequireComponent(typeof(NetworkObject))]
    [RequireComponent(typeof(EnemyHealth))]
    [RequireComponent(typeof(EnemyStatus))]
    public class EnemyMovement : NetworkBehaviour
    {
        // Snap threshold in world units. When sqrMagnitude to the waypoint center
        // drops below this, we count the tile as reached and pop it.
        private const float WaypointSnapSq = 0.04f; // 0.2 world units

        // ----- Pre-spawn init (server-local) ----------------------------------

        private float      pendingMoveSpeed;
        private Vector2Int pendingSpawnerTile;
        private PlayerSlot pendingOwnerSlot;
        private bool       pendingIsFlying;
        private bool       hasPendingInit;

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

        private float             moveSpeed;
        // When true this enemy flies: it paths on the baked terrain grid (ignoring
        // towers) and never re-paths, since the baked grid doesn't change. Grounded
        // enemies path on the runtime grid and re-path on every walkability change.
        private bool              isFlying;
        private List<Vector2Int>  remainingPath = new List<Vector2Int>();
        private PlayerSlot        currentZone   = PlayerSlot.None;
        // Zone the enemy was spawned in — i.e., which player "owns" this enemy as part
        // of their wave. Used so OnZoneLeaked fires only when the enemy escapes that
        // origin zone (not when it transits through any subsequent zone on its way to
        // the goal). Without this, every zone crossing was counted as a leak; only
        // the originating player should be credited a leak per the design.
        private PlayerSlot        originZone    = PlayerSlot.None;
        // Latches once the enemy has crossed its origin zone's leak volume, so we
        // never double-count a leak if the enemy re-enters its origin (rare but
        // possible if pathing is dynamic).
        private bool              hasLeakedOriginZone;
        private EnemyStatus       status;
        private EnemyHealth       health;
        private bool              hasReachedGoal;
        private bool              wasStuck;        // dedupes the "no path" warning

        // ----- Events ---------------------------------------------------------

        /// <summary>
        /// Fired on the server EXACTLY ONCE per enemy, when it crosses out of its
        /// origin zone (the zone it spawned in) into another zone. The argument is the
        /// origin zone — the player who "owns" this enemy and should be credited a
        /// leak. Transit through subsequent zones does NOT fire this event.
        /// <c>WaveManager</c> subscribes to increment the per-player leak counter.
        /// </summary>
        public event System.Action<PlayerSlot> OnZoneLeaked;

        /// <summary>
        /// Fired on the server when the enemy reaches the goal tile.
        /// Carries this component and the enemy's <see cref="EnemyHealth.LivesCost"/>
        /// so <c>WaveManager</c> can deduct the right number of lives in one call.
        /// The NetworkObject is despawned immediately after subscribers return.
        /// </summary>
        public event System.Action<EnemyMovement, int> OnReachedGoal;

        // ----- Server-only pre-spawn init -------------------------------------

        /// <summary>
        /// Called by <c>WaveManager</c> on the server after <c>Instantiate</c> and
        /// before <c>NetworkObject.Spawn()</c>. <paramref name="speed"/> comes from
        /// <see cref="EnemyDefinition.MoveSpeed"/>; <paramref name="spawnerTile"/> is
        /// the tile the enemy spawns on (used as the A* start node);
        /// <paramref name="ownerSlot"/> identifies which player "owns" this enemy
        /// for leak-attribution (the slot whose <c>PlayerZoneData</c> contained the
        /// spawner). It is NOT derived from the spawner tile's owner because spawner
        /// tiles sit inside <c>SpawnerVolume</c>, not <c>PlayerZoneVolume</c>, so
        /// their owner-grid entry is <see cref="PlayerSlot.None"/>.
        /// </summary>
        public void InitializeServer(float speed, Vector2Int spawnerTile, PlayerSlot ownerSlot,
                                     bool flying)
        {
            pendingMoveSpeed   = speed;
            pendingSpawnerTile = spawnerTile;
            pendingOwnerSlot   = ownerSlot;
            pendingIsFlying    = flying;
            hasPendingInit     = true;
        }

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

        public override void OnNetworkSpawn()
        {
            status = GetComponent<EnemyStatus>();
            health = GetComponent<EnemyHealth>();

            // Randomize the walk-cycle phase on every peer independently. Animations
            // are purely visual and not replicated, so each client picks its own offset —
            // the result looks correct everywhere without any network traffic.
            var animator = GetComponentInChildren<Animator>();
            if (animator != null)
                animator.Update(Random.Range(0f, 2f));

            if (!IsServer) return;

            if (!hasPendingInit)
            {
                Debug.LogError("[EnemyMovement] OnNetworkSpawn reached without InitializeServer " +
                               "having been called. Enemy will not move.");
                return;
            }

            moveSpeed = pendingMoveSpeed;
            isFlying  = pendingIsFlying;

            // Resolve starting zone from the spawner tile. This is what the enemy
            // observes as "the zone I am currently in." For SpawnerVolume tiles that
            // sit outside any PlayerZoneVolume (the common case) this is None — the
            // enemy will transition to its owner's zone on the first waypoint inside
            // that PlayerZoneVolume.
            var loader = LevelLoader.Instance;
            if (loader != null)
                currentZone = loader.GetOwner(pendingSpawnerTile);

            // Origin zone is the player who "owns" this enemy for leak attribution.
            // WaveManager passes it in (zone.Owner) because the spawner tile's owner-
            // grid entry is unreliable (typically None — see InitializeServer remarks).
            originZone = pendingOwnerSlot;
            hasLeakedOriginZone = false;

            // Compute the initial path from the spawn tile to the nearest goal.
            ComputeAndStorePath(pendingSpawnerTile);

            // Recompute when a tower is placed or sold — grounded enemies only.
            // Flyers path on the static baked grid, so tower changes can never affect
            // their route; subscribing would just trigger needless recomputes.
            if (!isFlying && PathfindingService.Instance != null)
                PathfindingService.Instance.OnPathsInvalidated += RecomputePath;
        }

        public override void OnNetworkDespawn()
        {
            if (PathfindingService.Instance != null)
                PathfindingService.Instance.OnPathsInvalidated -= RecomputePath;
        }

        // ----- Server update --------------------------------------------------

        private void Update()
        {
            if (!IsServer) return;
            if (health != null && health.IsHeld) return;
            if (remainingPath.Count == 0) return;

            float effectiveSpeed = moveSpeed * (status != null ? status.GetSpeedMultiplier() : 1f);
            Vector3 targetWorld  = GridCoordinates.GridToWorld(remainingPath[0]);

            // XZ-only delta. Some enemy prefabs (e.g. the skeleton) have their root
            // sitting above Y=0; if we measured 3D distance, the Y offset would push
            // it permanently above the snap threshold and waypoints would never
            // complete. Tile arrival is a horizontal-plane concept anyway.
            Vector3 toTarget = targetWorld - transform.position;
            toTarget.y = 0f;

            if (toTarget.sqrMagnitude <= WaypointSnapSq)
            {
                // Snap XZ to the tile center. Preserve Y so we don't drag a visual
                // pivot down through the plane.
                transform.position = new Vector3(
                    targetWorld.x, transform.position.y, targetWorld.z);
                AdvanceWaypoint();
            }
            else
            {
                transform.position += toTarget.normalized * (effectiveSpeed * Time.deltaTime);

                // Face the direction of travel.
                if (toTarget.sqrMagnitude > 0.0001f)
                    transform.rotation = Quaternion.LookRotation(toTarget);
            }

            // Per-frame zone tracking. With path smoothing, waypoints can be
            // multiple tiles apart and a straight-line segment may cross zone
            // boundaries. Checking the current tile every frame ensures
            // OnZoneLeaked fires the moment the enemy enters a new zone.
            CheckZoneTransition(GridCoordinates.WorldToGrid(transform.position));
        }

        // ----- Path management ------------------------------------------------

        private void AdvanceWaypoint()
        {
            Vector2Int arrivedTile = remainingPath[0];
            remainingPath.RemoveAt(0);

            CheckZoneTransition(arrivedTile);

            if (remainingPath.Count == 0)
                HandleGoalReached();
        }

        private void CheckZoneTransition(Vector2Int tile)
        {
            var loader = LevelLoader.Instance;
            if (loader == null) return;

            PlayerSlot newZone = loader.GetOwner(tile);
            if (newZone == currentZone) return;

            // The enemy is crossing a zone boundary. Only fire OnZoneLeaked if it's
            // the FIRST time this enemy escapes its ORIGIN zone — that's the
            // "I failed to stop it in my own maze" event the leak counter tracks.
            // Subsequent transit through other zones is just routing toward the goal
            // and doesn't credit any player a leak.
            if (!hasLeakedOriginZone
                && currentZone == originZone
                && currentZone != PlayerSlot.None)
            {
                hasLeakedOriginZone = true;
                OnZoneLeaked?.Invoke(originZone);
            }

            currentZone = newZone;
        }

        private void HandleGoalReached()
        {
            if (hasReachedGoal) return; // belt-and-suspenders: never fire twice
            hasReachedGoal = true;

            var health = GetComponent<EnemyHealth>();
            int livesCost = health != null ? health.LivesCost : 1;

            OnReachedGoal?.Invoke(this, livesCost);

            if (NetworkObject != null && NetworkObject.IsSpawned)
                NetworkObject.Despawn();
        }

        // ----- Path invalidation ----------------------------------------------

        // Called on server when LevelLoader.OnWalkabilityChanged fires (tower placed/sold).
        private void RecomputePath()
        {
            if (!IsServer) return;

            // Recompute from the enemy's current tile position.
            Vector2Int currentTile = GridCoordinates.WorldToGrid(transform.position);
            ComputeAndStorePath(currentTile);
        }

        private void ComputeAndStorePath(Vector2Int fromTile)
        {
            var service = PathfindingService.Instance;
            if (service == null)
            {
                Debug.LogWarning("[EnemyMovement] PathfindingService not found. Enemy will not move.");
                return;
            }

            remainingPath = service.ComputePath(fromTile, ignoreTowers: isFlying);

            // Dedupe the "no path" log: only emit on the transition into stuck
            // state, not every frame a walkability change re-fires the recompute.
            // Stuck enemies are a legitimate edge case (tower placement disconnected
            // a pocket the enemy was in — placement BFS only checks spawner→exit
            // reachability, not every enemy's current tile). The enemy will just
            // stand still until a placement change re-opens a route.
            if (remainingPath.Count == 0)
            {
                if (!wasStuck)
                {
                    Debug.Log($"[EnemyMovement] No path from {fromTile} — enemy is " +
                              "stuck in a disconnected pocket. Will retry on next walkability change.");
                    wasStuck = true;
                }
            }
            else
            {
                wasStuck = false;
            }
        }
    }
}
