using Rust;
using System;
using System.Collections.Generic;
using System.Globalization;
using Newtonsoft.Json;
using Oxide.Core;
using Oxide.Core.Plugins;
using UnityEngine;

namespace Oxide.Plugins
{
    [Info("Plagued" , "Krungh Crow" , "2.0.0")]
    [Description("Everyone is infected.")]
    public class Plagued : RustPlugin
    {
        #region Fields

        [PluginReference]
        private Plugin SimpleStatus;

        private PluginConfig _config;
        private GlobalData _globalData;

        private readonly Dictionary<ulong , PlayerData> _playerDataCache = new Dictionary<ulong , PlayerData>();
        private readonly Dictionary<ulong , RuntimeState> _runtimeStates = new Dictionary<ulong , RuntimeState>();
        private readonly HashSet<ulong> _dirtyPlayers = new HashSet<ulong>();

        private readonly List<BasePlayer> _sharedNearbyPlayers = new List<BasePlayer>();
        private readonly HashSet<ulong> _sharedNearbySeen = new HashSet<ulong>();

        private Timer _proximityTimer;
        private Timer _metabolismTimer;
        private Timer _saveDirtyTimer;

        private int _playerLayer;
        private bool _simpleStatusRegistered;

        private const string PermissionUseAdminCommands = "plagued.admin";
        private const string PlayerFolder = "Plagued/Players";
        private const string GlobalFile = "Plagued/Global";
        private const string SimpleStatusId = "plagued.status";
        private const ulong SteamIdMinimum = 76561197960265728UL;

        #endregion

        #region Configuration

        private class PluginConfig
        {
            [JsonProperty(PropertyName = "Plague Range")]
            public float PlagueRange = 20f;

            [JsonProperty(PropertyName = "Proximity Check Interval Seconds")]
            public float ProximityCheckInterval = 5.0f;

            [JsonProperty(PropertyName = "Metabolism Check Interval Seconds")]
            public float MetabolismCheckInterval = 5.0f;

            [JsonProperty(PropertyName = "Plague Point Loss Per Isolation Tick")]
            public int PlaguePointLossPerIsolationTick = 1;

            [JsonProperty(PropertyName = "Maximum Kin")]
            public int MaxKin = 2;

            [JsonProperty(PropertyName = "Maximum Kin Changes Per Wipe")]
            public int MaxKinChanges = 3;

            [JsonProperty(PropertyName = "Count Sleepers For Exposure")]
            public bool CountSleepersForExposure = false;

            [JsonProperty(PropertyName = "Chat Prefix")]
            public string ChatPrefix = "<color=#81F781>[Plagued]</color>";

            [JsonProperty(PropertyName = "Notify On Infection Start")]
            public bool NotifyOnInfectionStart = true;

            [JsonProperty(PropertyName = "Notify On Full Recovery")]
            public bool NotifyOnFullRecovery = true;

            [JsonProperty(PropertyName = "Notify On Grade Change")]
            public bool NotifyOnGradeChange = true;

            [JsonProperty(PropertyName = "Notify On Level Up")]
            public bool NotifyOnLevelUp = true;

            [JsonProperty(PropertyName = "Notify On Level Down")]
            public bool NotifyOnLevelDown = true;

            [JsonProperty(PropertyName = "Ignore Illness Effects While Godmode")]
            public bool IgnoreIllnessEffectsWhileGodmode = true;

            [JsonProperty(PropertyName = "Use Simple Status Support")]
            public bool UseSimpleStatusSupport = true;

            [JsonProperty(PropertyName = "Simple Status Title")]
            public string SimpleStatusTitle = "Plagued";

            [JsonProperty(PropertyName = "Simple Status Icon")]
            public string SimpleStatusIcon = "assets/icons/radiation.png";

            [JsonProperty(PropertyName = "Simple Status Icon Color")]
            public string SimpleStatusIconColor = "1 1 1 1";

            [JsonProperty(PropertyName = "Simple Status Healthy Remove")]
            public bool SimpleStatusRemoveWhenHealthy = true;

            [JsonProperty(PropertyName = "Simple Status Rank")]
            public int SimpleStatusRank = 1;

            [JsonProperty(PropertyName = "Simple Status Background Color")]
            public string SimpleStatusBackgroundColor = "0.40 0.30 0.10 0.85";

            [JsonProperty(PropertyName = "Simple Status Title Color")]
            public string SimpleStatusTitleColor = "1 1 1 1";

            [JsonProperty(PropertyName = "Simple Status Text Color")]
            public string SimpleStatusTextColor = "1 1 1 1";

            [JsonProperty(PropertyName = "Simple Status Progress Color")]
            public string SimpleStatusProgressColor = "0.40 0.70 0.10 0.85";

            [JsonProperty(PropertyName = "Debug Settings")]
            public DebugSettings Debug = new DebugSettings();

            [JsonProperty(PropertyName = "Point Gain Settings")]
            public PointGainSettings GainSettings = new PointGainSettings();

            [JsonProperty(PropertyName = "Illness Levels")]
            public List<IllnessLevel> IllnessLevels = new List<IllnessLevel>();
        }

        private class DebugSettings
        {
            [JsonProperty(PropertyName = "Enable Debug Logging")]
            public bool EnableDebugLogging = false;

            [JsonProperty(PropertyName = "Log To Server Console")]
            public bool LogToServerConsole = true;

            [JsonProperty(PropertyName = "Send Debug Messages To Admin Chat")]
            public bool SendDebugMessagesToAdminChat = false;
        }

        private class PointGainSettings
        {
            [JsonProperty(PropertyName = "Use Timed Gain")]
            public bool UseTimedGain = true;

            [JsonProperty(PropertyName = "Timed Gain Interval Seconds")]
            public float TimedGainIntervalSeconds = 5f;

            [JsonProperty(PropertyName = "Timed Gain Per Valid Player")]
            public int TimedGainPerValidPlayer = 3;

            [JsonProperty(PropertyName = "Timed Gain Flat If Any Valid Player")]
            public int TimedGainFlatIfAnyValidPlayer = 0;

            [JsonProperty(PropertyName = "Use Encounter Gain")]
            public bool UseEncounterGain = false;

            [JsonProperty(PropertyName = "Encounter Gain Per New Player")]
            public int EncounterGainPerNewPlayer = 10;

            [JsonProperty(PropertyName = "Encounter Cooldown Seconds")]
            public float EncounterCooldownSeconds = 30f;

            [JsonProperty(PropertyName = "Maximum Counted Players Per Tick")]
            public int MaximumCountedPlayersPerTick = 3;
        }

        private class IllnessLevel
        {
            [JsonProperty(PropertyName = "Level")]
            public int Level = 1;

            [JsonProperty(PropertyName = "Required Total Points")]
            public int RequiredTotalPoints = 50;

            [JsonProperty(PropertyName = "Grade")]
            public string Grade = "Early Symptoms";

            [JsonProperty(PropertyName = "Calories Loss")]
            public float CaloriesLoss = 0f;

            [JsonProperty(PropertyName = "Hydration Loss")]
            public float HydrationLoss = 0f;

            [JsonProperty(PropertyName = "Maximum Radiation")]
            public float MaximumRadiation = 0f;

            [JsonProperty(PropertyName = "Radiation Gain Per Tick")]
            public float RadiationGainPerTick = 0f;
        }

        private PluginConfig GetDefaultConfig()
        {
            return new PluginConfig
            {
                PlagueRange = 20f ,
                ProximityCheckInterval = 2.5f ,
                MetabolismCheckInterval = 1f ,
                PlaguePointLossPerIsolationTick = 1 ,
                MaxKin = 2 ,
                MaxKinChanges = 3 ,
                CountSleepersForExposure = false ,
                ChatPrefix = "<color=#81F781>[Plagued]</color>" ,
                NotifyOnInfectionStart = true ,
                NotifyOnFullRecovery = true ,
                NotifyOnGradeChange = true ,
                NotifyOnLevelUp = true ,
                NotifyOnLevelDown = true ,
                IgnoreIllnessEffectsWhileGodmode = true ,
                UseSimpleStatusSupport = true ,
                SimpleStatusTitle = "Plagued" ,
                SimpleStatusIcon = "assets/icons/radiation.png" ,
                SimpleStatusIconColor = "1 1 1 1" ,
                SimpleStatusRemoveWhenHealthy = true ,
                SimpleStatusRank = 20 ,
                SimpleStatusBackgroundColor = "0.40 0.70 0.10 0.85" ,
                SimpleStatusTitleColor = "1 1 1 1" ,
                SimpleStatusTextColor = "1 1 1 1" ,
                SimpleStatusProgressColor = "0.40 0.70 0.10 0.85" ,
                Debug = new DebugSettings
                {
                    EnableDebugLogging = false ,
                    LogToServerConsole = true ,
                    SendDebugMessagesToAdminChat = false
                } ,
                GainSettings = new PointGainSettings
                {
                    UseTimedGain = true ,
                    TimedGainIntervalSeconds = 5f ,
                    TimedGainPerValidPlayer = 3 ,
                    TimedGainFlatIfAnyValidPlayer = 0 ,
                    UseEncounterGain = false ,
                    EncounterGainPerNewPlayer = 10 ,
                    EncounterCooldownSeconds = 30f ,
                    MaximumCountedPlayersPerTick = 3
                } ,
                IllnessLevels = GetDefaultIllnessLevels()
            };
        }

        private static List<IllnessLevel> GetDefaultIllnessLevels()
        {
            return new List<IllnessLevel>
            {
                new IllnessLevel
                {
                    Level = 1,
                    RequiredTotalPoints = 250,
                    Grade = "Hunger",
                    CaloriesLoss = 0.15f
                },
                new IllnessLevel
                {
                    Level = 2,
                    RequiredTotalPoints = 500,
                    Grade = "Thirst",
                    HydrationLoss = 0.075f
                },
                new IllnessLevel
                {
                    Level = 3,
                    RequiredTotalPoints = 900,
                    Grade = "Radiation",
                    MaximumRadiation = 25f,
                    RadiationGainPerTick = 2f
                }
            };
        }

        protected override void LoadDefaultConfig()
        {
            _config = GetDefaultConfig();
            SaveConfig();
            PrintWarning("Created a new default configuration file.");
        }

        protected override void LoadConfig()
        {
            base.LoadConfig();

            try
            {
                _config = Config.ReadObject<PluginConfig>();
                if (_config == null)
                    throw new Exception("Configuration file returned null.");
            }
            catch (Exception ex)
            {
                PrintError($"Failed to load config, using defaults: {ex.Message}");
                _config = GetDefaultConfig();
            }

            ValidateConfig();
            SaveConfig();
        }

        protected override void SaveConfig()
        {
            Config.WriteObject(_config , true);
        }

