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

namespace TD.Gameplay
{
    /// <summary>
    /// Scene singleton that computes shortest-path routes from any tile to the
    /// nearest goal tile using A* on the runtime walkability grid.
    /// </summary>
    /// <remarks>
    /// <b>Algorithm:</b> A* with the octile-distance heuristic on an 8-connected grid.
    /// Cardinal steps cost 1.0, diagonal steps cost √2. Octile distance is the admissible
    /// heuristic for this cost model and yields optimal paths. Diagonal moves require
    /// both "shoulder" cardinals to be walkable (corner-cut prevention) — preserves
    /// maze-building, since a 1-tile diagonal gap between two walls won't admit enemies.
    ///
    /// <b>Path smoothing:</b> After A* produces a tile-by-tile path, <see cref="SmoothPath"/>
    /// greedily collapses intermediate waypoints whenever a grid line-of-sight is clear
    /// between the current anchor and a later waypoint. This produces the any-angle
    /// straight-line movement seen in WC3/Wintermaul — enemies walk directly across open
    /// space rather than hugging 45° grid steps.
    ///
    /// <b>Who calls this:</b>
    /// <list type="bullet">
    ///   <item><see cref="EnemyMovement"/> calls <see cref="ComputePath"/> once on
    ///         spawn and again whenever <see cref="OnPathsInvalidated"/> fires.</item>
    /// </list>
    ///
    /// <b>Invalidation:</b> Subscribes to <see cref="LevelLoader.OnWalkabilityChanged"/>.
    /// When a tower is placed or sold, <c>LevelLoader.SetWalkable</c> fires that event
    /// and <see cref="OnPathsInvalidated"/> is relayed to all active enemies, which
    /// each recompute their own path from their current tile.
    ///
    /// <b>Goal tile set:</b> Built once on <c>Start</c> from
    /// <c>LevelLoader.LevelData.Goals[].TileArea</c>. Goal tiles never change at
    /// runtime (they are baked into the level), so there is no need to rebuild the set.
    ///
    /// <b>No caching:</b> Paths are computed on demand per enemy. On typical TD grid
    /// sizes (50×50 or smaller) a single A* run takes &lt;1 ms. If profiling shows
    /// otherwise, add a per-startTile cache here.
    /// </remarks>
    public class PathfindingService : MonoBehaviour
    {
        // ----- Singleton --------------------------------------------------

        public static PathfindingService Instance { get; private set; }

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

        /// <summary>
        /// Fired on every peer when the walkability grid changes (tower placed/sold).
        /// <see cref="EnemyMovement"/> subscribes per-instance to recompute its path.
        /// </summary>
        public event System.Action OnPathsInvalidated;

        // ----- State ------------------------------------------------------

        // Built once on Start from LevelData.Goals[].TileArea.
        private HashSet<Vector2Int> goalTiles;

        // Precomputed octile-distance-to-nearest-goal, indexed by [y * gridWidth + x] in
        // grid-local space (subtract GridOriginTile to convert world-tile → array index).
        // Computed ONCE on Start. Without this, the A* heuristic scanned every goal tile
        // (~48 tiles for the 9-player map) on every node visit — making the heuristic
        // O(node visits * goal count) per A* run. With this table, it's O(1) per node.
        // Tiles outside the grid use Heuristic's fallback path (per-goal scan); they are
        // rare so the table isn't extended to cover them.
        private float[] heuristicTable;
        private Vector2Int heuristicOrigin;
        private Vector2Int heuristicSize;

        // A* scratch collections — allocated once and cleared per run to avoid GC.
        // PathfindingService is a singleton, so single-instance scratch is safe.
        // gScore is float to support diagonal cost √2; the priority queue matches.
        private readonly Dictionary<Vector2Int, Vector2Int>  cameFrom  = new Dictionary<Vector2Int, Vector2Int>();
        private readonly Dictionary<Vector2Int, float>       gScore    = new Dictionary<Vector2Int, float>();
        private readonly SimplePriorityQueue                  openSet   = new SimplePriorityQueue();

