using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Newtonsoft.Json;
using Oxide.Core;
using Oxide.Core.Configuration;
using Oxide.Core.Plugins;
using UnityEngine;

namespace Oxide.Plugins
{
    [Info("UGather", "dFxPhoeniX", "1.2.4")]
    [Description("Adds zones, permissions, and other options to modify gather rates!")]
    class UGather : RustPlugin
    {
        #region Variables
        [PluginReference] Plugin ZoneManager;
        public static UGather plugin;

        private DynamicConfigFile ZoneDataFile;
        private ZoneData zoneData;
        private Configuration config;

        private const string SphereEnt = "assets/prefabs/visualization/sphere.prefab"; // Thanks to ZoneDomes
        private readonly Dictionary<uint, ulong> _lastBreakers = new Dictionary<uint, ulong>();
        #endregion

        #region Data
        public class ZoneData
        {
            public List<GatherZone> Zones = new List<GatherZone>();

            public GatherZone GetZoneByID(string id)
            {
                return Zones.Find(x => x.id == id);
            }
        }

        public class GatherZone
        {
            // The message a user will see upon entering this gather zone, string.Empty for nothing
            public string enterMessage;
            // The message a user will see upon leaving this gather zone, string.Empty for nothing
            public string leaveMessage;

            // The permission we will use from the config to use certain gather rates.
            public string permissionToUse;
            // If a should have to have the permission to use these gather rates : This effects if a player will see the leave/enter mesage
            public bool requireUserHavePermission;
            // If this zone should have priority over everything else, or should it just leave it to the plugin to figure out.
            public bool overTakePriority;
            // If I should disable the regular permission, this prevents it from being used anywhere but the zone
            public bool restrictPermission;

            // The location of zone, probably dont need this. But just incase we have to recreate the zone
            public float x, y, z;
            // The id of the zone we use in ZoneManager, used to get at a later point.
            public string id;
            // The radius of the zone
            public int radius;

            public void OnPlayerEntered(BasePlayer player)
            {
                if (enterMessage == string.Empty) return; // No enter message, get out of here
                if (!plugin.permission.UserHasPermission(player.UserIDString, permissionToUse) && requireUserHavePermission) return;

                plugin.SendReply(player, enterMessage);
            }

            public void OnPlayerLeave(BasePlayer player)
            {
                if (leaveMessage == string.Empty) return; // No enter message, get out of here
                if (!plugin.permission.UserHasPermission(player.UserIDString, permissionToUse) && requireUserHavePermission) return;

                plugin.SendReply(player, leaveMessage);
            }
        }

        private void SaveData()
        {
            ZoneDataFile.WriteObject(zoneData);
        }

        private void LoadData()
        {
            plugin = this;
            try
            {
                ZoneDataFile.Settings.NullValueHandling = NullValueHandling.Ignore;
                zoneData = ZoneDataFile.ReadObject<ZoneData>();
                Puts($"Loaded {zoneData.Zones.Count} UGather Zones!");
            }
            catch
            {
                Puts("Failed to load ZoneData, creating new data.");
                zoneData = new ZoneData();
            }
        }
        #endregion

        #region ZoneManager
        private bool IsInZone(BasePlayer player, string zoneId)
        {
            if (ZoneManager == null || player == null || string.IsNullOrEmpty(zoneId)) return false;

            object r =
                ZoneManager?.CallHook("isPlayerInZone", zoneId, player) ??
                ZoneManager?.CallHook("IsPlayerInZone", zoneId, player) ??
                ZoneManager?.CallHook("isPlayerInZone", player, zoneId) ??
                ZoneManager?.CallHook("IsPlayerInZone", player, zoneId);

            return r is bool b && b;
        }

        private bool CreateOrUpdateZoneSafe(string id, string[] args, Vector3 pos)
        {
            if (ZoneManager == null) return false;
            var r = ZoneManager?.CallHook("CreateOrUpdateZone", id, args, pos);
            return r == null || (r is bool b && b);
        }

        private void EraseZoneSafe(string id)
        {
            ZoneManager?.CallHook("EraseZone", id);
        }

        private void OnEnterZone(string ZoneID, BasePlayer player)
        {
            GatherZone zone = zoneData.GetZoneByID(ZoneID);
            zone?.OnPlayerEntered(player);
        }

        private void OnExitZone(string ZoneID, BasePlayer player)
        {
            GatherZone zone = zoneData.GetZoneByID(ZoneID);
            zone?.OnPlayerLeave(player);
        }
        #endregion

        #region Configuration
        public class Configuration
        {
            [JsonProperty(PropertyName = "Gather Perms : The list of permissions that grant specified gather rates.")]
            public Dictionary<string, Dictionary<string, Dictionary<string, float>>> GatherPerms;

            [JsonProperty(PropertyName = "Base Permission : The basic permission given to group default.")]
            public string BasePerm;

            [JsonProperty(PropertyName = "Default Group : The group all players should be in, these players get the base perm.")]
            public string DefaultGroup;

            [JsonProperty(PropertyName = "Stackable Permissions : If a user has multiple perms, should they stack gather rate.")]
            public bool Stack;
        }

