#nullable enable

using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using Barotrauma.Extensions;
using Barotrauma.Networking;

namespace Barotrauma
{
    internal sealed partial class MedicalClinic
    {
        private const int RateLimitMaxRequests = 20,
                          RateLimitExpiry = 5;

        public enum NetworkHeader
        {
            REQUEST_AFFLICTIONS,
            AFFLICTION_UPDATE,
            UNSUBSCRIBE_ME,
            REQUEST_PENDING,
            ADD_PENDING,
            REMOVE_PENDING,
            CLEAR_PENDING,
            HEAL_PENDING,
            ADD_EVERYTHING_TO_PENDING
        }

        public enum AfflictionSeverity
        {
            Low,
            Medium,
            High
        }

        public enum MessageFlag
        {
            Response, // responding to your request
            Announce // responding to someone else's request
        }

        public enum HealRequestResult
        {
            Unknown, // everything is not ok
            Success, // everything ok
            InsufficientFunds, // not enough money
            Refused // the outpost has refused to provide medical assistance
        }

        [NetworkSerialize]
        public readonly record struct NetHealRequest(HealRequestResult Result) : INetSerializableStruct;

        [NetworkSerialize]
        public readonly record struct NetRemovedAffliction(NetCrewMember CrewMember, NetAffliction Affliction) : INetSerializableStruct;

        public struct NetAffliction : INetSerializableStruct
        {
            [NetworkSerialize]
            public Identifier Identifier;

            [NetworkSerialize]
            public ushort Strength;

            [NetworkSerialize]
            public int VitalityDecrease;

            [NetworkSerialize]
            public ushort Price;

            public void SetAffliction(Affliction affliction, CharacterHealth characterHealth)
            {
                Identifier = affliction.Identifier;
                Strength = (ushort)Math.Ceiling(affliction.Strength);
                Price = (ushort)(affliction.Prefab.BaseHealCost + Strength * affliction.Prefab.HealCostMultiplier);
                VitalityDecrease = (int)affliction.GetVitalityDecrease(characterHealth);
            }

            private AfflictionPrefab? cachedPrefab;

            public AfflictionPrefab? Prefab
            {
                get
                {
                    if (cachedPrefab is { } cached) { return cached; }

                    foreach (AfflictionPrefab prefab in AfflictionPrefab.List)
                    {
                        if (prefab.Identifier == Identifier)
                        {
                            cachedPrefab = prefab;
                            return prefab;
                        }
                    }

                    return null;
                }
                set
                {
                    cachedPrefab = value;
                    Identifier = value?.Identifier ?? Identifier.Empty;
                    Strength = 0;
                    Price = 0;
                }
            }

            public readonly bool AfflictionEquals(AfflictionPrefab prefab)
            {
                return prefab.Identifier == Identifier;
            }

            public readonly bool AfflictionEquals(NetAffliction affliction)
            {
                return affliction.Identifier == Identifier;
            }
        }

        public record struct NetCrewMember : INetSerializableStruct
        {
            [NetworkSerialize]
            public int CharacterInfoID;

            [NetworkSerialize]
            public ImmutableArray<NetAffliction> Afflictions;

            public NetCrewMember(CharacterInfo info)
            {
                CharacterInfoID = info.ID;
                Afflictions = ImmutableArray<NetAffliction>.Empty;
            }

            public NetCrewMember(CharacterInfo info, ImmutableArray<NetAffliction> afflictions): this(info)
            {
                Afflictions = afflictions;
            }

            public readonly CharacterInfo? FindCharacterInfo(ImmutableArray<CharacterInfo> crew)
            {
                foreach (CharacterInfo info in crew)
                {
                    if (info.ID == CharacterInfoID)
                    {
                        return info;
                    }
                }

                return null;
            }

            public readonly bool CharacterEquals(NetCrewMember crewMember)
            {
                return crewMember.CharacterInfoID == CharacterInfoID;
            }
        }

        public readonly List<NetCrewMember> PendingHeals = new List<NetCrewMember>();

        public Action? OnUpdate;

