using System;
using System.Collections.Generic;
using System.Text;
using Oxide.Core;
using Oxide.Core.Configuration;
using Oxide.Core.Libraries.Covalence;
using Facepunch;
using Newtonsoft.Json;
using UnityEngine;

namespace Oxide.Plugins
{
    [Info("Player Location", "seacowz", "1.1.0")]
    [Description("Find the location of players on the server")]
    public class PlayerLocation : CovalencePlugin
    {
        #region Variables

        private class PlayerData
        {
            public DateTime LastAwakeTime { get; set; }
            public DateTime LastDeathTime { get; set; }
            public Vector3 LastDeathLocation { get; set; }

            public PlayerData()
            {
                LastAwakeTime = DateTime.MinValue;
                LastDeathTime = DateTime.MinValue;
                LastDeathLocation = Vector3.zero;
            }
        }

        private Dictionary<string, PlayerData> _playerData = new Dictionary<string, PlayerData>();

        private const string permAdmin = "playerlocation.admin";

        // Cached monument data for fast lookups
        private class MonumentInfo
        {
            public string Name;
            public Vector3 Position;
            public float Radius;
        }

        private List<MonumentInfo> _monumentCache = new List<MonumentInfo>();

        // Cached event data for fast lookups
        private class EventInfo
        {
            public string Name;
            public Vector3 Position;
            public float Radius;
        }

        private List<EventInfo> _eventCache = new List<EventInfo>();

        #endregion

        #region Configuration

        private PluginConfig _config;

        private class PluginConfig
        {
            [JsonProperty("Event detection enabled")]
            public bool EventDetectionEnabled { get; set; } = true;

            [JsonProperty("Cargo ship radius")]
            public float CargoShipRadius { get; set; } = 200f;

            [JsonProperty("Chinook radius")]
            public float ChinookRadius { get; set; } = 300f;

            [JsonProperty("Patrol helicopter radius")]
            public float PatrolHelicopterRadius { get; set; } = 300f;

            [JsonProperty("Cargo plane radius")]
            public float CargoPlaneRadius { get; set; } = 300f;

            [JsonProperty("Supply drop radius")]
            public float SupplyDropRadius { get; set; } = 100f;
        }

        protected override void LoadDefaultConfig()
        {
            _config = new PluginConfig();
            SaveConfig();
        }

        protected override void LoadConfig()
        {
            base.LoadConfig();
            try
            {
                _config = Config.ReadObject<PluginConfig>();
                if (_config == null) throw new Exception();
            }
            catch
            {
                PrintWarning("Invalid config file, loading defaults...");
                LoadDefaultConfig();
            }
            SaveConfig();
        }

        protected override void SaveConfig() => Config.WriteObject(_config, true);

        #endregion Configuration

        #region Localization