        private Dictionary<string, Dictionary<string, float>> CreateDefaultRates()
        {
            return new Dictionary<string, Dictionary<string, float>>()
            {
                ["Barrels"] = new Dictionary<string, float> {
                    { "scrap", 2 },
                    { "lowgradefuel", 2 },
                    { "crude.oil", 2 },
                    { "metalpipe", 2 },
                    { "gears", 2 },
                    { "metalspring", 2 },
                    { "metalblade", 2 },
                    { "sewingkit", 2 },
                    { "rope", 2 },
                    { "tarp", 2 },
                    { "sheetmetal", 2 },
                    { "roadsigns", 2 },
                    { "semibody", 2 },
                    { "propanetank", 2 },
                    { "fuse", 2 }
                },

                ["Dispenser"] = new Dictionary<string, float> {
                    { "bone.fragments", 2 },
                    { "cloth", 2 },
                    { "fat.animal", 2 },
                    { "chicken.raw", 2 },
                    { "deermeat.raw", 2 },
                    { "horsemeat.raw", 2 },
                    { "meat.boar", 2 },
                    { "humanmeat.raw", 2 },
                    { "wolfmeat.raw", 2 },
                    { "bearmeat", 2 },
                    { "bigcatmeat", 2 },
                    { "crocodilemeat", 2 },
                    { "snakemeat", 2 },
                    { "leather", 2 },
                    { "metal.ore", 2 },
                    { "stones", 2 },
                    { "sulfur.ore", 2 },
                    { "hq.metal.ore", 2 },
                    { "wood", 2 },
                    { "metal.fragments", 2 },
                    { "metal.refined", 2 },
                    { "charcoal", 2 },
                    { "skull.wolf", 2 }
                },

                ["Bonus"] = new Dictionary<string, float> {
                    { "stones", 2 },
                    { "sulfur.ore", 2 },
                    { "metal.ore", 2 },
                    { "hq.metal.ore", 2 },
                    { "wood", 2 }
                },

                ["Quarry"] = new Dictionary<string, float> {
                    { "stones", 2 },
                    { "sulfur.ore", 2 },
                    { "metal.ore", 2 },
                    { "hq.metal.ore", 2 }
                },

                ["Survey"] = new Dictionary<string, float> {
                    { "stones", 2 },
                    { "sulfur.ore", 2 },
                    { "metal.ore", 2 },
                    { "hq.metal.ore", 2 }
                },

                ["Excavator"] = new Dictionary<string, float> {
                    { "stones", 2 },
                    { "sulfur.ore", 2 },
                    { "metal.ore", 2 },
                    { "hq.metal.ore", 2 }
                },

                ["Pickups"] = new Dictionary<string, float> {
                    { "stones", 2 },
                    { "sulfur.ore", 2 },
                    { "metal.ore", 2 },
                    { "wood", 2 },
                    { "mushroom", 2 },
                    { "cloth", 2 },
                    { "wheat", 2 },
                    { "corn", 2 },
                    { "pumpkin", 2 },
                    { "potato", 2 },
                    { "coconut", 2 },
                    { "blueberries", 2 },
                    { "black.berry", 2 },
                    { "blue.berry", 2 },
                    { "green.berry", 2 },
                    { "red.berry", 2 },
                    { "white.berry", 2 },
                    { "yellow.berry", 2 },
                    { "fish.raw", 2 },
                    { "seed.wheat", 2 },
                    { "seed.corn", 2 },
                    { "seed.hemp", 2 },
                    { "seed.pumpkin", 2 },
                    { "seed.potato", 2 },
                    { "seed.blue.berry", 2 },
                    { "seed.black.berry", 2 },
                    { "seed.green.berry", 2 },
                    { "seed.red.berry", 2 },
                    { "seed.white.berry", 2 },
                    { "seed.yellow.berry", 2 }
                }
            };
        }

        private Dictionary<string, Dictionary<string, float>> CloneRates(Dictionary<string, Dictionary<string, float>> source)
        {
            return source.ToDictionary(
                kv => kv.Key,
                kv => new Dictionary<string, float>(kv.Value, StringComparer.OrdinalIgnoreCase),
                StringComparer.OrdinalIgnoreCase
            );
        }

        protected override void LoadDefaultConfig()
        {
            PrintWarning("Creating a new configuration file.");
            var rates = CreateDefaultRates();

            config = new Configuration()
            {
                GatherPerms = new Dictionary<string, Dictionary<string, Dictionary<string, float>>>()
                {
                    { "ugather.basic",    CloneRates(rates) },
                    { "ugather.advanced", CloneRates(rates) },
                    { "ugather.donor",    CloneRates(rates) },
                    { "ugather.admin",    CloneRates(rates) }
                },
                BasePerm = "ugather.basic",
                Stack = false,
                DefaultGroup = "default"
            };

            SaveConfig(config);
        }

        void SaveConfig(Configuration config)
        {
            Config.WriteObject(config, true);
            SaveConfig();
        }

        public void LoadConfigVars()
        {
            PrintWarning("Loading configuration.");
            config = Config.ReadObject<Configuration>();
            Config.WriteObject(config, true);
        }
        #endregion

