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

namespace Oxide.Plugins
{
    [Info("Dungeons", "Marte6", "1.0.0")]
    public class Dungeons : RustPlugin
    {
        private ConfigData _configData;
        private DynamicConfigFile _dataFile;
        private DungeonData _dungeonData;
        private Timer _autoSpawnTimer;
        private Timer _dungeonCheckTimer;
        private Timer _updateMarkersTimer;
        private Dictionary<DungeonTierConfig, string> _tierNameMap;
        private const ulong OwnerID = 1020304050;

        #region Hooks

        private void OnServerInitialized()
        {
            InitializeTierNameMap();
            LoadData();
            RemoveAllDungeons();
            StartTimerAutoSpawnDungeons();
            StartTimerRemoveInactiveDungeons();
            StartTimerUpdateMarkers();
            SaveData();
        }

        private void OnServerSave() => SaveData();

        private void Unload()
        {
            RemoveAllDungeons();
            _autoSpawnTimer?.Destroy();
            _dungeonCheckTimer?.Destroy();
            _updateMarkersTimer?.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;
        }

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

            if (IsDungeonEntity(autoTurret))
            {
                if (hitinfo.InitiatorPlayer == null || !IsValidSteamPlayer(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 || !IsValidSteamPlayer(hitinfo.Initiator as BasePlayer))
                    return false;
                else
                    return true;
            }

            return null;
        }

        void OnEntityDeath(AutoTurret autoTurret, HitInfo info)
        {
            Puts("AutoTurret died");
        }

        void OnEntityDeath(StorageContainer storageContainer, HitInfo info)
        {
            Puts("StorageContainer died");
        }

        #endregion

        #region Chat Commands

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

            AutoSpawnDungeon(player);
        }

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

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

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

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

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

