﻿using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Oxide.Core.Plugins;
using System;
using System.Collections.Generic;
using System.Linq;
using Oxide.Core;
using Rust.Ai;
using UnityEngine;
using VLB;
using static SamSite;
using HumanNpc = global::HumanNPC;

namespace Oxide.Plugins
{
    [Info("Targetable Drones", "WhiteThunder", "1.2.8")]
    [Description("Allows RC drones to be targeted by Auto Turrets and SAM Sites.")]
    internal class TargetableDrones : CovalencePlugin
    {
        #region Fields

        [PluginReference]
        private readonly Plugin Clans, Friends, DroneScaleManager, IQGuardianDrone;

        private const string PermissionUntargetable = "targetabledrones.untargetable";

        private const BaseEntity.Flags UntargetableFlag = BaseEntity.Flags.Reserved10;

        private Configuration _config;

        private readonly object True = true;

        private float? SqrScanRadius;

        #endregion

        #region Hooks

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

            Unsubscribe(nameof(OnEntitySpawned));

            if (!_config.EnableSamTargeting)
            {
                Unsubscribe(nameof(OnSamSiteTargetScan));
                Unsubscribe(nameof(OnSamSiteTarget));
            }

            if (!_config.EnableTurretTargeting)
            {
                Unsubscribe(nameof(OnEntityEnter));
                Unsubscribe(nameof(OnTurretTarget));
                Unsubscribe(nameof(OnDroneScaled));
            }
        }

        private void OnServerInitialized()
        {
            if (_config.OnServerInitialized() && !_config.UsingDefaults)
            {
                SaveConfig();
            }

            foreach (var entity in BaseNetworkable.serverEntities)
            {
                var drone = entity as Drone;
                if (drone == null)
                    continue;

                OnEntitySpawned(drone);
            }

            Subscribe(nameof(OnEntitySpawned));

            if (!_config.EnableTurretTargeting && !_config.NPCTargetingSettings.DamageMultiplierEnabled)
            {
                Unsubscribe(nameof(OnEntityTakeDamage));
            }
        }

        private void Unload()
        {
            foreach (var entity in BaseNetworkable.serverEntities)
            {
                var drone = entity as Drone;
                if (drone == null || !IsDroneEligible(drone))
                    continue;

                if (_config.EnableTurretTargeting)
                {
                    TurretTargetComponent.RemoveFromDrone(this, drone);
                }

                if (_config.EnableSamTargeting)
                {
                    SAMTargetComponent.RemoveFromDrone(drone);
                }

                if (_config.NPCTargetingSettings.Enabled)
                {
                    NPCTargetComponent.RemoveFromDrone(drone);
                }
            }

            // Just in case since this is static.
            SAMTargetComponent.DroneComponents.Clear();
        }

        private void OnEntitySpawned(Drone drone)
        {
            if (!IsDroneEligible(drone))
                return;

            if (_config.EnableTurretTargeting)
            {
                TurretTargetComponent.AddToDroneIfMissing(this, drone);
            }

            if (_config.EnableSamTargeting)
            {
                SAMTargetComponent.AddToDroneIfMissing(this, drone);
            }

            if (_config.NPCTargetingSettings.Enabled)
            {
                NPCTargetComponent.AddToDrone(this, drone);
            }
        }

        // Avoid unwanted trigger interactions.
        private object OnEntityEnter(TriggerBase trigger, Drone drone)
        {
            if (trigger is PlayerDetectionTrigger)
            {
                // Only allow interaction with Laser Detectors.
                // This avoids NREs with HBHF sensors or anything unknown.
                if (trigger.GetComponentInParent<BaseEntity>() is LaserDetector)
                    return null;

                return True;
            }

            if (trigger is TargetTrigger)
            {
                // Only allow interaction with Auto Turrets.
                // This avoids NREs with flame turrets, shotgun traps, tesla coils, or anything unknown.
                if (trigger.GetComponentInParent<BaseEntity>() is AutoTurret)
                    return null;

                return True;
            }