        #region Oxide Hooks
        private void Loaded()
        {
            LoadConfigVars();

            if (!permission.PermissionExists("ugather.admin"))
                permission.RegisterPermission("ugather.admin", this);

            foreach (var x in config.GatherPerms)
                permission.RegisterPermission(x.Key, this);

            if (!permission.GroupHasPermission(config.DefaultGroup, config.BasePerm))
                permission.GrantGroupPermission(config.DefaultGroup, config.BasePerm, this);

            MigrateConfig();

            ZoneDataFile = Interface.Oxide.DataFileSystem.GetFile("UGather_ZoneData");
            LoadData();

            lang.RegisterMessages(new Dictionary<string, string>()
            {
                // General Command Lang
                {"Command : Prefix", "<color=#0dba86>[UGather] </color>"},
                {"Command : Help (General)",
                    "\nUse any of these commands to see more information about them:" +
                    "\n • /ugather rate - Show's information about config gather rates." +
                    "\n • /ugather zone - Show's information about the zone gather system." +
                    "\n\n - UGather Zone Status: <color={zonestatuscolor}>{zonestatus}</color>"
                },
                {"Command : No Permission", "You do not have permission to run that command!"},

                // Zone Command Lang
                {"Command : Help (Zone)", "Commands for the zoning system:" +
                    "\n <b><i>* = Optional Parameter</i></b>" +
                    "\n • /ugather zone add <permission> <radius> - Creates a gather zone using a permission from config for reference, and a radius using zonemanager." +
                    "\n • /ugather zone list *<page> - Show's the list of gather zones, the page param is optional if you have too many zones to show on one page." +
                    "\n • /ugather zone edit <number from list> <variable> <value> - Sets a flag/variable for the zone. Use /ugather zone editinfo for more info." +
                    "\n • /ugather zone show <number from list> - Creates a sphere showing the radius of the zone" +
                    "\n • /ugather zone variables - Shows a list of variables you can edit using the /ugather zone edit command" +
                    "\n • /ugather zone delete <number from list> - Deletes a zone based on the number from /ugather zone list!"
                },
                {"Command : Not A Number", "The parameter <i>{param}</i> requires a valid positive number! Ex: 5"},
                {"Command : Not A Boolean", "The paramter <i>{param}</i> requires a valid boolean! Ex: true or false"},
                {"Command : Value Updated", "The variable {var} has been changed to {val} for the selected zone!"},
                {"Command : Failed To Update", "Failed to update the following variable: {var}"},
                {"Command : No Available Zone With ID", "Unable to find zone from the list with the number of {num}"},
                {"Command : Zone Created", "A gather zone with the ID of {id} and radius of {rad} was successfully created! Permission: {perm}"},
                {"Command : Zone Failed", "Failed to create a zone with the id of {id}!"},
                {"Command : Zone Manager Not Loaded", "The zone portion of the plugin is not available as ZoneManager is not loaded!"},
                {"Command : No Zones", "There are currently no gather zones! Use /ugather zone to learn more about creating zones!"},
                {"Command : Delete Success", "You have successfully deleted the zone with the ID of {id}!"},
                {"Command : Zone List", "Available Zones:" +
                    "\nPage {p1} out of {p2}" +
                    "\n{zones}"
                },
                {"Command : Zone Variables", "Editable Variables:" +
                    "\n • position (No Arguments) (The location of the zone)" +
                    "\n • radius (Integer - Example: 50) (The radius of the zone)" +
                    "\n • enter_message (String - Example: \"Hi!\") (The message you get upon entering the zone)" +
                    "\n • leave_message (String - Example: \"Bye!\") (The message you get upon leaving the zone)" +
                    "\n • require_permission (Boolean - Example: true) (If you require permission to gain gather rates from the zone)" +
                    "\n • overtake_priority (Boolean - Example: true) (If it should override all other gather rates)" +
                    "\n • restrict_permission (Boolean - Example: true) (If the permission it is using should be disabled unless used for the zone)"
                },

                // Rate Command Lang
                {"Command : Help (Rate)", "Commands for the gather rates system:" +
                    "\n <b><i>* = Optional Parameter</i></b>" +
                    "\n • /ugather rate list - Show's a list of current permissions" +
                    "\n • /ugather rate info <permission> - Shows info about a permissions rates" +
                    "\n • /ugather rate stats <permission> <type> - Shows info about a permissions rates, in a category." +
                    "\n • /ugather rate types - Show's a list of valid types"
                },
                {"Command : Permission List", "Available Permissions:" +
                    "\n{perms}"
                },
                {"Command : Invalid Permission", "Could not find a permission with the name of {name}!"},
                {"Command : Perm Info", "Permission Info:" +
                    "\n • Types: {typeCount}" +
                    "\n • Registered Zone: {hasRegisteredZone}" +
                    "\n • Players With Permission: {playerCount}"
                },
                {"Command : Permission Info List", "Showing type of {type} for permission {perm}:" +
                    "\n{stats}"
                },
                {"Command : Gather Types", "Available Gather Types:\n" +
                    "\n • Dispenser - The materials you get from harvesting with a pickaxe, hatchet, etc." +
                    "\n • Bonus - That little extra you get at the end of a harvest." +
                    "\n • Pickups - The materials you get from picking things up, like food." +
                    "\n • Quarry - The materials you get from a quarry." +
                    "\n • Survey - The materials you get from a survey charge" +
                    "\n • Excavator - The materials produced by the giant Excavator arm" +
                    "\n • Barrels - Items gained from breaking barrels and road signs, including scrap, components, crude oil and low grade fuel"
                },
            }, this);
            lang.RegisterMessages(new Dictionary<string, string>()
            {
                // General Command Lang
                {"Command : Prefix", "<color=#0dba86>[UGather] </color>"},
                {"Command : Help (General)",
                    "\nFolosește oricare din aceste comenzi pentru a vedea mai multe informații despre ele:" +
                    "\n • /ugather rate - Afișează informații despre ratele de recoltare." +
                    "\n • /ugather zone - Afișează informații despre zona ratei de recoltare." +
                    "\n\n - Statusul zonei UGather: <color={zonestatuscolor}>{zonestatus}</color>"
                },
                {"Command : No Permission", "Nu ai permisiunea de a folosi această comandă!"},
            
                // Zone Command Lang
                {"Command : Help (Zone)", "Comenzi pentru sistemele de zonare:" +
                    "\n <b><i>* = Parametru Opțional</i></b>" +
                    "\n • /ugather zone add <permisiune> <raza> - Creează o zonă de recoltare folosind o permisiune din config pentru referință și o rază folosind ZoneManager." +
                    "\n • /ugather zone list *<pagina> - Afișează lista zonelor de recoltare. Parametrul paginii este opțional dacă ai prea multe zone de afișat într-o pagină." +
                    "\n • /ugather zone edit <număr din listă> <variabilă> <valoare> - Setează un steag/variabilă pentru zona. Folosește /ugather zone editinfo pentru mai multe informații." +
                    "\n • /ugather zone show <număr din listă> - Creează o sferă care arată raza zonei." +
                    "\n • /ugather zone variables - Afișează o listă de variabile pe care le poți edita folosind comanda /ugather zone edit." +
                    "\n • /ugather zone delete <număr din listă> - Șterge o zonă bazată pe numărul din /ugather zone list!"
                },
                {"Command : Not A Number", "Parametrul <i>{param}</i> necesită un număr valid pozitiv! Exemplu: 5"},
                {"Command : Not A Boolean", "Parametrul <i>{param}</i> necesită o valoare booleană validă! Exemplu: true sau false"},
                {"Command : Value Updated", "Variabila {var} a fost actualizată cu {val} pentru zona selectată!"},
                {"Command : Failed To Update", "Nu s-a putut actualiza următoarea variabilă: {var}"},
                {"Command : No Available Zone With ID", "Nu s-a putut găsi o zonă în lista cu numărul {num}"},
                {"Command : Zone Created", "O zonă de recoltare cu ID-ul {id} și rază de {rad} a fost creată cu succes! Permisiune: {perm}"},
                {"Command : Zone Failed", "Nu s-a putut crea o zonă cu ID-ul {id}!"},
                {"Command : Zone Manager Not Loaded", "Portiunea de zonă a pluginului nu este disponibilă deoarece ZoneManager nu este încărcat!"},
                {"Command : No Zones", "Momentan nu există zone de recoltare! Folosește /ugather zone pentru a afla mai multe despre crearea unei zone!"},
                {"Command : Delete Success", "Ai șters cu succes zona cu ID-ul {id}!"},
                {"Command : Zone List", "Zone disponibile:" +
                    "\nPagina {p1} din {p2}" +
                    "\n{zones}"
                },
                {"Command : Zone Variables", "Variabile editabile:" +
                    "\n • position (Fără argumente) (Locația zonei)" +
                    "\n • radius (Număr întreg - Exemplu: 50) (Raza zonei)" +
                    "\n • enter_message (Șir de caractere - Exemplu: \"Salut!\") (Mesajul pe care îl primești la intrarea în zonă)" +
                    "\n • leave_message (Șir de caractere - Exemplu: \"La revedere!\") (Mesajul pe care îl primești la părăsirea zonei)" +
                    "\n • require_permission (Valoare booleană - Exemplu: true) (Dacă necesită permisiune pentru a obține ratele de recoltare din zona respectivă)" +
                    "\n • overtake_priority (Valoare booleană - Exemplu: true) (Dacă ar trebui să suprascrie toate celelalte rate de recoltare)" +
                    "\n • restrict_permission (Valoare booleană - Exemplu: true) (Dacă permisiunea folosită ar trebui dezactivată doar dacă nu este utilizată pentru zona respectivă)"
                },
            
                // Rate Command Lang
                {"Command : Help (Rate)", "Comenzi pentru sistemul de rate de recoltare:" +
                    "\n <b><i>* = Parametru opțional</i></b>" +
                    "\n • /ugather rate list - Afișează o listă cu permisiunile curente" +
                    "\n • /ugather rate info <permisiune> - Afișează informații despre ratele permisiunii" +
                    "\n • /ugather rate stats <permisiune> <tip> - Afișează informații despre ratele permisiunii într-o categorie." +
                    "\n • /ugather rate types - Afișează o listă de tipuri valide"
                },
                {"Command : Permission List", "Permisiuni disponibile:" +
                    "\n{perms}"
                },
                {"Command : Invalid Permission", "Nu s-a putut găsi o permisiune cu numele {name}!"},
                {"Command : Perm Info", "Informații despre permisiune:" +
                    "\n • Tipuri: {typeCount}" +
                    "\n • Zonă înregistrată: {hasRegisteredZone}" +
                    "\n • Jucători cu permisiune: {playerCount}"
                },
                {"Command : Permission Info List", "Afișează tipul {type} pentru permisiunea {perm}:" +
                    "\n{stats}"
                },
                {"Command : Gather Types", "Tipuri de recoltare disponibile:\n" +
                    "\n • Dispenser - Materialele pe care le primești din recoltare cu un topor, o secure, etc." +
                    "\n • Bonus - Un mic bonus pe care îl primești la sfârșitul recoltării." +
                    "\n • Pickups - Materialele pe care le primești luându-le de jos, precum mâncarea." +
                    "\n • Cariera - Materialele pe care le primești dintr-o carieră." +
                    "\n • Studiu - Materialele pe care le primești dintr-un sondaj" +
                    "\n • Excavator - Materialele produse de Excavatorul mare" +
                    "\n • Barrels - Itemele obținute din spargerea barrel-urilor și road sign-urilor, inclusiv scrap, componente, crude oil și low grade fuel"
                },
            }, this, "ro");
        }