        // ----- Lifecycle --------------------------------------------------

        private void Awake()
        {
            if (Instance != null && Instance != this)
            {
                Debug.LogError("[PathfindingService] Duplicate instance detected. " +
                               "Only one PathfindingService should exist per scene.");
                Destroy(gameObject);
                return;
            }
            Instance = this;
        }

        private void Start()
        {
            BuildGoalTileSet();
            BuildHeuristicTable();

            var loader = LevelLoader.Instance;
            if (loader != null)
                loader.OnWalkabilityChanged += HandleWalkabilityChanged;
            else
                Debug.LogWarning("[PathfindingService] LevelLoader not found in Start. " +
                                 "Path invalidation will not work.");
        }

        private void OnDestroy()
        {
            if (Instance == this) Instance = null;

            var loader = LevelLoader.Instance;
            if (loader != null)
                loader.OnWalkabilityChanged -= HandleWalkabilityChanged;
        }

        // ----- Public API -------------------------------------------------

        /// <summary>
        /// Computes the shortest walkable path from <paramref name="startTile"/> to
        /// the nearest goal tile. Returns an ordered list of tiles to visit, starting
        /// with the first step AFTER <paramref name="startTile"/> and ending with the
        /// reached goal tile. Returns an empty list if no path exists or LevelLoader
        /// is unavailable (should not occur — TowerPlacementManager guarantees a path
        /// always exists after any placement).
        /// </summary>
        /// <param name="ignoreTowers">
        /// When true, pathing uses the baked terrain grid
        /// (<see cref="LevelLoader.IsBaseWalkable"/>) instead of the runtime
        /// tower-stamped grid, so the path soars over towers. Used by flying enemies.
        /// </param>
        public List<Vector2Int> ComputePath(Vector2Int startTile, bool ignoreTowers = false)
        {
            var loader = LevelLoader.Instance;
            if (loader == null || !loader.IsLoaded || goalTiles == null || goalTiles.Count == 0)
            {
                Debug.LogWarning("[PathfindingService] Cannot compute path: " +
                                 "LevelLoader unavailable or no goal tiles baked.");
                return new List<Vector2Int>();
            }

            // If the requested start is non-walkable, the enemy was caught
            // standing inside a freshly-stamped tower footprint (or any tile
            // that just became blocked). Nudge to the nearest walkable tile so
            // the enemy can resume routing instead of repeatedly failing.
            // Search outward in Chebyshev rings — closest 8-neighbor wins.
            // (For flyers this nudge is effectively never needed — the baked grid
            // doesn't change — but it's cheap and keeps the two paths uniform.)
            if (!IsTileWalkable(loader, startTile, ignoreTowers))
            {
                if (TryFindNearestWalkable(startTile, loader, maxRadius: 4, ignoreTowers, out var nudged))
                    startTile = nudged;
                else
                    return new List<Vector2Int>();
            }

            return RunAStar(startTile, loader, ignoreTowers);
        }

        // Single seam for "is this tile traversable for this enemy kind": grounded
        // enemies see the runtime grid (towers block them); flyers see only the baked
        // terrain grid (towers are invisible to them).
        private static bool IsTileWalkable(LevelLoader loader, Vector2Int tile, bool ignoreTowers)
            => ignoreTowers ? loader.IsBaseWalkable(tile) : loader.IsWalkable(tile);

        // Expanding ring search (Chebyshev / 8-connected) for the nearest
        // walkable tile around <paramref name="origin"/>. Caps at maxRadius to
        // keep this O(maxRadius²) in the worst case. Used by ComputePath when
        // an enemy's start tile becomes non-walkable mid-flight.
        private static bool TryFindNearestWalkable(Vector2Int origin, LevelLoader loader,
                                                   int maxRadius, bool ignoreTowers, out Vector2Int found)
        {
            for (int r = 1; r <= maxRadius; r++)
            {
                for (int dy = -r; dy <= r; dy++)
                {
                    for (int dx = -r; dx <= r; dx++)
                    {
                        // Only walk the OUTER ring at distance r (skip interior — already checked at smaller r).
                        if (Mathf.Abs(dx) != r && Mathf.Abs(dy) != r) continue;
                        var tile = new Vector2Int(origin.x + dx, origin.y + dy);
                        if (IsTileWalkable(loader, tile, ignoreTowers))
                        {
                            found = tile;
                            return true;
                        }
                    }
                }
            }
            found = default;
            return false;
        }