        protected override void LoadDefaultMessages()
        {
            lang.RegisterMessages(new Dictionary<string, string>
            {
                // Command responses
                ["NoPermission"] = "You do not have permission to use that command.",
                ["NotFound"] = "No players found.",
                ["Usage"] = "Usage: location [-d|-on|-off|-tp] [name|id]",
                ["TpUsage"] = "Usage: location -tp <name|id>",
                ["TpMultiple"] = "Multiple players found. Be more specific.",
                ["TpSuccess"] = "Teleported to {0}.",
                ["TpConsole"] = "Teleport can only be used in-game.",
                ["TpMatchEntry"] = "  {0} ({1})",

                // Player state tags
                ["Sleeping"] = "sleeping",
                ["SleepingMinutes"] = "sleeping {0} minutes",
                ["SleepingHours"] = "sleeping {0} hours",
                ["SleepingDays"] = "sleeping {0} days",
                ["Dead"] = "dead",
                ["Flying"] = "flying",
                ["Swimming"] = "swimming",
                ["Underground"] = "underground",
                ["InBase"] = "in base",
                ["Admin"] = "admin",
                ["God"] = "god",
                ["Near"] = "near {0}",

                // Death tags
                ["Died"] = "died",
                ["DiedMinutesAgo"] = "died {0} minutes ago",
                ["DiedHoursAgo"] = "died {0} hours ago",
                ["DiedDaysAgo"] = "died {0} days ago",

                // Vehicle tags
                ["OnTugboat"] = "on tugboat",
                ["OnRHIB"] = "on RHIB",
                ["OnRowboat"] = "on rowboat",
                ["InKayak"] = "in kayak",
                ["OnBoogieboard"] = "on boogieboard",
                ["OnBoat"] = "on boat",
                ["InSubmarine"] = "in submarine",
                ["InMinicopter"] = "in minicopter",
                ["InScrapHelicopter"] = "in scrap helicopter",
                ["InAttackHelicopter"] = "in attack helicopter",
                ["InBalloon"] = "in balloon",
                ["InCar"] = "in car",
                ["OnHorse"] = "on horse",
                ["OnMotorcycle"] = "on motorcycle",
                ["OnBicycle"] = "on bicycle",
                ["OnSnowmobile"] = "on snowmobile",
                ["OnTrain"] = "on train",

                // Help text
                ["HelpTitle"] = "Player Location v1.1.0 - Commands:",
                ["HelpAll"] = "  location                    - Show all players",
                ["HelpSearch"] = "  location [name|id]          - Search for a player",
                ["HelpOn"] = "  location -on                - Online players only",
                ["HelpOff"] = "  location -off               - Offline/sleeping players only",
                ["HelpOnSearch"] = "  location -on [name|id]      - Search online players",
                ["HelpOffSearch"] = "  location -off [name|id]     - Search offline players",
                ["HelpDeath"] = "  location -d                 - Last death locations",
                ["HelpDeathSearch"] = "  location -d [name|id]       - Death location for a player",
                ["HelpTp"] = "  location -tp [name|id]      - Teleport to a player (in-game)",
                ["HelpHelp"] = "  location -help              - Show this help",
                ["HelpTagsTitle"] = "Status Tags:",
                ["HelpTags"] = "  sleeping, dead, flying, swimming, underground",
                ["HelpTags2"] = "  in base, on boat, in submarine, admin, god",
                ["HelpTags3"] = "  near [Monument], near [Event]",
                ["HelpPermission"] = "Permission: playerlocation.admin"
            }, this);
        }

        #endregion Localization

        #region Hooks

        private void Init()
        {
            permission.RegisterPermission(permAdmin, this);
        }

        private void OnServerInitialized()
        {
            LoadDataFile();
            CacheMonuments();
        }

        private void CacheMonuments()
        {
            _monumentCache.Clear();

            if (TerrainMeta.Path == null || TerrainMeta.Path.Monuments == null)
                return;

            foreach (var monument in TerrainMeta.Path.Monuments)
            {
                if (monument == null) continue;

                string name = monument.displayPhrase.english;
                if (string.IsNullOrEmpty(name))
                    name = monument.name;

                // Clean up prefab path to a readable name
                if (name.Contains("/"))
                    name = name.Substring(name.LastIndexOf('/') + 1);
                name = name.Replace(".prefab", "").Replace("_", " ").Trim();

                // Estimate radius from monument bounds
                float radius = monument.Bounds.extents.Max();
                if (radius < 50f) radius = 50f; // minimum radius for small monuments

                _monumentCache.Add(new MonumentInfo
                {
                    Name = name,
                    Position = monument.transform.position,
                    Radius = radius
                });
            }
        }

