using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using Facepunch;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Oxide.Core;
using Oxide.Core.Configuration;
using Oxide.Core.Plugins;
using Rust;
using UnityEngine;
using Random = UnityEngine.Random;

namespace Oxide.Plugins
{
    [Info("Dungeon Events", "Marte6", "1.1.8")]
    [Description("Create dungeons populated with NPCs, turrets and loot containers.")]
    public class DungeonEvents : RustPlugin
    {
        [PluginReference]
        Plugin NpcSpawn = null,
            Friends = null,
            Economics = null,
            ServerRewards = null,
            Notify = null;

        private Configuration _config;
        private DynamicConfigFile _dataFile;
        private DungeonData _dungeonData;
        private Timer _autoSpawnTimer;
        private Timer _dungeonCheckTimer;
        private Timer _updateMarkersTimer;
        private Timer _spawnPointScanner;
        private Timer _retryFailedDungeonsTimer;
        private Dictionary<DungeonTierConfig, string> _tierNameMap;
        private List<(Vector3 Position, Quaternion Rotation)> _validSpawnPoints = new();
        private List<DungeonRequest> _failedDungeons = new List<DungeonRequest>();
        private Queue<DungeonRequest> _dungeonQueue = new Queue<DungeonRequest>();
        private VersionNumber PreviousVersion;
        private bool _isCreatingDungeon = false;
        private const float TimeTimers = 60f;
        private const ulong OwnerID = 10203040505;
        private const string PermissionAdmin = "dungeonevents.admin";
        private const string PermissionEnter = "dungeonevents.enter";

        #region Hooks

        private void OnServerInitialized()
        {
            if (!NpcSpawn)
            {
                NextTick(() =>
                {
                    PrintError("You don't have NpcSpawn free codefling.com plugin installed.");
                    Unsubscribe("Unload");
                    Interface.Oxide.UnloadPlugin("DungeonEvents");
                });
                return;
            }

            permission.RegisterPermission(PermissionEnter, this);
            permission.RegisterPermission(PermissionAdmin, this);

            InitializeTierNameMap();
            LoadData();
            RemoveAllDungeons();
            StartSpawnPointScanner();
            StartTimerAutoSpawnDungeons();
            StartTimerRemoveInactiveDungeons();
            StartTimerUpdateMarkers();
            StartRetryFailedDungeonTimer();
            SaveData();
        }

        private void OnServerSave() => SaveData();

        private void Unload()
        {
            if (CoroutineManager.Instance != null)
            {
                UnityEngine.Object.Destroy(CoroutineManager.Instance.gameObject);
            }

            RemoveAllDungeons();
            _autoSpawnTimer?.Destroy();
            _dungeonCheckTimer?.Destroy();
            _updateMarkersTimer?.Destroy();
            _spawnPointScanner?.Destroy();
            _retryFailedDungeonsTimer?.Destroy();
            SaveData();
        }

        object OnTurretTarget(AutoTurret turret, BaseCombatEntity target)
        {
            if (IsDungeonEntity(target))
            {
                return false;
            }

            return null;
        }

        object OnEntityTakeDamage(ScientistNPC scientistNPC, HitInfo info)
        {
            if (scientistNPC == null || info == null || info.Initiator == null)
                return null;

            AutoTurret autoTurret = info.Initiator as AutoTurret;
            if (autoTurret == null)
                return null;
            if (IsDungeonEntity(autoTurret))
                return true;

            return null;
        }

        private object CanEntityTakeDamage(BasePlayer victim, HitInfo info)
        {
            if (info == null || info.Initiator == null || info.Initiator.net == null || !IsPlayer(victim))
                return null;

            if (info.Initiator is ScientistNPC)
            {
                if (IsDungeonEntity(info.Initiator))
                {
                    return true;
                }
            }

            if (info.Initiator is AutoTurret)
            {
                if (IsDungeonEntity(info.Initiator))
                {
                    return true;
                }
            }

            return null;
        }

        object CanEntityTakeDamage(AutoTurret autoTurret, HitInfo hitinfo)
        {
            if (autoTurret == null || hitinfo == null)
                return null;

            if (IsDungeonEntity(autoTurret))
            {
                if (hitinfo.InitiatorPlayer == null || !IsPlayer(hitinfo.Initiator as BasePlayer))
                    return false;
                else
                    return true;
            }

            return null;
        }

        object CanEntityTakeDamage(StorageContainer storageContainer, HitInfo hitinfo)
        {
            if (storageContainer == null || hitinfo == null)
                return null;

            if (IsDungeonEntity(storageContainer))
            {
                if (hitinfo.InitiatorPlayer == null || !IsPlayer(hitinfo.Initiator as BasePlayer))
                    return false;
                else
                    return true;
            }

            return null;
        }

        private object OnPortalUse(BasePlayer player, BasePortal portal)
        {
            ActiveDungeon activeDungeon = GetActiveDungeonByPortal(portal);
            if (activeDungeon == null)
            {
                return null;
            }

            if (_config.LockDungeonToPlayer && !CanEnter(player, activeDungeon))
            {
                NotifyPlayer(player, "NoAccess", 0);
                return false;
            }

            if (!activeDungeon.Spawned)
            {
                NotifyPlayer(player, "DungeonStillSpawningMessage", 0);
                return false;
            }

            return null;
        }

        private void OnCorpsePopulate(ScientistNPC entity, NPCPlayerCorpse corpse)
        {
            if (entity == null)
                return;

            if (IsDungeonEntity(entity))
            {
                NextTick(() =>
                {
                    if (corpse == null)
                        return;

                    Effect.server.Run("assets/prefabs/weapons/flashbang/effects/fx-flashbang-boom.prefab", corpse.transform.position);

                    var container = corpse.containers[0];
                    container.Clear();
                    if (!corpse.IsDestroyed)
                        corpse.Kill();
                });
            }
        }

        #endregion

        #region Chat Commands

        [ChatCommand("buydungeon")]
        private void BuyDungeonCommand(BasePlayer player, string command, string[] args)
        {
            if (args.Length == 0)
            {
                NotifyPlayer(player, "UsageBuyMessage", 0);
                return;
            }

            if (_dungeonData.ActiveDungeons.Any(d => d.OwnerID == player.userID) || _dungeonQueue.Any(r => r.Player != null && r.Player.userID == player.userID))
            {
                NotifyPlayer(player, "AlreadyOwnsDungeonMessage", 0);
                return;
            }

            string tierName = args[0].ToLower();
            var tier = _config.Tiers.GetType().GetProperties().FirstOrDefault(prop => prop.Name.ToLower() == tierName)?.GetValue(_config.Tiers) as DungeonTierConfig;

            if (tier == null)
            {
                NotifyPlayer(player, "InvalidTierMessage", 0, tierName, GetAvailableTiers());
                return;
            }

            int cost = tier.BuyCost;

            if (!CheckBalance(player, cost))
            {
                NotifyPlayer(player, "InsufficientFundsMessage", 0, tierName, cost);
                return;
            }

            if (!WithdrawFunds(player, cost))
            {
                NotifyPlayer(player, "WithdrawFailedMessage", 0, tierName);
                return;
            }

            NotifyPlayer(player, "PurchaseSuccessMessage", 0, tierName);

            LogDebug("Received dungeon creation request.", 0);

            EnqueueDungeonRequest(player, tier, true);
        }

        [ChatCommand("createdungeon")]
        private void CreateDungeonCommand(BasePlayer player, string command, string[] args)
        {
            if (!IsAdmin(player))
            {
                NotifyPlayer(player, "NoAccess", 0);
                return;
            }

            if (args.Length == 0)
            {
                NotifyPlayer(player, "UsageCreateMessage", 0);
                return;
            }

            string tierName = args[0].ToLower();
            var tier = _config.Tiers.GetType().GetProperties().FirstOrDefault(prop => prop.Name.ToLower() == tierName)?.GetValue(_config.Tiers) as DungeonTierConfig;

            if (tier == null)
            {
                NotifyPlayer(player, "InvalidTierMessage", 0, tierName, GetAvailableTiers());
                return;
            }

            LogDebug("Received dungeon creation request.", 1);

            EnqueueDungeonRequest(player, tier, false);
        }

        [ChatCommand("removeinactivedungeons")]
        private void RemoveInactiveDungeonsCommand(BasePlayer player, string command, string[] args)
        {
            if (!IsAdmin(player))
                return;

            var removedDungeons = RemoveInactiveDungeons();
            NotifyPlayerOnRemoval(player, removedDungeons);
        }

        [ChatCommand("removealldungeons")]
        private void RemoveAllDungeonsCommand(BasePlayer player, string command, string[] args)
        {
            if (!IsAdmin(player))
                return;

            var removedDungeons = RemoveAllDungeons();
            NotifyPlayerOnRemoval(player, removedDungeons);
        }

        [ChatCommand("forceremovealldungeons")]
        private void ForceRemoveAllDungeonsCommand(BasePlayer player, string command, string[] args)
        {
            if (!IsAdmin(player))
                return;

            CleanupExistingEntities();
            NotifyPlayer(player, "CleanedUp", 0);
        }

        #endregion

        #region Dungeon Management

        private class DungeonRequest
        {
            public BasePlayer Player { get; set; }
            public DungeonTierConfig Tier { get; set; }
            public bool LockToPlayer { get; set; }
        }

        private void StartRetryFailedDungeonTimer()
        {
            _retryFailedDungeonsTimer = timer.Every(
                30,
                () =>
                {
                    if (_failedDungeons.Count > 0)
                    {
                        LogDebug("Retrying failed dungeons...", 2);

                        for (int i = _failedDungeons.Count - 1; i >= 0; i--)
                        {
                            var request = _failedDungeons[i];
                            _failedDungeons.RemoveAt(i);

                            EnqueueDungeonRequest(request.Player, request.Tier);
                        }
                    }
                }
            );
        }

        private void StartSpawnPointScanner()
        {
            _spawnPointScanner = timer.Every(
                TimeTimers,
                () =>
                {
                    ScanSpawnPoints();
                }
            );
        }

        private void StartTimerAutoSpawnDungeons()
        {
            if (_config.DungeonSpawn.EnableAutoSpawn)
            {
                _autoSpawnTimer = timer.Every(
                    TimeTimers,
                    () =>
                    {
                        AutoSpawnDungeon();
                    }
                );
            }
        }

        private void StartTimerRemoveInactiveDungeons()
        {
            _dungeonCheckTimer = timer.Every(
                TimeTimers,
                () =>
                {
                    var removedDungeons = RemoveInactiveDungeons();
                    if (removedDungeons.Count > 0)
                    {
                        foreach (var dungeon in removedDungeons)
                        {
                            LogDebug($"Removed inactive dungeon of tier {dungeon.TierName} at grid {dungeon.Grid}.", 1);
                        }
                    }
                }
            );
        }

        private void StartTimerUpdateMarkers()
        {
            _updateMarkersTimer = timer.Every(TimeTimers, () => UpdateMarkers());
        }

        private void ScanSpawnPoints()
        {
            int pointsScanned = 0;
            int maxPointsToFind = 1000;

            int initialCount = _validSpawnPoints.Count;

            while (pointsScanned < 100 && _validSpawnPoints.Count < maxPointsToFind)
            {
                if (TryFindDungeonSpawnPoint(out var position, out var rotation))
                {
                    if (IsSafeFromOtherDungeons(position) && !IsCloseToMonuments(position) && IsFarFromExistingSpawnPoints(position, _config.DungeonSpawn.MinDistanceBetweenDungeons))
                    {
                        _validSpawnPoints.Add((position, rotation));
                    }
                }

                pointsScanned++;
            }

            int addedPoints = _validSpawnPoints.Count - initialCount;
            if (addedPoints > 0)
            {
                LogDebug($"Added {addedPoints} dungeon spawn points. Total now: {_validSpawnPoints.Count}");
            }
        }