        // ----- A* implementation ------------------------------------------

        private List<Vector2Int> RunAStar(Vector2Int start, LevelLoader loader, bool ignoreTowers)
        {
            cameFrom.Clear();
            gScore.Clear();
            openSet.Clear();

            gScore[start] = 0f;
            openSet.Enqueue(start, Heuristic(start));

            while (openSet.Count > 0)
            {
                Vector2Int current = openSet.Dequeue();

                if (goalTiles.Contains(current))
                {
                    var tilePath = ReconstructPath(start, current);
                    return SmoothPath(start, tilePath, loader, ignoreTowers);
                }

                float currentG = gScore.TryGetValue(current, out float g) ? g : float.MaxValue;

                foreach (var neighbor in GridCoordinates.GetNeighbors8(current))
                {
                    if (!IsTileWalkable(loader, neighbor, ignoreTowers)) continue;

                    // Corner-cut prevention: for a diagonal step, both cardinal
                    // shoulder tiles must also be walkable. Otherwise enemies could
                    // squeeze through 1-tile diagonal gaps between walls — that
                    // would break the maze-building design intent.
                    if (GridCoordinates.IsDiagonal(current, neighbor))
                    {
                        GridCoordinates.GetCornerShoulders(current, neighbor,
                            out var shoulderA, out var shoulderB);
                        if (!IsTileWalkable(loader, shoulderA, ignoreTowers)
                            || !IsTileWalkable(loader, shoulderB, ignoreTowers))
                            continue;
                    }

                    float tentativeG = currentG + GridCoordinates.StepCost(current, neighbor);
                    if (gScore.TryGetValue(neighbor, out float existingG)
                        && tentativeG >= existingG) continue;

                    cameFrom[neighbor] = current;
                    gScore[neighbor]   = tentativeG;
                    float f = tentativeG + Heuristic(neighbor);

                    // Re-enqueue with updated priority. SimplePriorityQueue handles
                    // duplicate entries by ignoring higher-cost duplicates on dequeue.
                    openSet.Enqueue(neighbor, f);
                }
            }

            // No path found. This can legitimately happen during normal play
            // when an enemy is in a disconnected pocket created by a tower
            // placement (the placement BFS only verifies spawner→exit, not
            // every enemy's current tile). The EnemyMovement caller dedupes
            // its own warning so this doesn't spam the console on every
            // walkability change while an enemy is stuck.
            return new List<Vector2Int>();
        }

        // Octile distance to the nearest goal tile. Admissible heuristic for an
        // 8-connected uniform-cost grid (cardinal 1, diagonal √2). Hot path: served
        // from heuristicTable (O(1)) when the tile is in the grid bounds, otherwise
        // falls back to scanning every goal (O(goalCount)).
        private float Heuristic(Vector2Int tile)
        {
            if (heuristicTable != null)
            {
                int x = tile.x - heuristicOrigin.x;
                int y = tile.y - heuristicOrigin.y;
                if (x >= 0 && x < heuristicSize.x && y >= 0 && y < heuristicSize.y)
                {
                    return heuristicTable[y * heuristicSize.x + x];
                }
            }

            // Out-of-grid fallback. A* shouldn't usually visit these (it expands only
            // from walkable tiles, which are in-bounds), but we keep correctness for
            // any callers that hand us a stray tile.
            float best = float.MaxValue;
            foreach (var goal in goalTiles)
            {
                float d = GridCoordinates.OctileDistance(tile, goal);
                if (d < best) best = d;
            }
            return best;
        }