        private void RefreshEventCache()
        {
            _eventCache.Clear();

            if (!_config.EventDetectionEnabled) return;

            foreach (var entity in BaseNetworkable.serverEntities)
            {
                if (entity is CargoShip cs)
                    _eventCache.Add(new EventInfo { Name = "Cargo Ship", Position = cs.transform.position, Radius = _config.CargoShipRadius });
                else if (entity is CH47Helicopter ch)
                    _eventCache.Add(new EventInfo { Name = "Chinook", Position = ch.transform.position, Radius = _config.ChinookRadius });
                else if (entity is PatrolHelicopter ph)
                    _eventCache.Add(new EventInfo { Name = "Patrol Helicopter", Position = ph.transform.position, Radius = _config.PatrolHelicopterRadius });
                else if (entity is CargoPlane cp)
                    _eventCache.Add(new EventInfo { Name = "Cargo Plane", Position = cp.transform.position, Radius = _config.CargoPlaneRadius });
                else if (entity is SupplyDrop sd)
                    _eventCache.Add(new EventInfo { Name = "Supply Drop", Position = sd.transform.position, Radius = _config.SupplyDropRadius });
            }
        }

        private void OnPlayerSleep(BasePlayer player)
        {
            PlayerData playerdata;
            if (_playerData.TryGetValue(player.UserIDString, out playerdata))
            {
                playerdata.LastAwakeTime = DateTime.Now;
            }
            else
            {
                playerdata = new PlayerData();
                playerdata.LastAwakeTime = DateTime.Now;
                _playerData[player.UserIDString] = playerdata;
            }
        }

        private void OnPlayerSleepEnded(BasePlayer player)
        {
            PlayerData playerdata;
            if (_playerData.TryGetValue(player.UserIDString, out playerdata))
            {
                playerdata.LastAwakeTime = DateTime.MinValue;
            }
            else
            {
                playerdata = new PlayerData();
                playerdata.LastAwakeTime = DateTime.MinValue;
                _playerData[player.UserIDString] = playerdata;
            }
        }

        private void OnPlayerDeath(BasePlayer player, HitInfo info)
        {
            PlayerData playerdata;
            Vector3 location = player.transform.position;
            if (_playerData.TryGetValue(player.UserIDString, out playerdata))
            {
                playerdata.LastDeathTime = DateTime.Now;
                playerdata.LastDeathLocation = location;
            }
            else
            {
                playerdata = new PlayerData();
                playerdata.LastDeathTime = DateTime.Now;
                playerdata.LastDeathLocation = location;
                _playerData[player.UserIDString] = playerdata;
            }
        }

        private void OnNewSave()
        {
            _playerData.Clear();
            SaveDataFile();
        }

        void Unload()
        {
            SaveDataFile();
        }

        #endregion hooks

        #region Commands

        [Command("location")]
        private void LocationCmd(IPlayer player, string command, string[] args)
        {
            if (!player.HasPermission(permAdmin))
            {
                player.Reply(Lang("NoPermission", player.Id));
                return;
            }

            RefreshEventCache();

            List<Tuple<double, string>> locationlist = new List<Tuple<double, string>>();

            string flag = string.Empty;
            string search = string.Empty;

            // Parse args: first arg can be a flag, second is always a search term
            for (int i = 0; i < args.Length; i++)
            {
                if (args[i] == "-help" || args[i] == "-h")
                {
                    ShowHelp(player);
                    return;
                }
                if (args[i] == "-d" || args[i] == "-on" || args[i] == "-off" || args[i] == "-tp")
                    flag = args[i];
                else if (search == string.Empty)
                    search = args[i];
                else
                {
                    player.Reply(Lang("Usage", player.Id));
                    return;
                }
            }

            if (flag == "-tp")
            {
                TeleportToPlayer(player, search);
                return;
            }

            if (flag == "-d")
            {
                FindDLocation(player, search, ref locationlist);
            }
            else
            {
                int filter = 0; // 0 = all, 1 = online only, 2 = offline only
                if (flag == "-on") filter = 1;
                else if (flag == "-off") filter = 2;
                FindLocation(player, search, ref locationlist, filter);
            }

            if (locationlist.IsEmpty())
            {
                player.Reply(Lang("NotFound", player.Id));
            }
            else
            {
                locationlist.Sort((a, b) => b.Item1.CompareTo(a.Item1));
                foreach (Tuple<double, string> item in locationlist)
                {
                    player.Reply(item.Item2);
                }
            }
        }