        private bool IsFarFromExistingSpawnPoints(Vector3 position, float minDistance)
        {
            foreach (var existingPoint in _validSpawnPoints)
            {
                if (Vector3.Distance(existingPoint.Position, position) < minDistance)
                {
                    return false;
                }
            }
            return true;
        }

        private (Vector3 Position, Quaternion Rotation)? GetNextSpawnPoint()
        {
            if (_validSpawnPoints.Count == 0)
            {
                return null;
            }

            var nextPoint = _validSpawnPoints.First();
            _validSpawnPoints.RemoveAt(0);
            return nextPoint;
        }

        private void RecycleUsedSpawnPoints()
        {
            if (_validSpawnPoints.Count == 0)
            {
                LogDebug("Recycling used spawn points.");
                ScanSpawnPoints();
            }
        }

        private void UpdateMarkers()
        {
            var activeDungeons = new List<ActiveDungeon>(_dungeonData.ActiveDungeons);
            foreach (var activeDungeon in activeDungeons)
            {
                foreach (var marker in _dungeonData.ActiveDungeonMarkers.Where(m => m.Position == activeDungeon.Position).ToList())
                {
                    var vendingMarker = BaseNetworkable.serverEntities.Find(new NetworkableId(marker.VendingMarker)) as VendingMachineMapMarker;

                    if (activeDungeon?.OwnerName != string.Empty)
                    {
                        vendingMarker.markerShopName = $"{activeDungeon.TierConfig.MapName} ({activeDungeon.OwnerName})";
                        vendingMarker.SendNetworkUpdate();
                    }

                    var radiusMarker = BaseNetworkable.serverEntities.Find(new NetworkableId(marker.RadiusMarker)) as MapMarkerGenericRadius;
                    radiusMarker?.SendUpdate();
                }
            }
        }

        private void AutoSpawnDungeon()
        {
            var tier = GetDifficultyWithLeastActiveDungeons();
            EnqueueDungeonRequest(null, tier);
        }

        private DungeonTierConfig GetDifficultyWithLeastActiveDungeons()
        {
            var activeDungeonsByDifficulty = new Dictionary<string, int>
            {
                { "easy", 0 },
                { "normal", 0 },
                { "medium", 0 },
                { "hard", 0 },
                { "nightmare", 0 },
            };

            foreach (var dungeon in _dungeonData.ActiveDungeons)
            {
                var tierName = dungeon.TierName.ToLower();
                if (activeDungeonsByDifficulty.ContainsKey(tierName))
                {
                    activeDungeonsByDifficulty[tierName]++;
                }
            }

            var sortedDifficulties = activeDungeonsByDifficulty.OrderBy(d => d.Value).ThenBy(d => Array.IndexOf(new[] { "easy", "normal", "medium", "hard", "nightmare" }, d.Key)).ToList();

            return _config.Tiers.GetType().GetProperties().FirstOrDefault(prop => prop.Name.ToLower() == sortedDifficulties.First().Key)?.GetValue(_config.Tiers) as DungeonTierConfig;
        }

        private void EnqueueDungeonRequest(BasePlayer player, DungeonTierConfig tier, bool lockToPlayer = false)
        {
            var request = new DungeonRequest
            {
                Player = player,
                Tier = tier,
                LockToPlayer = lockToPlayer,
            };

            _dungeonQueue.Enqueue(request);

            if (player != null)
            {
                NotifyPlayer(player, "ReceivedRequest", 0);
            }

            ProcessDungeonQueue();
        }

        private void ProcessDungeonQueue()
        {
            if (_isCreatingDungeon || _dungeonQueue.Count == 0)
                return;

            ScanSpawnPoints();

            var request = _dungeonQueue.Dequeue();

            if (_dungeonData.ActiveDungeons.Count >= _config.DungeonSpawn.MaxTotalActiveDungeons)
            {
                LogDebug(lang.GetMessage("MaxActiveDungeonsReached", this));
                NotifyPlayer(request.Player, "MaxActiveDungeonsReached", 0);
                return;
            }

            var spawnPoint = GetNextSpawnPoint();
            if (spawnPoint == null)
            {
                LogDebug("No spawn points available. Rescanning for new points.", 1);
                ScanSpawnPoints();

                spawnPoint = GetNextSpawnPoint();
                if (spawnPoint == null)
                {
                    LogDebug("Still no spawn points available after rescanning.", 1);
                    NotifyPlayer(request.Player, "DungeonSpawnFailed", 0);
                    _failedDungeons.Add(request);
                    return;
                }
            }

            var (position, rotation) = spawnPoint.Value;

            if (!IsSafeFromOtherDungeons(position))
            {
                LogDebug($"The point {position} is not safe for creation.");
                RecycleUsedSpawnPoints();

                NotifyPlayer(request.Player, "DungeonSpawnFailed", 0);
                _failedDungeons.Add(request);
                return;
            }

            LogDebug($"Using spawn point: Position = {position}, Rotation = {rotation}");

            _isCreatingDungeon = true;

            CoroutineManager.Instance.StartCoroutine(
                TryCreateDungeon(
                    position,
                    rotation,
                    request.Tier,
                    request.Player,
                    request.LockToPlayer,
                    () =>
                    {
                        _isCreatingDungeon = false;
                        ProcessDungeonQueue();
                    }
                )
            );
        }

        private void PlaceEntryBarrier(ActiveDungeon activeDungeon, ProceduralDynamicDungeon dungeon)
        {
            Vector3 entrancePosition = dungeon.entranceHack.transform.position;
            Vector3 entranceForward = dungeon.entranceHack.transform.forward;
            float offsetDistance = 3f;
            Vector3 barrierPosition = entrancePosition + (entranceForward * offsetDistance);

            string prefabPath = "assets/prefabs/deployable/barricades/barricade.cover.wood.prefab";
            var barrier = GameManager.server.CreateEntity(prefabPath, barrierPosition, Quaternion.identity);
            barrier.OwnerID = OwnerID;

            LogDebug($"Placing barrier at the dungeon entrance: {barrierPosition}");
            barrier.Spawn();
            activeDungeon.EntityIds.Add(barrier.net.ID.Value);
        }

        private IEnumerator TryCreateDungeon(Vector3 position, Quaternion rotation, DungeonTierConfig tierConfig, BasePlayer player, bool lockToPlayer, Action onComplete)
        {
            bool success = false;
            yield return CreateDungeon(position, rotation, tierConfig, player, lockToPlayer, result => success = result);

            if (!success)
            {
                LogDebug("Failed to create the dungeon. We will try again shortly.", 1);
                NotifyPlayer(player, "DungeonSpawnFailed", 0);

                EnqueueDungeonRequest(player, tierConfig, lockToPlayer);
            }

            onComplete?.Invoke();
        }

        private IEnumerator CreateDungeon(
            Vector3 position,
            Quaternion rotation,
            DungeonTierConfig tierConfig = null,
            BasePlayer player = null,
            bool lockDungeonToPlayer = false,
            Action<bool> callback = null
        )
        {
            tierConfig ??= GetDifficultyWithLeastActiveDungeons();

            string prefabPath = GetRandomDungeonPrefab();
            if (!ValidatePrefabPath(prefabPath))
            {
                LogDebug("Invalid prefab path for the dungeon.", 2);
                callback?.Invoke(false);
                yield break;
            }

            if (_config.DungeonSpawn.MaxDungeonLifetime > 0)
            {
                XmasDungeon.xmaslifetime = _config.DungeonSpawn.MaxDungeonLifetime;
                HalloweenDungeon.lifetime = _config.DungeonSpawn.MaxDungeonLifetime;
            }
            else
            {
                XmasDungeon.xmaslifetime = 3600;
                HalloweenDungeon.lifetime = 3600;
            }

            var dungeonPortal = GameManager.server.CreateEntity(prefabPath, position, rotation) as BasePortal;
            if (dungeonPortal == null)
            {
                LogDebug("Failed to create the dungeon portal entity.", 2);
                callback?.Invoke(false);
                yield break;
            }

            dungeonPortal.OwnerID = OwnerID;
            dungeonPortal.Spawn();

            var grid = MapHelper.PositionToString(position);
            LogDebug($"Trying to spawn Dungeon at grid: {grid}.");

            ProceduralDynamicDungeon dungeon = GetProceduralDungeon(dungeonPortal);
            if (!ValidateProceduralDungeon(dungeon, dungeonPortal, grid))
            {
                dungeonPortal?.Kill(BaseNetworkable.DestroyMode.None);
                callback?.Invoke(false);
                yield break;
            }

            yield return new WaitForSeconds(3f);
            yield return CoroutineManager.Instance.StartCoroutine(RemoveOriginalEntities(dungeon));
            yield return new WaitForSeconds(3f);

            LogDebug($"SEED: {dungeon.baseseed} {dungeonPortal.GetType().Name}");

            var spawnPoints = GetAllSpawnPoints(dungeon, dungeonPortal.GetType().Name, tierConfig);
            if (spawnPoints.Count == 0)
            {
                LogDebug("Dungeon spawn failed! Not enough spawn points inside for entities.", 1);
                dungeonPortal?.Kill(BaseNetworkable.DestroyMode.None);
                callback?.Invoke(false);
                yield break;
            }

            LogDebug($"Selected dungeon tier: {_tierNameMap[tierConfig]} at grid: {grid}.");

            if (!tierConfig.IsValid())
            {
                LogDebug("Invalid tier configuration: Percentages must sum to 100%.", 2);
                dungeonPortal?.Kill(BaseNetworkable.DestroyMode.None);
                callback?.Invoke(false);
                yield break;
            }

            if (dungeonPortal == null)
            {
                LogDebug("Dungeon portal not found.", 2);
                callback?.Invoke(false);
                yield break;
            }

            var activeDungeon = RegisterActiveDungeon(dungeonPortal, dungeon, position, grid, tierConfig, player, lockDungeonToPlayer);

            int totalPoints = spawnPoints.Count;
            int turretCount = (int)(totalPoints * (tierConfig.TurretPercentage / 100f));
            int boxCount = (int)(totalPoints * (tierConfig.LootBoxPercentage / 100f));
            int npcCount = totalPoints - turretCount - boxCount;

            AssignTurretsNearBoxes(spawnPoints, turretCount, boxCount, npcCount, out var turretSpawns, out var boxSpawns, out var npcSpawns);

            LogDebug($"Spawn points found: {totalPoints}, Turrets: {turretCount}, Boxes: {boxCount}, NPCs: {npcCount}");

            SpawnEntities(turretSpawns, "AutoTurret", activeDungeon, dungeon);
            SpawnEntities(boxSpawns, "StorageContainer", activeDungeon, dungeon);
            SpawnEntities(npcSpawns, "NPC", activeDungeon, dungeon);

            LogDebug($"Finished placing entities. Total placed: {activeDungeon.EntityIds.Count}/{spawnPoints.Count}.");

            CreateDungeonMarkers(activeDungeon);
            NotifyPlayersOfNewDungeon(activeDungeon);

            LogDebug($"{_tierNameMap[tierConfig]} Dungeon spawned successfully at grid: {activeDungeon.Grid}.", 1);

            activeDungeon.Spawned = true;

            PlaceEntryBarrier(activeDungeon, dungeon);

            NotifyPlayer(player, "DungeonSpawnedSuccess", 0, activeDungeon.Grid);

            callback?.Invoke(true);
            yield return null;
        }