        private void Unload()
        {
            SaveData();
        }

        private void OnQuarryGather(MiningQuarry quarry, Item item)
        {
            if (quarry == null || item == null) return;
            BasePlayer player = BasePlayer.FindByID(quarry.OwnerID);
            if (player == null) return;

            string res = item.info.shortname;
            float resourceMult = config.Stack
                ? GetStackedMultiplier(player, "Quarry", res, false, quarry.transform.position)
                : GetSingleMultiplier(player, "Quarry", res, false, quarry.transform.position);

            if (resourceMult <= 0f) return;
            item.amount = (int)(item.amount * resourceMult);
        }

        private void OnCollectiblePickup(CollectibleEntity collectible, BasePlayer player)
        {
            if (collectible == null || player == null) return;

            var list = collectible.itemList;
            if (list == null || list.Length == 0) return;

            foreach (var ia in list)
            {
                if (ia == null || ia.itemDef == null) continue;

                string res = ia.itemDef.shortname;

                float mult = config.Stack
                    ? GetStackedMultiplier(player, "Pickups", res)
                    : GetSingleMultiplier(player, "Pickups", res);

                if (mult <= 0f || Mathf.Approximately(mult, 1f)) continue;

                ia.amount *= mult;
            }
        }

        Dictionary<BaseEntity, BasePlayer> SurveyCharges = new Dictionary<BaseEntity, BasePlayer>();
        private void OnExplosiveThrown(BasePlayer player, BaseEntity entity)
        {
            if (entity == null || entity.ShortPrefabName == null) return;
            if (!entity.ShortPrefabName.Contains("survey")) return;

            if (!SurveyCharges.ContainsKey(entity))
                SurveyCharges[entity] = player;
        }

        private void OnDispenserBonus(ResourceDispenser dispenser, BasePlayer player, Item item)
        {
            if (item == null || player == null) return;
            string res = item.info.shortname;

            float resourceMult = config.Stack
                ? GetStackedMultiplier(player, "Bonus", res)
                : GetSingleMultiplier(player, "Bonus", res);

            if (resourceMult <= 0f) return;
            item.amount = (int)(item.amount * resourceMult);
        }

        private void OnSurveyGather(SurveyCharge survey, Item item)
        {
            if (survey == null || item == null) return;

            BasePlayer player = null;
            if (SurveyCharges.TryGetValue(survey, out var p))
                player = p;

            if (player == null)
                return;

            SurveyCharges.Remove(survey);

            string res = item.info.shortname;
            float resourceMult = config.Stack
                ? GetStackedMultiplier(player, "Survey", res, false, survey.transform.position)
                : GetSingleMultiplier(player, "Survey", res, false, survey.transform.position);

            if (resourceMult <= 0f) return;
            item.amount = (int)(item.amount * resourceMult);
        }

        private void OnDispenserGather(ResourceDispenser dispenser, BaseEntity entity, Item item)
        {
            var player = entity?.ToPlayer();
            if (player == null || item == null) return;

            string res = item.info.shortname;
            float resourceMult = config.Stack
                ? GetStackedMultiplier(player, "Dispenser", res)
                : GetSingleMultiplier(player, "Dispenser", res);

            if (resourceMult <= 0f) return;
            item.amount = (int)(item.amount * resourceMult);
        }

        Dictionary<ExcavatorArm, ulong> _excavatorOperator = new Dictionary<ExcavatorArm, ulong>();

        private void OnEntityKill(BaseNetworkable ent)
        {
            var arm = ent as ExcavatorArm;
            if (arm != null) _excavatorOperator.Remove(arm);
        }

        private void OnEntityDestroy(BaseNetworkable ent)
        {
            var arm = ent as ExcavatorArm;
            if (arm != null) _excavatorOperator.Remove(arm);
        }

        private static readonly HashSet<string> BreakableLootPrefabs = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
        {
            // barrels
            "loot_barrel_1",
            "loot_barrel_2",
            "loot-barrel-1",
            "loot-barrel-2",
            "oil_barrel",
            "diesel_barrel_world",

            // road signs
            "roadsign1",
            "roadsign2",
            "roadsign3",
            "roadsign4",
            "roadsign5",
            "roadsign6",
            "roadsign7",
            "roadsign8",
            "roadsign9"
        };

        private void OnEntityDeath(BaseCombatEntity entity, HitInfo info)
        {
            if (entity == null || info == null) return;
            if (entity.net == null) return;
            if (string.IsNullOrEmpty(entity.ShortPrefabName)) return;
            if (!BreakableLootPrefabs.Contains(entity.ShortPrefabName)) return;

            BasePlayer player = info.InitiatorPlayer;
            if (player == null || !player.IsValid()) return;

            uint netId = (uint)entity.net.ID.Value;
            _lastBreakers[netId] = player.userID;

            timer.Once(3f, () =>
            {
                _lastBreakers.Remove(netId);
            });
        }