        private void ValidateConfig()
        {
            if (_config == null)
                _config = GetDefaultConfig();

            if (_config.PlagueRange < 1f)
                _config.PlagueRange = 1f;

            if (_config.ProximityCheckInterval < 0.1f)
                _config.ProximityCheckInterval = 0.1f;

            if (_config.MetabolismCheckInterval < 0.1f)
                _config.MetabolismCheckInterval = 0.1f;

            if (_config.PlaguePointLossPerIsolationTick < 0)
                _config.PlaguePointLossPerIsolationTick = 0;

            if (_config.MaxKin < 0)
                _config.MaxKin = 0;

            if (_config.MaxKinChanges < 0)
                _config.MaxKinChanges = 0;

            if (_config.Debug == null)
                _config.Debug = new DebugSettings();

            if (_config.GainSettings == null)
                _config.GainSettings = new PointGainSettings();

            if (_config.GainSettings.TimedGainIntervalSeconds < 0.1f)
                _config.GainSettings.TimedGainIntervalSeconds = 0.1f;

            if (_config.GainSettings.TimedGainPerValidPlayer < 0)
                _config.GainSettings.TimedGainPerValidPlayer = 0;

            if (_config.GainSettings.TimedGainFlatIfAnyValidPlayer < 0)
                _config.GainSettings.TimedGainFlatIfAnyValidPlayer = 0;

            if (_config.GainSettings.EncounterGainPerNewPlayer < 0)
                _config.GainSettings.EncounterGainPerNewPlayer = 0;

            if (_config.GainSettings.EncounterCooldownSeconds < 0f)
                _config.GainSettings.EncounterCooldownSeconds = 0f;

            if (_config.GainSettings.MaximumCountedPlayersPerTick < 1)
                _config.GainSettings.MaximumCountedPlayersPerTick = 1;

            if (_config.IllnessLevels == null || _config.IllnessLevels.Count == 0)
                _config.IllnessLevels = GetDefaultIllnessLevels();

            _config.IllnessLevels.Sort((a , b) =>
            {
                if (a == null && b == null) return 0;
                if (a == null) return 1;
                if (b == null) return -1;

                int pointCompare = a.RequiredTotalPoints.CompareTo(b.RequiredTotalPoints);
                if (pointCompare != 0)
                    return pointCompare;

                return a.Level.CompareTo(b.Level);
            });

            for (int i = 0; i < _config.IllnessLevels.Count; i++)
            {
                var level = _config.IllnessLevels[i];
                if (level == null)
                {
                    _config.IllnessLevels[i] = new IllnessLevel
                    {
                        Level = i + 1 ,
                        RequiredTotalPoints = Math.Max(50 , (i + 1) * 50) ,
                        Grade = $"Level {i + 1}"
                    };
                    continue;
                }

                if (level.RequiredTotalPoints < 0)
                    level.RequiredTotalPoints = 0;

                if (level.CaloriesLoss < 0f)
                    level.CaloriesLoss = 0f;

                if (level.HydrationLoss < 0f)
                    level.HydrationLoss = 0f;

                if (level.MaximumRadiation < 0f)
                    level.MaximumRadiation = 0f;

                if (level.RadiationGainPerTick < 0f)
                    level.RadiationGainPerTick = 0f;

                if (string.IsNullOrWhiteSpace(level.Grade))
                    level.Grade = $"Level {i + 1}";

                level.Level = i + 1;
            }
        }

        #endregion

        #region Lang

        protected override void LoadDefaultMessages()
        {
            lang.RegisterMessages(new Dictionary<string , string>
            {
                ["Welcome"] = "Welcome to Plagued. Use <color=#81F781>/plagued</color> for help." ,
                ["NoPermission"] = "You do not have permission." ,
                ["InvalidCommand"] = "Invalid Plagued command." ,
                ["UsageAddPoints"] = "Usage: /plagued addpoints <amount>" ,
                ["UsageSetPoints"] = "Usage: /plagued setpoints <amount>" ,
                ["UsageSetLevel"] = "Usage: /plagued setlevel <level>" ,
                ["PointsMustBeValid"] = "Points must be a valid number." ,
                ["LevelMustBeValid"] = "Level must be a valid number." ,
                ["DebugEnabled"] = "Debug is now <color=#F2F5A9>{0}</color>." ,
                ["TestModeEnabled"] = "Test mode is now <color=#F2F5A9>{0}</color>." ,
                ["ForceTickDone"] = "Forced a proximity evaluation." ,
                ["AddedPoints"] = "Added <color=#F2F5A9>{0}</color> plague points. Total is now <color=#F2F5A9>{1}</color> | Level: <color=#F2F5A9>{2}</color>." ,
                ["SetPoints"] = "Total plague points set to <color=#F2F5A9>{0}</color> | Level: <color=#F2F5A9>{1}</color>." ,
                ["ResetPoints"] = "Plague points and exposure tracking fully reset." ,
                ["SetLevel"] = "Plague level set to <color=#F2F5A9>{0}</color> | Total Points: <color=#F2F5A9>{1}</color>." ,
                ["InfoHeader"] = "===== Plagued =====" ,
                ["InfoLine1"] = "Stay near non-kin players and plague points build up using the timed interval settings." ,
                ["InfoLine2"] = "Isolation loss per tick: {0}" ,
                ["InfoLine3"] = "Settings: Max kin = {0}, Max kin changes per wipe = {1}, Range = {2}" ,
                ["InfoLine4"] = "Configured illness levels: {0}" ,
                ["HealthyStatus"] = "Grade: <color=#81F781>Healthy</color> | Level: <color=#F2F5A9>0</color> | Total Points: <color=#F2F5A9>{0}/{1}</color>" ,
                ["HealthyStatusNoThreshold"] = "Grade: <color=#81F781>Healthy</color> | Level: <color=#F2F5A9>0</color> | Total Points: <color=#F2F5A9>{0}</color>" ,
                ["InfectedStatusMax"] = "Grade: <color=#F2F5A9>{0}</color> | Level: <color=#F2F5A9>{1}</color> | Total Points: <color=#F2F5A9>{2}</color> | Max Level" ,
                ["InfectedStatusProgress"] = "Grade: <color=#F2F5A9>{0}</color> | Level: <color=#F2F5A9>{1}</color> | Progress: <color=#F2F5A9>{2}/{3}</color> | Total: <color=#F2F5A9>{4}</color>" ,
                ["DebugState"] = "Debug: <color=#F2F5A9>{0}</color> | Test Mode: <color=#F2F5A9>{1}</color>" ,
                ["InfectionStart"] = "I don't feel so good." ,
                ["Recovery"] = "I feel a bit better now." ,
                ["LevelUp"] = "You have level <color=#F2F5A9>{0}</color> sickness: <color=#F2F5A9>{1}</color>." ,
                ["LevelDown"] = "You have dropped to level <color=#F2F5A9>{0}</color> sickness: <color=#F2F5A9>{1}</color>." ,
                ["CannotAddSelfKin"] = "You cannot add yourself as kin." ,
                ["AlreadyKin"] = "{0} is already your kin." ,
                ["MaxKinReached"] = "You already have the maximum number of kin ({0})." ,
                ["OtherMaxKinReached"] = "{0} already has the maximum number of kin." ,
                ["KinCreateFailed"] = "That kin relationship could not be created." ,
                ["KinNow"] = "You are now kin with {0}!" ,
                ["KinRequested"] = "You have requested to be {0}'s kin." ,
                ["KinRequestReceived"] = "{0} has requested to be your kin. Add them back to accept." ,
                ["KinRequestExists"] = "That kin request already exists." ,
                ["NotKin"] = "{0} is not your kin." ,
                ["RemoveKinFailed"] = "Could not remove kin. You may have exceeded the maximum kin changes for this wipe." ,
                ["RemovedKin"] = "{0} was removed from your kin list." ,
                ["InvalidKinNumber"] = "Invalid kin list number." ,
                ["RemovedKinByIndex"] = "Successfully removed kin: {0}" ,
                ["NoKin"] = "You have no kin." ,
                ["KinListTitle"] = "Kin list" ,
                ["LookPlayerFailed"] = "You aren't looking at a player." ,
                ["RotationFailed"] = "Couldn't get player rotation." ,
                ["CannotUseNpc"] = "NPCs cannot be used by Plagued." ,
                ["LevelTitleFallback"] = "Level {0}" ,
                ["SimpleStatusText"] = "Lv {0} - {1}" ,
                ["HelpTitle"] = "===== Plagued Commands =====" ,
                ["HelpLine1"] = "<color=#81F781>/plagued addkin</color> => <color=#D8D8D8>Add the player you are looking at to your kin list.</color>" ,
                ["HelpLine2"] = "<color=#81F781>/plagued delkin</color> => <color=#D8D8D8>Remove the player you are looking at from your kin list.</color>" ,
                ["HelpLine3"] = "<color=#81F781>/plagued delkin</color> <color=#F2F5A9>number</color> => <color=#D8D8D8>Remove a kin by list number.</color>" ,
                ["HelpLine4"] = "<color=#81F781>/plagued lskin</color> => <color=#D8D8D8>Display your kin list.</color>" ,
                ["HelpLine5"] = "<color=#81F781>/plagued level</color> => <color=#D8D8D8>Display your plague level and points.</color>" ,
                ["HelpLine6"] = "<color=#81F781>/plagued debug</color> => <color=#D8D8D8>Toggle debug logging for yourself (requires <color=#F2F5A9>plagued.admin</color>).</color>" ,
                ["HelpLine7"] = "<color=#81F781>/plagued testmode</color> => <color=#D8D8D8>Toggle test mode (requires <color=#F2F5A9>plagued.admin</color>).</color>" ,
                ["HelpLine8"] = "<color=#81F781>/plagued forcetick</color> => <color=#D8D8D8>Force a proximity evaluation right now (requires <color=#F2F5A9>plagued.admin</color>).</color>" ,
                ["HelpLine9"] = "<color=#81F781>/plagued addpoints</color> <color=#F2F5A9>amount</color> => <color=#D8D8D8>Add plague points to yourself (requires <color=#F2F5A9>plagued.admin</color>).</color>" ,
                ["HelpLine10"] = "<color=#81F781>/plagued setpoints</color> <color=#F2F5A9>amount</color> => <color=#D8D8D8>Set total plague points (requires <color=#F2F5A9>plagued.admin</color>).</color>" ,
                ["HelpLine11"] = "<color=#81F781>/plagued setlevel</color> <color=#F2F5A9>level</color> => <color=#D8D8D8>Set yourself to a configured level threshold (requires <color=#F2F5A9>plagued.admin</color>).</color>" ,
                ["HelpLine12"] = "<color=#81F781>/plagued info</color> => <color=#D8D8D8>Display plugin info.</color>"
            } , this , "en");

            lang.RegisterMessages(new Dictionary<string , string>
            {
                ["Welcome"] = "Willkommen bei Plagued. Nutze <color=#81F781>/plagued</color> für Hilfe." ,
                ["NoPermission"] = "Du hast keine Berechtigung." ,
                ["InvalidCommand"] = "Ungültiger Plagued-Befehl." ,
                ["UsageAddPoints"] = "Verwendung: /plagued addpoints <Menge>" ,
                ["UsageSetPoints"] = "Verwendung: /plagued setpoints <Menge>" ,
                ["UsageSetLevel"] = "Verwendung: /plagued setlevel <Level>" ,
                ["PointsMustBeValid"] = "Punkte müssen eine gültige Zahl sein." ,
                ["LevelMustBeValid"] = "Level muss eine gültige Zahl sein." ,
                ["DebugEnabled"] = "Debug ist jetzt <color=#F2F5A9>{0}</color>." ,
                ["TestModeEnabled"] = "Testmodus ist jetzt <color=#F2F5A9>{0}</color>." ,
                ["ForceTickDone"] = "Eine Nähe-Auswertung wurde erzwungen." ,
                ["AddedPoints"] = "<color=#F2F5A9>{0}</color> Seuchenpunkte hinzugefügt. Gesamt ist jetzt <color=#F2F5A9>{1}</color> | Level: <color=#F2F5A9>{2}</color>." ,
                ["SetPoints"] = "Gesamte Seuchenpunkte auf <color=#F2F5A9>{0}</color> gesetzt | Level: <color=#F2F5A9>{1}</color>." ,
                ["ResetPoints"] = "Seuchenpunkte und Expositionsverfolgung wurden vollständig zurückgesetzt." ,
                ["SetLevel"] = "Seuchenlevel auf <color=#F2F5A9>{0}</color> gesetzt | Gesamtpunkte: <color=#F2F5A9>{1}</color>." ,
                ["InfoHeader"] = "===== Plagued =====" ,
                ["InfoLine1"] = "Bleibe in der Nähe von Nicht-Verwandten und Seuchenpunkte bauen sich mit den eingestellten Zeitintervallen auf." ,
                ["InfoLine2"] = "Isolationsverlust pro Tick: {0}" ,
                ["InfoLine3"] = "Einstellungen: Max Verwandte = {0}, Max Änderungen pro Wipe = {1}, Reichweite = {2}" ,
                ["InfoLine4"] = "Konfigurierte Krankheitslevel: {0}" ,
                ["HealthyStatus"] = "Grad: <color=#81F781>Gesund</color> | Level: <color=#F2F5A9>0</color> | Gesamtpunkte: <color=#F2F5A9>{0}/{1}</color>" ,
                ["HealthyStatusNoThreshold"] = "Grad: <color=#81F781>Gesund</color> | Level: <color=#F2F5A9>0</color> | Gesamtpunkte: <color=#F2F5A9>{0}</color>" ,
                ["InfectedStatusMax"] = "Grad: <color=#F2F5A9>{0}</color> | Level: <color=#F2F5A9>{1}</color> | Gesamtpunkte: <color=#F2F5A9>{2}</color> | Max Level" ,
                ["InfectedStatusProgress"] = "Grad: <color=#F2F5A9>{0}</color> | Level: <color=#F2F5A9>{1}</color> | Fortschritt: <color=#F2F5A9>{2}/{3}</color> | Gesamt: <color=#F2F5A9>{4}</color>" ,
                ["DebugState"] = "Debug: <color=#F2F5A9>{0}</color> | Testmodus: <color=#F2F5A9>{1}</color>" ,
                ["InfectionStart"] = "Ich fühle mich nicht so gut." ,
                ["Recovery"] = "Ich fühle mich jetzt etwas besser." ,
                ["LevelUp"] = "Du hast Krankheitslevel <color=#F2F5A9>{0}</color>: <color=#F2F5A9>{1}</color>." ,
                ["LevelDown"] = "Du bist auf Krankheitslevel <color=#F2F5A9>{0}</color> gesunken: <color=#F2F5A9>{1}</color>." ,
                ["CannotAddSelfKin"] = "Du kannst dich nicht selbst als Verwandten hinzufügen." ,
                ["AlreadyKin"] = "{0} ist bereits dein Verwandter." ,
                ["MaxKinReached"] = "Du hast bereits die maximale Anzahl an Verwandten ({0})." ,
                ["OtherMaxKinReached"] = "{0} hat bereits die maximale Anzahl an Verwandten." ,
                ["KinCreateFailed"] = "Diese Verwandtschaftsbeziehung konnte nicht erstellt werden." ,
                ["KinNow"] = "Du bist jetzt mit {0} verwandt!" ,
                ["KinRequested"] = "Du hast angefragt, mit {0} verwandt zu sein." ,
                ["KinRequestReceived"] = "{0} möchte mit dir verwandt sein. Füge die Person ebenfalls hinzu, um zu akzeptieren." ,
                ["KinRequestExists"] = "Diese Verwandtschaftsanfrage existiert bereits." ,
                ["NotKin"] = "{0} ist nicht dein Verwandter." ,
                ["RemoveKinFailed"] = "Verwandter konnte nicht entfernt werden. Du hast möglicherweise die maximale Anzahl an Änderungen für diesen Wipe überschritten." ,
                ["RemovedKin"] = "{0} wurde aus deiner Verwandtenliste entfernt." ,
                ["InvalidKinNumber"] = "Ungültige Nummer in der Verwandtenliste." ,
                ["RemovedKinByIndex"] = "Verwandter erfolgreich entfernt: {0}" ,
                ["NoKin"] = "Du hast keine Verwandten." ,
                ["KinListTitle"] = "Verwandtenliste" ,
                ["LookPlayerFailed"] = "Du schaust keinen Spieler an." ,
                ["RotationFailed"] = "Spielerrotation konnte nicht gelesen werden." ,
                ["CannotUseNpc"] = "NPCs können von Plagued nicht verwendet werden." ,
                ["LevelTitleFallback"] = "Stufe {0}" ,
                ["SimpleStatusText"] = "Lvl {0} - {1}" ,
                ["HelpTitle"] = "===== Plagued Befehle =====" ,
                ["HelpLine1"] = "<color=#81F781>/plagued addkin</color> => <color=#D8D8D8>Fügt den Spieler, den du ansiehst, deiner Verwandtenliste hinzu.</color>" ,
                ["HelpLine2"] = "<color=#81F781>/plagued delkin</color> => <color=#D8D8D8>Entfernt den Spieler, den du ansiehst, aus deiner Verwandtenliste.</color>" ,
                ["HelpLine3"] = "<color=#81F781>/plagued delkin</color> <color=#F2F5A9>nummer</color> => <color=#D8D8D8>Entfernt einen Verwandten über die Listennummer.</color>" ,
                ["HelpLine4"] = "<color=#81F781>/plagued lskin</color> => <color=#D8D8D8>Zeigt deine Verwandtenliste an.</color>" ,
                ["HelpLine5"] = "<color=#81F781>/plagued level</color> => <color=#D8D8D8>Zeigt dein Seuchenlevel und deine Punkte an.</color>" ,
                ["HelpLine6"] = "<color=#81F781>/plagued debug</color> => <color=#D8D8D8>Schaltet Debug-Logging für dich um (benötigt <color=#F2F5A9>plagued.admin</color>).</color>" ,
                ["HelpLine7"] = "<color=#81F781>/plagued testmode</color> => <color=#D8D8D8>Schaltet den Testmodus um (benötigt <color=#F2F5A9>plagued.admin</color>).</color>" ,
                ["HelpLine8"] = "<color=#81F781>/plagued forcetick</color> => <color=#D8D8D8>Erzwingt sofort eine Nähe-Auswertung (benötigt <color=#F2F5A9>plagued.admin</color>).</color>" ,
                ["HelpLine9"] = "<color=#81F781>/plagued addpoints</color> <color=#F2F5A9>menge</color> => <color=#D8D8D8>Fügt dir Seuchenpunkte hinzu (benötigt <color=#F2F5A9>plagued.admin</color>).</color>" ,
                ["HelpLine10"] = "<color=#81F781>/plagued setpoints</color> <color=#F2F5A9>menge</color> => <color=#D8D8D8>Setzt die gesamten Seuchenpunkte (benötigt <color=#F2F5A9>plagued.admin</color>).</color>" ,
                ["HelpLine11"] = "<color=#81F781>/plagued setlevel</color> <color=#F2F5A9>level</color> => <color=#D8D8D8>Setzt dich auf den Schwellwert eines konfigurierten Levels (benötigt <color=#F2F5A9>plagued.admin</color>).</color>" ,
                ["HelpLine12"] = "<color=#81F781>/plagued info</color> => <color=#D8D8D8>Zeigt Plugin-Informationen an.</color>"
            } , this , "de");
        }