        private void AssignTurretsNearBoxes(
            List<Vector3> spawnPoints,
            int turretCount,
            int boxCount,
            int npcCount,
            out List<Vector3> turretSpawns,
            out List<Vector3> boxSpawns,
            out List<Vector3> npcSpawns
        )
        {
            turretSpawns = new List<Vector3>();
            boxSpawns = new List<Vector3>();
            npcSpawns = new List<Vector3>();

            var usedSpawnPoints = new HashSet<Vector3>();
            float maxDistance = 10f;

            for (int i = 0; i < boxCount && spawnPoints.Count > 0; i++)
            {
                var boxPoint = spawnPoints.FirstOrDefault(p => !usedSpawnPoints.Contains(p));
                if (boxPoint == default)
                    break;

                boxSpawns.Add(boxPoint);
                usedSpawnPoints.Add(boxPoint);

                var turretPoint = spawnPoints.FirstOrDefault(p => !usedSpawnPoints.Contains(p) && Vector3.Distance(boxPoint, p) <= maxDistance);

                if (turretPoint != default && turretSpawns.Count < turretCount)
                {
                    turretSpawns.Add(turretPoint);
                    usedSpawnPoints.Add(turretPoint);
                }
            }

            foreach (var point in spawnPoints)
            {
                if (turretSpawns.Count >= turretCount)
                    break;

                if (!usedSpawnPoints.Contains(point))
                {
                    turretSpawns.Add(point);
                    usedSpawnPoints.Add(point);
                }
            }

            foreach (var point in spawnPoints.Except(usedSpawnPoints).ToList())
            {
                if (boxSpawns.Count < boxCount)
                {
                    boxSpawns.Add(point);
                    usedSpawnPoints.Add(point);
                }
                else if (npcSpawns.Count < npcCount)
                {
                    npcSpawns.Add(point);
                    usedSpawnPoints.Add(point);
                }
            }

            if (turretSpawns.Count < turretCount)
            {
                LogDebug($"Only {turretSpawns.Count}/{turretCount} turrets could be placed due to insufficient spawn points.", 1);
            }

            if (boxSpawns.Count < boxCount)
            {
                LogDebug($"Only {boxSpawns.Count}/{boxCount} loot boxes could be placed due to insufficient spawn points.", 1);
            }

            if (npcSpawns.Count < npcCount)
            {
                LogDebug($"Only {npcSpawns.Count}/{npcCount} NPCs could be placed due to insufficient spawn points.", 1);
            }
        }

        private ProceduralDynamicDungeon GetProceduralDungeon(BasePortal dungeonPortal)
        {
            return (dungeonPortal as XmasDungeon)?.dungeonInstance.Get(true) ?? (dungeonPortal as HalloweenDungeon)?.dungeonInstance.Get(true);
        }

        private List<Vector3> GetAllSpawnPoints(ProceduralDynamicDungeon dungeon, string dungeonType, DungeonTierConfig tierConfig)
        {
            var basePoints = dungeon
                .spawnedCells.SelectMany(cell => cell.spawnGroups)
                .SelectMany(group => group.spawnPoints)
                .Select(point =>
                {
                    point.GetLocation(out var position, out _);
                    return position;
                })
                .ToList();

            if (
                (tierConfig.MinSpawnPoints > 20 && basePoints.Count < 8)
                || (tierConfig.MinSpawnPoints > 15 && basePoints.Count < 6)
                || (tierConfig.MinSpawnPoints > 5 && basePoints.Count < 3)
                || (basePoints.Count <= 2)
            )
            {
                LogDebug("Not enough spawn points inside for entities. Spawn points: " + basePoints.Count);
                return new List<Vector3>();
            }

            if (tierConfig.MinSpawnPoints <= basePoints.Count)
            {
                LogDebug($"Total valid spawn points: {basePoints.Count}");

                var selectedPoints = basePoints.OrderBy(x => Random.value).Take(tierConfig.MinSpawnPoints).ToList();

                LogDebug($"Selected {tierConfig.MinSpawnPoints} random spawn points.");
                return selectedPoints;
            }

            if (tierConfig.MinSpawnPoints <= basePoints.Count)
            {
                LogDebug($"Total valid spawn points: {basePoints.Count}");
                return basePoints;
            }

            var existingSpawnPoints = new List<Vector3>(basePoints);
            while (existingSpawnPoints.Count < tierConfig.MinSpawnPoints)
            {
                var originalPoint = basePoints[Random.Range(0, basePoints.Count)];

                var nearbyPoint = GenerateNearbyPoint(originalPoint, existingSpawnPoints, dungeonType);
                if (nearbyPoint != originalPoint)
                {
                    LogDebug($"Generated nearby point: {nearbyPoint}");
                    existingSpawnPoints.Add(nearbyPoint);
                }
            }

            LogDebug($"Total valid spawn points: {existingSpawnPoints.Count}");
            return existingSpawnPoints;
        }

        private Vector3 GenerateNearbyPoint(Vector3 originalPosition, List<Vector3> existingPoints, string dungeonType)
        {
            float maxDistance = 30f;
            float minDistance = 3f;
            int maxAttempts = 50;

            for (int attempt = 0; attempt < maxAttempts; attempt++)
            {
                Vector3 nearbyPoint = GenerateRandomOffset(originalPosition, maxDistance);

                if (IsValidNearbyPoint(nearbyPoint, minDistance, existingPoints, dungeonType))
                {
                    return nearbyPoint;
                }
            }

            return originalPosition;
        }

        private Vector3 GenerateRandomOffset(Vector3 originalPosition, float maxDistance)
        {
            float offsetX = Random.Range(-maxDistance, maxDistance);
            float offsetZ = Random.Range(-maxDistance, maxDistance);

            return new Vector3(originalPosition.x + offsetX, originalPosition.y, originalPosition.z + offsetZ);
        }

        private bool IsValidNearbyPoint(Vector3 point, float minDistance, List<Vector3> existingPoints, string dungeonType)
        {
            if (!IsFarFromExistingPoints(point, minDistance, existingPoints))
                return false;

            if (LocationCheck.IsFlying(point, 0.5f))
                return false;

            if (LocationCheck.IsClipped(point, 0.5f))
                return false;

            if (LocationCheck.IsInsideObject(point, 0.5f))
                return false;

            if (!IsSurroundedByWalls(point, 10.0f))
                return false;

            return true;
        }

        public bool IsSurroundedByWalls(Vector3 position, float checkDistance)
        {
            Vector3[] directions = { Vector3.up, Vector3.down, Vector3.forward, Vector3.back, Vector3.right, Vector3.left };

            foreach (var direction in directions)
            {
                if (!Physics.Raycast(position, direction, checkDistance, Layers.Mask.World, QueryTriggerInteraction.Ignore))
                {
                    return false;
                }
            }

            return true;
        }

        private bool IsFarFromExistingPoints(Vector3 position, float minDistance, List<Vector3> existingPoints)
        {
            foreach (var existingPoint in existingPoints)
            {
                if (Vector3.Distance(existingPoint, position) < minDistance)
                {
                    return false;
                }
            }
            return true;
        }

        private bool TryPlaceTurretOnCeiling(Vector3 basePosition, out Vector3 ceilingPosition, out Quaternion ceilingRotation)
        {
            float maxCeilingDistance = 7f;

            RaycastHit hitInfo;
            if (Physics.Raycast(basePosition, Vector3.up, out hitInfo, maxCeilingDistance))
            {
                if (hitInfo.collider != null && hitInfo.collider.gameObject != null)
                {
                    ceilingPosition = hitInfo.point - (Vector3.up * 0.5f);
                    ceilingRotation = Quaternion.Euler(0f, 0f, 180f);
                    return true;
                }
            }

            ceilingPosition = Vector3.zero;
            ceilingRotation = Quaternion.identity;
            return false;
        }

        private JObject NpcSpawnObjectConfig(ActiveDungeon activeDungeon)
        {
            var npcConfig = activeDungeon.TierConfig.NpcConfig;
            var areaMask = activeDungeon.DungeonType == "XmasDungeon" ? -1 : 1;
            var agentTypeID = activeDungeon.DungeonType == "XmasDungeon" ? 0 : -1372625422;

            return new JObject
            {
                ["Name"] = npcConfig.Name,
                ["HostileTargetsOnly"] = false,
                ["DamageScale"] = npcConfig.DamageScale,
                ["States"] = new JArray { "RoamState", "ChaseState", "CombatState" },
                ["AttackRangeMultiplier"] = 1f,
                ["SenseRange"] = 60f,
                ["MemoryDuration"] = 30f,
                ["CheckVisionCone"] = false,
                ["VisionCone"] = 135f,
                ["Health"] = npcConfig.Health,
                ["RoamRange"] = npcConfig.RoamRange,
                ["ChaseRange"] = npcConfig.ChaseRange,
                ["WearItems"] = new JArray(npcConfig.WearItems.Items.Select(item => new JObject { ["ShortName"] = item.ShortName, ["SkinID"] = item.SkinID })),
                ["BeltItems"] = new JArray(
                    npcConfig.BeltItems.Items.Select(item => new JObject
                    {
                        ["ShortName"] = item.ShortName,
                        ["Amount"] = item.Amount,
                        ["SkinID"] = item.SkinID,
                        ["Mods"] = new JArray(item.Mods ?? new List<string>()),
                        ["Ammo"] = "",
                    })
                ),
                ["TurretDamageScale"] = 1f,
                ["AimConeScale"] = 1.5f,
                ["DisableRadio"] = false,
                ["Stationary"] = false,
                ["CanUseWeaponMounted"] = true,
                ["CanRunAwayWater"] = false,
                ["Speed"] = 5f,
                ["AreaMask"] = areaMask,
                ["AgentTypeID"] = agentTypeID,
                ["HomePosition"] = string.Empty,
            };
        }

        private void SpawnEntities(List<Vector3> points, string entityType, ActiveDungeon activeDungeon, ProceduralDynamicDungeon dungeon)
        {
            foreach (var point in points)
            {
                if (entityType == "NPC")
                {
                    var npcConfig = NpcSpawnObjectConfig(activeDungeon);
                    ScientistNPC entity = (ScientistNPC)NpcSpawn.Call("SpawnNpc", point, npcConfig);
                    entity.OwnerID = OwnerID;

                    if (entity != null && !entity.IsDestroyed)
                    {
                        LogDebug($"Successfully spawned NPC at {point} with entity ID: {entity.net.ID}");
                        InitializeEntity(entity, activeDungeon);
                    }
                }
                else
                {
                    var prefabPath = entityType switch
                    {
                        "AutoTurret" => "assets/prefabs/npc/autoturret/autoturret_deployed.prefab",
                        "StorageContainer" => "assets/prefabs/deployable/woodenbox/woodbox_deployed.prefab",
                        _ => throw new ArgumentException($"Unknown entity type: {entityType}"),
                    };

                    Vector3 spawnPosition = point;
                    Quaternion spawnRotation = Quaternion.identity;

                    if (entityType == "AutoTurret")
                    {
                        bool shouldPlaceOnCeiling = Random.value < 0.8f;

                        if (shouldPlaceOnCeiling && TryPlaceTurretOnCeiling(point, out var ceilingPosition, out var ceilingRotation))
                        {
                            spawnPosition = ceilingPosition;
                            spawnRotation = ceilingRotation;
                            LogDebug($"Placing turret on ceiling at {ceilingPosition} with rotation {ceilingRotation.eulerAngles}");
                        }
                        else
                        {
                            LogDebug($"Placing turret on floor at {spawnPosition}");
                        }
                    }

                    var entity = GameManager.server.CreateEntity(prefabPath, spawnPosition, spawnRotation);
                    if (entity != null)
                    {
                        entity.OwnerID = OwnerID;
                        entity.SetParent(dungeon, true, true);
                        entity.Spawn();

                        LogDebug($"Successfully spawned {entityType} at {spawnPosition} with entity ID: {entity.net.ID}");
                        InitializeEntity(entity, activeDungeon);
                    }
                    else
                    {
                        LogDebug($"Failed to create {entityType} at position {spawnPosition}. Prefab: {prefabPath}");
                    }
                }
            }
        }

        private bool ValidatePrefabPath(string prefabPath)
        {
            if (!string.IsNullOrEmpty(prefabPath))
                return true;

            LogDebug("No prefab path available for dungeon spawning.");
            return false;
        }