        private void ShowHelp(IPlayer player)
        {
            string id = player.Id;
            player.Reply(Lang("HelpTitle", id));
            player.Reply(Lang("HelpAll", id));
            player.Reply(Lang("HelpSearch", id));
            player.Reply(Lang("HelpOn", id));
            player.Reply(Lang("HelpOff", id));
            player.Reply(Lang("HelpOnSearch", id));
            player.Reply(Lang("HelpOffSearch", id));
            player.Reply(Lang("HelpDeath", id));
            player.Reply(Lang("HelpDeathSearch", id));
            player.Reply(Lang("HelpTp", id));
            player.Reply(Lang("HelpHelp", id));
            player.Reply(Lang("HelpTagsTitle", id));
            player.Reply(Lang("HelpTags", id));
            player.Reply(Lang("HelpTags2", id));
            player.Reply(Lang("HelpTags3", id));
            player.Reply(Lang("HelpPermission", id));
        }

        private void TeleportToPlayer(IPlayer caller, string search)
        {
            if (caller.LastCommand == CommandType.Console)
            {
                caller.Reply(Lang("TpConsole", caller.Id));
                return;
            }

            if (string.IsNullOrEmpty(search))
            {
                caller.Reply(Lang("TpUsage", caller.Id));
                return;
            }

            var callerBase = caller.Object as BasePlayer;
            if (callerBase == null) return;

            // Find matching players
            var matches = new List<BasePlayer>();
            foreach (BasePlayer player in BasePlayer.allPlayerList)
            {
                if (player.IsNpc) continue;

                if (player.displayName.Contains(search, System.Globalization.CompareOptions.IgnoreCase) || player.UserIDString.Contains(search))
                {
                    matches.Add(player);
                }
            }

            if (matches.Count == 0)
            {
                caller.Reply(Lang("NotFound", caller.Id));
                return;
            }

            if (matches.Count > 1)
            {
                caller.Reply(Lang("TpMultiple", caller.Id));
                foreach (var match in matches)
                {
                    caller.Reply(string.Format(Lang("TpMatchEntry", caller.Id), match.displayName, match.UserIDString));
                }
                return;
            }

            var target = matches[0];
            Vector3 targetPos = target.transform.position;

            // Teleport caller to target
            callerBase.Teleport(targetPos);
            caller.Reply(string.Format(Lang("TpSuccess", caller.Id), target.displayName));
        }

        #endregion Commands

        #region Datafile

        private void LoadDataFile()
        {
            DynamicConfigFile file = Interface.Oxide.DataFileSystem.GetDatafile(Name);
            try
            {
                _playerData = file.ReadObject<Dictionary<string, PlayerData>>()
                    ?? new Dictionary<string, PlayerData>();
            }
            catch
            {
                _playerData = new Dictionary<string, PlayerData>();
            }
        }

        private void SaveDataFile()
        {
            DynamicConfigFile file = Interface.Oxide.DataFileSystem.GetDatafile(Name);
            file.WriteObject<Dictionary<string, PlayerData>>(_playerData);
        }

        #endregion Datafile

        #region Helper

        private string Lang(string key, string playerId = null, params object[] args)
        {
            string msg = lang.GetMessage(key, this, playerId);
            return args.Length > 0 ? string.Format(msg, args) : msg;
        }

        private void AppendTag(StringBuilder sb, string key, string playerId, params object[] args)
        {
            sb.Append(" (").Append(Lang(key, playerId, args)).Append(')');
        }