            return null;
        }

        private static ulong GetDroneControllerOrOwnerId(Drone drone)
        {
            var controllerSteamId = drone.ControllingViewerId?.SteamId ?? 0;
            if (controllerSteamId != 0)
                return controllerSteamId;

            var droneTurret = GetDroneTurret(drone);
            if ((object)droneTurret != null)
            {
                controllerSteamId = droneTurret.ControllingViewerId?.SteamId ?? 0;
                if (controllerSteamId != 0)
                    return controllerSteamId;

                var turretOwnerId = droneTurret.OwnerID;
                if (turretOwnerId != 0)
                    return turretOwnerId;
            }

            return drone.OwnerID;
        }

        private object OnTurretTarget(AutoTurret turret, Drone drone)
        {
            if (drone == null || drone.IsDestroyed)
                return null;

            if (!ShouldTurretTargetDrone(turret, drone))
                return True;

            return null;
        }

        private void OnSamSiteTargetScan(SamSite samSite, List<ISamSiteTarget> targetList)
        {
            if (SAMTargetComponent.DroneComponents.Count == 0)
                return;

            var samSitePosition = samSite.transform.position;
            SqrScanRadius ??= Mathf.Pow(targetTypeVehicle.scanRadius, 2);

            foreach (var droneComponent in SAMTargetComponent.DroneComponents)
            {
                // Distance checking is way more efficient than collider checking, even with hundreds of drones.
                if ((samSitePosition - droneComponent.Position).sqrMagnitude <= SqrScanRadius.Value)
                {
                    targetList.Add(droneComponent);
                }
            }
        }

        private object OnSamSiteTarget(SamSite samSite, SAMTargetComponent droneComponent)
        {
            var drone = droneComponent.Drone;
            if (drone == null || drone.IsDestroyed)
                return null;

            if (!ShouldSamSiteTargetDrone(samSite, drone)
                || ExposedHooks.OnSamSiteTarget(samSite, drone) is false)
                return True;

            return null;
        }

        private void OnEntityTakeDamage(Drone drone, HitInfo hitInfo)
        {
            if (hitInfo == null)
                return;

            // Multiply damage taken by NPCs.
            var humanNpc = hitInfo.Initiator as HumanNpc;
            if ((object)humanNpc != null)
            {
                if (!_config.NPCTargetingSettings.DamageMultiplierEnabled
                    || !_config.NPCTargetingSettings.IsAllowed(humanNpc))
                    return;

                hitInfo.damageTypes.ScaleAll(_config.NPCTargetingSettings.DamageMultiplier);
                return;
            }

            // Make drone turrets retaliate against other turrets.
            var turret = hitInfo.Initiator as AutoTurret;
            if ((object)turret != null)
            {
                if (turret == null || turret.IsDestroyed)
                    return;

                // Ignore if this drone does not have a turret since it can't retaliate.
                var droneTurret = GetDroneTurret(drone);
                if (droneTurret == null)
                    return;

                // Ignore if the turret damaged its owner drone.
                if (droneTurret == turret)
                    return;

                // Ignore if the turret is not online or has an existing visible target.
                if (!droneTurret.IsOnline()
                    || droneTurret.HasTarget() && droneTurret.targetVisible)
                    return;

                var attackerDrone = GetParentDrone(turret);
                if (attackerDrone != null)
                {
                    // If the attacker turret is on a drone, target that drone.
                    droneTurret.SetTarget(attackerDrone);
                    return;
                }

                // Shoot back at the turret.
                droneTurret.SetTarget(turret);
                return;
            }
        }

        private void OnDroneScaled(Drone drone, BaseEntity rootEntity, float scale, float previousScale)
        {
            if (scale == 1 || rootEntity.IsDestroyed)
                return;

            TurretTargetComponent.AddToRootEntityIfMissing(drone, rootEntity);
        }