        private string Lang(string key , string playerId = null , params object[] args)
        {
            string msg = lang.GetMessage(key , this , playerId);
            if (string.IsNullOrEmpty(msg))
                msg = key;

            if (args == null || args.Length == 0)
                return msg;

            return string.Format(msg , args);
        }

        private string LevelTitleKey(int level)
        {
            return $"LevelTitle_{level}";
        }

        private string GetLocalizedLevelTitle(int level , string playerId = null , string fallback = null)
        {
            string key = LevelTitleKey(level);
            string text = lang.GetMessage(key , this , playerId);

            if (!string.IsNullOrEmpty(text) && !string.Equals(text , key , StringComparison.Ordinal))
                return text;

            if (!string.IsNullOrWhiteSpace(fallback))
                return fallback;

            return Lang("LevelTitleFallback" , playerId , level);
        }

        private string GetLocalizedGrade(ulong userId , string playerId = null)
        {
            if (!IsRealPlayerId(userId))
                return null;

            var data = GetOrCreatePlayerData(userId);
            if (data == null)
                return null;

            var matchedLevel = GetLevelFromTotalPoints(data.TotalPlaguePoints);
            if (matchedLevel == null)
                return null;

            return GetLocalizedLevelTitle(matchedLevel.Level , playerId , matchedLevel.Grade);
        }

        private void SyncLevelTitlesToLang()
        {
            try
            {
                if (_config == null || _config.IllnessLevels == null || _config.IllnessLevels.Count == 0)
                    return;

                SyncLevelTitlesForLanguage("en" , false);
                SyncLevelTitlesForLanguage("de" , true);
            }
            catch (Exception ex)
            {
                PrintWarning($"Failed syncing level titles to lang: {ex.Message}");
            }
        }

        private void SyncLevelTitlesForLanguage(string language , bool useGermanFallback)
        {
            Dictionary<string , string> messages = lang.GetMessages(language , this);
            if (messages == null)
                messages = new Dictionary<string , string>();

            bool changed = false;
            int addedCount = 0;

            for (int i = 0; i < _config.IllnessLevels.Count; i++)
            {
                var level = _config.IllnessLevels[i];
                if (level == null || level.Level <= 0)
                    continue;

                string key = LevelTitleKey(level.Level);

                if (messages.ContainsKey(key))
                    continue;

                messages[key] = !string.IsNullOrWhiteSpace(level.Grade)
                    ? level.Grade
                    : useGermanFallback
                        ? $"Stufe {level.Level}"
                        : $"Level {level.Level}";

                changed = true;
                addedCount++;
            }

            if (changed)
            {
                lang.RegisterMessages(messages , this , language);

                if (language == "de")
                    PrintWarning($"Added {addedCount} missing illness level title entries to the German lang file.");
                else
                    PrintWarning($"Added {addedCount} missing illness level title entries to the English lang file.");
            }
        }

        #endregion

        #region Data

        private class GlobalData
        {
            [JsonProperty(PropertyName = "Known Names")]
            public Dictionary<ulong , string> KnownNames = new Dictionary<ulong , string>();

            [JsonProperty(PropertyName = "Debug Enabled Players")]
            public HashSet<ulong> DebugEnabledPlayers = new HashSet<ulong>();

            [JsonProperty(PropertyName = "Admin Test Mode Players")]
            public HashSet<ulong> AdminTestModePlayers = new HashSet<ulong>();
        }

        private class PlayerData
        {
            [JsonProperty(PropertyName = "Total Plague Points")]
            public int TotalPlaguePoints = 0;

            [JsonProperty(PropertyName = "Current Plague Level")]
            public int PlagueLevel = 0;

            [JsonProperty(PropertyName = "Kin Changes Count")]
            public int KinChangesCount = 0;

            [JsonProperty(PropertyName = "Kins")]
            public HashSet<ulong> Kins = new HashSet<ulong>();

            [JsonProperty(PropertyName = "Pending Kin Requests")]
            public HashSet<ulong> PendingKinRequests = new HashSet<ulong>();

            [JsonProperty(PropertyName = "Encounter Times")]
            public Dictionary<ulong , double> EncounterTimes = new Dictionary<ulong , double>();

            [JsonProperty(PropertyName = "Last Timed Gain At")]
            public double LastTimedGainAt = 0d;
        }

        private class RuntimeState
        {
            public string LastGrade;

            public RuntimeState(string lastGrade)
            {
                LastGrade = lastGrade;
            }
        }