        private void FindLocation(IPlayer callingplayer, string search, ref List<Tuple<double, string>> locationlist, int filter = 0)
        {
            bool console = false;
            string pid = callingplayer.Id;

            if (callingplayer.LastCommand == CommandType.Console)
            {
                console = true;
            }

            foreach (BasePlayer player in BasePlayer.allPlayerList)
            {
                var options = new StringBuilder();
                string name = player.displayName;
                string id = player.UserIDString;
                PlayerData playerdata;
                double sleeptime = 0;

                if (player.IsNpc)
                {
                    continue;
                }

                // Online filter: skip sleepers/disconnected
                if (filter == 1 && (player.IsSleeping() || !player.IsConnected))
                {
                    continue;
                }

                // Offline filter: skip awake/connected players
                if (filter == 2 && !player.IsSleeping() && player.IsConnected)
                {
                    continue;
                }

                if (search.Length > 0 && name.Contains(search, System.Globalization.CompareOptions.IgnoreCase) == false && id.Contains(search) == false)
                {
                    continue;
                }

                if (!_playerData.TryGetValue(id, out playerdata))
                {
                    playerdata = new PlayerData();
                    _playerData[id] = playerdata;
                }

                if (player.IsSleeping())
                {
                    if (playerdata.LastAwakeTime != DateTime.MinValue)
                    {
                        TimeSpan diff = DateTime.Now - playerdata.LastAwakeTime;
                        sleeptime = diff.TotalSeconds;
                    }

                    if (!console || sleeptime <= 0)
                    {
                        AppendTag(options, "Sleeping", pid);
                    }
                    else if (sleeptime < 3600) // hour
                    {
                        AppendTag(options, "SleepingMinutes", pid, Math.Floor(sleeptime / 60));
                    }
                    else if (sleeptime < 86400) // day
                    {
                        AppendTag(options, "SleepingHours", pid, Math.Floor(sleeptime / 3600));
                    }
                    else
                    {
                        AppendTag(options, "SleepingDays", pid, Math.Floor(sleeptime / 86400));
                    }
                }
                else if (player.IsDead())
                {
                    AppendTag(options, "Dead", pid);
                }
                else if (player.IsFlying)
                {
                    AppendTag(options, "Flying", pid);
                }

                Vector3 location = player.transform.position;

                // Check monument first — oil rigs are outside the map grid
                // but should not show as "deep sea"
                string monument = GetNearestMonument(location);
                bool isOutsideGrid = IsOutsideGrid(location);

                // Only show monument tag if the player is inside the grid
                // (outside grid, the monument name becomes the grid coordinate instead)
                if (monument != null && !isOutsideGrid)
                {
                    AppendTag(options, "Near", pid, monument);
                }

                if (!isOutsideGrid && (player.IsSwimming() || IsInWater(location)))
                {
                    AppendTag(options, "Swimming", pid);
                }

                if (player.isMounted)
                {
                    bool vehicleFound = false;
                    var mounted = player.GetMountedVehicle();
                    if (mounted != null)
                    {
                        vehicleFound = true;
                        if (mounted is RidableHorse)
                            AppendTag(options, "OnHorse", pid);
                        else if (mounted is Tugboat)
                            AppendTag(options, "OnTugboat", pid);
                        else if (mounted is RHIB)
                            AppendTag(options, "OnRHIB", pid);
                        else if (mounted is MotorRowboat)
                            AppendTag(options, "OnRowboat", pid);
                        else if (mounted is Kayak)
                            AppendTag(options, "InKayak", pid);
                        else if (mounted is BaseBoat)
                            AppendTag(options, "OnBoat", pid);
                        else if (mounted is BaseSubmarine)
                            AppendTag(options, "InSubmarine", pid);
                        else if (mounted is ScrapTransportHelicopter)
                            AppendTag(options, "InScrapHelicopter", pid);
                        else if (mounted is AttackHelicopter)
                            AppendTag(options, "InAttackHelicopter", pid);
                        else if (mounted is Minicopter)
                            AppendTag(options, "InMinicopter", pid);
                        else if (mounted is ModularCar || mounted is BasicCar)
                            AppendTag(options, "InCar", pid);
                        else if (mounted is Bike)
                        {
                            if (mounted.ShortPrefabName.Contains("motor"))
                                AppendTag(options, "OnMotorcycle", pid);
                            else
                                AppendTag(options, "OnBicycle", pid);
                        }
                        else if (mounted is Snowmobile)
                            AppendTag(options, "OnSnowmobile", pid);
                        else if (mounted is TrainEngine)
                            AppendTag(options, "OnTrain", pid);
                        else if (mounted.ShortPrefabName.Contains("boogie"))
                            AppendTag(options, "OnBoogieboard", pid);
                        else
                            vehicleFound = false;
                    }
                    // Check for non-vehicle mounts
                    if (!vehicleFound)
                    {
                        // Check the mount point itself and parent chain
                        var mountPoint = player.GetMounted();
                        string mountPrefab = mountPoint?.ShortPrefabName ?? "";
                        string mountParentPrefab = mountPoint?.GetParentEntity()?.ShortPrefabName ?? "";

                        if (mountPrefab.Contains("boogie") || mountParentPrefab.Contains("boogie"))
                        {
                            AppendTag(options, "OnBoogieboard", pid);
                        }
                        else
                        {
                            BaseEntity check = player.GetParentEntity();
                            for (int i = 0; i < 5 && check != null; i++)
                            {
                                if (check is RidableHorse) { AppendTag(options, "OnHorse", pid); break; }
                                if (check is HotAirBalloon) { AppendTag(options, "InBalloon", pid); break; }
                                if (check is AttackHelicopter) { AppendTag(options, "InAttackHelicopter", pid); break; }
                                check = check.GetParentEntity();
                            }
                        }
                    }
                }

                if (IsUnderground(location))
                {
                    AppendTag(options, "Underground", pid);
                }

                if (IsInsideBase(player))
                {
                    AppendTag(options, "InBase", pid);
                }

                if (monument == null)
                {
                    string nearEvent = GetNearbyEvent(location);
                    if (nearEvent != null)
                    {
                        AppendTag(options, "Near", pid, nearEvent);
                    }
                }

                if (player.IsAdmin)
                {
                    AppendTag(options, "Admin", pid);
                }

                if (player.IsGod())
                {
                    AppendTag(options, "God", pid);
                }

                string pos = MapPosition(location);
                string locationstr;

                if (console)
                {
                    locationstr = string.Concat(id, " ", name, options, ": ", pos, ", ", location.x, ", ", location.z);
                }
                else
                {
                    locationstr = string.Concat("<color=#FFA500>", name, "</color>", options, ": ", pos, ", ", location.x, ", ", location.z);
                }

                locationlist.Add(Tuple.Create(sleeptime, locationstr));
            }
        }