        #endregion

        #region Exposed Hooks

        private static class ExposedHooks
        {
            public static object OnSamSiteTarget(SamSite samSite, Drone drone)
            {
                return Interface.CallHook("OnSamSiteTarget", samSite, drone);
            }
        }

        #endregion

        #region Helper Methods

        public static void LogInfo(string message) => Interface.Oxide.LogInfo($"[Targetable Drones] {message}");
        public static void LogWarning(string message) => Interface.Oxide.LogWarning($"[Targetable Drones] {message}");
        public static void LogError(string message) => Interface.Oxide.LogError($"[Targetable Drones] {message}");

        private static bool SameTeam(ulong userId, ulong otherUserId)
        {
            return RelationshipManager.ServerInstance.FindPlayersTeam(userId)?.members.Contains(otherUserId) ?? false;
        }

        private static bool IsDroneEligible(Drone drone)
        {
            return drone is not DeliveryDrone && !drone.HasFlag(UntargetableFlag);
        }

        private static Drone GetParentDrone(BaseEntity entity)
        {
            var sphereEntity = entity.GetParentEntity() as SphereEntity;
            return sphereEntity != null ? sphereEntity.GetParentEntity() as Drone : null;
        }

        private static AutoTurret GetDroneTurret(Drone drone)
        {
            return drone.GetSlot(BaseEntity.Slot.UpperModifier) as AutoTurret;
        }

        private static void RemoveFromAutoTurretTriggers(BaseEntity entity)
        {
            if (entity.triggers == null || entity.triggers.Count == 0)
                return;

            foreach (var trigger in entity.triggers.ToArray())
            {
                if (trigger is not TargetTrigger)
                    continue;

                var autoTurret = trigger.gameObject.ToBaseEntity() as AutoTurret;
                if (autoTurret != null && autoTurret.targetTrigger == trigger)
                {
                    trigger.RemoveEntity(entity);
                }
            }
        }

        private bool IsTargetableIQGuardianDrone(Drone drone)
        {
            return IQGuardianDrone?.Call("IsValidDrone", drone) is true;
        }

        private bool IsDroneBeingControlled(Drone drone)
        {
            // Valid IQGuardian Drones are considered to be controlled.
            return drone.IsBeingControlled || IsTargetableIQGuardianDrone(drone);
        }

        private bool IsPlayerTargetExempt(ulong userId)
        {
            return userId != 0 && permission.UserHasPermission(userId.ToString(), PermissionUntargetable);
        }

        private bool IsTargetExempt(Drone drone)
        {
            return drone.isGrounded || drone.InSafeZone() || IsPlayerTargetExempt(GetDroneControllerOrOwnerId(drone));
        }

        private BaseEntity GetRootEntity(Drone drone)
        {
            return DroneScaleManager?.Call("API_GetRootEntity", drone) as BaseEntity;
        }

        private bool HasFriend(ulong userId, ulong otherUserId)
        {
            return Friends?.Call("HasFriend", userId, otherUserId) is true;
        }

        private bool SameClan(ulong userId, ulong otherUserId)
        {
            return Clans?.Call("IsClanMember", userId.ToString(), otherUserId.ToString()) is true;
        }

        private bool AreAllies(ulong userId, ulong otherUserId)
        {
            return Clans?.Call("IsAllyPlayer", userId.ToString(), otherUserId.ToString()) is true;
        }