        private bool ValidateProceduralDungeon(ProceduralDynamicDungeon proceduralDungeon, BasePortal dungeonPortal, string grid)
        {
            if (proceduralDungeon == null)
            {
                LogDebug($"ProceduralDynamicDungeon not found, destroying the Dungeon at grid: {grid}.");
                dungeonPortal.Kill(BaseNetworkable.DestroyMode.None);
                return false;
            }

            if (proceduralDungeon.spawnedCells.Count < 1)
            {
                LogDebug($"No cells found, destroying the Dungeon at grid: {grid}.");
                dungeonPortal.Kill(BaseNetworkable.DestroyMode.None);
                return false;
            }

            return true;
        }

        private ActiveDungeon RegisterActiveDungeon(
            BasePortal dungeonPortal,
            ProceduralDynamicDungeon proceduralDungeon,
            Vector3 position,
            string grid,
            DungeonTierConfig selectedTier,
            BasePlayer player,
            bool lockToPlayer
        )
        {
            var activeDungeon = new ActiveDungeon
            {
                PortalId = dungeonPortal.net.ID.Value,
                DynamicDungeonId = proceduralDungeon.net.ID.Value,
                Position = position,
                Grid = grid,
                TierName = _tierNameMap[selectedTier],
                DungeonType = dungeonPortal.GetType().Name,
                TierConfig = selectedTier,
                EntityIds = new List<ulong>(),
                Spawned = false,
            };

            if (lockToPlayer && player != null)
            {
                activeDungeon.OwnerID = player.userID;
                activeDungeon.OwnerName = player.displayName;
            }

            _dungeonData.ActiveDungeons.Add(activeDungeon);

            return activeDungeon;
        }

        private List<(Vector3 Position, Quaternion Rotation)> GetAvailableSpawnLocations(ProceduralDynamicDungeon proceduralDungeon, string prefabPath)
        {
            GameObject prefab = GameManager.server.FindPrefab(prefabPath);
            if (prefab == null)
            {
                Puts($"Prefab not found: {prefabPath}");
                return new List<(Vector3, Quaternion)>();
            }

            BaseEntity prefabEntity = prefab.GetComponent<BaseEntity>();
            if (prefabEntity == null)
            {
                Puts($"Prefab does not have a BaseEntity: {prefabPath}");
                return new List<(Vector3, Quaternion)>();
            }

            return proceduralDungeon
                .spawnedCells.Where(cell => cell != null)
                .SelectMany(cell => cell.spawnGroups)
                .Where(group => group != null)
                .SelectMany(group => group.spawnPoints)
                .Where(spawnPoint => spawnPoint != null && spawnPoint.IsAvailableTo(prefab))
                .Select(spawnPoint =>
                {
                    spawnPoint.GetLocation(out var position, out var rotation);
                    return (position, rotation);
                })
                .ToList();
        }

        private void InitializeEntity(BaseEntity entity, ActiveDungeon activeDungeon)
        {
            if (entity != null)
            {
                activeDungeon.EntityIds.Add(entity.net.ID.Value);

                if (entity is NPCPlayer)
                {
                    activeDungeon.NpcIds.Add(entity.net.ID.Value);
                }
                else if (entity is AutoTurret turret)
                {
                    ConfigureTurret(turret, activeDungeon.TierConfig.AutoTurretConfig);
                }
                else if (entity is StorageContainer storage)
                {
                    ConfigureStorage(storage, activeDungeon.TierConfig);
                }
            }
        }

        private IEnumerator RemoveOriginalEntities(ProceduralDynamicDungeon proceduralDungeon)
        {
            foreach (var cell in proceduralDungeon.spawnedCells)
            {
                foreach (var group in cell.spawnGroups)
                {
                    for (int i = group.spawnInstances.Count - 1; i >= 0; i--)
                    {
                        SpawnPointInstance spawnPointInstance = group.spawnInstances[i];
                        BaseEntity entity = spawnPointInstance?.gameObject?.ToBaseEntity();

                        if (entity != null && !IsDungeonEntity(entity) && (entity is NPCDwelling || entity is ScarecrowNPC || entity is LootContainer))
                        {
                            LogDebug($"Removing original entity: {entity.ShortPrefabName} at {entity.transform.position}");
                            entity.Kill();
                        }
                    }
                }
            }

            yield return null;
        }

        private void ConfigureTurret(AutoTurret turret, TurretConfig turretConfig)
        {
            turret.InitializeHealth(turretConfig.Health, turretConfig.Health);

            var weapon = ItemManager.CreateByName(turretConfig.WeaponShortName);
            weapon?.MoveToContainer(turret.inventory, 0);

            var laserSight = ItemManager.CreateByName("weapon.mod.lasersight");
            laserSight?.MoveToContainer(weapon.contents);

            turret.UpdateAttachedWeapon();
            turret.gameObject.AddComponent<TurretMonoBehaviour>();
        }

        public class TurretMonoBehaviour : MonoBehaviour
        {
            private AutoTurret _turret;

            private void Awake()
            {
                _turret = GetComponent<AutoTurret>();

                _turret.SetPeacekeepermode(false);
                _turret.InitiateStartup();
                _turret.isLootable = false;
                _turret.dropFloats = false;
                _turret.dropsLoot = false;

                InvokeRepeating(nameof(RefillAmmo), 2f, 30f);
            }

            private void RefillAmmo()
            {
                int maxReserveAmmo = 1000;

                if (_turret.AttachedWeapon is not BaseProjectile baseProjectile || baseProjectile.primaryMagazine?.ammoType == null)
                    return;

                var ammoType = baseProjectile.primaryMagazine.ammoType;
                int currentAmmoCount = _turret.inventory.itemList.Where(item => item.info == ammoType).Sum(item => item.amount);

                if (currentAmmoCount < maxReserveAmmo)
                {
                    int ammoNeeded = maxReserveAmmo - currentAmmoCount;
                    Item ammoItem = ItemManager.Create(ammoType, ammoNeeded);
                    ammoItem?.MoveToContainer(_turret.inventory);

                    _turret.UpdateTotalAmmo();
                    _turret.EnsureReloaded();
                    _turret.SendNetworkUpdateImmediate();
                }
            }

            private void OnDestroy()
            {
                CancelInvoke(nameof(RefillAmmo));
            }
        }

        private void ConfigureStorage(StorageContainer storage, DungeonTierConfig tierConfig)
        {
            storage.skinID = tierConfig.SmallWoodBoxSkinID;
            PopulateStorage(storage.inventory, tierConfig, _config.LootBoxConfig.LootItems);
            AddLock(storage);
        }

        private void AddLock(StorageContainer storage)
        {
            var prefabName = "assets/prefabs/locks/keylock/lock.key.prefab";
            var keyLock = GameManager.server.CreateEntity(prefabName) as KeyLock;
            if (keyLock == null)
                return;

            keyLock.gameObject.Identity();
            keyLock.SetParent(storage, storage.GetSlotAnchorName(BaseEntity.Slot.Lock));
            keyLock.Spawn();
            storage.SetSlot(BaseEntity.Slot.Lock, keyLock);
            keyLock.keyCode = Random.Range(1000, 9999);
            keyLock.OwnerID = 0;
            keyLock.firstKeyCreated = true;
            keyLock.SetFlag(BaseEntity.Flags.Locked, true);
        }

        private void PopulateStorage(ItemContainer container, DungeonTierConfig tierConfig, List<ItemConfig> lootItems)
        {
            var shuffledItems = lootItems.OrderBy(_ => Random.value).ToList();
            var differentItemsCount = 0;

            foreach (var itemConfig in shuffledItems)
            {
                if (differentItemsCount >= tierConfig.MaxItemsPerBox)
                {
                    break;
                }

                if (Random.value * 100f <= itemConfig.InclusionChancePercentage)
                {
                    var itemDefinition = ItemManager.FindItemDefinition(itemConfig.ShortName);
                    if (itemDefinition != null)
                    {
                        var amount = Random.Range(itemConfig.MinimumAmount, itemConfig.MaximumAmount + 1);
                        if (container.itemList.Count < container.capacity)
                        {
                            var item = ItemManager.Create(itemDefinition, amount, itemConfig.SkinID);

                            if (!string.IsNullOrEmpty(itemConfig.CustomName))
                                item.name = itemConfig.CustomName;

                            item.MoveToContainer(container);
                            differentItemsCount++;
                        }
                    }
                }
            }
        }

        private List<ActiveDungeon> RemoveInactiveDungeons()
        {
            var removedDungeons = new List<ActiveDungeon>();

            foreach (var dungeon in _dungeonData.ActiveDungeons.ToList())
            {
                if (CanBeRemoved(dungeon))
                {
                    removedDungeons.Add(dungeon);
                    DestroyDungeon(dungeon);
                }
            }

            SaveData();

            return removedDungeons;
        }

        private void DestroyDungeon(ActiveDungeon dungeon)
        {
            var proceduralDynamicDungeon = BaseNetworkable.serverEntities.Find(new NetworkableId(dungeon.DynamicDungeonId)) as ProceduralDynamicDungeon;
            if (proceduralDynamicDungeon != null)
            {
                RemoveLootableCorpse(proceduralDynamicDungeon);
                RemoveItemDrop(proceduralDynamicDungeon);
                RemoveDroppedItemContainer(proceduralDynamicDungeon);
            }

            var dungeonEntity = BaseNetworkable.serverEntities.Find(new NetworkableId(dungeon.PortalId)) as HalloweenDungeon;
            if (dungeonEntity != null)
            {
                dungeonEntity?.Kill(BaseNetworkable.DestroyMode.None);
            }

            DestroyEntities(dungeon);
            RemoveDungeonMarkers(dungeon);
            _dungeonData.ActiveDungeons.Remove(dungeon);
        }

        private void RemoveDroppedItemContainer(ProceduralDynamicDungeon dungeon)
        {
            List<DroppedItemContainer> obj = Pool.Get<List<DroppedItemContainer>>();
            Vis.Entities(dungeon.transform.position, 80f, obj);
            foreach (DroppedItemContainer item in obj)
            {
                if (item.IsValid() && !item.IsDestroyed)
                {
                    item?.Kill();
                }
            }

            Pool.FreeUnmanaged(ref obj);
        }

        private void RemoveItemDrop(ProceduralDynamicDungeon dungeon)
        {
            List<DroppedItem> obj = Pool.Get<List<DroppedItem>>();
            Vis.Entities(dungeon.transform.position, 80f, obj);
            foreach (DroppedItem item in obj)
            {
                if (item.IsValid() && !item.IsDestroyed)
                {
                    item?.Kill();
                }
            }

            Pool.FreeUnmanaged(ref obj);
        }

        private void RemoveLootableCorpse(ProceduralDynamicDungeon dungeon)
        {
            List<LootableCorpse> obj = Pool.Get<List<LootableCorpse>>();
            Vis.Entities(dungeon.transform.position, 80f, obj);
            foreach (LootableCorpse item in obj)
            {
                if (item.IsValid() && !item.IsDestroyed)
                {
                    item?.Kill();
                }
            }

            Pool.FreeUnmanaged(ref obj);
        }

        private void DestroyEntities(ActiveDungeon dungeon)
        {
            foreach (var entityId in dungeon.EntityIds)
            {
                var entity = BaseNetworkable.serverEntities.Find(new NetworkableId(entityId)) as BaseEntity;
                entity?.Kill(BaseNetworkable.DestroyMode.None);
            }

            dungeon.EntityIds.Clear();
        }

        private void RemoveDungeonMarkers(ActiveDungeon dungeon)
        {
            foreach (var marker in _dungeonData.ActiveDungeonMarkers.Where(m => m.Position == dungeon.Position).ToList())
            {
                var vendingMarker = BaseNetworkable.serverEntities.Find(new NetworkableId(marker.VendingMarker)) as BaseEntity;
                var radiusMarker = BaseNetworkable.serverEntities.Find(new NetworkableId(marker.RadiusMarker)) as BaseEntity;

                vendingMarker?.Kill(BaseNetworkable.DestroyMode.None);
                radiusMarker?.Kill(BaseNetworkable.DestroyMode.None);

                _dungeonData.ActiveDungeonMarkers.Remove(marker);
            }
        }