        private void OnContainerDropItems(ItemContainer container)
        {
            if (container == null || container.entityOwner == null) return;
            if (container.itemList == null) return;

            var owner = container.entityOwner;
            if (owner.net == null) return;
            if (string.IsNullOrEmpty(owner.ShortPrefabName)) return;
            if (!BreakableLootPrefabs.Contains(owner.ShortPrefabName)) return;

            if (!_lastBreakers.TryGetValue((uint)owner.net.ID.Value, out var userId)) return;

            BasePlayer player = BasePlayer.FindByID(userId);
            if (player == null || !player.IsValid()) return;

            foreach (var item in container.itemList)
            {
                if (item?.info == null) continue;

                string shortname = item.info.shortname;

                float mult = config.Stack
                    ? GetStackedMultiplier(player, "Barrels", shortname, false, owner.transform.position)
                    : GetSingleMultiplier(player, "Barrels", shortname, false, owner.transform.position);

                if (mult <= 1f) continue;

                int newAmount = Mathf.RoundToInt(item.amount * mult);
                if (newAmount <= item.amount) continue;

                item.amount = newAmount;
                item.MarkDirty();
            }

            _lastBreakers.Remove((uint)owner.net.ID.Value);
        }

        private void OnExcavatorResourceSet(ExcavatorArm arm, string resourceShortname, BasePlayer player)
        {
            if (arm == null || player == null) return;
            _excavatorOperator[arm] = player.userID;
        }

        private void OnExcavatorMiningToggled(ExcavatorArm arm, BasePlayer player, bool isOn)
        {
            if (arm == null || player == null) return;
            if (isOn) _excavatorOperator[arm] = player.userID;
        }

        private void OnExcavatorGather(ExcavatorArm arm, Item item)
        {
            if (arm == null || item == null) return;

            BasePlayer player = null;
            if (_excavatorOperator.TryGetValue(arm, out var uid))
                player = BasePlayer.FindByID(uid);

            string res = item.info.shortname;

            float mult = 1f;

            if (player != null)
            {
                mult = config.Stack
                    ? GetStackedMultiplier(player, "Excavator", res, false, arm.transform.position)
                    : GetSingleMultiplier(player, "Excavator", res, false, arm.transform.position);
            }
            else
            {
                if (config.GatherPerms != null &&
                    config.GatherPerms.TryGetValue(config.BasePerm, out var basePerm) &&
                    basePerm.TryGetValue("Excavator", out var exDict) &&
                    exDict.TryGetValue(res, out var baseRate))
                {
                    mult = baseRate;
                }
                else
                {
                    mult = 1f;
                }
            }

            if (mult <= 0f) return;
            item.amount = (int)(item.amount * mult);
        }
        #endregion