        private bool ShouldTurretTargetDrone(AutoTurret turret, Drone drone)
        {
            // Drones are not inherently hostile.
            // TODO: Revisit this for player-deployed sentry turrets
            if (turret is NPCAutoTurret)
                return false;

            if (IsTargetExempt(drone))
                return false;

            // Don't allow a drone turret to target its parent drone.
            if (GetParentDrone(turret) == drone)
                return false;

            // Skip auth/team/friends/clan logic for drones that have no apparent controller/owner (likely NPC drones).
            var droneControllerOrOwnerId = GetDroneControllerOrOwnerId(drone);
            if (droneControllerOrOwnerId == 0)
                return true;

            // Direct authorization trumps anything else.
            if (turret.IsAuthed(droneControllerOrOwnerId))
                return false;

            // In case the owner lost authorization, don't share with team/friends/clan.
            if (turret.OwnerID == 0 || !turret.IsAuthed(turret.OwnerID))
                return true;

            if (turret.OwnerID == droneControllerOrOwnerId
                || _config.DefaultSharingSettings.Team && SameTeam(turret.OwnerID, droneControllerOrOwnerId)
                || _config.DefaultSharingSettings.Friends && HasFriend(turret.OwnerID, droneControllerOrOwnerId)
                || _config.DefaultSharingSettings.Clan && SameClan(turret.OwnerID, droneControllerOrOwnerId)
                || _config.DefaultSharingSettings.Allies && AreAllies(turret.OwnerID, droneControllerOrOwnerId))
                return false;

            return true;
        }

        private bool ShouldSamSiteTargetDrone(SamSite samSite, Drone drone)
        {
            if (samSite.staticRespawn || samSite.OwnerID == 0)
                return true;

            var droneOwnerId = GetDroneControllerOrOwnerId(drone);
            if (droneOwnerId == 0)
                return true;

            if (samSite.OwnerID == droneOwnerId
                || _config.DefaultSharingSettings.Team && SameTeam(samSite.OwnerID, droneOwnerId)
                || _config.DefaultSharingSettings.Friends && HasFriend(samSite.OwnerID, droneOwnerId)
                || _config.DefaultSharingSettings.Clan && SameClan(samSite.OwnerID, droneOwnerId)
                || _config.DefaultSharingSettings.Allies && AreAllies(samSite.OwnerID, droneOwnerId))
                return false;

            return true;
        }

        #endregion

        #region Custom Targeting

        private class NPCTargetTriggerComponent : TriggerBase
        {
            public static NPCTargetTriggerComponent AddToDrone(TargetableDrones plugin, Drone drone, GameObject host)
            {
                var component = host.AddComponent<NPCTargetTriggerComponent>();
                component._plugin = plugin;
                component._drone = drone;
                component.interestLayers = Rust.Layers.Mask.Player_Server;
                return component;
            }

            private const int LayerMask = Rust.Layers.Mask.Default
                | Rust.Layers.Mask.Vehicle_Detailed
                | Rust.Layers.Mask.World
                | Rust.Layers.Mask.Construction
                | Rust.Layers.Mask.Terrain
                | Rust.Layers.Mask.Vehicle_Large
                | Rust.Layers.Mask.Tree;

            private TargetableDrones _plugin;
            private Drone _drone;
            private Action _checkTriggerContents;

            private NPCTargetTriggerComponent()
            {
                _checkTriggerContents = CheckTriggerContents;
            }

            public void CheckTriggerContents()
            {
                if (!HasAnyEntityContents)
                {
                    CancelInvoke(_checkTriggerContents);
                    return;
                }

                foreach (var entity in entityContents)
                {
                    var humanNpc = entity as HumanNpc;
                    if (humanNpc == null)
                        continue;

                    var memory = GetMemory(entity, out var senses);
                    if (memory == null)
                        continue;

                    if (IsTargetableBy(humanNpc))
                    {
                        AddToMemory(memory, senses);
                    }
                    else
                    {
                        RemoveFromMemory(memory);
                    }
                }
            }

            public override GameObject InterestedInObject(GameObject obj)
            {
                obj = base.InterestedInObject(obj);
                if (obj == null)
                    return null;

                var humanNpc = obj.ToBaseEntity() as HumanNpc;
                if (humanNpc == null || GetMemory(humanNpc) == null)
                    return null;

                if (!_plugin._config.NPCTargetingSettings.IsAllowed(humanNpc))
                    return null;

                return humanNpc.gameObject;
            }