        private IEnumerable<ActiveDungeon> RemoveAllDungeons()
        {
            var removedDungeons = new List<ActiveDungeon>(_dungeonData.ActiveDungeons);
            foreach (var dungeon in removedDungeons)
            {
                DestroyDungeon(dungeon);
            }
            _dungeonData = new DungeonData();
            SaveData();
            return removedDungeons;
        }

        #endregion

        #region Utility Methods

        private string GetAvailableTiers()
        {
            return string.Join(", ", _config.Tiers.GetType().GetProperties().Select(prop => prop.Name));
        }

        private bool CheckBalance(BasePlayer player, int cost)
        {
            if (_config.EconomyPlugin == 1)
            {
                return CheckEconomicsBalance(player, cost);
            }
            else if (_config.EconomyPlugin == 2)
            {
                return CheckServerRewardsBalance(player, cost);
            }

            return false;
        }

        private bool WithdrawFunds(BasePlayer player, int cost)
        {
            if (_config.EconomyPlugin == 1)
            {
                return WithdrawEconomicsFunds(player, cost);
            }
            else if (_config.EconomyPlugin == 2)
            {
                return WithdrawServerRewardsFunds(player, cost);
            }

            return false;
        }

        private bool CheckEconomicsBalance(BasePlayer player, int cost)
        {
            if (Economics == null)
            {
                PrintWarning("Economics plugin is not loaded.");
                return false;
            }

            double balance = Convert.ToDouble(Economics?.CallHook("Balance", player.userID));
            return balance >= cost;
        }

        private bool CheckServerRewardsBalance(BasePlayer player, int cost)
        {
            if (ServerRewards == null)
            {
                PrintWarning("ServerRewards plugin is not loaded.");
                return false;
            }

            int balance = Convert.ToInt32(ServerRewards?.CallHook("CheckPoints", player.userID));
            return balance >= cost;
        }

        private bool WithdrawEconomicsFunds(BasePlayer player, int cost)
        {
            if (Economics == null)
            {
                PrintWarning("Economics plugin is not loaded.");
                return false;
            }

            return Convert.ToBoolean(Economics?.CallHook("Withdraw", player.userID, (double)cost));
        }

        private bool WithdrawServerRewardsFunds(BasePlayer player, int cost)
        {
            if (ServerRewards == null)
            {
                PrintWarning("ServerRewards plugin is not loaded.");
                return false;
            }

            return Convert.ToBoolean(ServerRewards?.CallHook("TakePoints", player.userID, cost));
        }

        private bool CanEnter(BasePlayer player, ActiveDungeon activeDungeon)
        {
            if (!permission.UserHasPermission(player.UserIDString, PermissionEnter) && !permission.UserHasPermission(player.UserIDString, PermissionAdmin))
                return false;

            if (activeDungeon.OwnerID == 0UL)
            {
                activeDungeon.OwnerID = player.userID;
                activeDungeon.OwnerName = player.displayName;
                return true;
            }

            if (activeDungeon.OwnerID == player.userID)
                return true;

            if (_config.AllowTeam && player.currentTeam != 0UL)
            {
                var team = RelationshipManager.ServerInstance.FindTeam(player.currentTeam);
                if (team != null && team.members.Contains(activeDungeon.OwnerID))
                    return true;
            }

            if (_config.AllowFriends && Friends != null)
            {
                bool areFriends = (bool)(Friends.CallHook("AreFriends", activeDungeon.OwnerID, player.userID) ?? false);
                if (areFriends)
                    return true;
            }

            return permission.UserHasPermission(player.UserIDString, PermissionAdmin);
        }

        private ActiveDungeon GetActiveDungeonByPortal(BasePortal portal)
        {
            return _dungeonData.ActiveDungeons.FirstOrDefault(d => d.PortalId == portal.net.ID.Value);
        }

        private void LogDebug(string message, int level = 0)
        {
            if (string.IsNullOrEmpty(message))
                return;

            string formattedMessage = $"[{Name}] {message}";

            switch (level)
            {
                case 1:
                    if (_config.EnableWarningLogs)
                        Debug.LogWarning(formattedMessage);
                    break;
                case 2:
                    if (_config.EnableWarningLogs)
                        Debug.LogError(formattedMessage);
                    break;
                default:
                    if (_config.EnableDebugLogs)
                        Debug.Log(formattedMessage);
                    break;
            }
        }

        private void CleanupExistingEntities()
        {
            foreach (
                var entityType in new Type[]
                {
                    typeof(VendingMachineMapMarker),
                    typeof(MapMarkerGenericRadius),
                    typeof(BasePortal),
                    typeof(ProceduralDynamicDungeon),
                    typeof(AutoTurret),
                    typeof(NPCPlayer),
                    typeof(StorageContainer),
                }
            )
            {
                foreach (var obj in UnityEngine.Object.FindObjectsOfType(entityType))
                {
                    if (obj is BaseEntity baseEntity && baseEntity.OwnerID == OwnerID)
                    {
                        baseEntity.Kill(BaseNetworkable.DestroyMode.None);
                    }
                }
            }

            _dungeonData = new DungeonData();
        }

        private void InitializeTierNameMap()
        {
            _tierNameMap = new Dictionary<DungeonTierConfig, string>
            {
                { _config.Tiers.Easy, "Easy" },
                { _config.Tiers.Normal, "Normal" },
                { _config.Tiers.Medium, "Medium" },
                { _config.Tiers.Hard, "Hard" },
                { _config.Tiers.Nightmare, "Nightmare" },
            };
        }

        private void LoadData()
        {
            _dataFile = Interface.Oxide.DataFileSystem.GetFile($"{Name}_Data");
            _dungeonData = _dataFile.ReadObject<DungeonData>() ?? new DungeonData();
        }

        private void SaveData()
        {
            _dataFile.WriteObject(_dungeonData);
        }

        private static bool IsPlayer(BasePlayer targetPlayer) => targetPlayer != null && targetPlayer.userID.IsSteamId();

        private void NotifyPlayersOfNewDungeon(ActiveDungeon activeDungeon)
        {
            foreach (var player in BasePlayer.activePlayerList)
            {
                NotifyPlayer(player, "DungeonAppeared", 0, activeDungeon.TierConfig.MapName, activeDungeon.Grid);

                if (_config.EnableShowToast)
                {
                    var message = Msg(player.UserIDString, "DungeonAppeared", activeDungeon.TierConfig.MapName, activeDungeon.Grid);
                    player.ShowToast(GameTip.Styles.Blue_Normal, message);
                }
            }
        }

        private void NotifyPlayerOnRemoval(BasePlayer player, IEnumerable<ActiveDungeon> removedDungeons)
        {
            if (!removedDungeons.Any())
            {
                NotifyPlayer(player, "NoInactiveDungeonsToRemove", 0);
                return;
            }

            foreach (var dungeon in removedDungeons)
            {
                NotifyPlayer(player, "DungeonRemovedDetail", 0, dungeon.TierName, dungeon.Grid);
            }
        }

        private bool IsAdmin(BasePlayer player) => player != null && player.IsAdmin;

        private bool IsDungeonEntity(BaseEntity entity) => entity != null && _dungeonData.ActiveDungeons.Any(d => d.EntityIds.Contains(entity.net.ID.Value));

        private string GetRandomDungeonPrefab()
        {
            var prefabs = new List<string>();
            if (_config.DungeonSpawn.EnableXmasDungeon && false)
                prefabs.Add("assets/prefabs/missions/portal/xmasportalentry.prefab");
            if (_config.DungeonSpawn.EnableHalloweenDungeon || true)
                prefabs.Add("assets/prefabs/missions/portal/halloweenportalentry.prefab");
            return prefabs.Count > 0 ? prefabs[Random.Range(0, prefabs.Count)] : null;
        }

        private static bool IsValidPosition(Vector3 position, out Vector3 finalPosition, out Quaternion finalRotation)
        {
            finalPosition = Vector3.zero;
            finalRotation = Quaternion.identity;

            if (!LocationCheck.TerrainDetection(position, out var hitInfo))
                return false;
            if (LocationCheck.IsInsideRock(position, 10f) || LocationCheck.InWater(position) || LocationCheck.IsOnRoad(position) || LocationCheck.InNoBuildZone(position, 10f))
                return false;

            if (LocationCheck.HasCollider(position, 10f))
                return false;
            if (LocationCheck.IsInMonument(position))
                return false;

            return SetSuitablePositionAndRotation(position, hitInfo, out finalPosition, out finalRotation);
        }

        private static bool SetSuitablePositionAndRotation(Vector3 position, RaycastHit hitInfo, out Vector3 suitablePosition, out Quaternion suitableRotation)
        {
            suitablePosition = hitInfo.point;
            suitableRotation = Quaternion.FromToRotation(Vector3.up, hitInfo.normal);
            return true;
        }

        private void CreateDungeonMarkers(ActiveDungeon activeDungeon)
        {
            var prefabVendingMarker = "assets/prefabs/deployable/vendingmachine/vending_mapmarker.prefab";
            var vendingMarker = CreateMapMarker<VendingMachineMapMarker>(prefabVendingMarker, activeDungeon.Position);
            vendingMarker.OwnerID = OwnerID;
            vendingMarker.markerShopName = activeDungeon.TierConfig.MapName;
            vendingMarker.SendNetworkUpdate();

            var prefabRadiusMarker = "assets/prefabs/tools/map/genericradiusmarker.prefab";
            var radiusMarker = CreateMapMarker<MapMarkerGenericRadius>(prefabRadiusMarker, activeDungeon.Position);
            radiusMarker.OwnerID = OwnerID;
            radiusMarker.alpha = 0.75f;
            radiusMarker.radius = 0.5f;
            radiusMarker.color2 = GetMarkerColorByTier(activeDungeon.TierName);
            radiusMarker.SendUpdate();
            radiusMarker.SendNetworkUpdate();

            _dungeonData.ActiveDungeonMarkers.Add(
                new DungeonMarker
                {
                    VendingMarker = vendingMarker.net.ID.Value,
                    RadiusMarker = radiusMarker.net.ID.Value,
                    Position = activeDungeon.Position,
                    Tier = activeDungeon.TierName,
                }
            );
        }

        private T CreateMapMarker<T>(string prefab, Vector3 position)
            where T : BaseEntity
        {
            var entity = GameManager.server.CreateEntity(prefab, position) as T;
            entity?.Spawn();
            return entity;
        }

        private static Color GetMarkerColorByTier(string tierName)
        {
            return tierName switch
            {
                "Easy" => new Color(0.2f, 0.8f, 0.2f, 1f),
                "Normal" => new Color(1f, 0.8f, 0f, 1f),
                "Medium" => new Color(1f, 0.5f, 0f, 1f),
                "Hard" => new Color(0.8f, 0.1f, 0.1f, 1f),
                "Nightmare" => new Color(0.6f, 0.3f, 1f, 1f),
                _ => new Color(0.5f, 0.5f, 0.5f, 1f),
            };
        }

        private bool CanBeRemoved(ActiveDungeon dungeon)
        {
            if (_config.DungeonSpawn.MaxDungeonLifetime > 0 && HasDungeonExceededMaxLifetime(dungeon))
            {
                return NoPlayersInside(dungeon) && dungeon.Spawned;
            }

            if (AreAllNpcsGone(dungeon))
            {
                return NoPlayersInside(dungeon) && dungeon.Spawned;
            }

            return false;
        }

        private bool HasDungeonExceededMaxLifetime(ActiveDungeon dungeon)
        {
            var timeElapsed = DateTime.UtcNow - dungeon.CreationTime;
            return timeElapsed.TotalSeconds >= _config.DungeonSpawn.MaxDungeonLifetime;
        }