        #region Commands
        [ChatCommand("ugather")]
        private void UGather_Command(BasePlayer player, string command, string[] args)
        {
            if (!permission.UserHasPermission(player.UserIDString, "ugather.admin"))
            {
                Reply(player, "Command : No Permission");
                return;
            }
            if (args.Length == 0)
            {
                Reply(player, "Command : Help (General)", "zonestatuscolor", (ZoneManager == null) ? "red" : "green", "zonestatus", (ZoneManager == null) ? "Disabled" : "Enabled");
                return;
            }

            if (args[0].ToLower() == "zone")
            {
                if (ZoneManager == null)
                {
                    Reply(player, "Command : Zone Manager Not Loaded");
                    return;
                }
                if (args.Length < 2)
                {
                    Reply(player, "Command : Help (Zone)");
                    return;
                }

                switch (args[1])
                {
                    case "variables":
                        Reply(player, "Command : Zone Variables");
                        break;
                    case "add":
                        if (args.Length != 4)
                        {
                            Reply(player, "Command : Help (Zone)");
                            return;
                        }

                        string permission = args[2];
                        int radius = 0;
                        if (!int.TryParse(args[3], out radius))
                        {
                            Reply(player, "Command : Not A Number", "param", "radius");
                            return;
                        }

                        if (!config.GatherPerms.ContainsKey(permission))
                        {
                            Reply(player, "Command : Invalid Permission", "name", permission);
                            return;
                        }

                        GatherZone zone = new GatherZone()
                        {
                            enterMessage = string.Empty,
                            leaveMessage = string.Empty,

                            id = $"UGATHER_ZONE_{permission}_{DateTime.UtcNow.Ticks}",
                            overTakePriority = false,
                            permissionToUse = permission,
                            requireUserHavePermission = true,
                            x = player.transform.position.x,
                            y = player.transform.position.y,
                            z = player.transform.position.z,
                            restrictPermission = false,
                            radius = radius
                        };

                        List<string> Arguments = new List<string>()
                        {
                            "radius",
                            radius.ToString()
                        };

                        bool zoneManagerZone = CreateOrUpdateZoneSafe(
                            zone.id,
                            Arguments.ToArray(),
                            new Vector3(zone.x, zone.y, zone.z)
                        );
                        if (zoneManagerZone)
                        {
                            zoneData.Zones.Add(zone);
                            Reply(player, "Command : Zone Created", "id", zone.id, "rad", radius.ToString(), "perm", zone.permissionToUse);
                            SaveData();
                        }
                        else
                        {
                            Reply(player, "Command : Zone Failed", "id", zone.id);
                            EraseZoneSafe(zone.id);
                        }

                        break;
                    case "list":
                        int page = 1;
                        if (args.Length == 3)
                        {
                            if (int.TryParse(args[2], out page))
                            {
                                if (page < 1) page = 1;
                            }
                            else
                            {
                                Reply(player, "Command : Not A Number", "param", "page");
                                return;
                            }
                        }

                        StringBuilder zoneBuilder = new StringBuilder();
                        if (zoneData.Zones.Count == 0)
                        {
                            Reply(player, "Command : No Zones");
                            return;
                        }
                        for (var i = (page - 1) * 8; i < page * 8; i++)
                        {
                            if (!(zoneData.Zones.Count >= i + 1)) break;

                            zoneBuilder.AppendLine($" • {i + 1} - {zoneData.Zones[i].id}");
                        }

                        Reply(player, "Command : Zone List", "zones", zoneBuilder.ToString(), "p1", page.ToString(), "p2", Math.Ceiling(zoneData.Zones.Count / 8.0).ToString());
                        break;
                    case "show":
                        if (args.Length != 3)
                        {
                            Reply(player, "Command : Help (Zone)");
                            return;
                        }

                        int set2;
                        if (!int.TryParse(args[2], out set2))
                        {
                            Reply(player, "Command : Not A Number", "param", "id");
                            return;
                        }

                        if (set2 < 1 || set2 > zoneData.Zones.Count)
                        {
                            Reply(player, "Command : No Available Zone With ID", "num", set2.ToString());
                            return;
                        }

                        GatherZone showingZone = zoneData.Zones[set2 - 1];

                        for (int i = 0; i < 15; i++)
                        {
                            BaseEntity sphere = GameManager.server.CreateEntity(SphereEnt, new Vector3(showingZone.x, showingZone.y, showingZone.z), new Quaternion(), true);
                            SphereEntity ent = sphere.GetComponent<SphereEntity>();
                            ent.currentRadius = showingZone.radius * 2;
                            ent.lerpSpeed = 0f;

                            sphere.Spawn();

                            timer.Once(15f, () =>
                            {
                                sphere.Kill();
                            });
                        }
                        break;
                    case "delete":
                        if (args.Length != 3)
                        {
                            Reply(player, "Command : Help (Zone)");
                            return;
                        }

                        int set3;
                        if (!int.TryParse(args[2], out set3))
                        {
                            Reply(player, "Command : Not A Number", "param", "id");
                            return;
                        }

                        if (set3 < 1 || set3 > zoneData.Zones.Count)
                        {
                            Reply(player, "Command : No Available Zone With ID", "num", set3.ToString());
                            return;
                        }

                        GatherZone editingZone2 = zoneData.Zones[set3 - 1];
                        EraseZoneSafe(editingZone2.id);
                        Reply(player, "Command : Delete Success", "id", editingZone2.id);
                        zoneData.Zones.Remove(editingZone2);
                        SaveData();

                        break;
                    case "edit":
                        if (args.Length != 5)
                        {
                            Reply(player, "Command : Help (Zone)");
                            return;
                        }

                        int set;
                        if (!int.TryParse(args[2], out set))
                        {
                            Reply(player, "Command : Not A Number", "param", "id");
                            return;
                        }

                        if (set < 1 || set > zoneData.Zones.Count)
                        {
                            Reply(player, "Command : No Available Zone With ID", "num", set.ToString());
                            return;
                        }

                        GatherZone editingZone = zoneData.Zones[set - 1];

                        switch (args[3])
                        {
                            case "radius":
                                int editingZoneRadius = 0;

                                if (!int.TryParse(args[4], out editingZoneRadius))
                                {
                                    Reply(player, "Command : Not A Number", "param", "radius");
                                    return;
                                }

                                if (editingZoneRadius <= 0)
                                {
                                    Reply(player, "Command : Not A Number", "param", "radius");
                                    return;
                                }

                                List<string> editingZoneArgumentsRadius = new List<string>()
                                {
                                    "radius",
                                    editingZoneRadius.ToString()
                                };

                                bool wasZoneRadiusUpdated = CreateOrUpdateZoneSafe(
                                    editingZone.id,
                                    editingZoneArgumentsRadius.ToArray(),
                                    new Vector3(editingZone.x, editingZone.y, editingZone.z)
                                );
                                if (wasZoneRadiusUpdated)
                                {
                                    Reply(player, "Command : Value Updated", "var", "radius", "val", editingZoneRadius.ToString());
                                    editingZone.radius = editingZoneRadius;
                                }
                                else
                                    Reply(player, "Command : Failed To Update", "var", "radius");

                                break;
                            case "position":

                                List<string> editingPositionZone = new List<string>()
                                {
                                    "location",
                                    $"{player.transform.position.x} {player.transform.position.y} {player.transform.position.z}"
                                };

                                bool wasZonePositionUpdated = CreateOrUpdateZoneSafe(
                                    editingZone.id,
                                    editingPositionZone.ToArray(),
                                    new Vector3(editingZone.x, editingZone.y, editingZone.z)
                                );
                                if (wasZonePositionUpdated)
                                {
                                    Reply(player, "Command : Value Updated", "var", "position", "val", player.transform.position.ToString());
                                    editingZone.x = player.transform.position.x;
                                    editingZone.y = player.transform.position.y;
                                    editingZone.z = player.transform.position.z;
                                }
                                else
                                    Reply(player, "Command : Failed To Update", "var", "position");

                                break;
                            case "enter_message":
                                Reply(player, "Command : Value Updated", "var", "enter_message", "val", args[4]);
                                editingZone.enterMessage = args[4];
                                break;
                            case "leave_message":
                                Reply(player, "Command : Value Updated", "var", "leave_message", "val", args[4]);
                                editingZone.leaveMessage = args[4];
                                break;

                            case "require_permission":
                                bool shouldUsePerm = editingZone.requireUserHavePermission;
                                if (bool.TryParse(args[4], out shouldUsePerm))
                                {
                                    Reply(player, "Command : Value Updated", "var", "require_permission", "val", shouldUsePerm.ToString());
                                    editingZone.requireUserHavePermission = shouldUsePerm;
                                }
                                else
                                {
                                    Reply(player, "Command : Failed To Update", "var", "require_permission");
                                    Reply(player, "Command : Not A Boolean", "param", "require_permission");
                                }
                                break;

                            case "overtake_priority":
                                bool shouldOvertakePriority = editingZone.overTakePriority;
                                if (bool.TryParse(args[4], out shouldOvertakePriority))
                                {
                                    Reply(player, "Command : Value Updated", "var", "overtake_priority", "val", shouldOvertakePriority.ToString());
                                    editingZone.overTakePriority = shouldOvertakePriority;
                                }
                                else
                                {
                                    Reply(player, "Command : Failed To Update", "var", "overtake_priority");
                                    Reply(player, "Command : Not A Boolean", "param", "overtake_priority");
                                }
                                break;

                            case "restrict_permission":
                                bool shouldRestrictPermission = editingZone.restrictPermission;
                                if (bool.TryParse(args[4], out shouldRestrictPermission))
                                {
                                    Reply(player, "Command : Value Updated", "var", "restrict_permission", "val", shouldRestrictPermission.ToString());
                                    editingZone.restrictPermission = shouldRestrictPermission;
                                }
                                else
                                {
                                    Reply(player, "Command : Failed To Update", "var", "restrict_permission");
                                    Reply(player, "Command : Not A Boolean", "param", "restrict_permission");
                                }
                                break;
                            default:
                                Reply(player, "Command : Zone Variables");
                                break;
                        }
                        break;
                    default:
                        Reply(player, "Command : Help (Zone)");
                        break;
                }

                return;
            }
            else if (args[0].ToLower() == "rate")
            {
                if (args.Length < 2)
                {
                    Reply(player, "Command : Help (Rate)");
                    return;
                }

                switch (args[1])
                {
                    case "list":
                        StringBuilder permissionList = new StringBuilder();
                        foreach (var gatherPerm in config.GatherPerms)
                            permissionList.AppendLine($" • {gatherPerm.Key}");

                        Reply(player, "Command : Permission List", "perms", permissionList.ToString());
                        break;
                    case "info":
                        if (args.Length != 3)
                        {
                            Reply(player, "Command : Help (Rate)");
                            return;
                        }

                        if (config.GatherPerms.Where(x => x.Key.ToLower() == args[2].ToLower()).Count() == 0)
                        {
                            Reply(player, "Command : Invalid Permission", "name", args[2]);
                            return;
                        }

                        int countTypes = config.GatherPerms[args[2]].Count();
                        bool regZone = zoneData.Zones.Any(x => x.permissionToUse.ToLower() == args[2].ToLower());
                        int playersUsing = covalence.Players.All.Where(x => permission.UserHasPermission(x.Id, args[2].ToLower())).Count();

                        Reply(player, "Command : Perm Info", "typeCount", countTypes.ToString(), "hasRegisteredZone", regZone.ToString(), "playerCount", playersUsing.ToString());
                        break;
                    case "stats":
                        if (args.Length != 4)
                        {
                            Reply(player, "Command : Help (Rate)");
                            return;
                        }

                        if (config.GatherPerms.Where(x => x.Key.ToLower() == args[2].ToLower()).Count() == 0)
                        {
                            Reply(player, "Command : Invalid Permission", "name", args[2]);
                            return;
                        }

                        if (!config.GatherPerms[args[2]].ContainsKey(args[3]))
                        {
                            Reply(player, "Command : Invalid Permission Type", "name", args[2], "type", args[3]);
                            return;
                        }

                        StringBuilder statBuilder = new StringBuilder();
                        foreach (var v in config.GatherPerms[args[2]][args[3]])
                            statBuilder.AppendLine($" • {v.Key} : {v.Value}x");

                        Reply(player, "Command : Permission Info List", "type", args[3], "perm", args[2], "stats", statBuilder.ToString());
                        break;
                    case "types":
                        Reply(player, "Command : Gather Types");
                        break;
                    default:
                        Reply(player, "Command : Help (Rate)");
                        break;
                }
            }
            else
            {
                Reply(player, "Command : Help (General)", "zonestatuscolor", (ZoneManager == null) ? "red" : "green", "zonestatus", (ZoneManager == null) ? "Disabled" : "Enabled");
            }
        }
        #endregion

