using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using FishNet;
using FishNet.Object;
using IStalk.Data;
using IStalk.Network;

namespace IStalk.Core
{
    public class MapGenerator : MonoBehaviour
    {
        private GlobalRules_SO Rules => GlobalRules_SO.Instance;

        private System.Random _rng;
        private List<PlacedRoom> _placedRooms = new();
        private HashSet<Vector2Int> _occupiedCells = new();
        private List<Vector3> _runnerSpawnPoints = new();
        private List<Vector3> _altarPositions = new();
        private List<Vector3> _lootSlotPositions = new();
        private List<Vector3> _shrinePositions = new();
        private Vector3 _mapCenter;

        [SerializeField] private NetworkObject _shrinePrefab;

        public IReadOnlyList<PlacedRoom> PlacedRooms => _placedRooms;
        public IReadOnlyList<Vector3> AltarPositions => _altarPositions;
        public IReadOnlyList<Vector3> LootSlotPositions => _lootSlotPositions;
        public IReadOnlyList<Vector3> ShrinePositions => _shrinePositions;

        public void Generate()
        {
            _rng = new System.Random(GameStartData.MapSeed);
            _placedRooms.Clear();
            _occupiedCells.Clear();
            _runnerSpawnPoints.Clear();
            _altarPositions.Clear();
            _lootSlotPositions.Clear();
            _shrinePositions.Clear();

            var allRooms = ItemDatabase.Instance.AllRooms.ToList();
            var startRooms = allRooms.Where(r => r.category == RoomCategory.StartingArea).ToList();
            var corridors = allRooms.Where(r => r.category == RoomCategory.Corridor).ToList();
            var smallRooms = allRooms.Where(r => r.category == RoomCategory.SmallRoom).ToList();
            var largeRooms = allRooms.Where(r => r.category == RoomCategory.LargeRoom).ToList();

            int targetRooms = _rng.Next(Rules.minRoomsPerMap, Rules.maxRoomsPerMap + 1);
            int numFloors = _rng.Next(Rules.minFloors, Rules.maxFloors + 1);

            PlaceStartingRoom(startRooms.Count > 0 ? startRooms : smallRooms);

            int largeCount = 0;
            int attempts = 0;
            int maxAttempts = targetRooms * 10;

            while (_placedRooms.Count < targetRooms && attempts < maxAttempts)
            {
                attempts++;
                RoomNode_SO candidate = PickCandidate(corridors, smallRooms, largeRooms, largeCount);
                if (candidate == null) continue;

                if (TryPlace(candidate, numFloors))
                {
                    if (candidate.category == RoomCategory.LargeRoom)
                        largeCount++;
                }
            }

            ValidateConnectivity();
            PopulateContent();
            CalculateMapCenter();

            if (InstanceFinder.IsServerStarted)
                SpawnRoomPrefabs();
        }

        private void PlaceStartingRoom(List<RoomNode_SO> candidates)
        {
            if (candidates.Count == 0) return;
            var room = candidates[_rng.Next(candidates.Count)];
            var placed = new PlacedRoom(room, Vector2Int.zero);
            _placedRooms.Add(placed);
            OccupyCells(placed);
        }

        private RoomNode_SO PickCandidate(List<RoomNode_SO> corridors, List<RoomNode_SO> smallRooms, List<RoomNode_SO> largeRooms, int largeCount)
        {
            float corridorRatio = _placedRooms.Count > 0
                ? (float)_placedRooms.Count(r => r.Data.category == RoomCategory.Corridor) / _placedRooms.Count
                : 0f;

            List<RoomNode_SO> pool;
            if (corridorRatio < Rules.corridorRatioMin && corridors.Count > 0)
                pool = corridors;
            else if (largeCount < Rules.maxLargeRooms && largeRooms.Count > 0 && _rng.NextDouble() < 0.15)
                pool = largeRooms;
            else
                pool = smallRooms.Count > 0 ? smallRooms : corridors;

            if (pool.Count == 0) return null;

            float totalWeight = pool.Sum(r => r.spawnWeight);
            float roll = (float)(_rng.NextDouble() * totalWeight);
            float cumulative = 0;
            foreach (var room in pool)
            {
                cumulative += room.spawnWeight;
                if (roll <= cumulative)
                {
                    if (room.maxPerMap > 0 && _placedRooms.Count(p => p.Data == room) >= room.maxPerMap)
                        continue;
                    return room;
                }
            }
            return pool[_rng.Next(pool.Count)];
        }