            public override void OnEntityEnter(BaseEntity entity)
            {
                base.OnEntityEnter(entity);

                if (!HasAnyEntityContents || !entityContents.Contains(entity))
                    return;

                if (!IsInvoking(_checkTriggerContents))
                {
                    InvokeRepeating(_checkTriggerContents, UnityEngine.Random.Range(0, 1), 1);
                }
            }

            public override void OnEntityLeave(BaseEntity entity)
            {
                base.OnEntityLeave(entity);
                var memory = GetMemory(entity);
                if (memory != null)
                {
                    RemoveFromMemory(memory);
                }
            }

            private SimpleAIMemory GetMemory(BaseEntity entity, out AIBrainSenses senses)
            {
                var brain = (entity as HumanNpc)?.Brain;
                senses = brain?.Senses;
                return senses?.Memory;
            }

            private SimpleAIMemory GetMemory(BaseEntity entity)
            {
                return GetMemory(entity, out _);
            }

            private bool IsTargetableBy(HumanNpc humanNpc)
            {
                if (!_plugin.IsDroneBeingControlled(_drone))
                    return false;

                var eyesPosition = humanNpc.isMounted
                    ? humanNpc.eyes.worldMountedPosition
                    : humanNpc.IsDucked()
                        ? humanNpc.eyes.worldCrouchedPosition
                        : !humanNpc.IsCrawling()
                            ? humanNpc.eyes.worldStandingPosition
                            : humanNpc.eyes.worldCrawlingPosition;

                var layerMask = LayerMask;

                if (humanNpc.AdditionalLosBlockingLayer != 0)
                {
                    layerMask |= 1 << humanNpc.AdditionalLosBlockingLayer;
                }

                return humanNpc.IsVisibleSpecificLayers(_drone.CenterPoint(), eyesPosition, layerMask);
            }

            private bool AddToMemory(SimpleAIMemory memory, AIBrainSenses senses)
            {
                if (_drone.ControllingViewerId.HasValue
                    && _plugin.IsPlayerTargetExempt(_drone.ControllingViewerId.Value.SteamId))
                    return false;

                if (!memory.LOS.Add(_drone))
                    return false;

                senses.LastThreatTimestamp = Time.realtimeSinceStartup;

                memory.All.Add(new SimpleAIMemory.SeenInfo
                {
                    Entity = _drone,
                    Position = _drone.transform.position,
                    Timestamp = Time.realtimeSinceStartup,
                });

                memory.Players.Add(_drone);
                memory.Targets.Add(_drone);
                memory.Threats.Add(_drone);
                return true;
            }

            private bool RemoveFromMemory(SimpleAIMemory memory)
            {
                if (!memory.LOS.Remove(_drone))
                    return false;

                for (var i = 0; i < memory.All.Count; i++)
                {
                    var seenInfo = memory.All[i];
                    if (seenInfo.Entity == _drone)
                    {
                        memory.All.RemoveAt(i);
                        break;
                    }
                }

                memory.Players.Remove(_drone);
                memory.Targets.Remove(_drone);
                memory.Threats.Remove(_drone);
                return true;
            }

            private void OnDestroy()
            {
                if (HasAnyEntityContents)
                {
                    foreach (var entity in entityContents)
                    {
                        var memory = GetMemory(entity);
                        if (memory == null)
                            continue;

                        RemoveFromMemory(memory);
                    }
                }
            }
        }