        #region Methods
        public void Reply(BasePlayer player, string raw, params string[] args)
        {
            var msg = lang.GetMessage(raw, this, player.UserIDString);
            if (args.Length > 0)
                for (var i = 0; i < args.Length; i += 2)
                    msg = msg.Replace("{" + args[i] + "}", args[i + 1]);

            SendReply(player, lang.GetMessage("Command : Prefix", this, player.UserIDString) + msg);
        }

        public float GetZoneRate(string permission, string type, string name)
        {
            if (!config.GatherPerms.ContainsKey(permission)) return 1f;

            if (!config.GatherPerms[permission].ContainsKey(type)) return 1f;

            if (!config.GatherPerms[permission][type].ContainsKey(name))
            {
                config.GatherPerms[permission][type].Add(name, 1f);
                SaveConfig();
            }

            return config.GatherPerms[permission][type][name];
        }

        public float GetStackedMultiplier(BasePlayer player, string type, string name, bool forced = false, Vector3 usePosition = new Vector3())
        {
            float highestZone = 1f;

            if (ZoneManager != null && zoneData?.Zones != null)
            {
                foreach (GatherZone zone in zoneData.Zones)
                {
                    // Distance check
                    bool inside = false;
                    if (usePosition != Vector3.zero)
                    {
                        inside = Vector3.Distance(usePosition, new Vector3(zone.x, zone.y, zone.z)) <= zone.radius;
                    }
                    else
                    {
                        inside = IsInZone(player, zone.id);
                    }
                    if (!inside) continue;

                    if (zone.requireUserHavePermission && !permission.UserHasPermission(player.UserIDString, zone.permissionToUse))
                        continue;

                    var zr = GetZoneRate(zone.permissionToUse, type, name);
                    if (zone.overTakePriority) return zr;
                    if (zr > highestZone) highestZone = zr;
                }
            }

            if (!config.Stack) return highestZone;

            // stacking = 1.0 + sum(r_i - 1.0)
            float stacked = highestZone <= 1f ? 1f : highestZone;
            foreach (var pair in config.GatherPerms)
            {
                if (!forced && !permission.UserHasPermission(player.UserIDString, pair.Key)) continue;
                if (!pair.Value.TryGetValue(type, out var typeDict)) continue;
                if (!typeDict.TryGetValue(name, out var rate)) continue;
                stacked += Math.Max(0f, rate - 1f);
            }

            return stacked < 1f ? 1f : stacked;
        }

        public float GetSingleMultiplier(BasePlayer player, string type, string name, bool forced = false, Vector3 usePosition = new Vector3())
        {
            float highestValue = 1f;

            if (ZoneManager != null && zoneData?.Zones != null)
            {
                foreach (GatherZone zone in zoneData.Zones)
                {
                    bool inside = false;
                    if (usePosition != Vector3.zero)
                    {
                        if (Vector3.Distance(usePosition, new Vector3(zone.x, zone.y, zone.z)) <= zone.radius) inside = true;
                    }
                    else
                    {
                        inside = IsInZone(player, zone.id);
                    }
                    if (!inside) continue;

                    if (zone.requireUserHavePermission && !permission.UserHasPermission(player.UserIDString, zone.permissionToUse))
                        continue;

                    var zr = GetZoneRate(zone.permissionToUse, type, name);
                    if (zone.overTakePriority) return zr;
                    if (zr > highestValue) highestValue = zr;
                }
            }

            foreach (var pair in config.GatherPerms.Where(x => permission.UserHasPermission(player.UserIDString, x.Key) || forced))
            {
                if (zoneData.Zones.Any(x => x.restrictPermission && x.permissionToUse == pair.Key))
                    continue;

                if (!pair.Value.ContainsKey(type))
                {
                    PrintWarning($"GetSingleMultiplier :: Unable to find type of {type}");
                    return 1.0f;
                }
                if (!pair.Value[type].ContainsKey(name))
                {
                    PrintWarning($"GetSingleMultiplier :: Missing resource {name} in type {type}. Adding 1.0 and saving.");
                    config.GatherPerms[pair.Key][type][name] = 1f;
                    SaveConfig(config);
                }
                else
                {
                    var val = pair.Value[type][name];
                    if (val > highestValue) highestValue = val;
                }
            }

            return highestValue < 1f ? 1f : highestValue;
        }
        #endregion

        #region Migrate Configuration
        private static readonly string[] GatherTypes = new[] { "Dispenser", "Bonus", "Quarry", "Survey", "Pickups", "Excavator", "Barrels" };