        private bool AreAllNpcsGone(ActiveDungeon dungeon)
        {
            return dungeon.NpcIds.All(npcId =>
            {
                var npc = BaseNetworkable.serverEntities.Find(new NetworkableId(npcId)) as BaseEntity;
                return npc == null || npc.IsDestroyed;
            });
        }

        private bool NoPlayersInside(ActiveDungeon dungeon)
        {
            var dynamicDungeon = BaseNetworkable.serverEntities.Find(new NetworkableId(dungeon.DynamicDungeonId)) as ProceduralDynamicDungeon;
            return dynamicDungeon == null || !dynamicDungeon.ContainsAnyPlayers();
        }

        private bool TryFindDungeonSpawnPoint(out Vector3 position, out Quaternion rotation)
        {
            var mapSize = World.Size;
            for (var attempt = 0; attempt < 1000; attempt++)
            {
                var candidatePosition = new Vector3(Random.Range(-mapSize / 2, mapSize / 2), 0, Random.Range(-mapSize / 2, mapSize / 2));

                if (IsValidPosition(candidatePosition, out position, out rotation) && IsSafeFromOtherDungeons(position) && !IsCloseToMonuments(position))
                {
                    return true;
                }
            }

            position = Vector3.zero;
            rotation = Quaternion.identity;
            return false;
        }

        private bool IsSafeFromOtherDungeons(Vector3 position)
        {
            return _dungeonData.ActiveDungeons.All(dungeon => Vector3.Distance(position, dungeon.Position) >= _config.DungeonSpawn.MinDistanceBetweenDungeons);
        }

        private bool IsCloseToMonuments(Vector3 position)
        {
            return TerrainMeta.Path.Monuments.Any(mon => Vector3.Distance(position, mon.transform.position) < _config.DungeonSpawn.MinDistanceFromMonuments);
        }

        private DungeonTierConfig GetDungeonTier(int spawnPointCount)
        {
            if (spawnPointCount < _config.Tiers.Normal.MinSpawnPoints)
                return _config.Tiers.Easy;
            if (spawnPointCount < _config.Tiers.Medium.MinSpawnPoints)
                return _config.Tiers.Normal;
            if (spawnPointCount < _config.Tiers.Hard.MinSpawnPoints)
                return _config.Tiers.Medium;
            if (spawnPointCount < _config.Tiers.Nightmare.MinSpawnPoints)
                return _config.Tiers.Hard;

            return _config.Tiers.Nightmare;
        }

        #endregion

        #region Configuration

        private class Configuration
        {
            [JsonProperty(PropertyName = "Use Notify Plugin")]
            public bool UseNotify { get; set; } = true;

            [JsonProperty(PropertyName = "Enable Debug Logs")]
            public bool EnableDebugLogs { get; set; } = false;

            [JsonProperty(PropertyName = "Enable Warning Logs")]
            public bool EnableWarningLogs { get; set; } = true;

            [JsonProperty(PropertyName = "Enable Toast Messages")]
            public bool EnableShowToast { get; set; } = true;

            [JsonProperty(PropertyName = "Lock dungeon for first player")]
            public bool LockDungeonToPlayer { get; set; } = true;

            [JsonProperty(PropertyName = "Economy Plugin (1 - Economics, 2 - ServerRewards)")]
            public int EconomyPlugin { get; set; } = 1;

            [JsonProperty(PropertyName = "Allow team members")]
            public bool AllowTeam { get; set; } = true;

            [JsonProperty(PropertyName = "Allow friends")]
            public bool AllowFriends { get; set; } = true;

            [JsonProperty(PropertyName = "Dungeon Settings")]
            public DungeonSpawnSettings DungeonSpawn { get; set; } = new DungeonSpawnSettings();

            [JsonProperty(PropertyName = "Tiers")]
            public DungeonTiers Tiers { get; set; } = new DungeonTiers();

            [JsonProperty(PropertyName = "Loot Box Config")]
            public LootBoxConfig LootBoxConfig { get; set; } = new LootBoxConfig();

            [JsonProperty(PropertyName = "Version")]
            public VersionNumber Version { get; set; }
        }

        private class DungeonTiers
        {
            [JsonProperty(PropertyName = "Easy")]
            public DungeonTierConfig Easy { get; set; } =
                new DungeonTierConfig
                {
                    MinSpawnPoints = 5,
                    MapName = "Dungeon: Easy",
                    BuyCost = 300,
                    LootBoxPercentage = 30,
                    TurretPercentage = 20,
                    NpcPercentage = 50,
                    AutoTurretConfig = new TurretConfig { Health = 300, WeaponShortName = "pistol.revolver" },
                    SmallWoodBoxSkinID = 2998755525,
                    MaxItemsPerBox = 3,
                    NpcConfig = new NpcConfig
                    {
                        Name = "Wild Scavenger",
                        Health = 100,
                        DamageScale = 0.5f,
                        RoamRange = 15f,
                        ChaseRange = 15f,
                        WearItems = new WearConfig
                        {
                            Items = new List<WearItemConfig>
                            {
                                new() { ShortName = "tshirt.long", SkinID = 10118 },
                                new() { ShortName = "burlap.gloves", SkinID = 949616124 },
                                new() { ShortName = "attire.snowman.helmet", SkinID = 0 },
                                new() { ShortName = "pants", SkinID = 2352962213 },
                                new() { ShortName = "shoes.boots", SkinID = 2380731293 },
                            },
                        },
                        BeltItems = new BeltConfig
                        {
                            Items = new List<BeltItemConfig>
                            {
                                new()
                                {
                                    ShortName = "pistol.revolver",
                                    Amount = 1,
                                    SkinID = 3140577175,
                                },
                            },
                        },
                    },
                };

            [JsonProperty(PropertyName = "Normal")]
            public DungeonTierConfig Normal { get; set; } =
                new DungeonTierConfig
                {
                    MinSpawnPoints = 7,
                    MapName = "Dungeon: Normal",
                    BuyCost = 500,
                    LootBoxPercentage = 30,
                    TurretPercentage = 25,
                    NpcPercentage = 45,
                    AutoTurretConfig = new TurretConfig { Health = 400, WeaponShortName = "pistol.revolver" },
                    SmallWoodBoxSkinID = 2998755525,
                    MaxItemsPerBox = 4,
                    NpcConfig = new NpcConfig
                    {
                        Name = "Battle Forager",
                        Health = 150,
                        DamageScale = 0.8f,
                        RoamRange = 15f,
                        ChaseRange = 15f,
                        WearItems = new WearConfig
                        {
                            Items = new List<WearItemConfig>
                            {
                                new() { ShortName = "bucket.helmet", SkinID = 1073904216 },
                                new() { ShortName = "jacket", SkinID = 2350426469 },
                                new() { ShortName = "tshirt", SkinID = 10039 },
                                new() { ShortName = "burlap.gloves.new", SkinID = 0 },
                                new() { ShortName = "pants", SkinID = 1441311938 },
                                new() { ShortName = "shoes.boots", SkinID = 2075527039 },
                            },
                        },
                        BeltItems = new BeltConfig
                        {
                            Items = new List<BeltItemConfig>
                            {
                                new()
                                {
                                    ShortName = "smg.2",
                                    Amount = 1,
                                    SkinID = 2386688842,
                                    Mods = new List<string> { "weapon.mod.flashlight" },
                                },
                            },
                        },
                    },
                };

            [JsonProperty(PropertyName = "Medium")]
            public DungeonTierConfig Medium { get; set; } =
                new DungeonTierConfig
                {
                    MinSpawnPoints = 10,
                    MapName = "Dungeon: Medium",
                    BuyCost = 700,
                    LootBoxPercentage = 25,
                    TurretPercentage = 30,
                    NpcPercentage = 45,
                    AutoTurretConfig = new TurretConfig { Health = 600, WeaponShortName = "rifle.ak" },
                    SmallWoodBoxSkinID = 2998755525,
                    MaxItemsPerBox = 5,
                    NpcConfig = new NpcConfig
                    {
                        Name = "Crimson Raider",
                        Health = 200,
                        DamageScale = 1.1f,
                        RoamRange = 15f,
                        ChaseRange = 15f,
                        WearItems = new WearConfig
                        {
                            Items = new List<WearItemConfig>
                            {
                                new() { ShortName = "coffeecan.helmet", SkinID = 1727561127 },
                                new() { ShortName = "roadsign.jacket", SkinID = 1727562915 },
                                new() { ShortName = "roadsign.gloves", SkinID = 2799639349 },
                                new() { ShortName = "pants", SkinID = 2814837980 },
                                new() { ShortName = "shoes.boots", SkinID = 1839313604 },
                                new() { ShortName = "tshirt.long", SkinID = 566893368 },
                            },
                        },
                        BeltItems = new BeltConfig
                        {
                            Items = new List<BeltItemConfig>
                            {
                                new()
                                {
                                    ShortName = "rifle.semiauto",
                                    Amount = 1,
                                    SkinID = 2617680693,
                                    Mods = new List<string> { "weapon.mod.flashlight" },
                                },
                                new()
                                {
                                    ShortName = "syringe.medical",
                                    Amount = 10,
                                    SkinID = 0,
                                },
                            },
                        },
                    },
                };

            [JsonProperty(PropertyName = "Hard")]
            public DungeonTierConfig Hard { get; set; } =
                new DungeonTierConfig
                {
                    MinSpawnPoints = 15,
                    MapName = "Dungeon: Hard",
                    BuyCost = 1000,
                    LootBoxPercentage = 20,
                    TurretPercentage = 35,
                    NpcPercentage = 45,
                    AutoTurretConfig = new TurretConfig { Health = 800, WeaponShortName = "rifle.bolt" },
                    SmallWoodBoxSkinID = 2998755525,
                    MaxItemsPerBox = 7,
                    NpcConfig = new NpcConfig
                    {
                        Name = "Elite Outlaw",
                        Health = 250,
                        DamageScale = 1.3f,
                        RoamRange = 15f,
                        ChaseRange = 15f,
                        WearItems = new WearConfig
                        {
                            Items = new List<WearItemConfig>
                            {
                                new() { ShortName = "metal.facemask", SkinID = 3284864766 },
                                new() { ShortName = "metal.plate.torso", SkinID = 2105505757 },
                                new() { ShortName = "burlap.gloves", SkinID = 2090790324 },
                                new() { ShortName = "hoodie", SkinID = 2080975449 },
                                new() { ShortName = "shoes.boots", SkinID = 10023 },
                                new() { ShortName = "pants", SkinID = 2080977144 },
                                new() { ShortName = "roadsign.kilt", SkinID = 2120628865 },
                            },
                        },
                        BeltItems = new BeltConfig
                        {
                            Items = new List<BeltItemConfig>
                            {
                                new()
                                {
                                    ShortName = "rifle.ak",
                                    Amount = 1,
                                    SkinID = 3190379864,
                                    Mods = new List<string> { "weapon.mod.flashlight" },
                                },
                                new()
                                {
                                    ShortName = "syringe.medical",
                                    Amount = 20,
                                    SkinID = 0,
                                },
                            },
                        },
                    },
                };