            CleanupExistingEntities();
            player.ChatMessage("All entities related to this plugin have been cleaned up.");
        }

        #endregion

        #region Dungeon Management

        private void StartTimerUpdateMarkers()
        {
            _updateMarkersTimer = timer.Every(30f, UpdateMarkers);
        }

        private void UpdateMarkers()
        {
            var mapMarkers = UnityEngine.Object.FindObjectsOfType<MapMarkerGenericRadius>();
            foreach (var marker in mapMarkers.Where(marker => marker.IsValid()))
            {
                marker.SendUpdate();
            }
        }

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

        private void StartTimerRemoveInactiveDungeons()
        {
            _dungeonCheckTimer = timer.Every(
                60f,
                () =>
                {
                    var removedDungeons = RemoveInactiveDungeons();
                    if (removedDungeons.Count > 0)
                    {
                        foreach (var dungeon in removedDungeons)
                        {
                            LogDungeonRemoval(dungeon);
                        }
                    }
                }
            );
        }

        private void AutoSpawnDungeon(BasePlayer player = null)
        {
            if (_dungeonData.ActiveDungeons.Count >= _configData.DungeonSpawn.MaxTotalActiveDungeons)
            {
                if (_configData.EnableDebug)
                    Puts(lang.GetMessage("MaxActiveDungeonsReached", this));

                player?.ChatMessage(Msg(player.UserIDString, "MaxActiveDungeonsReached"));
            }
            else if (TryFindDungeonSpawnPoint(out var position, out var rotation))
            {
                CreateDungeon(position, rotation, player);
            }
            else
            {
                if (_configData.EnableDebug)
                    Puts(lang.GetMessage("DungeonSpawnFailed", this));

                player?.ChatMessage(Msg(player.UserIDString, "DungeonSpawnFailed"));
            }
        }

        private void CreateDungeon(Vector3 position, Quaternion rotation, BasePlayer player = null)
        {
            string prefabPath = GetRandomDungeonPrefab();
            if (string.IsNullOrEmpty(prefabPath))
            {
                if (_configData.EnableDebug)
                    Puts("No prefab path available for dungeon spawning.");
                return;
            }

            if (!(GameManager.server.CreateEntity(prefabPath, position, rotation) is BasePortal dungeonPortal))
            {
                if (_configData.EnableDebug)
                    Puts("Failed to create dungeon portal.");
                return;
            }

            dungeonPortal.OwnerID = OwnerID;
            dungeonPortal.Spawn();
            Puts($"Dungeon portal spawned successfully at position: {position}.");

            var proceduralDungeon = GetProceduralDungeon(dungeonPortal);
            if (proceduralDungeon == null)
            {
                Puts("Procedural Dungeon is null. Destroying the Dungeon.");
                return;
            }

            if (proceduralDungeon.spawnedCells.Count < 1)
            {
                Puts("Spawned cells count is zero. Destroying the Dungeon.");
                dungeonPortal.Kill(BaseNetworkable.DestroyMode.None);
                return;
            }

            var spawnLocations = GetAvailableSpawnLocations(proceduralDungeon);
            if (spawnLocations.Count == 0)
            {
                Puts("No valid spawn locations available inside Dungeon. Destroying the Dungeon.");
                dungeonPortal.Kill(BaseNetworkable.DestroyMode.None);
                return;
            }

            var selectedTier = GetDungeonTier(proceduralDungeon.spawnedCells.Count);
            Puts($"Selected dungeon tier: {selectedTier}.");

            var activeDungeon = new ActiveDungeon
            {
                PortalId = dungeonPortal.net.ID.Value,
                DynamicDungeonId = proceduralDungeon.net.ID.Value,
                Position = position,
                TierName = _tierNameMap[selectedTier],
                TierConfig = selectedTier,
                EntityIds = new List<ulong>(),
            };

            Puts($"Creating dungeon with ID: {activeDungeon.PortalId}.");

            ServerMgr.Instance.StartCoroutine(PopulateDungeonCoroutine(proceduralDungeon, activeDungeon, spawnLocations, player));
        }

        private IEnumerator PopulateDungeonCoroutine(
            ProceduralDynamicDungeon proceduralDungeon,
            ActiveDungeon activeDungeon,
            List<(Vector3 Position, Quaternion Rotation)> spawnLocations,
            BasePlayer player = null
        )
        {
            yield return new WaitForSeconds(3f);

            if (_configData.EnableDebug)
                Puts($"Starting to populate dungeon ID: {activeDungeon.PortalId}, Tier: {activeDungeon.TierName}.");

            RemoveOriginalEntities(proceduralDungeon);

            var allSpawnEntries = GenerateSpawnEntries(activeDungeon.TierConfig);
            if (_configData.EnableDebug)
                Puts($"Generated {allSpawnEntries.Count} spawn entries for dungeon.");

            int spawnIndex = 0;

            foreach (var entry in allSpawnEntries)
            {
                var (position, rotation) = spawnLocations[spawnIndex];
                var entity = GameManager.server.CreateEntity(entry.prefab.resourcePath, position, rotation) ?? null;

                if (entity != null)
                {
                    entity.OwnerID = OwnerID;
                    entity.Spawn();
                    InitializeEntity(entity, activeDungeon);

                    yield return new WaitForSeconds(3f);

                    if (entity.IsDestroyed || !entity.isSpawned)
                    {
                        if (_configData.EnableDebug)
                            Puts($"Entity: {entry.prefab.resourcePath} was destroyed immediately after spawning.");
                    }

                    if (_configData.EnableDebug)
                        Puts($"Placed entity: {entity.ShortPrefabName} at position: {position}.");
                }
                else
                {
                    if (_configData.EnableDebug)
                        Puts($"Failed to create entity: {entry.prefab.resourcePath} at position: {position}.");
                }

                spawnIndex = (spawnIndex + 1) % spawnLocations.Count;
            }

            if (_configData.EnableDebug)
                Puts($"Finished placing entities. Total placed: {activeDungeon.EntityIds.Count}/{allSpawnEntries.Count}.");

            CreateDungeonMarkers(activeDungeon.Position, activeDungeon.TierName);
            NotifyPlayersOfNewDungeon(activeDungeon.Position, activeDungeon.TierName);
            var grid = PhoneController.PositionToGridCoord(activeDungeon.Position);

            if (_configData.EnableDebug)
                Puts($"Dungeon spawned successfully at grid: {grid}.");

            player?.ChatMessage(Msg(player.UserIDString, "DungeonSpawnedSuccess", grid));

            _dungeonData.ActiveDungeons.Add(activeDungeon);
        }

        private List<(Vector3 Position, Quaternion Rotation)> GetAvailableSpawnLocations(ProceduralDynamicDungeon proceduralDungeon)
        {
            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(null))
                .Select(spawnPoint =>
                {
                    spawnPoint.GetLocation(out var position, out var rotation);
                    return (position, rotation);
                })
                .ToList();
        }

        private void InitializeEntity(BaseEntity entity, ActiveDungeon activeDungeon)
        {
            if (entity != null)
            {
                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.EntityIds.Add(entity.net.ID.Value);
            }
        }

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

                        if (entity != null && !IsDungeonEntity(entity) && (entity is NPCDwelling || entity is ScarecrowNPC || entity is LootContainer))
                        {
                            if (_configData.EnableDebug)
                                Puts($"Removing original entity: {entity.ShortPrefabName}");

                            entity.Kill();
                        }
                    }
                }
            }
        }

        private List<SpawnGroup.SpawnEntry> GenerateSpawnEntries(DungeonTierConfig tierConfig)
        {
            var allSpawnEntries = new List<SpawnGroup.SpawnEntry>();
            AddNpcEntries(tierConfig, allSpawnEntries);
            AddTurretEntries(tierConfig, allSpawnEntries);
            AddLootEntries(tierConfig, allSpawnEntries);
            return allSpawnEntries.OrderBy(_ => Random.value).ToList();
        }

        private void AddNpcEntries(DungeonTierConfig tierConfig, List<SpawnGroup.SpawnEntry> spawnEntries)
        {
            foreach (var config in tierConfig.NpcSpawnConfigs)
            {
                if (GameManifest.pathToGuid.TryGetValue(config.PrefabName, out var guid))
                {
                    spawnEntries.AddRange(
                        Enumerable.Repeat(
                            new SpawnGroup.SpawnEntry
                            {
                                prefab = new GameObjectRef { guid = guid },
                                weight = 1,
                                mobile = true,
                            },
                            config.Total
                        )
                    );
                }
            }
        }

        private void AddTurretEntries(DungeonTierConfig tierConfig, List<SpawnGroup.SpawnEntry> spawnEntries)
        {
            var prefabName = "assets/prefabs/npc/autoturret/autoturret_deployed.prefab";
            if (GameManifest.pathToGuid.TryGetValue(prefabName, out var turretGuid))
            {
                spawnEntries.AddRange(
                    Enumerable.Repeat(
                        new SpawnGroup.SpawnEntry
                        {
                            prefab = new GameObjectRef { guid = turretGuid },
                            weight = 1,
                            mobile = false,
                        },
                        tierConfig.AutoTurretConfig.Total
                    )
                );
            }
        }

        private void AddLootEntries(DungeonTierConfig tierConfig, List<SpawnGroup.SpawnEntry> spawnEntries)
        {
            var prefabName = "assets/prefabs/deployable/woodenbox/woodbox_deployed.prefab";
            if (GameManifest.pathToGuid.TryGetValue(prefabName, out var boxGuid))
            {
                spawnEntries.AddRange(
                    Enumerable.Repeat(
                        new SpawnGroup.SpawnEntry
                        {
                            prefab = new GameObjectRef { guid = boxGuid },
                            weight = 1,
                            mobile = false,
                        },
                        tierConfig.TotalLootBoxes
                    )
                );
            }
        }

        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)
        {
            storage.skinID = _configData.LootBoxConfig.SmallWoodBoxSkinID;
            AddCodeLock(storage);
            PopulateStorage(storage.inventory, _configData.LootBoxConfig.LootItems);
        }

        private void AddCodeLock(StorageContainer storage)
        {
            var prefabName = "assets/prefabs/locks/keypad/lock.code.prefab";
            var codeLock = GameManager.server.CreateEntity(prefabName) as CodeLock;
            if (codeLock == null)
                return;

            codeLock.SetParent(storage, storage.GetSlotAnchorName(BaseEntity.Slot.Lock));
            codeLock.Spawn();
            codeLock.code = Random.Range(1000, 9999).ToString();
            codeLock.hasCode = true;
            codeLock.SetFlag(BaseEntity.Flags.Locked, true);
        }

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

            foreach (var itemConfig in shuffledItems)
            {
                if (differentItemsCount >= _configData.LootBoxConfig.MaxDifferentItemsPerBox)
                {
                    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);
                            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 dungeonEntity = BaseNetworkable.serverEntities.Find(new NetworkableId(dungeon.PortalId)) as HalloweenDungeon;
            if (dungeonEntity != null)
            {
                dungeonEntity?.Kill(BaseNetworkable.DestroyMode.None);
            }

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

            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 ProceduralDynamicDungeon GetProceduralDungeon(BasePortal dungeonPortal)
        {
            return (dungeonPortal as XmasDungeon)?.dungeonInstance.Get(true) ?? (dungeonPortal as HalloweenDungeon)?.dungeonInstance.Get(true);
        }

        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>
            {
                { _configData.Tiers.Easy, "Easy" },
                { _configData.Tiers.Normal, "Normal" },
                { _configData.Tiers.Medium, "Medium" },
                { _configData.Tiers.Hard, "Hard" },
                { _configData.Tiers.Nightmare, "Nightmare" },
            };
        }

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

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

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

        private void NotifyPlayersOfNewDungeon(Vector3 position, string tierName)
        {
            var gridPosition = PhoneController.PositionToGridCoord(position);
            foreach (var player in BasePlayer.activePlayerList)
            {
                var message = Msg(player.UserIDString, "DungeonAppeared", tierName, gridPosition);
                player.ChatMessage(message);
                player.ShowToast(GameTip.Styles.Blue_Normal, message);
            }
        }

        private void NotifyPlayer(BasePlayer player, string messageKey)
        {
            player.ChatMessage(Msg(player.UserIDString, messageKey));
        }

        private void NotifyPlayerOnRemoval(BasePlayer player, IEnumerable<ActiveDungeon> removedDungeons, string noDungeonMessage)
        {
            if (!removedDungeons.Any())
            {
                player.ChatMessage(Msg(player.UserIDString, noDungeonMessage));
                return;
            }

            foreach (var dungeon in removedDungeons)
            {
                var gridPosition = PhoneController.PositionToGridCoord(dungeon.Position);
                player.ChatMessage(Msg(player.UserIDString, "DungeonRemovedDetail", dungeon.TierName, gridPosition));
            }
        }

        private void LogDungeonRemoval(ActiveDungeon dungeon)
        {
            var gridPosition = PhoneController.PositionToGridCoord(dungeon.Position);
            Puts($"Removed inactive dungeon of tier {dungeon.TierName} at grid {gridPosition}.");
        }

        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 (_configData.DungeonSpawn.EnableXmasDungeon)
                prefabs.Add("assets/prefabs/missions/portal/xmasportalentry.prefab");
            if (_configData.DungeonSpawn.EnableHalloweenDungeon)
                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 suitablePosition, out Quaternion suitableRotation)
        {
            suitablePosition = Vector3.zero;
            suitableRotation = Quaternion.identity;

            if (!CollidersChecks.GetTerrainInfo(position, out var hitInfo))
                return false;
            if (CollidersChecks.InsideRock(position, 10f))
                return false;
            if (CollidersChecks.InWater(position))
                return false;
            if (CollidersChecks.OnRoadOrRail(position))
                return false;
            if (CollidersChecks.InNoBuildZone(position, 10f))
                return false;

            return SetSuitablePositionAndRotation(position, hitInfo, out suitablePosition, out suitableRotation);
        }

        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(Vector3 position, string tierName)
        {
            var prefabVendingMarker = "assets/prefabs/deployable/vendingmachine/vending_mapmarker.prefab";
            var vendingMarker = CreateMapMarker<VendingMachineMapMarker>(prefabVendingMarker, position);
            vendingMarker.OwnerID = OwnerID;
            vendingMarker.markerShopName = $"Dungeon: {tierName}";
            vendingMarker.SendNetworkUpdate();

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

            _dungeonData.ActiveDungeonMarkers.Add(
                new DungeonMarker
                {
                    VendingMarker = vendingMarker.net.ID.Value,
                    RadiusMarker = radiusMarker.net.ID.Value,
                    Position = position,
                    Tier = 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" => Color.green,
                "Normal" or "Medium" => Color.yellow,
                "Hard" => Color.red,
                "Nightmare" => Color.black,
                _ => Color.white,
            };
        }

        private bool CanBeRemoved(ActiveDungeon dungeon)
        {
            return AreAllNpcsGone(dungeon) && NoPlayersInside(dungeon);
        }

        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) >= _configData.DungeonSpawn.MinDistanceBetweenDungeons);
        }

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

        private DungeonTierConfig GetDungeonTier(int cellCount)
        {
            return cellCount switch
            {
                < 5 => _configData.Tiers.Easy,
                < 8 => _configData.Tiers.Normal,
                < 12 => _configData.Tiers.Medium,
                < 16 => _configData.Tiers.Hard,
                _ => _configData.Tiers.Nightmare,
            };
        }

        #endregion

        #region Configuration

        private class ConfigData
        {
            [JsonProperty(PropertyName = "Dungeon Spawn 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("Enable Debug")]
            public bool EnableDebug { get; set; } = true;

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

        private class DungeonTiers
        {
            [JsonProperty(PropertyName = "Easy")]
            public DungeonTierConfig Easy { get; set; } =
                new DungeonTierConfig
                {
                    TotalLootBoxes = 1,
                    NpcSpawnConfigs = new List<NpcSpawnConfig>
                    {
                        new NpcSpawnConfig { PrefabName = "assets/rust.ai/agents/npcplayer/humannpc/scientist/scientistnpc_full_pistol.prefab", Total = 1 },
                    },
                    AutoTurretConfig = new TurretConfig
                    {
                        Total = 1,
                        Health = 300,
                        WeaponShortName = "pistol.revolver",
                    },
                };

            [JsonProperty(PropertyName = "Normal")]
            public DungeonTierConfig Normal { get; set; } =
                new DungeonTierConfig
                {
                    TotalLootBoxes = 2,
                    NpcSpawnConfigs = new List<NpcSpawnConfig>
                    {
                        new NpcSpawnConfig { PrefabName = "assets/rust.ai/agents/npcplayer/humannpc/scientist/scientistnpc_full_pistol.prefab", Total = 3 },
                    },
                    AutoTurretConfig = new TurretConfig
                    {
                        Total = 2,
                        Health = 600,
                        WeaponShortName = "smg.2",
                    },
                };

            [JsonProperty(PropertyName = "Medium")]
            public DungeonTierConfig Medium { get; set; } =
                new DungeonTierConfig
                {
                    TotalLootBoxes = 4,
                    NpcSpawnConfigs = new List<NpcSpawnConfig>
                    {
                        new NpcSpawnConfig { PrefabName = "assets/rust.ai/agents/npcplayer/humannpc/scientist/scientistnpc_full_pistol.prefab", Total = 4 },
                    },
                    AutoTurretConfig = new TurretConfig
                    {
                        Total = 3,
                        Health = 1000,
                        WeaponShortName = "smg.mp5",
                    },
                };

            [JsonProperty(PropertyName = "Hard")]
            public DungeonTierConfig Hard { get; set; } =
                new DungeonTierConfig
                {
                    TotalLootBoxes = 5,
                    NpcSpawnConfigs = new List<NpcSpawnConfig>
                    {
                        new NpcSpawnConfig { PrefabName = "assets/rust.ai/agents/npcplayer/humannpc/scientist/scientistnpc_full_pistol.prefab", Total = 4 },
                        new NpcSpawnConfig { PrefabName = "assets/rust.ai/agents/npcplayer/humannpc/scientist/scientistnpc_full_pistol.prefab", Total = 3 },
                    },
                    AutoTurretConfig = new TurretConfig
                    {
                        Total = 4,
                        Health = 1500,
                        WeaponShortName = "rifle.ak",
                    },
                };

            [JsonProperty(PropertyName = "Nightmare")]
            public DungeonTierConfig Nightmare { get; set; } =
                new DungeonTierConfig
                {
                    TotalLootBoxes = 6,
                    NpcSpawnConfigs = new List<NpcSpawnConfig>
                    {
                        new NpcSpawnConfig { PrefabName = "assets/rust.ai/agents/npcplayer/humannpc/scientist/scientistnpc_full_pistol.prefab", Total = 5 },
                        new NpcSpawnConfig { PrefabName = "assets/rust.ai/agents/npcplayer/humannpc/scientist/scientistnpc_full_pistol.prefab", Total = 5 },
                    },
                    AutoTurretConfig = new TurretConfig
                    {
                        Total = 6,
                        Health = 2000,
                        WeaponShortName = "rifle.ak",
                    },
                };
        }

        private class DungeonTierConfig
        {
            [JsonProperty(PropertyName = "Maximum Loot Boxes")]
            public int TotalLootBoxes { get; set; }

            [JsonProperty(PropertyName = "NPC Spawn Configs", ObjectCreationHandling = ObjectCreationHandling.Replace)]
            public List<NpcSpawnConfig> NpcSpawnConfigs { get; set; }

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

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

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

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

        private class NpcSpawnConfig
        {
            [JsonProperty(PropertyName = "Prefab")]
            public string PrefabName { get; set; }

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

        private class TurretConfig
        {
            [JsonProperty(PropertyName = "Maximum")]
            public int Total { get; set; } = 3;

            [JsonProperty(PropertyName = "Health")]
            public float Health { get; set; } = 1000f;

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

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

            [JsonProperty(PropertyName = "Auto Spawn Cycle Interval")]
            public float AutoSpawnCycleInterval { get; set; } = 60f;

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

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

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

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

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

        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; }
        }

        protected override void LoadConfig()
        {
            base.LoadConfig();
            try
            {
                _configData = Config.ReadObject<ConfigData>() ?? new ConfigData();
            }
            catch (Exception ex)
            {
                PrintError($"The configuration file is corrupt: {ex}");
                LoadDefaultConfig();
            }
            SaveConfig();
        }

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

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

        #endregion

        #region Localization

        protected override void LoadDefaultMessages()
        {
            lang.RegisterMessages(
                new Dictionary<string, string>
                {
                    ["NoInactiveDungeonsToRemove"] = "There are no inactive dungeons to remove.",
                    ["NoDungeonsToRemove"] = "There are no dungeons to remove.",
                    ["DungeonRemovedDetail"] = "Removed dungeon of tier {0} at grid {1}.",
                    ["AttemptSpawnDungeons"] = "Attempting to spawn dungeons.",
                    ["DungeonAppeared"] = "A {0} Dungeon has appeared at grid {1}!",
                    ["DungeonSpawnFailed"] = "Could not find a valid location to spawn a new dungeon.",
                    ["DungeonSpawnedSuccess"] = "Dungeon spawned successfully at grid {0}!",
                    ["MaxActiveDungeonsReached"] = "Cannot spawn: Maximum number of active dungeons reached.",
                },
                this
            );
        }

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

        #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 Vector3 Position { get; set; }
            public string TierName { 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>();
        }

        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 CollidersChecks
        {
            public static bool InsideRock(Vector3 position, float radius)
            {
                List<Collider> colliders = Pool.GetList<Collider>();

                Vis.Colliders(position, radius, colliders, Layers.Mask.World, QueryTriggerInteraction.Ignore);

                bool isInsideRock = colliders.Any(collider =>
                    collider.name.Contains("rock", StringComparison.OrdinalIgnoreCase)
                    || collider.name.Contains("cliff", StringComparison.OrdinalIgnoreCase)
                    || collider.name.Contains("formation", StringComparison.OrdinalIgnoreCase)
                );

                Pool.FreeList(ref colliders);
                return isInsideRock;
            }

            public static bool GetTerrainInfo(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 OnRoadOrRail(Vector3 position) => (TerrainMeta.TopologyMap.GetTopology(position) & (int)TerrainTopology.Enum.Road) != 0;

            public static bool InNoBuildZone(Vector3 position, float radius) => Physics.CheckSphere(position, radius, Layers.Mask.Prevent_Building, QueryTriggerInteraction.Ignore);
        }

        #endregion
    }
}