        private static readonly Dictionary<string, string> KeyMap = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
        {
            ["metal_ore"] = "metal.ore",
            ["sulfur_ore"] = "sulfur.ore",
            ["hq_metal_ore"] = "hq.metal.ore",
            ["metal_fragments"] = "metal.fragments",
            ["metal_refined"] = "metal.refined",
            ["bone_fragments"] = "bone.fragments",
            ["skull_wolf"] = "skull.wolf",
            ["animal_fat"] = "fat.animal",
            ["lowgrade_fuel"] = "lowgradefuel",
            ["crude_oil"] = "crude.oil",

            ["humanmeat_raw"] = "humanmeat.raw",
            ["meat.wolf.raw"] = "wolfmeat.raw",
            ["meat.horse.raw"] = "horsemeat.raw",
            ["meat.pork.raw"] = "meat.boar",
            ["meat.bear.raw"] = "bearmeat",

            ["big_cat_meat"] = "bigcatmeat",
            ["crocodile_meat"] = "crocodilemeat",
            ["snake_meat"] = "snakemeat",

            ["corn_seed"] = "seed.corn",
            ["hemp_seed"] = "seed.hemp",
            ["pumpkin_seed"] = "seed.pumpkin",
            ["potato_seed"] = "seed.potato",
            ["black_berry_seed"] = "seed.black.berry",
            ["blue_berry_seed"] = "seed.blue.berry",
            ["green_berry_seed"] = "seed.green.berry",
            ["red_berry_seed"] = "seed.red.berry",
            ["white_berry_seed"] = "seed.white.berry",
            ["yellow_berry_seed"] = "seed.yellow.berry",

            ["corn.seed"] = "seed.corn",
            ["pumpkin.seed"] = "seed.pumpkin",
            ["potato.seed"] = "seed.potato",
            ["electric.fuse"] = "fuse",
            ["blue.berry.seed"] = "seed.blue.berry",
            ["black.berry.seed"] = "seed.black.berry",
            ["green.berry.seed"] = "seed.green.berry",
            ["red.berry.seed"] = "seed.red.berry",
            ["white.berry.seed"] = "seed.white.berry",
            ["yellow.berry.seed"] = "seed.yellow.berry",
            ["wheat_seed"] = "seed.wheat",
            ["seed_wheat"] = "seed.wheat",
            ["wheat.seed"] = "seed.wheat",

            ["blackberry"] = "black.berry",
            ["blueberry"] = "blue.berry",
            ["greenberry"] = "green.berry",
            ["redberry"] = "red.berry",
            ["whiteberry"] = "white.berry",
            ["yellowberry"] = "yellow.berry",

            ["metal_pipe"] = "metalpipe",
            ["metal_spring"] = "metalspring",
            ["metal_blade"] = "metalblade",
            ["sewing_kit"] = "sewingkit",
            ["road_signs"] = "roadsigns",
            ["semi_body"] = "semibody",
            ["propane_tank"] = "propanetank",

            ["hq_metal_ore.item"] = "hq.metal.ore",
            ["sulfur_ore.item"] = "sulfur.ore",
            ["metal_ore.item"] = "metal.ore",
            ["bone_fragments.item"] = "bone.fragments",
        };

        private string NormalizeResourceKey(string key)
        {
            if (string.IsNullOrEmpty(key)) return key;
            if (KeyMap.TryGetValue(key, out var mapped)) return mapped;

            var k = key.Replace("_", ".").Replace(".item", "");
            return k;
        }

        private void TryMoveKey(Dictionary<string, float> dict, string oldKey, string newKey, ref int moved, ref int removed)
        {
            if (dict.TryGetValue(oldKey, out var val))
            {
                if (dict.ContainsKey(newKey))
                {
                    dict.Remove(oldKey);
                    removed++;
                }
                else
                {
                    dict[newKey] = val;
                    dict.Remove(oldKey);
                    moved++;
                }
            }
        }

        private void EnsureTypeExists(Dictionary<string, Dictionary<string, float>> perm, string typeName)
        {
            if (!perm.ContainsKey(typeName))
                perm[typeName] = new Dictionary<string, float>(StringComparer.OrdinalIgnoreCase);
        }

        private void EnsureExcavatorFromQuarry(Dictionary<string, Dictionary<string, float>> perm)
        {
            EnsureTypeExists(perm, "Excavator");
            if (perm["Excavator"].Count == 0)
            {
                if (perm.ContainsKey("Quarry"))
                {
                    foreach (var kv in perm["Quarry"])
                        if (!perm["Excavator"].ContainsKey(kv.Key))
                            perm["Excavator"][kv.Key] = kv.Value;
                }
            }
        }

        private void FillMissingWithDefaults(Dictionary<string, Dictionary<string, float>> perm, Dictionary<string, Dictionary<string, float>> defaults)
        {
            foreach (var type in GatherTypes)
            {
                EnsureTypeExists(perm, type);

                if (defaults.TryGetValue(type, out var defRes))
                {
                    foreach (var kv in defRes)
                    {
                        if (!perm[type].ContainsKey(kv.Key))
                            perm[type][kv.Key] = 1f;
                    }
                }
            }
        }

        private void NormalizeAllKeys(Dictionary<string, Dictionary<string, float>> perm, out int moved, out int removed, out int addedOneDefaults)
        {
            moved = 0; removed = 0; addedOneDefaults = 0;

            foreach (var type in perm.Keys.ToList())
            {
                var resDict = perm[type];
                var keys = resDict.Keys.ToList();

                foreach (var k in keys)
                {
                    var nk = NormalizeResourceKey(k);
                    if (!string.Equals(k, nk, StringComparison.OrdinalIgnoreCase))
                    {
                        TryMoveKey(resDict, k, nk, ref moved, ref removed);
                    }
                }
            }
        }

        private void MigrateConfig()
        {
            try
            {
                if (config == null || config.GatherPerms == null)
                {
                    PrintWarning("MigrateConfig: config/gather perms missing — skipping migration.");
                    return;
                }

                var defaults = CreateDefaultRates();

                int totalPerms = 0, totalMoved = 0, totalRemoved = 0, totalFilled = 0, totalAddedTypes = 0, totalAddedExcavator = 0;

                foreach (var permName in config.GatherPerms.Keys.ToList())
                {
                    totalPerms++;
                    var perm = config.GatherPerms[permName];

                    foreach (var t in GatherTypes)
                    {
                        if (!perm.ContainsKey(t))
                        {
                            perm[t] = new Dictionary<string, float>(StringComparer.OrdinalIgnoreCase);
                            totalAddedTypes++;
                        }
                    }

                    NormalizeAllKeys(perm, out var moved, out var removed, out var addedOneDefaults);
                    totalMoved += moved;
                    totalRemoved += removed;

                    int before = perm["Excavator"].Count;
                    EnsureExcavatorFromQuarry(perm);
                    if (before == 0 && perm["Excavator"].Count > 0) totalAddedExcavator++;

                    int beforeFill = perm.Sum(p => p.Value.Count);
                    FillMissingWithDefaults(perm, defaults);
                    int afterFill = perm.Sum(p => p.Value.Count);
                    totalFilled += Math.Max(0, afterFill - beforeFill);
                }

                Config.WriteObject(config, true);
                SaveConfig();

                PrintWarning($"UGather Migration: perms={totalPerms}, movedKeys={totalMoved}, removedDupes={totalRemoved}, addedDefaults(1x)={totalFilled}, addedTypes={totalAddedTypes}, seedExcavatorFromQuarry={totalAddedExcavator}");
            }
            catch (Exception e)
            {
                PrintError($"MigrateConfig failed: {e}");
            }
        }
        #endregion
    }
}