        private bool TryPlace(RoomNode_SO room, int numFloors)
        {
            var openConnectors = GetOpenConnectors();
            if (openConnectors.Count == 0) return false;

            Shuffle(openConnectors);

            foreach (var (pos, direction) in openConnectors)
            {
                ConnectionTag needed = GetOppositeConnection(direction);
                if (!room.openConnections.Contains(needed)) continue;

                Vector2Int targetCell = pos + GetDirectionOffset(direction);
                if (targetCell.y >= numFloors) continue;

                if (CanOccupy(targetCell, room.gridSize))
                {
                    var placed = new PlacedRoom(room, targetCell);
                    _placedRooms.Add(placed);
                    OccupyCells(placed);
                    return true;
                }
            }
            return false;
        }

        private List<(Vector2Int pos, ConnectionTag dir)> GetOpenConnectors()
        {
            var result = new List<(Vector2Int, ConnectionTag)>();
            foreach (var placed in _placedRooms)
            {
                foreach (var conn in placed.Data.openConnections)
                {
                    Vector2Int neighbor = placed.GridPosition + GetDirectionOffset(conn);
                    if (!_occupiedCells.Contains(neighbor))
                        result.Add((placed.GridPosition, conn));
                }
            }
            return result;
        }

        private void OccupyCells(PlacedRoom room)
        {
            for (int x = 0; x < room.Data.gridSize.x; x++)
            for (int y = 0; y < room.Data.gridSize.y; y++)
                _occupiedCells.Add(room.GridPosition + new Vector2Int(x, y));
        }

        private bool CanOccupy(Vector2Int origin, Vector3Int size)
        {
            for (int x = 0; x < size.x; x++)
            for (int y = 0; y < size.y; y++)
                if (_occupiedCells.Contains(origin + new Vector2Int(x, y)))
                    return false;
            return true;
        }

        private void ValidateConnectivity()
        {
            if (_placedRooms.Count <= 1) return;

            var visited = new HashSet<int> { 0 };
            var queue = new Queue<int>();
            queue.Enqueue(0);

            while (queue.Count > 0)
            {
                int idx = queue.Dequeue();
                var current = _placedRooms[idx];

                for (int i = 0; i < _placedRooms.Count; i++)
                {
                    if (visited.Contains(i)) continue;
                    if (AreAdjacent(current, _placedRooms[i]))
                    {
                        visited.Add(i);
                        queue.Enqueue(i);
                    }
                }
            }

            if (visited.Count < _placedRooms.Count)
            {
                for (int i = _placedRooms.Count - 1; i >= 0; i--)
                {
                    if (!visited.Contains(i))
                        _placedRooms.RemoveAt(i);
                }
            }
        }

        private bool AreAdjacent(PlacedRoom a, PlacedRoom b)
        {
            Vector2Int diff = b.GridPosition - a.GridPosition;
            if (Mathf.Abs(diff.x) + Mathf.Abs(diff.y) != 1) return false;

            ConnectionTag dirFromA = GetConnectionFromOffset(diff);
            ConnectionTag dirFromB = GetOppositeConnection(dirFromA);

            return a.Data.openConnections.Contains(dirFromA) && b.Data.openConnections.Contains(dirFromB);
        }

        private void PopulateContent()
        {
            int altarsPlaced = 0;
            int targetAltars = _rng.Next(Rules.minAltars, Rules.maxAltars + 1);

            foreach (var room in _placedRooms)
            {
                Vector3 worldPos = GridToWorld(room.GridPosition);

                if (room.Data.isValidRunnerSpawn)
                    _runnerSpawnPoints.Add(worldPos);

                if (room.Data.isValidAltarLocation && altarsPlaced < targetAltars)
                {
                    int distFromSpawn = Mathf.Abs(room.GridPosition.x) + Mathf.Abs(room.GridPosition.y);
                    if (distFromSpawn >= Rules.altarMinDistanceFromSpawn)
                    {
                        _altarPositions.Add(worldPos);
                        altarsPlaced++;
                    }
                }

                for (int i = 0; i < room.Data.lootSpawnSlots; i++)
                {
                    if (_rng.NextDouble() < Rules.lootSpawnProbability)
                    {
                        Vector3 offset = new(
                            (float)(_rng.NextDouble() - 0.5) * Rules.cellWidth * 0.8f,
                            0.5f,
                            (float)(_rng.NextDouble() - 0.5) * Rules.cellWidth * 0.8f
                        );
                        _lootSlotPositions.Add(worldPos + offset);
                    }
                }

                if (room.Data.canContainShrine && _shrinePositions.Count < Rules.maxShrinesPerMap)
                {
                    _shrinePositions.Add(worldPos + Vector3.up * 0.5f);
                }
            }
        }