        private void LoadGlobalData()
        {
            try
            {
                _globalData = Interface.Oxide.DataFileSystem.ReadObject<GlobalData>(GlobalFile);
            }
            catch
            {
                _globalData = new GlobalData();
            }

            if (_globalData == null)
                _globalData = new GlobalData();

            CleanupGlobalDataNpcEntries();
        }

        private void SaveGlobalData()
        {
            CleanupGlobalDataNpcEntries();
            Interface.Oxide.DataFileSystem.WriteObject(GlobalFile , _globalData);
        }

        private void CleanupGlobalDataNpcEntries()
        {
            if (_globalData == null)
                _globalData = new GlobalData();

            RemoveNonPlayerEntries(_globalData.KnownNames);
            RemoveNonPlayerEntries(_globalData.DebugEnabledPlayers);
            RemoveNonPlayerEntries(_globalData.AdminTestModePlayers);
        }

        private void RemoveNonPlayerEntries<T>(Dictionary<ulong , T> dictionary)
        {
            if (dictionary == null || dictionary.Count == 0)
                return;

            List<ulong> remove = null;

            foreach (var entry in dictionary)
            {
                if (IsRealPlayerId(entry.Key))
                    continue;

                if (remove == null)
                    remove = new List<ulong>();

                remove.Add(entry.Key);
            }

            if (remove == null)
                return;

            for (int i = 0; i < remove.Count; i++)
                dictionary.Remove(remove[i]);
        }

        private void RemoveNonPlayerEntries(HashSet<ulong> set)
        {
            if (set == null || set.Count == 0)
                return;

            List<ulong> remove = null;

            foreach (var userId in set)
            {
                if (IsRealPlayerId(userId))
                    continue;

                if (remove == null)
                    remove = new List<ulong>();

                remove.Add(userId);
            }

            if (remove == null)
                return;

            for (int i = 0; i < remove.Count; i++)
                set.Remove(remove[i]);
        }

        private string GetPlayerDataPath(ulong userId)
        {
            return $"{PlayerFolder}/{userId}";
        }

        private PlayerData LoadPlayerData(ulong userId)
        {
            if (!IsRealPlayerId(userId))
                return null;

            PlayerData data;
            if (_playerDataCache.TryGetValue(userId , out data))
                return data;

            try
            {
                data = Interface.Oxide.DataFileSystem.ReadObject<PlayerData>(GetPlayerDataPath(userId));
            }
            catch
            {
                data = new PlayerData();
            }

            if (data == null)
                data = new PlayerData();

            CleanupPlayerData(data);

            _playerDataCache[userId] = data;
            return data;
        }

        private void SavePlayerData(ulong userId)
        {
            if (!IsRealPlayerId(userId))
                return;

            PlayerData data;
            if (!_playerDataCache.TryGetValue(userId , out data) || data == null)
                return;

            CleanupPlayerData(data);
            Interface.Oxide.DataFileSystem.WriteObject(GetPlayerDataPath(userId) , data);
        }

        private void SaveAllCachedPlayerData()
        {
            foreach (var entry in _playerDataCache)
            {
                if (!IsRealPlayerId(entry.Key) || entry.Value == null)
                    continue;

                CleanupPlayerData(entry.Value);
                Interface.Oxide.DataFileSystem.WriteObject(GetPlayerDataPath(entry.Key) , entry.Value);
            }
        }

        private void CleanupPlayerData(PlayerData data)
        {
            if (data == null)
                return;

            RemoveNonPlayerEntries(data.Kins);
            RemoveNonPlayerEntries(data.PendingKinRequests);
            RemoveNonPlayerEntries(data.EncounterTimes);

            if (data.TotalPlaguePoints < 0)
                data.TotalPlaguePoints = 0;

            if (data.PlagueLevel < 0)
                data.PlagueLevel = 0;

            if (data.KinChangesCount < 0)
                data.KinChangesCount = 0;
        }

        #endregion

        #region Hooks

        private void Init()
        {
            permission.RegisterPermission(PermissionUseAdminCommands , this);
            LoadConfig();
            LoadGlobalData();
        }

        private void OnServerInitialized()
        {
            _playerLayer = LayerMask.GetMask("Player (Server)");
            SyncLevelTitlesToLang();
            RegisterSimpleStatusDefinition();

            foreach (var player in BasePlayer.activePlayerList)
                InitializePlayer(player);

            _proximityTimer = timer.Every(_config.ProximityCheckInterval , RunProximityChecks);
            _metabolismTimer = timer.Every(_config.MetabolismCheckInterval , RunMetabolismChecks);
            _saveDirtyTimer = timer.Every(30f , SaveDirtyPlayers);
        }

        private void OnPluginLoaded(Plugin plugin)
        {
            if (plugin == null)
                return;

            if (plugin.Name == "SimpleStatus")
            {
                NextTick(() =>
                {
                    RegisterSimpleStatusDefinition();
                    foreach (var player in BasePlayer.activePlayerList)
                        UpdateSimpleStatus(player);
                });
            }
        }

        private void OnSimpleStatusReady()
        {
            RegisterSimpleStatusDefinition();

            foreach (var player in BasePlayer.activePlayerList)
                UpdateSimpleStatus(player);
        }

        private void OnPlayerSleepEnded(BasePlayer player)
        {
            if (!IsRealPlayer(player))
                return;

            InitializePlayer(player);
            Reply(player , Lang("Welcome" , player.UserIDString));
        }

        private void OnPlayerDisconnected(BasePlayer player , string reason)
        {
            if (!IsRealPlayer(player))
                return;

            RemoveSimpleStatus(player);
            SavePlayerData(player.userID);

            _runtimeStates.Remove(player.userID);
            _playerDataCache.Remove(player.userID);
            _dirtyPlayers.Remove(player.userID);
        }

        private void OnServerSave()
        {
            SaveDirtyPlayers();
            SaveAllCachedPlayerData();
            SaveGlobalData();
        }

        private void OnNewSave(string filename)
        {
            _globalData.KnownNames.Clear();

            foreach (var player in BasePlayer.activePlayerList)
            {
                if (!IsRealPlayer(player))
                    continue;

                var data = GetOrCreatePlayerData(player.userID , player.displayName);
                if (data == null)
                    continue;

                data.KinChangesCount = 0;
                MarkPlayerDataDirty(player.userID);
            }

            SaveDirtyPlayers();
            SaveGlobalData();
        }

        private void Unload()
        {
            if (_proximityTimer != null && !_proximityTimer.Destroyed)
                _proximityTimer.Destroy();

            if (_metabolismTimer != null && !_metabolismTimer.Destroyed)
                _metabolismTimer.Destroy();

            if (_saveDirtyTimer != null && !_saveDirtyTimer.Destroyed)
                _saveDirtyTimer.Destroy();

            SaveDirtyPlayers();

            foreach (var player in BasePlayer.activePlayerList)
            {
                if (!IsRealPlayer(player))
                    continue;

                RemoveSimpleStatus(player);
            }

            SaveAllCachedPlayerData();
            SaveGlobalData();

            _runtimeStates.Clear();
            _playerDataCache.Clear();
            _dirtyPlayers.Clear();
            _sharedNearbyPlayers.Clear();
            _sharedNearbySeen.Clear();
        }

        #endregion

        #region Commands

        [ChatCommand("plagued")]
        private void CommandPlagued(BasePlayer player , string command , string[] args)
        {
            if (!IsRealPlayer(player))
                return;

            if (args == null || args.Length == 0)
            {
                SendHelp(player);
                return;
            }

            switch (args[0].ToLower())
            {
                case "addkin":
                    HandleAddKin(player);
                    return;

                case "delkin":
                    if (args.Length >= 2)
                    {
                        int index;
                        if (int.TryParse(args[1] , out index))
                            HandleRemoveKinByIndex(player , index);
                        else
                            Reply(player , Lang("InvalidKinNumber" , player.UserIDString));
                    }
                    else
                    {
                        HandleRemoveKinLook(player);
                    }
                    return;

                case "lskin":
                    HandleListKin(player);
                    return;

                case "level":
                case "status":
                    ShowLevelInfo(player);
                    return;

                case "info":
                    HandleInfo(player);
                    return;

                case "debug":
                    HandleDebugToggle(player);
                    return;

                case "testmode":
                    HandleTestModeToggle(player);
                    return;

                case "forcetick":
                    HandleForceTick(player);
                    return;

                case "addpoints":
                    HandleAddPoints(player , args);
                    return;

                case "setpoints":
                    HandleSetPoints(player , args);
                    return;

                case "setlevel":
                    HandleSetLevel(player , args);
                    return;

                default:
                    Reply(player , Lang("InvalidCommand" , player.UserIDString));
                    return;
            }
        }

        private void SendHelp(BasePlayer player)
        {
            var lines = new List<string>
            {
                Lang("HelpLine1", player.UserIDString),
                Lang("HelpLine2", player.UserIDString),
                Lang("HelpLine3", player.UserIDString),
                Lang("HelpLine4", player.UserIDString),
                Lang("HelpLine5", player.UserIDString),
                Lang("HelpLine12", player.UserIDString)
            };

            if (HasAdminPermission(player))
            {
                lines.Add("");
                lines.Add("<color=#F2F5A9>Admin Commands</color>");
                lines.Add(Lang("HelpLine6" , player.UserIDString));
                lines.Add(Lang("HelpLine7" , player.UserIDString));
                lines.Add(Lang("HelpLine8" , player.UserIDString));
                lines.Add(Lang("HelpLine9" , player.UserIDString));
                lines.Add(Lang("HelpLine10" , player.UserIDString));
                lines.Add(Lang("HelpLine11" , player.UserIDString));
            }

            ReplyList(player , Lang("HelpTitle" , player.UserIDString) , lines);
        }

        private void HandleInfo(BasePlayer player)
        {
            var lines = new List<string>
            {
                Lang("InfoLine1", player.UserIDString),
                Lang("InfoLine2", player.UserIDString, _config.PlaguePointLossPerIsolationTick),
                Lang("InfoLine3", player.UserIDString, _config.MaxKin, _config.MaxKinChanges, _config.PlagueRange),
                Lang("InfoLine4", player.UserIDString, _config.IllnessLevels.Count)
            };

            ReplyList(player , Lang("InfoHeader" , player.UserIDString) , lines);
        }

        private void ShowLevelInfo(BasePlayer player)
        {
            var data = GetOrCreatePlayerData(player.userID , player.displayName);
            if (data == null)
                return;

            int plagueLevel = GetPlagueLevel(player.userID);
            string grade = GetLocalizedGrade(player.userID , player.UserIDString);
            var current = GetLevelConfig(plagueLevel);
            var next = GetNextLevelConfig(plagueLevel);

            if (plagueLevel <= 0 || current == null)
            {
                var first = GetFirstLevelConfig();
                if (first != null)
                    Reply(player , Lang("HealthyStatus" , player.UserIDString , data.TotalPlaguePoints , first.RequiredTotalPoints));
                else
                    Reply(player , Lang("HealthyStatusNoThreshold" , player.UserIDString , data.TotalPlaguePoints));

                Reply(player , Lang("DebugState" , player.UserIDString , IsDebugEnabledFor(player.userID) , IsTestModeEnabledFor(player.userID)));
                return;
            }

            if (next == null)
            {
                Reply(player , Lang("InfectedStatusMax" , player.UserIDString , grade , plagueLevel , data.TotalPlaguePoints));
            }
            else
            {
                int floor = current.RequiredTotalPoints;
                int ceil = next.RequiredTotalPoints;
                int progress = Math.Max(0 , data.TotalPlaguePoints - floor);
                int needed = Math.Max(1 , ceil - floor);

                Reply(player , Lang("InfectedStatusProgress" , player.UserIDString , grade , plagueLevel , progress , needed , data.TotalPlaguePoints));
            }

            Reply(player , Lang("DebugState" , player.UserIDString , IsDebugEnabledFor(player.userID) , IsTestModeEnabledFor(player.userID)));
        }