        private void FindDLocation(IPlayer callingplayer, string search, ref List<Tuple<double, string>> locationlist)
        {
            bool console = false;
            string pid = callingplayer.Id;

            if (callingplayer.LastCommand == CommandType.Console)
            {
                console = true;
            }

            foreach (BasePlayer player in BasePlayer.allPlayerList)
            {
                var options = new StringBuilder();
                string name = player.displayName;
                string id = player.UserIDString;
                PlayerData playerdata;
                double deathtime = 0;

                if (player.IsNpc)
                {
                    continue;
                }

                if (search.Length > 0 && name.Contains(search, System.Globalization.CompareOptions.IgnoreCase) == false && id.Contains(search) == false)
                {
                    continue;
                }

                if (!_playerData.TryGetValue(id, out playerdata))
                {
                    playerdata = new PlayerData();
                    _playerData[id] = playerdata;
                }

                if (playerdata.LastDeathTime == DateTime.MinValue)
                {
                    continue;
                }

                TimeSpan diff = DateTime.Now - playerdata.LastDeathTime;
                deathtime = diff.TotalSeconds;

                if (!console || deathtime <= 0)
                {
                    AppendTag(options, "Died", pid);
                }
                else if (deathtime < 3600) // hour
                {
                    AppendTag(options, "DiedMinutesAgo", pid, Math.Floor(deathtime / 60));
                }
                else if (deathtime < 86400) // day
                {
                    AppendTag(options, "DiedHoursAgo", pid, Math.Floor(deathtime / 3600));
                }
                else
                {
                    AppendTag(options, "DiedDaysAgo", pid, Math.Floor(deathtime / 86400));
                }

                Vector3 location = playerdata.LastDeathLocation;

                string monument = GetNearestMonument(location);
                bool isOutsideGrid = IsOutsideGrid(location);

                if (monument != null && !isOutsideGrid)
                {
                    AppendTag(options, "Near", pid, monument);
                }

                if (IsUnderground(location))
                {
                    AppendTag(options, "Underground", pid);
                }

                if (IsInsideBaseAtPosition(location))
                {
                    AppendTag(options, "InBase", pid);
                }

                string pos = MapPosition(location);
                string locationstr;

                if (console)
                {
                    locationstr = string.Concat(id, " ", name, options, ": ", pos, ", ", location.x, ", ", location.z);
                }
                else
                {
                    locationstr = string.Concat("<color=#FFA500>", name, "</color>", options, ": ", pos, ", ", location.x, ", ", location.z);
                }

                locationlist.Add(Tuple.Create(deathtime, locationstr));
            }
        }