        public void SpawnShrines()
        {
            if (!InstanceFinder.IsServerStarted) return;
            if (_shrinePrefab == null) return;

            foreach (var pos in _shrinePositions)
            {
                var go = Instantiate(_shrinePrefab, pos, Quaternion.identity);
                InstanceFinder.ServerManager.Spawn(go.GetComponent<NetworkObject>());
            }
        }

        private void SpawnRoomPrefabs()
        {
            foreach (var room in _placedRooms)
            {
                if (room.Data.roomPrefab == null) continue;

                Vector3 worldPos = GridToWorld(room.GridPosition);
                var nob = room.Data.roomPrefab.GetComponent<NetworkObject>();

                if (nob != null)
                {
                    var spawned = Instantiate(room.Data.roomPrefab, worldPos, Quaternion.identity);
                    InstanceFinder.ServerManager.Spawn(spawned.GetComponent<NetworkObject>());
                }
                else
                {
                    Instantiate(room.Data.roomPrefab, worldPos, Quaternion.identity);
                }
            }
        }

        private void CalculateMapCenter()
        {
            if (_placedRooms.Count == 0)
            {
                _mapCenter = Vector3.zero;
                return;
            }

            Vector3 sum = Vector3.zero;
            foreach (var room in _placedRooms)
                sum += GridToWorld(room.GridPosition);
            _mapCenter = sum / _placedRooms.Count;
        }

        public Vector3 GetMapCenter() => _mapCenter;

        public Vector3 GetRunnerSpawnPoint()
        {
            if (_runnerSpawnPoints.Count == 0)
                return _placedRooms.Count > 0 ? GridToWorld(_placedRooms[0].GridPosition) : Vector3.zero;
            return _runnerSpawnPoints[_rng.Next(_runnerSpawnPoints.Count)];
        }

        public Bounds GetMapBounds()
        {
            if (_placedRooms.Count == 0) return new Bounds(Vector3.zero, Vector3.one * 10);

            Vector3 min = Vector3.one * float.MaxValue;
            Vector3 max = Vector3.one * float.MinValue;

            foreach (var room in _placedRooms)
            {
                Vector3 wp = GridToWorld(room.GridPosition);
                min = Vector3.Min(min, wp);
                max = Vector3.Max(max, wp + new Vector3(Rules.cellWidth, Rules.cellHeight, Rules.cellWidth));
            }
            var bounds = new Bounds();
            bounds.SetMinMax(min, max);
            return bounds;
        }

        private Vector3 GridToWorld(Vector2Int cell)
        {
            return new Vector3(cell.x * Rules.cellWidth, 0, cell.y * Rules.cellWidth);
        }

        private Vector2Int GetDirectionOffset(ConnectionTag dir) => dir switch
        {
            ConnectionTag.Left => Vector2Int.left,
            ConnectionTag.Right => Vector2Int.right,
            ConnectionTag.Up => Vector2Int.up,
            ConnectionTag.Down => Vector2Int.down,
            _ => Vector2Int.zero
        };

        private ConnectionTag GetOppositeConnection(ConnectionTag dir) => dir switch
        {
            ConnectionTag.Left => ConnectionTag.Right,
            ConnectionTag.Right => ConnectionTag.Left,
            ConnectionTag.Up => ConnectionTag.Down,
            ConnectionTag.Down => ConnectionTag.Up,
            _ => ConnectionTag.Left
        };

        private ConnectionTag GetConnectionFromOffset(Vector2Int offset)
        {
            if (offset == Vector2Int.left) return ConnectionTag.Left;
            if (offset == Vector2Int.right) return ConnectionTag.Right;
            if (offset == Vector2Int.up) return ConnectionTag.Up;
            return ConnectionTag.Down;
        }

        private void Shuffle<T>(List<T> list)
        {
            for (int i = list.Count - 1; i > 0; i--)
            {
                int j = _rng.Next(i + 1);
                (list[i], list[j]) = (list[j], list[i]);
            }
        }
    }

    public class PlacedRoom
    {
        public RoomNode_SO Data { get; }
        public Vector2Int GridPosition { get; }

        public PlacedRoom(RoomNode_SO data, Vector2Int gridPosition)
        {
            Data = data;
            GridPosition = gridPosition;
        }
    }
}