        private void HandleDebugToggle(BasePlayer player)
        {
            if (!HasAdminPermission(player))
            {
                Reply(player , Lang("NoPermission" , player.UserIDString));
                return;
            }

            bool enabled;
            if (_globalData.DebugEnabledPlayers.Contains(player.userID))
            {
                _globalData.DebugEnabledPlayers.Remove(player.userID);
                enabled = false;
            }
            else
            {
                _globalData.DebugEnabledPlayers.Add(player.userID);
                enabled = true;
            }

            SaveGlobalData();
            Reply(player , Lang("DebugEnabled" , player.UserIDString , enabled ? "enabled" : "disabled"));
        }

        private void HandleTestModeToggle(BasePlayer player)
        {
            if (!HasAdminPermission(player))
            {
                Reply(player , Lang("NoPermission" , player.UserIDString));
                return;
            }

            bool enabled;
            if (_globalData.AdminTestModePlayers.Contains(player.userID))
            {
                _globalData.AdminTestModePlayers.Remove(player.userID);
                enabled = false;
            }
            else
            {
                _globalData.AdminTestModePlayers.Add(player.userID);
                enabled = true;
            }

            SaveGlobalData();
            Reply(player , Lang("TestModeEnabled" , player.UserIDString , enabled ? "enabled" : "disabled"));
            DebugPlayer(player , $"Test mode changed => {enabled}");
        }

        private void HandleForceTick(BasePlayer player)
        {
            if (!HasAdminPermission(player))
            {
                Reply(player , Lang("NoPermission" , player.UserIDString));
                return;
            }

            GetNearbyPlayersNonAlloc(player , _sharedNearbyPlayers , _sharedNearbySeen);
            DebugPlayer(player , $"Force tick requested. Nearby players collected: {_sharedNearbyPlayers.Count}");

            if (_sharedNearbyPlayers.Count == 0)
                ProcessPlayerIsolation(player);
            else
                ProcessPlayerProximity(player , _sharedNearbyPlayers);

            ProcessPlayerMetabolismTick(player);
            Reply(player , Lang("ForceTickDone" , player.UserIDString));
        }

        private void HandleAddPoints(BasePlayer player , string[] args)
        {
            if (!HasAdminPermission(player))
            {
                Reply(player , Lang("NoPermission" , player.UserIDString));
                return;
            }

            if (args.Length < 2)
            {
                Reply(player , Lang("UsageAddPoints" , player.UserIDString));
                return;
            }

            int amount;
            if (!int.TryParse(args[1] , out amount))
            {
                Reply(player , Lang("PointsMustBeValid" , player.UserIDString));
                return;
            }

            AddPlaguePoints(player.userID , amount);
            Reply(player , Lang("AddedPoints" , player.UserIDString , amount , GetTotalPlaguePoints(player.userID) , GetPlagueLevel(player.userID)));
        }

        private void HandleSetPoints(BasePlayer player , string[] args)
        {
            if (!HasAdminPermission(player))
            {
                Reply(player , Lang("NoPermission" , player.UserIDString));
                return;
            }

            if (args.Length < 2)
            {
                Reply(player , Lang("UsageSetPoints" , player.UserIDString));
                return;
            }

            int amount;
            if (!int.TryParse(args[1] , out amount))
            {
                Reply(player , Lang("PointsMustBeValid" , player.UserIDString));
                return;
            }

            SetTotalPlaguePoints(player.userID , amount);

            if (amount <= 0)
            {
                Reply(player , Lang("ResetPoints" , player.UserIDString));
                return;
            }

            Reply(player , Lang("SetPoints" , player.UserIDString , GetTotalPlaguePoints(player.userID) , GetPlagueLevel(player.userID)));
        }

        private void HandleSetLevel(BasePlayer player , string[] args)
        {
            if (!HasAdminPermission(player))
            {
                Reply(player , Lang("NoPermission" , player.UserIDString));
                return;
            }

            if (args.Length < 2)
            {
                Reply(player , Lang("UsageSetLevel" , player.UserIDString));
                return;
            }

            int level;
            if (!int.TryParse(args[1] , out level))
            {
                Reply(player , Lang("LevelMustBeValid" , player.UserIDString));
                return;
            }

            SetPlagueLevelByThreshold(player.userID , level);

            if (level <= 0)
            {
                Reply(player , Lang("ResetPoints" , player.UserIDString));
                return;
            }

            Reply(player , Lang("SetLevel" , player.UserIDString , GetPlagueLevel(player.userID) , GetTotalPlaguePoints(player.userID)));
        }

        #endregion

        #region Player Init

        private void InitializePlayer(BasePlayer player)
        {
            if (!IsRealPlayer(player))
                return;

            GetOrCreatePlayerData(player.userID , player.displayName);
            SyncPlayerIllnessStateSilent(player.userID);
            GetOrCreateRuntimeState(player);
            UpdateSimpleStatus(player);
        }

        private RuntimeState GetOrCreateRuntimeState(BasePlayer player)
        {
            RuntimeState state;
            if (!_runtimeStates.TryGetValue(player.userID , out state))
            {
                state = new RuntimeState(GetCurrentGrade(player.userID));
                _runtimeStates[player.userID] = state;
            }
            else
            {
                state.LastGrade = GetCurrentGrade(player.userID);
            }

            return state;
        }

        private RuntimeState GetOrCreateRuntimeState(ulong userId)
        {
            RuntimeState state;
            if (_runtimeStates.TryGetValue(userId , out state))
                return state;

            state = new RuntimeState(GetCurrentGrade(userId));
            _runtimeStates[userId] = state;
            return state;
        }

        private PlayerData GetOrCreatePlayerData(ulong userId , string name = null)
        {
            if (!IsRealPlayerId(userId))
                return null;

            var data = LoadPlayerData(userId);
            if (data == null)
                return null;

            if (!string.IsNullOrEmpty(name))
                _globalData.KnownNames[userId] = name;

            return data;
        }

        #endregion

        #region Loops

        private void RunProximityChecks()
        {
            var players = BasePlayer.activePlayerList;
            for (int i = 0; i < players.Count; i++)
            {
                var player = players[i];
                if (!IsRealPlayer(player) || !player.IsConnected)
                    continue;

                GetNearbyPlayersNonAlloc(player , _sharedNearbyPlayers , _sharedNearbySeen);

                if (_sharedNearbyPlayers.Count == 0)
                    ProcessPlayerIsolation(player);
                else
                    ProcessPlayerProximity(player , _sharedNearbyPlayers);
            }
        }

        private void RunMetabolismChecks()
        {
            var players = BasePlayer.activePlayerList;
            for (int i = 0; i < players.Count; i++)
            {
                var player = players[i];
                if (!IsRealPlayer(player))
                    continue;

                if (player.IsDestroyed || player.metabolism == null)
                    continue;

                ProcessPlayerMetabolismTick(player);
            }
        }

        #endregion

        #region Proximity Logic

        private void ProcessPlayerProximity(BasePlayer player , List<BasePlayer> nearbyPlayers)
        {
            if (!IsRealPlayer(player))
                return;

            var data = GetOrCreatePlayerData(player.userID , player.displayName);
            if (data == null)
                return;

            int validCount = 0;
            List<BasePlayer> validTargets = _config.GainSettings.UseEncounterGain ? new List<BasePlayer>() : null;

            DebugPlayer(player , $"Proximity tick started. Nearby raw count: {nearbyPlayers?.Count ?? 0}");

            for (int i = 0; i < nearbyPlayers.Count; i++)
            {
                var target = nearbyPlayers[i];

                if (!IsValidProximityTarget(player , target))
                {
                    if (target != null)
                        DebugPlayer(player , $"Skipped target: {target.displayName} ({target.userID})");
                    continue;
                }

                DebugPlayer(player , $"Target {target.displayName} | Exposure: True");

                if (validTargets != null)
                    validTargets.Add(target);

                validCount++;

                if (validCount >= _config.GainSettings.MaximumCountedPlayersPerTick)
                    break;
            }

            if (validCount <= 0)
            {
                DebugPlayer(player , "No valid exposure targets found.");
                ProcessPlayerIsolation(player);
                return;
            }

            DebugPlayer(player , $"Valid exposure count: {validCount}");
            ProcessTimedPointGain(player , data , validCount);
            ProcessEncounterGain(player , data , validTargets);
        }

        private void ProcessPlayerIsolation(BasePlayer player)
        {
            if (!IsRealPlayer(player))
                return;

            var data = GetOrCreatePlayerData(player.userID , player.displayName);
            if (data == null || data.TotalPlaguePoints <= 0)
                return;

            DebugPlayer(player , "Isolation tick.");

            CleanupEncounterTimes(data);

            if (_config.PlaguePointLossPerIsolationTick > 0)
            {
                int before = data.TotalPlaguePoints;
                AddPlaguePoints(player.userID , -_config.PlaguePointLossPerIsolationTick);

                DebugPlayer(player ,
                    $"Isolation decay applied. Lost: {_config.PlaguePointLossPerIsolationTick} | Points: {before} -> {GetTotalPlaguePoints(player.userID)}");
            }
        }

        private bool IsValidProximityTarget(BasePlayer self , BasePlayer target)
        {
            if (!IsRealPlayer(self) || !IsRealPlayer(target))
                return false;

            if (self.userID == target.userID)
                return false;

            bool testMode = IsTestModeEnabledFor(self.userID);

            if (!testMode)
            {
                if (target is NPCPlayer)
                    return false;

                if (!_config.CountSleepersForExposure && !target.IsConnected)
                    return false;
            }

            if (IsKin(self.userID , target.userID))
                return false;

            return true;
        }

        private void ProcessTimedPointGain(BasePlayer player , PlayerData data , int validCount)
        {
            if (!_config.GainSettings.UseTimedGain)
            {
                DebugPlayer(player , "Timed gain skipped: disabled.");
                return;
            }

            double now = Interface.Oxide.Now;
            double elapsed = now - data.LastTimedGainAt;

            if (elapsed < _config.GainSettings.TimedGainIntervalSeconds)
            {
                DebugPlayer(player , $"Timed gain skipped: cooldown {elapsed:0.00}/{_config.GainSettings.TimedGainIntervalSeconds:0.00}s");
                return;
            }

            int gained = 0;

            if (_config.GainSettings.TimedGainPerValidPlayer > 0)
                gained += validCount * _config.GainSettings.TimedGainPerValidPlayer;

            if (_config.GainSettings.TimedGainFlatIfAnyValidPlayer > 0 && validCount > 0)
                gained += _config.GainSettings.TimedGainFlatIfAnyValidPlayer;

            data.LastTimedGainAt = now;

            if (gained > 0)
            {
                int before = data.TotalPlaguePoints;
                DebugPlayer(player , $"Timed gain applied. ValidCount: {validCount} | Gained: {gained} | Points: {before} -> {before + gained}");
                AddPlaguePoints(player.userID , gained);
            }
            else
            {
                DebugPlayer(player , "Timed gain tick occurred but gained 0 points.");
            }
        }