        private List<Vector2Int> ReconstructPath(Vector2Int start, Vector2Int goal)
        {
            var path = new List<Vector2Int>();
            Vector2Int current = goal;

            while (current != start)
            {
                path.Add(current);
                if (!cameFrom.TryGetValue(current, out current))
                    break; // shouldn't happen
            }

            path.Reverse();
            return path;
        }

        // ----- Path smoothing ---------------------------------------------

        /// <summary>
        /// Greedy line-of-sight path simplification. Walks the input tile path and
        /// collapses runs of tiles into single waypoints whenever a straight grid
        /// line-of-sight is clear. Produces any-angle paths from the otherwise
        /// 45°-stepped 8-connected A* output.
        /// </summary>
        /// <remarks>
        /// Algorithm: maintain an "anchor" (initially the start tile). For each
        /// position in the path, find the FARTHEST subsequent waypoint that has clear
        /// LOS from the anchor. Add it to the smoothed result and make it the new
        /// anchor. Repeat. O(n²) in path length, acceptable for typical TD path
        /// lengths (&lt; 100 tiles).
        ///
        /// LOS check uses Bresenham line walk with the same corner-cut rules as A*
        /// — a smoothed segment never crosses through a non-walkable tile and never
        /// squeezes through a diagonal corner.
        /// </remarks>
        private List<Vector2Int> SmoothPath(Vector2Int start, List<Vector2Int> path,
                                             LevelLoader loader, bool ignoreTowers)
        {
            if (path.Count <= 1) return path;

            var result = new List<Vector2Int>(path.Count);
            Vector2Int anchor = start;
            int i = 0;

            while (i < path.Count)
            {
                // Find the farthest waypoint we can directly reach from the anchor.
                int farthest = i;
                for (int j = path.Count - 1; j > i; j--)
                {
                    if (HasLineOfSight(anchor, path[j], loader, ignoreTowers))
                    {
                        farthest = j;
                        break;
                    }
                }

                result.Add(path[farthest]);
                anchor = path[farthest];
                i = farthest + 1;
            }

            return result;
        }

        /// <summary>
        /// Grid line-of-sight test from <paramref name="a"/> to <paramref name="b"/>.
        /// Returns true if a straight Bresenham line between the two tiles only
        /// crosses walkable tiles, with no diagonal corner-cuts. Used by
        /// <see cref="SmoothPath"/> to decide whether two waypoints can be collapsed.
        /// </summary>
        private static bool HasLineOfSight(Vector2Int a, Vector2Int b, LevelLoader loader,
                                           bool ignoreTowers)
        {
            int x0 = a.x, y0 = a.y;
            int x1 = b.x, y1 = b.y;
            int dx = Mathf.Abs(x1 - x0);
            int dy = Mathf.Abs(y1 - y0);
            int sx = (x0 < x1) ? 1 : -1;
            int sy = (y0 < y1) ? 1 : -1;
            int err = dx - dy;

            int x = x0, y = y0;

            while (x != x1 || y != y1)
            {
                int e2 = 2 * err;
                bool steppedX = false, steppedY = false;

                if (e2 > -dy)
                {
                    err -= dy;
                    x   += sx;
                    steppedX = true;
                }
                if (e2 < dx)
                {
                    err += dx;
                    y   += sy;
                    steppedY = true;
                }

                // Diagonal step: enforce corner-cut prevention against the two
                // shoulder tiles (same rule A* uses).
                if (steppedX && steppedY)
                {
                    if (!IsTileWalkable(loader, new Vector2Int(x - sx, y), ignoreTowers)) return false;
                    if (!IsTileWalkable(loader, new Vector2Int(x, y - sy), ignoreTowers)) return false;
                }

                if (!IsTileWalkable(loader, new Vector2Int(x, y), ignoreTowers)) return false;
            }

            return true;
        }

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