        private readonly CampaignMode? campaign;

        /// <summary>
        /// Characters whose afflictions have changed (processed periodically, refreshing the UI client-side, sending updates to clients server-side
        /// </summary>
        private readonly HashSet<Character> charactersWithAfflictionChanges = new HashSet<Character>();

        private float processAfflictionChangesTimer;
        private const float ProcessAfflictionChangesInterval = 1.0f;

        public MedicalClinic(CampaignMode campaign)
        {
            this.campaign = campaign;
#if CLIENT
            campaign.OnMoneyChanged.RegisterOverwriteExisting(nameof(MedicalClinic).ToIdentifier(), OnMoneyChanged);
#endif
        }

        private static bool IsOutpostInCombat()
        {
            if (Level.Loaded is not { Type: LevelData.LevelType.Outpost }) { return false; }

            IEnumerable<Character> crew = GetCrewCharacters().Where(static c => c.Character != null).Select(static c => c.Character).ToImmutableHashSet();

            foreach (Character npc in Character.CharacterList.Where(static c => c.TeamID == CharacterTeamType.FriendlyNPC))
            {
                bool isInCombatWithCrew = !npc.IsInstigator && npc.AIController is HumanAIController { ObjectiveManager: { CurrentObjective: AIObjectiveCombat combatObjective } } && crew.Contains(combatObjective.Enemy);
                if (isInCombatWithCrew) { return true; }
            }

            return false;
        }

        private HealRequestResult HealAllPending(bool force = false, Client? client = null)
        {
            int totalCost = GetTotalCost();
            if (!force)
            {
                if (IsOutpostInCombat()) { return HealRequestResult.Refused; }
                if (!(campaign?.TryPurchase(client, totalCost) ?? false)) { return HealRequestResult.InsufficientFunds; }
            }

            ImmutableArray<CharacterInfo> crew = GetCrewCharacters();
            foreach (NetCrewMember crewMember in PendingHeals)
            {
                CharacterInfo? targetCharacter = crewMember.FindCharacterInfo(crew);
                if (!(targetCharacter?.Character is { CharacterHealth: { } health })) { continue; }

                foreach (NetAffliction affliction in crewMember.Afflictions)
                {
                    health.ReduceAfflictionOnAllLimbs(affliction.Identifier, affliction.Prefab?.MaxStrength ?? affliction.Strength);
                }
            }

            ClearPendingHeals();

            return HealRequestResult.Success;
        }

        private void ClearPendingHeals()
        {
            PendingHeals.Clear();
        }

        private void AddEverythingToPending()
        {
            foreach (CharacterInfo info in GetCrewCharacters())
            {
                if (info.Character?.CharacterHealth is not { } health) { continue; }

                var afflictions = GetAllAfflictions(health);

                if (afflictions.Length is 0) { continue; }

                InsertPendingCrewMember(new NetCrewMember(info, afflictions));
            }
        }

        private void RemovePendingAffliction(NetCrewMember crewMember, NetAffliction affliction)
        {
            foreach (NetCrewMember listMember in PendingHeals.ToList())
            {
                PendingHeals.Remove(listMember);
                NetCrewMember pendingMember = listMember;

                if (pendingMember.CharacterEquals(crewMember))
                {
                    List<NetAffliction> newAfflictions = new List<NetAffliction>();
                    foreach (NetAffliction pendingAffliction in pendingMember.Afflictions)
                    {
                        if (pendingAffliction.AfflictionEquals(affliction)) { continue; }

                        newAfflictions.Add(pendingAffliction);
                    }

                    pendingMember.Afflictions = newAfflictions.ToImmutableArray();
                }

                if (!pendingMember.Afflictions.Any()) { continue; }

                PendingHeals.Add(pendingMember);
            }
        }

        private void InsertPendingCrewMember(NetCrewMember crewMember)
        {
            if (PendingHeals.FirstOrNull(m => m.CharacterEquals(crewMember)) is { } foundHeal)
            {
                PendingHeals.Remove(foundHeal);
            }

            PendingHeals.Add(crewMember);
        }