        private void ProcessEncounterGain(BasePlayer player , PlayerData data , List<BasePlayer> validTargets)
        {
            if (!_config.GainSettings.UseEncounterGain || validTargets == null || validTargets.Count == 0)
                return;

            double now = Interface.Oxide.Now;

            for (int i = 0; i < validTargets.Count; i++)
            {
                var target = validTargets[i];
                if (!IsRealPlayer(target))
                    continue;

                double lastEncounter;

                if (data.EncounterTimes.TryGetValue(target.userID , out lastEncounter))
                {
                    if (now - lastEncounter < _config.GainSettings.EncounterCooldownSeconds)
                    {
                        DebugPlayer(player , $"Encounter gain skipped for {target.displayName} due to cooldown.");
                        continue;
                    }
                }

                data.EncounterTimes[target.userID] = now;

                if (_config.GainSettings.EncounterGainPerNewPlayer > 0)
                {
                    int before = data.TotalPlaguePoints;
                    DebugPlayer(player , $"Encounter gain applied from {target.displayName}: +{_config.GainSettings.EncounterGainPerNewPlayer} | Points: {before} -> {before + _config.GainSettings.EncounterGainPerNewPlayer}");
                    AddPlaguePoints(player.userID , _config.GainSettings.EncounterGainPerNewPlayer);
                }
            }

            CleanupEncounterTimes(data);
        }

        private void CleanupEncounterTimes(PlayerData data)
        {
            if (data == null || data.EncounterTimes.Count == 0)
                return;

            double now = Interface.Oxide.Now;
            double cutoff = Math.Max(_config.GainSettings.EncounterCooldownSeconds , 1f) * 2d;

            List<ulong> remove = null;

            foreach (var entry in data.EncounterTimes)
            {
                if (!IsRealPlayerId(entry.Key) || now - entry.Value > cutoff)
                {
                    if (remove == null)
                        remove = new List<ulong>();

                    remove.Add(entry.Key);
                }
            }

            if (remove == null)
                return;

            for (int i = 0; i < remove.Count; i++)
                data.EncounterTimes.Remove(remove[i]);
        }

        private void GetNearbyPlayersNonAlloc(BasePlayer player , List<BasePlayer> result , HashSet<ulong> seen)
        {
            result.Clear();
            seen.Clear();

            if (!IsRealPlayer(player))
                return;

            int count = Physics.OverlapSphereNonAlloc(player.transform.position , _config.PlagueRange , Vis.colBuffer , _playerLayer);

            for (int i = 0; i < count; i++)
            {
                var collider = Vis.colBuffer[i];
                Vis.colBuffer[i] = null;

                if (collider == null)
                    continue;

                var found = collider.GetComponentInParent<BasePlayer>();
                if (found == null)
                    continue;

                if (!seen.Add(found.userID))
                    continue;

                result.Add(found);
            }
        }

        #endregion

        #region Illness Logic

        private void ProcessPlayerMetabolismTick(BasePlayer player)
        {
            if (!IsRealPlayer(player) || player.metabolism == null)
                return;

            bool changed = false;

            if (player.metabolism.bleeding.value != 0f)
            {
                player.metabolism.bleeding.value = 0f;
                changed = true;
            }

            if (player.metabolism.poison.value != 0f)
            {
                player.metabolism.poison.value = 0f;
                changed = true;
            }

            if (ShouldIgnoreIllnessEffects(player))
            {
                changed |= ResetRuntimeEffects(player.userID);

                if (changed)
                    player.metabolism.SendChangesToClient();

                return;
            }

            var data = GetOrCreatePlayerData(player.userID , player.displayName);
            if (data == null)
                return;

            int plagueLevel = GetPlagueLevelFromPoints(data.TotalPlaguePoints);

            if (plagueLevel <= 0)
            {
                changed |= ResetRuntimeEffects(player.userID);

                if (changed)
                    player.metabolism.SendChangesToClient();

                return;
            }

            changed |= ApplyIllnessEffects(player , plagueLevel);

            if (changed)
                player.metabolism.SendChangesToClient();
        }

        private bool ApplyIllnessEffects(BasePlayer player , int plagueLevel)
        {
            if (!IsRealPlayer(player) || player.metabolism == null)
                return false;

            var current = GetLevelConfig(plagueLevel);
            return current != null && ApplyLevelEffects(player , current);
        }

        private bool ApplyLevelEffects(BasePlayer player , IllnessLevel level)
        {
            if (!IsRealPlayer(player) || player.metabolism == null || level == null)
                return false;

            if (ShouldIgnoreIllnessEffects(player))
                return ResetRuntimeEffects(player.userID);

            var metabolism = player.metabolism;
            bool changed = false;

            if (level.CaloriesLoss > 0f)
            {
                float newCalories = Mathf.Max(0f , metabolism.calories.value - level.CaloriesLoss);
                if (!Mathf.Approximately(newCalories , metabolism.calories.value))
                {
                    metabolism.calories.value = newCalories;
                    changed = true;
                }
            }

            if (level.HydrationLoss > 0f)
            {
                float newHydration = Mathf.Max(0f , metabolism.hydration.value - level.HydrationLoss);
                if (!Mathf.Approximately(newHydration , metabolism.hydration.value))
                {
                    metabolism.hydration.value = newHydration;
                    changed = true;
                }
            }

            if (level.RadiationGainPerTick > 0f)
            {
                float currentRadiation = metabolism.radiation_poison.value;

                if (level.MaximumRadiation > 0f)
                {
                    float missingRadiation = Mathf.Max(0f , level.MaximumRadiation - currentRadiation);
                    float radiationThisTick = Mathf.Min(level.RadiationGainPerTick , missingRadiation);

                    if (radiationThisTick > 0f)
                    {
                        metabolism.radiation_poison.Add(radiationThisTick);
                        changed = true;
                    }
                }
                else
                {
                    metabolism.radiation_poison.Add(level.RadiationGainPerTick);
                    changed = true;
                }
            }

            if (changed)
                Interface.Oxide.CallHook("OnPlagueLevelEffectApplied" , player , level.Level , level.Grade , level);

            return changed;
        }

        private IllnessLevel GetFirstLevelConfig()
        {
            if (_config.IllnessLevels == null || _config.IllnessLevels.Count == 0)
                return null;

            return _config.IllnessLevels[0];
        }

        private IllnessLevel GetLevelConfig(int level)
        {
            if (level <= 0 || _config.IllnessLevels == null || _config.IllnessLevels.Count == 0)
                return null;

            for (int i = 0; i < _config.IllnessLevels.Count; i++)
            {
                if (_config.IllnessLevels[i].Level == level)
                    return _config.IllnessLevels[i];
            }

            return null;
        }

        private IllnessLevel GetNextLevelConfig(int currentLevel)
        {
            if (_config.IllnessLevels == null || _config.IllnessLevels.Count == 0)
                return null;

            for (int i = 0; i < _config.IllnessLevels.Count; i++)
            {
                if (_config.IllnessLevels[i].Level > currentLevel)
                    return _config.IllnessLevels[i];
            }

            return null;
        }

        private IllnessLevel GetLevelFromTotalPoints(int totalPoints)
        {
            if (_config.IllnessLevels == null || _config.IllnessLevels.Count == 0)
                return null;

            IllnessLevel highestMatch = null;

            for (int i = 0; i < _config.IllnessLevels.Count; i++)
            {
                var level = _config.IllnessLevels[i];
                if (level == null)
                    continue;

                if (totalPoints >= level.RequiredTotalPoints)
                    highestMatch = level;
            }

            return highestMatch;
        }

        private int GetPlagueLevelFromPoints(int totalPoints)
        {
            var level = GetLevelFromTotalPoints(totalPoints);
            return level != null ? level.Level : 0;
        }

        private string GetGradeFromPoints(int totalPoints)
        {
            var level = GetLevelFromTotalPoints(totalPoints);
            return level?.Grade;
        }

        private string GetCurrentGrade(ulong userId)
        {
            if (!IsRealPlayerId(userId))
                return null;

            var data = GetOrCreatePlayerData(userId);
            return data == null ? null : GetGradeFromPoints(data.TotalPlaguePoints);
        }

        #endregion

        #region Levels and Points

        private int GetPlagueLevel(ulong userId)
        {
            if (!IsRealPlayerId(userId))
                return 0;

            var data = GetOrCreatePlayerData(userId);
            return data == null ? 0 : GetPlagueLevelFromPoints(data.TotalPlaguePoints);
        }

        private int GetTotalPlaguePoints(ulong userId)
        {
            if (!IsRealPlayerId(userId))
                return 0;

            var data = GetOrCreatePlayerData(userId);
            return data == null ? 0 : data.TotalPlaguePoints;
        }

        private void ResetPlayerPlagueProgress(PlayerData data)
        {
            if (data == null)
                return;

            data.TotalPlaguePoints = 0;
            data.PlagueLevel = 0;
            data.EncounterTimes.Clear();
            data.LastTimedGainAt = 0d;
        }

        private void SetTotalPlaguePoints(ulong userId , int amount)
        {
            if (!IsRealPlayerId(userId))
                return;

            var data = GetOrCreatePlayerData(userId);
            if (data == null)
                return;

            int oldPoints = data.TotalPlaguePoints;

            if (amount <= 0)
            {
                ResetPlayerPlagueProgress(data);
                ResetRuntimeEffects(userId);
            }
            else
            {
                data.TotalPlaguePoints = amount;
            }

            RefreshPlayerIllnessState(userId , oldPoints , data.TotalPlaguePoints);
            MarkPlayerDataDirty(userId);
        }

        private void AddPlaguePoints(ulong userId , int amount)
        {
            if (!IsRealPlayerId(userId) || amount == 0)
                return;

            var data = GetOrCreatePlayerData(userId);
            if (data == null)
                return;

            int oldPoints = data.TotalPlaguePoints;
            data.TotalPlaguePoints = Math.Max(0 , data.TotalPlaguePoints + amount);

            if (data.TotalPlaguePoints <= 0)
                ResetRuntimeEffects(userId);

            RefreshPlayerIllnessState(userId , oldPoints , data.TotalPlaguePoints);
            MarkPlayerDataDirty(userId);
        }

        private void SetPlagueLevelByThreshold(ulong userId , int level)
        {
            if (!IsRealPlayerId(userId))
                return;

            var data = GetOrCreatePlayerData(userId);
            if (data == null)
                return;

            int oldPoints = data.TotalPlaguePoints;

            if (level <= 0)
            {
                ResetPlayerPlagueProgress(data);
                ResetRuntimeEffects(userId);
                RefreshPlayerIllnessState(userId , oldPoints , data.TotalPlaguePoints);
                MarkPlayerDataDirty(userId);
                return;
            }

            var config = GetLevelConfig(level);
            if (config == null)
                return;

            data.TotalPlaguePoints = Math.Max(0 , config.RequiredTotalPoints);
            RefreshPlayerIllnessState(userId , oldPoints , data.TotalPlaguePoints);
            MarkPlayerDataDirty(userId);
        }

        private List<IllnessLevel> GetCrossedLevels(int oldPoints , int newPoints)
        {
            var crossed = new List<IllnessLevel>();

            if (_config.IllnessLevels == null || _config.IllnessLevels.Count == 0)
                return crossed;

            for (int i = 0; i < _config.IllnessLevels.Count; i++)
            {
                var level = _config.IllnessLevels[i];
                if (level == null)
                    continue;

                if (oldPoints < level.RequiredTotalPoints && newPoints >= level.RequiredTotalPoints)
                    crossed.Add(level);
            }

            return crossed;
        }