        private class NPCTargetComponent : FacepunchBehaviour
        {
            public static void AddToDrone(TargetableDrones plugin, Drone drone)
            {
                var component = drone.gameObject.AddComponent<NPCTargetComponent>();

                var child = drone.gameObject.CreateChild();
                child.layer = (int)Rust.Layer.Trigger;

                // HACK: Prevent the drone's sweep test from using incorporating the child collider.
                child.AddComponent<Rigidbody>().isKinematic = true;

                var collider = child.AddComponent<SphereCollider>();
                collider.isTrigger = true;
                collider.radius = plugin._config.NPCTargetingSettings.MaxRange;

                component._trigger = NPCTargetTriggerComponent.AddToDrone(plugin, drone, child);
            }

            public static void RemoveFromDrone(Drone drone)
            {
                DestroyImmediate(drone.gameObject.GetComponent<NPCTargetComponent>());
            }

            private NPCTargetTriggerComponent _trigger;

            private void OnDestroy()
            {
                if (_trigger != null)
                {
                    Destroy(_trigger.gameObject);
                }
            }
        }

        private class TurretTargetComponent : EntityComponent<BaseEntity>
        {
            public static void AddToRootEntityIfMissing(Drone drone, BaseEntity rootEntity)
            {
                rootEntity.GetOrAddComponent<TurretTargetComponent>().InitForDrone(drone);
            }

            public static void AddToDroneIfMissing(TargetableDrones plugin, Drone drone)
            {
                // Must be added to the drone itself since the root entity (SphereEntity) is not a BaseCombatEntity.
                drone.GetOrAddComponent<TurretTargetComponent>().InitForDrone(drone);

                // Add to the root entity to ensure consistency with side effect of landing on cargo ship.
                var rootEntity = plugin.GetRootEntity(drone);
                if (rootEntity != null)
                {
                    AddToRootEntityIfMissing(drone, rootEntity);
                }
            }

            private static void RemoveFromEntity(BaseEntity entity)
            {
                var turretComponent = entity.GetComponent<TurretTargetComponent>();
                if (turretComponent != null)
                {
                    DestroyImmediate(turretComponent);
                    RemoveFromAutoTurretTriggers(entity);
                }
            }

            public static void RemoveFromDrone(TargetableDrones plugin, Drone drone)
            {
                RemoveFromEntity(drone);

                var rootEntity = plugin.GetRootEntity(drone);
                if (rootEntity != null)
                {
                    RemoveFromEntity(rootEntity);
                }
            }

            private Drone _ownerDrone;
            private GameObject _child;

            private TurretTargetComponent InitForDrone(Drone drone)
            {
                _ownerDrone = drone;
                AddChildLayerForAutoTurrets();
                return this;
            }

            private void AddChildLayerForAutoTurrets()
            {
                _child = gameObject.CreateChild();
                _child.layer = (int)Rust.Layer.Player_Server;

                var triggerCollider = _child.gameObject.AddComponent<BoxCollider>();
                triggerCollider.size = _ownerDrone.bounds.extents;
                triggerCollider.isTrigger = true;
            }

            private void OnDestroy()
            {
                if (_child != null)
                {
                    Destroy(_child);
                }
            }
        }

        private class SAMTargetComponent : FacepunchBehaviour, ISamSiteTarget
        {
            public static HashSet<SAMTargetComponent> DroneComponents = new();

            public static void AddToDroneIfMissing(TargetableDrones plugin, Drone drone)
            {
                var component = drone.GetOrAddComponent<SAMTargetComponent>();
                component._plugin = plugin;
            }

            public static void RemoveFromDrone(Drone drone)
            {
                var samComponent = drone.GetComponent<SAMTargetComponent>();
                if (samComponent != null)
                {
                    DestroyImmediate(samComponent);
                }
            }

            public Drone Drone { get; private set; }
            private TargetableDrones _plugin;
            private Transform _transform;

            private void Awake()
            {
                Drone = GetComponent<Drone>();
                _transform = transform;
                DroneComponents.Add(this);
            }

            public Vector3 Position => _transform.position;

            public SamTargetType SAMTargetType => targetTypeVehicle;

            public bool isClient => false;