            [JsonProperty(PropertyName = "Nightmare")]
            public DungeonTierConfig Nightmare { get; set; } =
                new DungeonTierConfig
                {
                    MinSpawnPoints = 20,
                    MapName = "Dungeon: Nightmare",
                    BuyCost = 1500,
                    LootBoxPercentage = 30,
                    TurretPercentage = 30,
                    NpcPercentage = 40,
                    AutoTurretConfig = new TurretConfig { Health = 1000, WeaponShortName = "rifle.bolt" },
                    SmallWoodBoxSkinID = 2998755525,
                    MaxItemsPerBox = 10,
                    NpcConfig = new NpcConfig
                    {
                        Name = "Nightmare Hunter",
                        Health = 300,
                        DamageScale = 1.5f,
                        RoamRange = 15f,
                        ChaseRange = 15f,
                        WearItems = new WearConfig
                        {
                            Items = new List<WearItemConfig>
                            {
                                new() { ShortName = "metal.facemask", SkinID = 3343860599 },
                                new() { ShortName = "metal.plate.torso", SkinID = 3343861569 },
                                new() { ShortName = "hoodie", SkinID = 1369835131 },
                                new() { ShortName = "roadsign.kilt", SkinID = 1727564168 },
                                new() { ShortName = "burlap.gloves", SkinID = 1210780157 },
                                new() { ShortName = "pants", SkinID = 810745264 },
                                new() { ShortName = "shoes.boots", SkinID = 1210771348 },
                            },
                        },
                        BeltItems = new BeltConfig
                        {
                            Items = new List<BeltItemConfig>
                            {
                                new()
                                {
                                    ShortName = "lmg.m249",
                                    Amount = 1,
                                    SkinID = 1883947256,
                                    Mods = new List<string> { "weapon.mod.flashlight" },
                                },
                                new()
                                {
                                    ShortName = "syringe.medical",
                                    Amount = 50,
                                    SkinID = 0,
                                },
                            },
                        },
                    },
                };
        }

        private class DungeonTierConfig
        {
            [JsonProperty(PropertyName = "Minimum Entity Count for Dungeon Tier")]
            public int MinSpawnPoints { get; set; }

            [JsonProperty(PropertyName = "Map Name")]
            public string MapName { get; set; }

            [JsonProperty(PropertyName = "Buy Cost")]
            public int BuyCost { get; set; }

            [JsonProperty(PropertyName = "Loot Box Percentage")]
            public int LootBoxPercentage { get; set; }

            [JsonProperty(PropertyName = "Turret Percentage")]
            public int TurretPercentage { get; set; }

            [JsonProperty(PropertyName = "NPC Percentage")]
            public int NpcPercentage { get; set; }

            [JsonProperty(PropertyName = "NPC Config")]
            public NpcConfig NpcConfig { get; set; } = new NpcConfig();

            [JsonProperty(PropertyName = "Small Wood Box Skin ID")]
            public ulong SmallWoodBoxSkinID { get; set; } = 2998755525;

            [JsonProperty(PropertyName = "Max Items Per Box")]
            public int MaxItemsPerBox { get; set; } = 6;

            [JsonProperty(PropertyName = "Auto Turret Config")]
            public TurretConfig AutoTurretConfig { get; set; } = new TurretConfig();

            public bool IsValid() => LootBoxPercentage + TurretPercentage + NpcPercentage == 100;
        }

        public class NpcConfig
        {
            [JsonProperty(PropertyName = "NPC Name")]
            public string Name { get; set; }

            [JsonProperty(PropertyName = "NPC Health")]
            public int Health { get; set; }

            [JsonProperty(PropertyName = "Damage Scale")]
            public float DamageScale { get; set; }

            [JsonProperty(PropertyName = "NPC Roam Range")]
            public float RoamRange { get; set; }

            [JsonProperty(PropertyName = "NPC Chase Range")]
            public float ChaseRange { get; set; }

            [JsonProperty(PropertyName = "Wear Items")]
            public WearConfig WearItems { get; set; } = new WearConfig();

            [JsonProperty(PropertyName = "Belt Items")]
            public BeltConfig BeltItems { get; set; } = new BeltConfig();
        }

        public class WearConfig
        {
            [JsonProperty(PropertyName = "Items", ObjectCreationHandling = ObjectCreationHandling.Replace)]
            public List<WearItemConfig> Items { get; set; } = new List<WearItemConfig>();
        }

        public class BeltConfig
        {
            [JsonProperty(PropertyName = "Items", ObjectCreationHandling = ObjectCreationHandling.Replace)]
            public List<BeltItemConfig> Items { get; set; } = new List<BeltItemConfig>();
        }

        public class BeltItemConfig
        {
            [JsonProperty(PropertyName = "ShortName")]
            public string ShortName { get; set; }

            [JsonProperty(PropertyName = "Amount")]
            public int Amount { get; set; }

            [JsonProperty(PropertyName = "SkinID")]
            public ulong SkinID { get; set; }

            [JsonProperty(PropertyName = "Mods")]
            public List<string> Mods { get; set; } = new List<string>();
        }

        public class WearItemConfig
        {
            [JsonProperty(PropertyName = "ShortName")]
            public string ShortName { get; set; }

            [JsonProperty(PropertyName = "SkinID")]
            public ulong SkinID { get; set; }
        }

        public class LootBoxConfig
        {
            [JsonProperty(PropertyName = "Loot Items", ObjectCreationHandling = ObjectCreationHandling.Replace)]
            public List<ItemConfig> LootItems { get; set; } =
                new List<ItemConfig>
                {
                    new()
                    {
                        ShortName = "ammo.rifle",
                        InclusionChancePercentage = 15,
                        MinimumAmount = 100,
                        MaximumAmount = 300,
                    },
                };
        }

        private class TurretConfig
        {
            [JsonProperty(PropertyName = "Health")]
            public float Health { get; set; }

            [JsonProperty(PropertyName = "Weapon Short Name")]
            public string WeaponShortName { get; set; }
        }

        private class DungeonSpawnSettings
        {
            [JsonProperty(PropertyName = "Enable Auto Spawn")]
            public bool EnableAutoSpawn { get; set; } = false;

            [JsonProperty(PropertyName = "Enable Halloween Dungeon")]
            public bool EnableHalloweenDungeon { get; set; } = true;

            [JsonProperty(PropertyName = "Enable Xmas Dungeon")]
            public bool EnableXmasDungeon { get; set; } = false;

            [JsonProperty(PropertyName = "Max Total Active Dungeons")]
            public int MaxTotalActiveDungeons { get; set; } = 5;

            [JsonProperty(PropertyName = "Min Distance From Monuments")]
            public float MinDistanceFromMonuments { get; set; } = 100f;

            [JsonProperty(PropertyName = "Min Distance Between Dungeons")]
            public float MinDistanceBetweenDungeons { get; set; } = 100f;

            [JsonProperty(PropertyName = "Remove Dungeon if All NPCs Are Dead")]
            public bool RemoveIfAllNpcsDead { get; set; } = true;

            [JsonProperty(PropertyName = "Max Time (in seconds) to Keep Dungeon Alive")]
            public int MaxDungeonLifetime { get; set; } = 1800;
        }

        public class ItemConfig
        {
            [JsonProperty(PropertyName = "ShortName")]
            public string ShortName { get; set; }

            [JsonProperty(PropertyName = "Inclusion Chance Percentage")]
            public float InclusionChancePercentage { get; set; }

            [JsonProperty(PropertyName = "Minimum Amount")]
            public int MinimumAmount { get; set; }

            [JsonProperty(PropertyName = "Maximum Amount")]
            public int MaximumAmount { get; set; }

            [JsonProperty(PropertyName = "CustomName")]
            public string CustomName { get; set; } = string.Empty;

            [JsonProperty(PropertyName = "SkinID")]
            public ulong SkinID { get; set; } = 0;
        }

        protected override void LoadConfig()
        {
            PreviousVersion = Version;
            base.LoadConfig();
            try
            {
                _config = Config.ReadObject<Configuration>();
                bool configChanged = false;

                if (_config == null)
                    LoadDefaultConfig();

                if (_config.Version == null || _config.Version < Version)
                {
                    PreviousVersion = _config.Version;
                    PrintWarning("Updating configuration file to new version.");
                    _config.Version = Version;
                    configChanged = true;
                }

                if (configChanged)
                {
                    PrintWarning("Configuration changes detected, saving changes.");
                    SaveConfig();
                }
            }
            catch (Exception ex)
            {
                PrintError($"Error reading config: {ex.Message}");
                LoadDefaultConfig();
            }
            SaveConfig();
        }

        protected override void LoadDefaultConfig()
        {
            PrintWarning("Creating a new configuration file");
            _config = new Configuration();
            _config.Version = Version;
        }

        protected override void SaveConfig()
        {
            Config.WriteObject(_config, true);
        }

        #endregion

        #region Localization