        private void RefreshPlayerIllnessState(ulong userId , int oldPoints , int newPoints)
        {
            if (!IsRealPlayerId(userId))
                return;

            var data = GetOrCreatePlayerData(userId);
            if (data == null)
                return;

            var oldMatchedLevel = GetLevelFromTotalPoints(oldPoints);
            var newMatchedLevel = GetLevelFromTotalPoints(newPoints);

            int oldLevel = oldMatchedLevel != null ? oldMatchedLevel.Level : 0;
            int newLevel = newMatchedLevel != null ? newMatchedLevel.Level : 0;

            string oldGrade = oldMatchedLevel?.Grade;
            string newGrade = newMatchedLevel?.Grade;

            data.PlagueLevel = newLevel;

            BasePlayer player = BasePlayer.FindAwakeOrSleeping(userId.ToString());

            bool recovered = oldLevel > 0 && newLevel <= 0;
            bool gradeChanged = oldGrade != newGrade;
            bool levelDown = oldLevel > newLevel && newLevel > 0;

            var crossedLevels = GetCrossedLevels(oldPoints , newPoints);

            if (recovered || newLevel <= 0)
                ResetRuntimeEffects(userId);

            if (IsRealPlayer(player) && player.IsConnected)
            {
                if (crossedLevels.Count > 0)
                {
                    if (_config.NotifyOnInfectionStart && oldLevel <= 0)
                        Reply(player , Lang("InfectionStart" , player.UserIDString));

                    if (_config.NotifyOnLevelUp)
                    {
                        for (int i = 0; i < crossedLevels.Count; i++)
                        {
                            var crossed = crossedLevels[i];
                            string displayGrade = GetLocalizedLevelTitle(crossed.Level , player.UserIDString , crossed.Grade);
                            Reply(player , Lang("LevelUp" , player.UserIDString , crossed.Level , displayGrade));
                        }
                    }
                }
                else
                {
                    if (levelDown && _config.NotifyOnLevelDown)
                    {
                        string displayGrade = GetLocalizedLevelTitle(newLevel , player.UserIDString , newGrade);
                        Reply(player , Lang("LevelDown" , player.UserIDString , newLevel , displayGrade));
                    }

                    if (recovered && _config.NotifyOnFullRecovery)
                        Reply(player , Lang("Recovery" , player.UserIDString));
                }

                UpdateSimpleStatus(player);
            }

            if (oldLevel != newLevel)
            {
                DebugPlayer(userId , $"Level changed: {oldLevel} -> {newLevel} | OldPoints: {oldPoints} | NewPoints: {newPoints}");
                Interface.Oxide.CallHook("OnPlagueLevelChanged" , player , oldLevel , newLevel);
            }

            if (gradeChanged)
            {
                DebugPlayer(userId , $"Grade changed: {oldGrade ?? "Healthy"} -> {newGrade ?? "Healthy"} | OldPoints: {oldPoints} | NewPoints: {newPoints}");
                Interface.Oxide.CallHook("OnPlagueGradeChanged" , player , oldGrade , newGrade);
            }

            SetRuntimeLastGrade(userId , newGrade);
        }

        private void SyncPlayerIllnessStateSilent(ulong userId)
        {
            if (!IsRealPlayerId(userId))
                return;

            var data = GetOrCreatePlayerData(userId);
            if (data == null)
                return;

            var matchedLevel = GetLevelFromTotalPoints(data.TotalPlaguePoints);
            data.PlagueLevel = matchedLevel != null ? matchedLevel.Level : 0;

            if (data.PlagueLevel <= 0)
                ResetRuntimeEffects(userId);

            SetRuntimeLastGrade(userId , matchedLevel?.Grade);
            MarkPlayerDataDirty(userId);
        }

        #endregion

        #region Kin

        private bool IsKin(ulong playerId , ulong targetId)
        {
            if (!IsRealPlayerId(playerId) || !IsRealPlayerId(targetId))
                return false;

            var data = GetOrCreatePlayerData(playerId);
            return data != null && data.Kins.Contains(targetId);
        }

        private bool AddKinRelationship(ulong playerId , ulong targetId)
        {
            if (!IsRealPlayerId(playerId) || !IsRealPlayerId(targetId))
                return false;

            if (playerId == targetId)
                return false;

            var self = GetOrCreatePlayerData(playerId);
            var target = GetOrCreatePlayerData(targetId);

            if (self == null || target == null)
                return false;

            if (self.Kins.Contains(targetId))
                return true;

            if (self.Kins.Count >= _config.MaxKin || target.Kins.Count >= _config.MaxKin)
                return false;

            self.Kins.Add(targetId);
            target.Kins.Add(playerId);
            self.PendingKinRequests.Remove(targetId);
            target.PendingKinRequests.Remove(playerId);

            MarkPlayerDataDirty(playerId);
            MarkPlayerDataDirty(targetId);
            return true;
        }

        private bool RemoveKinRelationship(ulong initiatorId , ulong targetId)
        {
            if (!IsRealPlayerId(initiatorId) || !IsRealPlayerId(targetId))
                return false;

            var self = GetOrCreatePlayerData(initiatorId);
            var target = GetOrCreatePlayerData(targetId);

            if (self == null || target == null)
                return false;

            if (!self.Kins.Contains(targetId))
                return false;

            if (self.KinChangesCount + 1 > _config.MaxKinChanges)
                return false;

            self.KinChangesCount++;
            self.Kins.Remove(targetId);
            target.Kins.Remove(initiatorId);

            self.PendingKinRequests.Remove(targetId);
            target.PendingKinRequests.Remove(initiatorId);

            MarkPlayerDataDirty(initiatorId);
            MarkPlayerDataDirty(targetId);
            return true;
        }

        private void HandleAddKin(BasePlayer player)
        {
            BasePlayer target;
            if (!TryGetLookPlayer(player , out target))
                return;

            if (!IsRealPlayer(target))
            {
                Reply(player , Lang("CannotUseNpc" , player.UserIDString));
                return;
            }

            if (target.userID == player.userID)
            {
                Reply(player , Lang("CannotAddSelfKin" , player.UserIDString));
                return;
            }

            var self = GetOrCreatePlayerData(player.userID , player.displayName);
            var other = GetOrCreatePlayerData(target.userID , target.displayName);

            if (self == null || other == null)
            {
                Reply(player , Lang("CannotUseNpc" , player.UserIDString));
                return;
            }

            if (self.Kins.Contains(target.userID))
            {
                Reply(player , Lang("AlreadyKin" , player.UserIDString , target.displayName));
                return;
            }

            if (self.Kins.Count >= _config.MaxKin)
            {
                Reply(player , Lang("MaxKinReached" , player.UserIDString , _config.MaxKin));
                return;
            }

            if (other.Kins.Count >= _config.MaxKin)
            {
                Reply(player , Lang("OtherMaxKinReached" , player.UserIDString , target.displayName));
                return;
            }

            if (other.PendingKinRequests.Contains(player.userID))
            {
                if (!AddKinRelationship(player.userID , target.userID))
                {
                    Reply(player , Lang("KinCreateFailed" , player.UserIDString));
                    return;
                }

                Reply(player , Lang("KinNow" , player.UserIDString , target.displayName));
                Reply(target , Lang("KinNow" , target.UserIDString , player.displayName));
                return;
            }

            if (other.PendingKinRequests.Add(player.userID))
            {
                MarkPlayerDataDirty(target.userID);
                Reply(player , Lang("KinRequested" , player.UserIDString , target.displayName));
                Reply(target , Lang("KinRequestReceived" , target.UserIDString , player.displayName));
                return;
            }

            Reply(player , Lang("KinRequestExists" , player.UserIDString));
        }

        private void HandleRemoveKinLook(BasePlayer player)
        {
            BasePlayer target;
            if (!TryGetLookPlayer(player , out target))
                return;

            if (!IsRealPlayer(target))
            {
                Reply(player , Lang("CannotUseNpc" , player.UserIDString));
                return;
            }

            if (!IsKin(player.userID , target.userID))
            {
                Reply(player , Lang("NotKin" , player.UserIDString , target.displayName));
                return;
            }

            if (!RemoveKinRelationship(player.userID , target.userID))
            {
                Reply(player , Lang("RemoveKinFailed" , player.UserIDString));
                return;
            }

            Reply(player , Lang("RemovedKin" , player.UserIDString , target.displayName));
            if (target.IsConnected)
                Reply(target , Lang("RemovedKin" , target.UserIDString , player.displayName));
        }

        private void HandleRemoveKinByIndex(BasePlayer player , int index)
        {
            var ids = GetSortedKinIds(player.userID);
            if (index <= 0 || index > ids.Count)
            {
                Reply(player , Lang("InvalidKinNumber" , player.UserIDString));
                return;
            }

            ulong targetId = ids[index - 1];
            if (!RemoveKinRelationship(player.userID , targetId))
            {
                Reply(player , Lang("RemoveKinFailed" , player.UserIDString));
                return;
            }

            Reply(player , Lang("RemovedKinByIndex" , player.UserIDString , GetPlayerName(targetId)));
        }

        private void HandleListKin(BasePlayer player)
        {
            var ids = GetSortedKinIds(player.userID);
            if (ids.Count == 0)
            {
                Reply(player , Lang("NoKin" , player.UserIDString));
                return;
            }

            var lines = new List<string>();
            for (int i = 0; i < ids.Count; i++)
                lines.Add($"{i + 1}. {GetPlayerName(ids[i])}");

            ReplyList(player , Lang("KinListTitle" , player.UserIDString) , lines);
        }

        private List<ulong> GetSortedKinIds(ulong userId)
        {
            var result = new List<ulong>();
            if (!IsRealPlayerId(userId))
                return result;

            var data = GetOrCreatePlayerData(userId);
            if (data == null)
                return result;

            result.AddRange(data.Kins);
            result.Sort((a , b) => string.Compare(GetPlayerName(a) , GetPlayerName(b) , StringComparison.OrdinalIgnoreCase));
            return result;
        }

        #endregion

        #region SimpleStatus

        private void RegisterSimpleStatusDefinition()
        {
            _simpleStatusRegistered = false;

            if (!_config.UseSimpleStatusSupport)
                return;

            if (SimpleStatus == null || !SimpleStatus.IsLoaded)
                return;

            SimpleStatus.Call("CreateStatus" , this , SimpleStatusId , new Dictionary<string , object>
            {
                ["color"] = _config.SimpleStatusBackgroundColor ,
                ["title"] = _config.SimpleStatusTitle ,
                ["titleColor"] = _config.SimpleStatusTitleColor ,
                ["text"] = "Infected" ,
                ["textColor"] = _config.SimpleStatusTextColor ,
                ["icon"] = _config.SimpleStatusIcon ,
                ["iconColor"] = _config.SimpleStatusIconColor ,
                ["progress"] = "0" ,
                ["progressColor"] = _config.SimpleStatusProgressColor ,
                ["rank"] = _config.SimpleStatusRank
            });

            _simpleStatusRegistered = true;
        }