            public bool IsValidSAMTarget(bool isStaticSamSite)
            {
                if (_plugin.IsTargetExempt(Drone))
                    return false;

                return isStaticSamSite
                    ? _plugin._config.EnableStaticSAMTargeting
                    : _plugin._config.EnablePlayerSAMTargeting;
            }

            public Vector3 CenterPoint() => Drone.CenterPoint();

            public Vector3 GetWorldVelocity() => Drone.body.velocity;

            public bool IsVisible(Vector3 position, float distance) => Drone.IsVisible(position, distance);

            private void OnDestroy() => DroneComponents.Remove(this);
        }

        #endregion

        #region Configuration

        private class CaseInsensitiveDictionary<TValue> : Dictionary<string, TValue>
        {
            public CaseInsensitiveDictionary() : base(StringComparer.OrdinalIgnoreCase) {}

            public CaseInsensitiveDictionary(IEnumerable<KeyValuePair<string, TValue>> collection)
                : base(collection, StringComparer.OrdinalIgnoreCase) {}
        }

        [JsonObject(MemberSerialization.OptIn)]
        private class NPCTargetingSettings
        {
            [JsonProperty("MaxRange")]
            public float MaxRange = 45;

            [JsonProperty("DamageMultiplier")]
            public float DamageMultiplier = 4f;

            [JsonProperty("EnabledByNpcPrefab")]
            private CaseInsensitiveDictionary<bool> EnabledByNpcPrefabName = new();

            private Dictionary<uint, bool> EnabledByNpcPrefabId = new();

            public bool Enabled { get; private set; }

            public bool DamageMultiplierEnabled => Enabled && DamageMultiplier != 1;

            public bool IsAllowed(BaseEntity entity)
            {
                return EnabledByNpcPrefabId.TryGetValue(entity.prefabID, out var canTarget) && canTarget;
            }

            public bool OnServerInitialized()
            {
                var changed = AddMissingNpcPrefabs();

                foreach (var (prefabPath, value) in EnabledByNpcPrefabName)
                {
                    var humanNpc = GameManager.server.FindPrefab(prefabPath)?.GetComponent<HumanNpc>();
                    if (humanNpc == null)
                    {
                        LogWarning($"Invalid HumanNPC prefab in config: {prefabPath}");
                        continue;
                    }

                    EnabledByNpcPrefabId[humanNpc.prefabID] = value;
                    Enabled = true;
                }

                return changed;
            }

            private bool AddMissingNpcPrefabs()
            {
                var changed = false;

                foreach (var prefabPath in GameManifest.Current.entities)
                {
                    var humanNpc = GameManager.server.FindPrefab(prefabPath)?.GetComponent<HumanNpc>();
                    if (humanNpc == null)
                        continue;

                    if (humanNpc.GetComponent<ScientistBrain>()?.HostileTargetsOnly ?? false)
                        continue;

                    if (!EnabledByNpcPrefabName.ContainsKey(prefabPath))
                    {
                        EnabledByNpcPrefabName[prefabPath.ToLower()] = false;
                        changed = true;
                    }
                }

                if (changed)
                {
                    EnabledByNpcPrefabName = new CaseInsensitiveDictionary<bool>(EnabledByNpcPrefabName.OrderBy(entry => entry.Key));
                }

                return changed;
            }
        }

        [JsonObject(MemberSerialization.OptIn)]
        private class SharingSettings
        {
            [JsonProperty("Team")]
            public bool Team = false;

            [JsonProperty("Friends")]
            public bool Friends = false;

            [JsonProperty("Clan")]
            public bool Clan = false;

            [JsonProperty("Allies")]
            public bool Allies = false;
        }

        [JsonObject(MemberSerialization.OptIn)]
        private class Configuration : BaseConfiguration
        {
            [JsonProperty("EnableTurretTargeting")]
            public bool EnableTurretTargeting = true;

            [JsonProperty("EnablePlayerSAMTargeting")]
            public bool EnablePlayerSAMTargeting = true;