        private string GetNearestMonument(Vector3 position)
        {
            string closest = null;
            float closestDist = float.MaxValue;

            foreach (var monument in _monumentCache)
            {
                float dist = Vector3.Distance(position, monument.Position);
                if (dist <= monument.Radius && dist < closestDist)
                {
                    closestDist = dist;
                    closest = monument.Name;
                }
            }

            return closest;
        }

        private string GetNearbyEvent(Vector3 position)
        {
            if (!_config.EventDetectionEnabled) return null;

            string closest = null;
            float closestDist = float.MaxValue;

            foreach (var evt in _eventCache)
            {
                float dist = Vector3.Distance(position, evt.Position);
                if (dist <= evt.Radius && dist < closestDist)
                {
                    closestDist = dist;
                    closest = evt.Name;
                }
            }

            return closest;
        }

        private bool IsInsideBase(BasePlayer player)
        {
            // Check if player is within a tool cupboard's authorized zone
            var privs = player.GetBuildingPrivilege();
            return privs != null;
        }

        private bool IsInsideBaseAtPosition(Vector3 position)
        {
            // For death locations — check if a TC covers this position
            var list = Pool.Get<List<BuildingPrivlidge>>();
            try
            {
                Vis.Entities(position, 16f, list);
                return list.Count > 0;
            }
            finally
            {
                Pool.FreeUnmanaged(ref list);
            }
        }

        private bool IsUnderground(Vector3 position)
        {
            // Below terrain surface = underground (tunnels, caves, bunkers)
            float terrainHeight = TerrainMeta.HeightMap?.GetHeight(position) ?? 0f;
            return position.y < (terrainHeight - 3f);
        }

        private bool IsInWater(Vector3 position)
        {
            float terrainHeight = TerrainMeta.HeightMap?.GetHeight(position) ?? 0f;

            // Skip if underground (caves)
            if (position.y < terrainHeight - 3f)
                return false;

            // Use the game's water level check at the exact position
            float waterDepth = WaterLevel.GetWaterDepth(position, true, true);
            return waterDepth > 1f;
        }

        private bool IsOutsideGrid(Vector3 position)
        {
            float half = ConVar.Server.worldsize / 2f;
            return position.x < -half || position.x > half || position.z < -half || position.z > half;
        }

        private string MapPosition(Vector3 position)
        {
            var g = MapHelper.PositionToGrid(position);
            if (g.x >= 0)
            {
                string col = g.x < 26
                    ? ((char)('A' + g.x)).ToString()
                    : string.Concat((char)('A' + (g.x / 26) - 1), (char)('A' + (g.x % 26)));
                return string.Concat(col, g.y);
            }

            // Fallback for outside-grid positions (oil rigs, deep sea)
            string monument = GetNearestMonument(position);
            return monument ?? "Deep Sea";
        }

        #endregion Helper
    }
}