        public static bool IsHealable(Affliction affliction)
        {
            return affliction.Prefab.HealableInMedicalClinic && affliction.Strength > GetShowTreshold(affliction);
            static float GetShowTreshold(Affliction affliction) => Math.Max(0, Math.Min(affliction.Prefab.ShowIconToOthersThreshold, affliction.Prefab.ShowInHealthScannerThreshold));
        }

        private ImmutableArray<NetAffliction> GetAllAfflictions(CharacterHealth health)
        {
            IEnumerable<Affliction> rawAfflictions = health.GetAllAfflictions().Where(IsHealable);

            List<NetAffliction> afflictions = new List<NetAffliction>();

            foreach (Affliction affliction in rawAfflictions)
            {
                NetAffliction newAffliction;
                if (afflictions.FirstOrNull(netAffliction => netAffliction.AfflictionEquals(affliction.Prefab)) is { } foundAffliction)
                {
                    afflictions.Remove(foundAffliction);
                    foundAffliction.Strength += (ushort)affliction.Strength;
                    foundAffliction.Price += (ushort)GetAdjustedPrice(GetHealPrice(affliction));
                    newAffliction = foundAffliction;
                }
                else
                {
                    newAffliction = new NetAffliction();
                    newAffliction.SetAffliction(affliction, health);
                    newAffliction.Price = (ushort)GetAdjustedPrice(newAffliction.Price);
                }

                afflictions.Add(newAffliction);
            }

            return afflictions.ToImmutableArray();

            static int GetHealPrice(Affliction affliction) => (int)(affliction.Prefab.BaseHealCost + (affliction.Prefab.HealCostMultiplier * affliction.Strength));
        }

        public static void OnAfflictionCountChanged(Character character) =>
            GameMain.GameSession?.Campaign?.MedicalClinic?.OnAfflictionCountChangedPrivate(character);

        private void OnAfflictionCountChangedPrivate(Character character)
        {
            if (character?.Info == null) { return; }
            charactersWithAfflictionChanges.Add(character);
        }

        public int GetTotalCost() => PendingHeals.SelectMany(static h => h.Afflictions).Aggregate(0, static (current, affliction) => current + affliction.Price);

        private int GetAdjustedPrice(int price) => campaign?.Map?.CurrentLocation is { Type: { HasOutpost: true } } currentLocation ? currentLocation.GetAdjustedHealCost(price) : int.MaxValue;

        public int GetBalance() => campaign?.GetBalance() ?? 0;

        public static ImmutableArray<CharacterInfo> GetCrewCharacters()
        {
#if DEBUG && CLIENT
            if (Screen.Selected is TestScreen)
            {
                return TestInfos.ToImmutableArray();
            }
#endif

            return Character.CharacterList.Where(static c => c.Info != null && c.TeamID == CharacterTeamType.Team1).Select(static c => c.Info).ToImmutableArray();
        }

#if DEBUG && CLIENT
        private static readonly CharacterInfo[] TestInfos =
        {
            new CharacterInfo(CharacterPrefab.HumanSpeciesName),
            new CharacterInfo(CharacterPrefab.HumanSpeciesName),
            new CharacterInfo(CharacterPrefab.HumanSpeciesName),
            new CharacterInfo(CharacterPrefab.HumanSpeciesName),
            new CharacterInfo(CharacterPrefab.HumanSpeciesName),
            new CharacterInfo(CharacterPrefab.HumanSpeciesName),
            new CharacterInfo(CharacterPrefab.HumanSpeciesName)
        };

        private static readonly NetAffliction[] TestAfflictions =
        {
            new NetAffliction { Identifier = "internaldamage".ToIdentifier(), Strength = 80, Price = 10 },
            new NetAffliction { Identifier = "blunttrauma".ToIdentifier(), Strength = 50, Price = 10 },
            new NetAffliction { Identifier = "lacerations".ToIdentifier(), Strength = 20, Price = 10 },
            new NetAffliction { Identifier = AfflictionPrefab.DamageType, Strength = 10, Price = 10 }
        };
#endif
    }
}