        private void UpdateSimpleStatus(BasePlayer player)
        {
            if (!IsRealPlayer(player))
                return;

            if (!_config.UseSimpleStatusSupport)
                return;

            if (!_simpleStatusRegistered)
                return;

            if (SimpleStatus == null || !SimpleStatus.IsLoaded)
                return;

            var data = GetOrCreatePlayerData(player.userID , player.displayName);
            if (data == null)
                return;

            int level = GetPlagueLevelFromPoints(data.TotalPlaguePoints);

            if (level <= 0)
            {
                if (_config.SimpleStatusRemoveWhenHealthy)
                    RemoveSimpleStatus(player);

                return;
            }

            var current = GetLevelConfig(level);
            var next = GetNextLevelConfig(level);
            string grade = GetLocalizedLevelTitle(level , player.UserIDString , current?.Grade ?? "Infected");

            float progress = 1f;
            if (current != null && next != null)
            {
                int floor = current.RequiredTotalPoints;
                int ceil = next.RequiredTotalPoints;
                int span = Math.Max(1 , ceil - floor);
                int currentProgress = Math.Max(0 , data.TotalPlaguePoints - floor);
                progress = Mathf.Clamp01((float)currentProgress / span);
            }

            string text = Lang("SimpleStatusText" , player.UserIDString , level , grade);

            SimpleStatus.Call("SetStatus" , player.userID , SimpleStatusId , int.MaxValue , true);
            SimpleStatus.Call("SetStatusProperty" , player.userID , SimpleStatusId , new Dictionary<string , object>
            {
                ["color"] = _config.SimpleStatusBackgroundColor ,
                ["title"] = _config.SimpleStatusTitle ,
                ["titleColor"] = _config.SimpleStatusTitleColor ,
                ["text"] = text ,
                ["textColor"] = _config.SimpleStatusTextColor ,
                ["icon"] = _config.SimpleStatusIcon ,
                ["iconColor"] = _config.SimpleStatusIconColor ,
                ["progress"] = progress.ToString(CultureInfo.InvariantCulture) ,
                ["progressColor"] = _config.SimpleStatusProgressColor
            });
        }

        private void RemoveSimpleStatus(BasePlayer player)
        {
            if (!IsRealPlayer(player))
                return;

            if (!_config.UseSimpleStatusSupport)
                return;

            if (SimpleStatus == null || !SimpleStatus.IsLoaded)
                return;

            SimpleStatus.Call("SetStatus" , player.userID , SimpleStatusId , 0 , true);
        }

        #endregion

        #region Helpers

        private bool IsRealPlayerId(ulong userId)
        {
            return userId >= SteamIdMinimum;
        }

        private bool IsRealPlayer(BasePlayer player)
        {
            return player != null && IsRealPlayerId(player.userID) && !(player is NPCPlayer);
        }

        private bool HasAdminPermission(BasePlayer player)
        {
            return IsRealPlayer(player) && permission.UserHasPermission(player.UserIDString , PermissionUseAdminCommands);
        }

        private bool IsDebugEnabledFor(ulong userId)
        {
            return IsRealPlayerId(userId) && _globalData.DebugEnabledPlayers.Contains(userId);
        }

        private bool IsTestModeEnabledFor(ulong userId)
        {
            return IsRealPlayerId(userId) && _globalData.AdminTestModePlayers.Contains(userId);
        }

        private bool ShouldIgnoreIllnessEffects(BasePlayer player)
        {
            if (!IsRealPlayer(player))
                return true;

            if (player.IsDead())
                return true;

            if (_config.IgnoreIllnessEffectsWhileGodmode && player.IsGod())
                return true;

            return false;
        }

        private bool ResetRuntimeEffects(ulong userId)
        {
            if (!IsRealPlayerId(userId))
                return false;

            var player = BasePlayer.FindAwakeOrSleeping(userId.ToString());
            if (!IsRealPlayer(player) || player.metabolism == null)
                return false;

            bool changed = false;

            if (player.metabolism.radiation_poison.value != 0f)
            {
                player.metabolism.radiation_poison.value = 0f;
                changed = true;
            }

            if (player.metabolism.radiation_level.value != 0f)
            {
                player.metabolism.radiation_level.value = 0f;
                changed = true;
            }

            if (player.metabolism.bleeding.value != 0f)
            {
                player.metabolism.bleeding.value = 0f;
                changed = true;
            }

            if (player.metabolism.poison.value != 0f)
            {
                player.metabolism.poison.value = 0f;
                changed = true;
            }

            return changed;
        }

        private void SetRuntimeLastGrade(ulong userId , string grade)
        {
            if (!IsRealPlayerId(userId))
                return;

            RuntimeState state;
            if (_runtimeStates.TryGetValue(userId , out state))
            {
                state.LastGrade = grade;
                return;
            }

            _runtimeStates[userId] = new RuntimeState(grade);
        }

        private void MarkPlayerDataDirty(ulong userId)
        {
            if (!IsRealPlayerId(userId))
                return;

            _dirtyPlayers.Add(userId);
        }

        private void SaveDirtyPlayers()
        {
            if (_dirtyPlayers.Count == 0)
                return;

            foreach (var userId in _dirtyPlayers)
                SavePlayerData(userId);

            _dirtyPlayers.Clear();
        }

        private void Reply(BasePlayer player , string message)
        {
            SendReply(player , $"{_config.ChatPrefix} {message}");
        }

        private void ReplyList(BasePlayer player , string title , List<string> lines)
        {
            if (lines == null || lines.Count == 0)
            {
                Reply(player , $"{title}: none.");
                return;
            }

            string text = $"{title}:\n";
            for (int i = 0; i < lines.Count; i++)
                text += $"> {lines[i]}\n";

            Reply(player , text);
        }

        private string GetPlayerName(ulong userId)
        {
            if (!IsRealPlayerId(userId))
                return "NPC";

            var player = BasePlayer.FindAwakeOrSleeping(userId.ToString());
            if (IsRealPlayer(player) && !string.IsNullOrEmpty(player.displayName))
            {
                _globalData.KnownNames[userId] = player.displayName;
                return player.displayName;
            }

            string name;
            if (_globalData.KnownNames.TryGetValue(userId , out name) && !string.IsNullOrEmpty(name))
                return name;

            return userId.ToString();
        }

        private void DebugPlayer(BasePlayer player , string message)
        {
            if (!IsRealPlayer(player))
                return;

            DebugPlayer(player.userID , message);
        }

        private void DebugPlayer(ulong userId , string message)
        {
            if (!_config.Debug.EnableDebugLogging)
                return;

            if (!IsDebugEnabledFor(userId))
                return;

            string text = $"[Plagued Debug] {message}";

            if (_config.Debug.LogToServerConsole)
                Puts($"{GetPlayerName(userId)} ({userId}) => {text}");

            if (_config.Debug.SendDebugMessagesToAdminChat)
            {
                var player = BasePlayer.FindAwakeOrSleeping(userId.ToString());
                if (IsRealPlayer(player) && player.IsConnected)
                    SendReply(player , $"<color=#F2F5A9>{text}</color>");
            }
        }

        private bool TryGetLookPlayer(BasePlayer player , out BasePlayer targetPlayer)
        {
            targetPlayer = null;

            Quaternion currentRot;
            if (!TryGetPlayerView(player , out currentRot))
            {
                Reply(player , Lang("RotationFailed" , player.UserIDString));
                return false;
            }

            object closestEnt;
            Vector3 closestHitpoint;
            if (!TryGetClosestRayPoint(player.transform.position , currentRot , out closestEnt , out closestHitpoint))
            {
                Reply(player , Lang("LookPlayerFailed" , player.UserIDString));
                return false;
            }

            var collider = closestEnt as Collider;
            if (collider == null)
            {
                Reply(player , Lang("LookPlayerFailed" , player.UserIDString));
                return false;
            }

            targetPlayer = collider.GetComponentInParent<BasePlayer>();
            if (targetPlayer == null)
            {
                Reply(player , Lang("LookPlayerFailed" , player.UserIDString));
                return false;
            }

            if (!IsRealPlayer(targetPlayer))
            {
                Reply(player , Lang("CannotUseNpc" , player.UserIDString));
                targetPlayer = null;
                return false;
            }

            return true;
        }

        private bool TryGetClosestRayPoint(Vector3 sourcePos , Quaternion sourceDir , out object closestEnt , out Vector3 closestHitpoint)
        {
            Vector3 sourceEye = sourcePos + new Vector3(0f , 1.5f , 0f);
            Ray ray = new Ray(sourceEye , sourceDir * Vector3.forward);

            RaycastHit[] hits = Physics.RaycastAll(ray);
            float closestDist = float.MaxValue;
            closestHitpoint = sourcePos;
            closestEnt = null;

            for (int i = 0; i < hits.Length; i++)
            {
                var hit = hits[i];
                if (hit.collider == null)
                    continue;

                if (hit.collider.GetComponentInParent<TriggerBase>() != null)
                    continue;

                if (hit.distance >= closestDist)
                    continue;

                closestDist = hit.distance;
                closestEnt = hit.collider;
                closestHitpoint = hit.point;
            }

            return closestEnt != null;
        }

        private bool TryGetPlayerView(BasePlayer player , out Quaternion viewAngle)
        {
            viewAngle = Quaternion.identity;

            if (player?.serverInput?.current == null)
                return false;

            viewAngle = Quaternion.Euler(player.serverInput.current.aimAngles);
            return true;
        }

        #endregion

        #region API

        private int API_GetPlagueLevel(ulong userId) => GetPlagueLevel(userId);
        private int API_GetTotalPlaguePoints(ulong userId) => GetTotalPlaguePoints(userId);
        private string API_GetCurrentGrade(ulong userId) => GetCurrentGrade(userId);
        private bool API_IsPlayerInfected(ulong userId) => GetPlagueLevel(userId) > 0;
        private void API_SetTotalPlaguePoints(ulong userId , int amount) => SetTotalPlaguePoints(userId , amount);
        private void API_AddPlaguePoints(ulong userId , int amount) => AddPlaguePoints(userId , amount);
        private void API_SetPlagueLevelByThreshold(ulong userId , int level) => SetPlagueLevelByThreshold(userId , level);
        private bool API_IsKin(ulong playerId , ulong targetId) => IsKin(playerId , targetId);
        private bool API_AddKin(ulong playerId , ulong targetId) => AddKinRelationship(playerId , targetId);
        private bool API_RemoveKin(ulong playerId , ulong targetId) => RemoveKinRelationship(playerId , targetId);

        object GetPlagueLevelAPI(ulong userId) => API_GetPlagueLevel(userId);
        object GetTotalPlaguePointsAPI(ulong userId) => API_GetTotalPlaguePoints(userId);
        object GetCurrentGradeAPI(ulong userId) => API_GetCurrentGrade(userId);
        object IsPlayerInfectedAPI(ulong userId) => API_IsPlayerInfected(userId);
        object SetTotalPlaguePointsAPI(ulong userId , int amount) { API_SetTotalPlaguePoints(userId , amount); return null; }
        object AddPlaguePointsAPI(ulong userId , int amount) { API_AddPlaguePoints(userId , amount); return null; }
        object SetPlagueLevelByThresholdAPI(ulong userId , int level) { API_SetPlagueLevelByThreshold(userId , level); return null; }
        object IsKinAPI(ulong playerId , ulong targetId) => API_IsKin(playerId , targetId);
        object AddKinAPI(ulong playerId , ulong targetId) => API_AddKin(playerId , targetId);
        object RemoveKinAPI(ulong playerId , ulong targetId) => API_RemoveKin(playerId , targetId);

        private List<Dictionary<string , object>> API_GetIllnessLevels()
        {
            var result = new List<Dictionary<string , object>>();

            if (_config?.IllnessLevels == null)
                return result;

            for (int i = 0; i < _config.IllnessLevels.Count; i++)
            {
                var level = _config.IllnessLevels[i];
                if (level == null)
                    continue;

                result.Add(new Dictionary<string , object>
                {
                    ["Level"] = level.Level ,
                    ["RequiredTotalPoints"] = level.RequiredTotalPoints ,
                    ["Grade"] = level.Grade ,
                    ["CaloriesLoss"] = level.CaloriesLoss ,
                    ["HydrationLoss"] = level.HydrationLoss ,
                    ["MaximumRadiation"] = level.MaximumRadiation ,
                    ["RadiationGainPerTick"] = level.RadiationGainPerTick
                });
            }

            return result;
        }

        object GetIllnessLevelsAPI() => API_GetIllnessLevels();

        #endregion
    }
}