            [JsonProperty("EnableStaticSAMTargeting")]
            public bool EnableStaticSAMTargeting = true;

            [JsonProperty("EnableSAMTargeting")]
            public bool DeprecatedEnableSAMTargeting
            {
                set
                {
                    EnablePlayerSAMTargeting = value;
                    EnableStaticSAMTargeting = value;
                }
            }

            [JsonProperty("NPCTargeting")]
            public NPCTargetingSettings NPCTargetingSettings = new();

            [JsonProperty("DefaultSharingSettings")]
            public SharingSettings DefaultSharingSettings = new();

            [JsonIgnore]
            public bool EnableSamTargeting => EnablePlayerSAMTargeting || EnableStaticSAMTargeting;

            public bool OnServerInitialized()
            {
                return NPCTargetingSettings.OnServerInitialized();
            }
        }

        private Configuration GetDefaultConfig() => new();

        #endregion

        #region Configuration Helpers

        [JsonObject(MemberSerialization.OptIn)]
        private class BaseConfiguration
        {
            public bool UsingDefaults;

            public string ToJson() => JsonConvert.SerializeObject(this);

            public Dictionary<string, object> ToDictionary() => JsonHelper.Deserialize(ToJson()) as Dictionary<string, object>;
        }

        private static class JsonHelper
        {
            public static object Deserialize(string json) => ToObject(JToken.Parse(json));

            private static object ToObject(JToken token)
            {
                switch (token.Type)
                {
                    case JTokenType.Object:
                        return token.Children<JProperty>()
                                    .ToDictionary(prop => prop.Name,
                                                  prop => ToObject(prop.Value));

                    case JTokenType.Array:
                        return token.Select(ToObject).ToList();

                    default:
                        return ((JValue)token).Value;
                }
            }
        }

        private bool MaybeUpdateConfig(BaseConfiguration config)
        {
            var currentWithDefaults = config.ToDictionary();
            var currentRaw = Config.ToDictionary(x => x.Key, x => x.Value);
            return MaybeUpdateConfigDict(currentWithDefaults, currentRaw);
        }

        private bool MaybeUpdateConfigDict(Dictionary<string, object> currentWithDefaults, Dictionary<string, object> currentRaw)
        {
            var changed = false;

            foreach (var key in currentWithDefaults.Keys)
            {
                if (currentRaw.TryGetValue(key, out var currentRawValue))
                {
                    var currentDictValue = currentRawValue as Dictionary<string, object>;
                    if (currentWithDefaults[key] is Dictionary<string, object> defaultDictValue)
                    {
                        if (currentDictValue == null)
                        {
                            currentRaw[key] = currentWithDefaults[key];
                            changed = true;
                        }
                        else if (MaybeUpdateConfigDict(defaultDictValue, currentDictValue))
                            changed = true;
                    }
                }
                else
                {
                    currentRaw[key] = currentWithDefaults[key];
                    changed = true;
                }
            }

            return changed;
        }

        protected override void LoadDefaultConfig() => _config = GetDefaultConfig();

        protected override void LoadConfig()
        {
            base.LoadConfig();
            try
            {
                _config = Config.ReadObject<Configuration>();
                if (_config == null)
                {
                    throw new JsonException();
                }

                if (MaybeUpdateConfig(_config))
                {
                    LogWarning("Configuration appears to be outdated; updating and saving");
                    SaveConfig();
                }
            }
            catch (Exception e)
            {
                LogError(e.Message);
                LogWarning($"Configuration file {Name}.json is invalid; using defaults");
                LoadDefaultConfig();
                if (_config != null)
                {
                    _config.UsingDefaults = true;
                }
            }
        }

        protected override void SaveConfig()
        {
            Log($"Configuration changes saved to {Name}.json");
            Config.WriteObject(_config, true);
        }

        #endregion
    }
}