        protected override void LoadDefaultMessages()
        {
            if (PreviousVersion < Version)
            {
                lang.RegisterMessages(new Dictionary<string, string> { }, this);
                lang.RegisterMessages(new Dictionary<string, string> { }, this, "pt-BR");
                lang.RegisterMessages(new Dictionary<string, string> { }, this, "de");
                lang.RegisterMessages(new Dictionary<string, string> { }, this, "es");
                lang.RegisterMessages(new Dictionary<string, string> { }, this, "ru");
            }

            lang.RegisterMessages(
                new Dictionary<string, string>
                {
                    ["AlreadyOwnsDungeonMessage"] = "You already own an active dungeon. You cannot purchase another one until it expires or is removed.",
                    ["DungeonAppeared"] = "A {0} has appeared at grid {1}!",
                    ["DungeonRemovedDetail"] = "Removed dungeon of tier {0} at grid {1}.",
                    ["DungeonSpawnedSuccess"] = "Dungeon spawned successfully at grid {0}!",
                    ["DungeonSpawnFailed"] = "Failed to create the dungeon. We will try again shortly.",
                    ["DungeonStillSpawningMessage"] = "The dungeon is still spawning. Please wait until it's ready before entering.",
                    ["InsufficientFundsMessage"] = "You don't have enough funds to buy the {0} tier. Required: {1}.",
                    ["InvalidTierMessage"] = "Invalid tier name '{0}'. Available tiers: {1}.",
                    ["MaxActiveDungeonsReached"] = "Cannot spawn: Maximum number of active dungeons reached.",
                    ["NoAccess"] = "You don't have permission to do this.",
                    ["PurchaseSuccessMessage"] = "You have successfully purchased the {0} tier!",
                    ["ReceivedRequest"] = "Your request has been added to the queue. Please wait.",
                    ["UsageBuyMessage"] = "Usage: /buydungeon <tier_name>",
                    ["UsageCreateMessage"] = "Usage: /createdungeon <tier_name>",
                    ["CleanedUp"] = "All entities related to this plugin have been cleaned up.",
                },
                this
            );

            lang.RegisterMessages(
                new Dictionary<string, string>
                {
                    ["AlreadyOwnsDungeonMessage"] = "Você já possui uma masmorra ativa. Não é possível comprar outra antes que ela expire ou seja removida.",
                    ["DungeonAppeared"] = "Uma {0} apareceu na grade {1}!",
                    ["DungeonRemovedDetail"] = "Masmorra de nível {0} removida na grade {1}.",
                    ["DungeonSpawnedSuccess"] = "Masmorra gerada com sucesso na grade {0}!",
                    ["DungeonSpawnFailed"] = "Falha ao criar a masmorra. Tentaremos novamente em breve.",
                    ["DungeonStillSpawningMessage"] = "A masmorra ainda está sendo gerada. Por favor, aguarde até que esteja pronta antes de entrar.",
                    ["InsufficientFundsMessage"] = "Você não tem fundos suficientes para comprar o nível {0}. Requerido: {1}.",
                    ["InvalidTierMessage"] = "Nome de nível inválido '{0}'. Níveis disponíveis: {1}.",
                    ["MaxActiveDungeonsReached"] = "Não é possível gerar: número máximo de masmorras ativas alcançado.",
                    ["NoAccess"] = "Você não tem permissão para fazer isso.",
                    ["PurchaseSuccessMessage"] = "Você comprou com sucesso o nível {0}!",
                    ["ReceivedRequest"] = "Seu pedido foi adicionado à fila. Por favor, aguarde.",
                    ["UsageBuyMessage"] = "Uso: /buydungeon <nome_do_nível>",
                    ["UsageCreateMessage"] = "Uso: /createdungeon <nome_do_nível>",
                    ["CleanedUp"] = "Todos os objetos relacionados a este plugin foram removidos.",
                },
                this,
                "pt-BR"
            );

            lang.RegisterMessages(
                new Dictionary<string, string>
                {
                    ["AlreadyOwnsDungeonMessage"] = "Du besitzt bereits ein aktives Verlies. Du kannst kein weiteres kaufen, bevor es abläuft oder entfernt wird.",
                    ["DungeonAppeared"] = "Ein {0} ist bei Gitter {1} erschienen!",
                    ["DungeonRemovedDetail"] = "Verlies der Stufe {0} bei Gitter {1} wurde entfernt.",
                    ["DungeonSpawnedSuccess"] = "Verlies erfolgreich bei Gitter {0} erstellt!",
                    ["DungeonSpawnFailed"] = "Fehler beim Erstellen des Verlieses. Wir versuchen es bald erneut.",
                    ["DungeonStillSpawningMessage"] = "Das Verlies wird noch erstellt. Bitte warte, bis es bereit ist, bevor du es betrittst.",
                    ["InsufficientFundsMessage"] = "Du hast nicht genug Geld, um die Stufe {0} zu kaufen. Erforderlich: {1}.",
                    ["InvalidTierMessage"] = "Ungültiger Stufenname '{0}'. Verfügbare Stufen: {1}.",
                    ["MaxActiveDungeonsReached"] = "Kann nicht erstellt werden: Maximale Anzahl aktiver Verliese erreicht.",
                    ["NoAccess"] = "Du hast keine Berechtigung, dies zu tun.",
                    ["PurchaseSuccessMessage"] = "Du hast die Stufe {0} erfolgreich gekauft!",
                    ["ReceivedRequest"] = "Deine Anfrage wurde der Warteschlange hinzugefügt. Bitte warte.",
                    ["UsageBuyMessage"] = "Verwendung: /buydungeon <stufen_name>",
                    ["UsageCreateMessage"] = "Verwendung: /createdungeon <stufen_name>",
                    ["CleanedUp"] = "Alle mit diesem Plugin verbundenen Objekte wurden entfernt.",
                },
                this,
                "de"
            );

            lang.RegisterMessages(
                new Dictionary<string, string>
                {
                    ["AlreadyOwnsDungeonMessage"] = "Ya posees una mazmorra activa. No puedes comprar otra hasta que expire o se elimine.",
                    ["DungeonAppeared"] = "¡Una {0} ha aparecido en la cuadrícula {1}!",
                    ["DungeonRemovedDetail"] = "Mazmorra de nivel {0} eliminada en la cuadrícula {1}.",
                    ["DungeonSpawnedSuccess"] = "Mazmorra creada exitosamente en la cuadrícula {0}!",
                    ["DungeonSpawnFailed"] = "Error al crear la mazmorra. Intentaremos de nuevo pronto.",
                    ["DungeonStillSpawningMessage"] = "La mazmorra aún se está generando. Por favor, espera hasta que esté lista antes de entrar.",
                    ["InsufficientFundsMessage"] = "No tienes suficientes fondos para comprar el nivel {0}. Requerido: {1}.",
                    ["InvalidTierMessage"] = "Nombre de nivel inválido '{0}'. Niveles disponibles: {1}.",
                    ["MaxActiveDungeonsReached"] = "No se puede generar: se alcanzó el número máximo de mazmorras activas.",
                    ["NoAccess"] = "No tienes permiso para hacer esto.",
                    ["PurchaseSuccessMessage"] = "¡Completaste exitosamente la compra del nivel {0}!",
                    ["ReceivedRequest"] = "Tu solicitud se ha añadido a la cola. Por favor espera.",
                    ["UsageBuyMessage"] = "Uso: /buydungeon <nombre_del_nivel>",
                    ["UsageCreateMessage"] = "Uso: /createdungeon <nombre_del_nivel>",
                    ["CleanedUp"] = "Todos los objetos relacionados con este complemento han sido eliminados.",
                },
                this,
                "es"
            );

            lang.RegisterMessages(
                new Dictionary<string, string>
                {
                    ["AlreadyOwnsDungeonMessage"] = "Вы уже владеете активным подземельем. Вы не можете приобрести другое, пока оно не истечет или не будет удалено.",
                    ["DungeonAppeared"] = "Подземелье {0} появилось в сетке {1}!",
                    ["DungeonRemovedDetail"] = "Удалено подземелье уровня {0} в сетке {1}.",
                    ["DungeonSpawnedSuccess"] = "Подземелье успешно создано в сетке {0}!",
                    ["DungeonSpawnFailed"] = "Не удалось создать подземелье. Мы скоро попробуем снова.",
                    ["DungeonStillSpawningMessage"] = "Подземелье все еще создается. Пожалуйста, подождите, пока оно не будет готово перед входом.",
                    ["InsufficientFundsMessage"] = "У вас недостаточно средств для покупки уровня {0}. Требуется: {1}.",
                    ["InvalidTierMessage"] = "Недопустимое имя уровня '{0}'. Доступные уровни: {1}.",
                    ["MaxActiveDungeonsReached"] = "Невозможно создать: достигнуто максимальное количество активных подземелий.",
                    ["NoAccess"] = "У вас нет разрешения для этого.",
                    ["PurchaseSuccessMessage"] = "Вы успешно приобрели уровень {0}!",
                    ["ReceivedRequest"] = "Ваш запрос добавлен в очередь. Пожалуйста, подождите.",
                    ["UsageBuyMessage"] = "Использование: /buydungeon <имя_уровня>",
                    ["UsageCreateMessage"] = "Использование: /createdungeon <имя_уровня>",
                    ["CleanedUp"] = "Все объекты, связанные с этим плагином, были удалены.",
                },
                this,
                "ru"
            );
        }

        private string Msg(string userId, string key, params object[] obj)
        {
            return string.Format(lang.GetMessage(key, this, userId), obj);
        }

        private void NotifyPlayer(BasePlayer player, string key, int type, params object[] obj)
        {
            if (player != null && player.IsConnected)
            {
                var userIdString = player.UserIDString;
                if (_config.UseNotify && Notify != null)
                    Interface.Oxide.CallHook("SendNotify", userIdString, type, Msg(userIdString, key, obj));
                else
                    player.ChatMessage(Msg(userIdString, key, obj));
            }
        }

        #endregion

        #region Data Classes

        private class DungeonData
        {
            public List<ActiveDungeon> ActiveDungeons { get; set; } = new List<ActiveDungeon>();
            public List<DungeonMarker> ActiveDungeonMarkers { get; set; } = new List<DungeonMarker>();
        }

        private class ActiveDungeon
        {
            public ulong PortalId { get; set; }
            public ulong DynamicDungeonId { get; set; }
            public ulong OwnerID { get; set; } = 0UL;
            public string OwnerName { get; set; } = string.Empty;
            public Vector3 Position { get; set; }
            public string Grid { get; set; }
            public string TierName { get; set; }
            public string DungeonType { get; set; }
            public DungeonTierConfig TierConfig { get; set; }
            public List<ulong> EntityIds { get; set; } = new List<ulong>();
            public List<ulong> NpcIds { get; set; } = new List<ulong>();
            public bool Spawned { get; set; } = false;
            public DateTime CreationTime { get; set; } = DateTime.UtcNow;
        }

        private class DungeonMarker
        {
            public ulong VendingMarker { get; set; }
            public ulong RadiusMarker { get; set; }
            public Vector3 Position { get; set; }
            public string Tier { get; set; }
        }

        #endregion

        #region Collider Checks

        public static class LocationCheck
        {
            public static bool IsInsideRock(Vector3 position, float radius)
            {
                List<Collider> colliders = Pool.GetList<Collider>();
                Vis.Colliders(position, radius, colliders, Layers.Mask.World, QueryTriggerInteraction.Ignore);

                bool result = false;
                foreach (Collider collider in colliders)
                {
                    if (
                        collider.name.Contains("rock", CompareOptions.OrdinalIgnoreCase)
                        || collider.name.Contains("cliff", CompareOptions.OrdinalIgnoreCase)
                        || collider.name.Contains("formation", CompareOptions.OrdinalIgnoreCase)
                    )
                    {
                        result = true;
                        break;
                    }
                }

                Pool.FreeUnmanaged(ref colliders);
                return result;
            }

            public static bool IsFlying(Vector3 position, float minHeight)
            {
                RaycastHit hitInfo;
                return !Physics.Raycast(position, Vector3.down, out hitInfo, minHeight, -5);
            }

            public static bool IsInsideObject(Vector3 position, float radius)
            {
                return GamePhysics.CheckSphere(
                    position,
                    radius,
                    Layers.Mask.Player_Server | Layers.Server.Deployed | Layers.Mask.Prevent_Movement | Layers.Mask.Trigger,
                    QueryTriggerInteraction.Ignore
                );
            }

            public static bool IsClipped(Vector3 position, float radius, float margin = -0.30f)
            {
                var colliders = Pool.Get<List<Collider>>();
                Vis.Colliders(position, radius, colliders, Layers.Mask.World);

                string[] ignoredColliders = { "prevent_building", "UndergroundTrigger", "heatSource", "radiation", "comfortSource" };

                bool isClipped = false;

                foreach (var collider in colliders)
                {
                    if (ignoredColliders.Contains(collider.name))
                    {
                        continue;
                    }

                    var bounds = collider.bounds;

                    bool isInside =
                        position.x >= (bounds.min.x - margin)
                        && position.x <= (bounds.max.x + margin)
                        && position.z >= (bounds.min.z - margin)
                        && position.z <= (bounds.max.z + margin)
                        && position.y >= (bounds.min.y - margin)
                        && position.y <= (bounds.max.y + margin);

                    if (isInside)
                    {
                        isClipped = true;
                        break;
                    }
                }

                colliders.Clear();
                Pool.FreeUnmanaged(ref colliders);

                return isClipped;
            }

            public static bool TerrainDetection(Vector3 position, out RaycastHit hitInfo)
            {
                return Physics.Linecast(position + Vector3.up, position - Vector3.up, out hitInfo, Layers.Mask.Terrain);
            }

            public static bool InWater(Vector3 position) => WaterLevel.Test(position, false, false);

            public static bool IsOnRoad(Vector3 position)
            {
                return (TerrainMeta.TopologyMap.GetTopology(position) & (int)TerrainTopology.Enum.Road) != 0;
            }

            public static bool InNoBuildZone(Vector3 position, float radius)
            {
                return Physics.CheckSphere(position, radius, Layers.Mask.Prevent_Building);
            }

            public static bool IsInMonument(Vector3 position)
            {
                return TerrainMeta.Path.Monuments.Any(monument => monument.IsInBounds(position) && monument.shouldDisplayOnMap) || OnSpecificTopology(position, TerrainTopology.Enum.Monument);
            }

            public static bool HasCollider(Vector3 position, float radius)
            {
                List<Collider> hitColliders = Pool.Get<List<Collider>>();
                GamePhysics.OverlapSphere(position, radius, hitColliders);

                bool hasColliderNearby = false;
                foreach (Collider collider in hitColliders)
                {
                    BaseEntity entity = collider.gameObject.ToBaseEntity();
                    if (entity != null)
                    {
                        hasColliderNearby = true;
                        break;
                    }
                }

                Pool.FreeUnmanaged(ref hitColliders);
                return hasColliderNearby;
            }

            private static bool OnSpecificTopology(Vector3 position, TerrainTopology.Enum topology)
            {
                return (TerrainMeta.TopologyMap.GetTopology(position) & (int)topology) != 0;
            }
        }

        #endregion

        public class CoroutineManager : MonoBehaviour
        {
            private static CoroutineManager _instance;

            public static CoroutineManager Instance
            {
                get
                {
                    if (_instance == null)
                    {
                        var obj = new GameObject("CoroutineManager");
                        _instance = obj.AddComponent<CoroutineManager>();
                        GameObject.DontDestroyOnLoad(obj);
                    }
                    return _instance;
                }
            }
        }
    }
}