        private void BuildGoalTileSet()
        {
            goalTiles = new HashSet<Vector2Int>();
            var loader = LevelLoader.Instance;
            if (loader == null || loader.LevelData == null || loader.LevelData.Goals == null)
            {
                Debug.LogWarning("[PathfindingService] No LevelData or Goals found. " +
                                 "Enemies will have no destination.");
                return;
            }

            foreach (var goal in loader.LevelData.Goals)
            {
                if (goal.TileArea == null) continue;
                foreach (var tile in goal.TileArea)
                    goalTiles.Add(tile);
            }

            Debug.Log($"[PathfindingService] Goal tile set built: {goalTiles.Count} tiles.");
        }

        // Precomputes the octile-distance-to-nearest-goal for every tile in the grid.
        // Runs once on Start (cheap: ~3000 tiles × ~50 goals = 150K octile evaluations on
        // the 9-player map, well under a millisecond). The output is a flat float[] keyed
        // by grid-local index, hot for the A* heuristic.
        private void BuildHeuristicTable()
        {
            var loader = LevelLoader.Instance;
            if (loader == null || loader.LevelData == null)
            {
                heuristicTable = null;
                return;
            }

            var levelData = loader.LevelData;
            heuristicOrigin = levelData.GridOriginTile;
            heuristicSize = levelData.GridSize;

            int total = heuristicSize.x * heuristicSize.y;
            if (total <= 0 || goalTiles == null || goalTiles.Count == 0)
            {
                heuristicTable = null;
                return;
            }

            heuristicTable = new float[total];
            for (int y = 0; y < heuristicSize.y; y++)
            {
                for (int x = 0; x < heuristicSize.x; x++)
                {
                    Vector2Int worldTile = new Vector2Int(
                        heuristicOrigin.x + x,
                        heuristicOrigin.y + y);

                    float best = float.MaxValue;
                    foreach (var goal in goalTiles)
                    {
                        float d = GridCoordinates.OctileDistance(worldTile, goal);
                        if (d < best) best = d;
                    }
                    heuristicTable[y * heuristicSize.x + x] = best;
                }
            }

            Debug.Log($"[PathfindingService] Heuristic table built: {total} tiles, " +
                      $"origin={heuristicOrigin}, size={heuristicSize}.");
        }

        private void HandleWalkabilityChanged()
        {
            OnPathsInvalidated?.Invoke();
        }
    }

    // -------------------------------------------------------------------------
    // Minimal priority queue for A*. Uses a sorted list of (priority, tile) pairs.
    // Suitable for the grid sizes in this project (typically < 10,000 tiles).
    // Replace with a binary heap if profiling shows this as a bottleneck.
    // -------------------------------------------------------------------------
    internal sealed class SimplePriorityQueue
    {
        // Float priority to support diagonal cost (√2) in 8-connected A*.
        private readonly List<(float priority, Vector2Int tile)> heap
            = new List<(float, Vector2Int)>();

        public int Count => heap.Count;

        public void Clear() => heap.Clear();

        public void Enqueue(Vector2Int tile, float priority)
        {
            heap.Add((priority, tile));
            SiftUp(heap.Count - 1);
        }

        public Vector2Int Dequeue()
        {
            Vector2Int result = heap[0].tile;
            int last = heap.Count - 1;
            heap[0] = heap[last];
            heap.RemoveAt(last);
            if (heap.Count > 0) SiftDown(0);
            return result;
        }

        private void SiftUp(int i)
        {
            while (i > 0)
            {
                int parent = (i - 1) / 2;
                if (heap[parent].priority <= heap[i].priority) break;
                (heap[i], heap[parent]) = (heap[parent], heap[i]);
                i = parent;
            }
        }

        private void SiftDown(int i)
        {
            int count = heap.Count;
            while (true)
            {
                int smallest = i;
                int left  = 2 * i + 1;
                int right = 2 * i + 2;
                if (left  < count && heap[left].priority  < heap[smallest].priority) smallest = left;
                if (right < count && heap[right].priority < heap[smallest].priority) smallest = right;
                if (smallest == i) break;
                (heap[i], heap[smallest]) = (heap[smallest], heap[i]);
                i = smallest;
            }
        }
    }
}
