using Barotrauma.Abilities;
using Barotrauma.Extensions;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Globalization;
using System.Linq;
using System.Xml.Linq;

namespace Barotrauma
{
    abstract partial class Mission
    {
        public readonly MissionPrefab Prefab;
        private bool completed;
        protected bool failed;

        protected Level level;

        protected int state;
        public virtual int State
        {
            get { return state; }
            set
            {
                if (state != value)
                {
                    int previousState = state;
                    state = value;
                    TryTriggerEvents(state);
#if SERVER
                    GameMain.Server?.UpdateMissionState(this);
#elif CLIENT
                    if (Prefab.ShowProgressBar)
                    {
                        CharacterHUD.ShowMissionProgressBar(this);
                    }
#endif
                    ShowMessage(State);
                    OnMissionStateChanged?.Invoke(this);
                    MissionStateChanged(previousState);
                }
            }
        }

        public int TimesAttempted { get; set; }

        protected static bool IsClient => GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient;

        protected readonly CheckDataAction completeCheckDataAction;

        public readonly ImmutableArray<LocalizedString> Headers;
        public readonly ImmutableArray<LocalizedString> Messages;

        /// <summary>
        /// The reward that was actually given from completing the mission, taking any talent bonuses into account 
        /// (some of which may not be possible to determine in advance)
        /// </summary>
        private int? finalReward;

        public virtual LocalizedString Name => Prefab.Name;

        private readonly LocalizedString successMessage;
        public virtual LocalizedString SuccessMessage
        {
            get { return successMessage; }
            //private set { successMessage = value; }
        }

        private readonly LocalizedString failureMessage;
        public virtual LocalizedString FailureMessage
        {
            get { return failureMessage; }
            //private set { failureMessage = value; }
        }

        protected LocalizedString description;
        public virtual LocalizedString Description
        {
            get { return description; }
            //private set { description = value; }
        }

        protected LocalizedString descriptionWithoutReward;

        public virtual bool AllowUndocking
        {
            get { return true; }
        }

        public virtual int Reward
        {
            get 
            {
                return Prefab.Reward;
            }
        }

        public ImmutableList<MissionPrefab.ReputationReward> ReputationRewards
        {
            get { return Prefab.ReputationRewards; }
        }

        public bool Completed
        {
            get { return completed; }
            set { completed = value; }
        }

        public bool Failed
        {
            get { return failed || ForceFailure; }
        }

        public bool ForceFailure;

        public virtual bool AllowRespawning
        {
            get { return true; }
        }

        public virtual int TeamCount
        {
            get { return 1; }
        }

        public virtual SubmarineInfo EnemySubmarineInfo
        {
            get { return null; }
        }

        public virtual IEnumerable<(LocalizedString Label, Vector2 Position)> SonarLabels
        {
            get { return Enumerable.Empty<(LocalizedString Label, Vector2 Position)>(); }
        }

        public Identifier SonarIconIdentifier => Prefab.SonarIconIdentifier;

        /// <summary>
        /// Where was this mission received from? Affects which faction we give reputation for if the mission is configured to give reputation for the faction that gave the mission.
        /// Defaults to Locations[0]
        /// </summary>
        public Location OriginLocation;

        public readonly Location[] Locations;

        public int? Difficulty
        {
            get { return Prefab.Difficulty; }
        }

        private class DelayedTriggerEvent
        {
            public readonly MissionPrefab.TriggerEvent TriggerEvent;
            public float Delay;

            public DelayedTriggerEvent(MissionPrefab.TriggerEvent triggerEvent, float delay)
            {
                TriggerEvent = triggerEvent;
                Delay = delay;
            }
        }

        private readonly List<DelayedTriggerEvent> delayedTriggerEvents = new List<DelayedTriggerEvent>();

        public Action<Mission> OnMissionStateChanged;
        
        protected readonly ContentXElement characterConfig;
        protected readonly List<Character> characters = new List<Character>();
        protected readonly Dictionary<Character, List<Item>> characterItems = new Dictionary<Character, List<Item>>();

        public Mission(MissionPrefab prefab, Location[] locations, Submarine sub)
        {
            System.Diagnostics.Debug.Assert(locations.Length == 2);

            Prefab = prefab;

            description = prefab.Description.Value;
            successMessage = prefab.SuccessMessage.Value;
            failureMessage = prefab.FailureMessage.Value;
            Headers = prefab.Headers;
            var messages = prefab.Messages.ToArray();

            OriginLocation = locations[0];
            Locations = locations;

            var endConditionElement = prefab.ConfigElement.GetChildElement(nameof(completeCheckDataAction));
            if (endConditionElement != null)
            {
                completeCheckDataAction = new CheckDataAction(endConditionElement, $"Mission ({prefab.Identifier})");
            }

            descriptionWithoutReward = ReplaceVariablesInMissionMessage(description, sub, replaceReward: false);
            description = ReplaceVariablesInMissionMessage(description, sub);
            successMessage = ReplaceVariablesInMissionMessage(successMessage, sub);
            failureMessage = ReplaceVariablesInMissionMessage(failureMessage, sub);
            for (int m = 0; m < messages.Length; m++)
            {
                messages[m] = ReplaceVariablesInMissionMessage(messages[m], sub);
            }
            Messages = messages.ToImmutableArray();
            
            characterConfig = prefab.ConfigElement.GetChildElement("Characters");
            if (prefab.ConfigElement.GetChildElements("Characters").Count() > 1)
            {
                DebugConsole.AddWarning($"Error in mission {Prefab.Identifier}: multiple <Characters> elements found. Only the first one will be used.",
                    contentPackage: prefab.ContentPackage);
            }
        }

        public LocalizedString ReplaceVariablesInMissionMessage(LocalizedString message, Submarine sub, bool replaceReward = true)
        {
            for (int locationIndex = 0; locationIndex < 2; locationIndex++)
            {
                string locationName = $"‖color:gui.orange‖{Locations[locationIndex].DisplayName}‖end‖";
                message = message.Replace("[location" + (locationIndex + 1) + "]", locationName);
            }
            if (replaceReward)
            {
                string rewardText = $"‖color:gui.orange‖{string.Format(CultureInfo.InvariantCulture, "{0:N0}", GetReward(sub))}‖end‖";
                message = message.Replace("[reward]", rewardText);
            }
            return message;
        }
        
        protected virtual void MissionStateChanged(int previousState) {}

        public virtual void SetLevel(LevelData level) { }

        public static Mission LoadRandom(Location[] locations, string seed, bool requireCorrectLocationType, IEnumerable<Identifier> missionTypes, bool isSinglePlayer = false, float? difficultyLevel = null)
        {
            return LoadRandom(locations, new MTRandom(ToolBox.StringToInt(seed)), requireCorrectLocationType, missionTypes, isSinglePlayer, difficultyLevel);
        }

        public static Mission LoadRandom(Location[] locations, MTRandom rand, bool requireCorrectLocationType, IEnumerable<Identifier> missionTypes, bool isSinglePlayer = false, float? difficultyLevel = null)
        {
            List<MissionPrefab> allowedMissions = new List<MissionPrefab>();
            if (missionTypes.None())
            {
                return null;
            }
            else
            {
                allowedMissions.AddRange(MissionPrefab.Prefabs.Where(m => missionTypes.Contains(m.Type)));
            }
            allowedMissions.RemoveAll(m => isSinglePlayer ? m.MultiplayerOnly : m.SingleplayerOnly);
            if (requireCorrectLocationType)
            {
                allowedMissions.RemoveAll(m => !m.IsAllowed(locations[0], locations[1]));
            }
            if (difficultyLevel.HasValue)
            {
                allowedMissions.RemoveAll(m => !m.IsAllowedDifficulty(difficultyLevel.Value));
            }
            if (allowedMissions.Count == 0) { return null; }
            MissionPrefab missionPrefab = ToolBox.SelectWeightedRandom(allowedMissions, m => m.Commonness, rand);
            return missionPrefab.Instantiate(locations, Submarine.MainSub);
        }

        /// <summary>
        /// Calculates the base reward, can be overridden for different mission types
        /// </summary>
        public virtual float GetBaseReward(Submarine sub)
        {
            return Prefab.Reward;
        }

        /// <summary>
        /// Calculates the available monetary reward, taking into account universal modifiers such as campaign settings.
        /// </summary>
        public int GetReward(Submarine sub)
        {
            float reward = GetBaseReward(sub);
            // Some modifiers should apply universally to all implementations of GetBaseReward
            if (GameMain.GameSession?.Campaign is CampaignMode campaign)
            {
                reward *= campaign.Settings.MissionRewardMultiplier;
            }
            return (int)Math.Round(reward);
        }
        
        /// <summary>
        /// Call to load character elements to be spawned. Has to be implemented (and synced) separately per each mission.
        /// </summary>
        protected void InitCharacters(Submarine submarine)
        {
            characters.Clear();
            characterItems.Clear();

            if (characterConfig != null) 
            { 
                foreach (XElement element in characterConfig.Elements())
                {
                    if (GameMain.NetworkMember == null && element.GetAttributeBool("multiplayeronly", false)) { continue; }

                    int defaultCount = element.GetAttributeInt("count", -1);
                    if (defaultCount < 0)
                    {
                        defaultCount = element.GetAttributeInt("amount", 1);
                    }
                    int min = Math.Min(element.GetAttributeInt("min", defaultCount), 255);
                    int max = Math.Min(Math.Max(min, element.GetAttributeInt("max", defaultCount)), 255);
                    int count = Rand.Range(min, max + 1);

                    if (element.Attribute("identifier") != null && element.Attribute("from") != null)
                    {
                        HumanPrefab humanPrefab = GetHumanPrefabFromElement(element); 
                        if (humanPrefab == null)
                        {
                            DebugConsole.ThrowError($"Couldn't spawn a human character for a mission: human prefab \"{element.GetAttributeString("identifier", string.Empty)}\" not found",
                                contentPackage: Prefab.ContentPackage);
                            continue;
                        }
                        for (int i = 0; i < count; i++)
                        {
                            LoadHuman(humanPrefab, element, submarine);
                        }
                    }
                    else
                    {
                        Identifier speciesName = element.GetAttributeIdentifier("character", element.GetAttributeIdentifier("identifier", Identifier.Empty));
                        var characterPrefab = CharacterPrefab.FindBySpeciesName(speciesName);
                        if (characterPrefab == null)
                        {
                            DebugConsole.ThrowError($"Couldn't spawn a character for a mission: character prefab \"{speciesName}\" not found",
                                contentPackage: Prefab.ContentPackage);
                            continue;
                        }
                        for (int i = 0; i < count; i++)
                        {
                            LoadMonster(characterPrefab, element, submarine);
                        }
                    }
                }
            }            
        }
        
        private SpawnAction.SpawnLocationType GetSpawnLocationTypeFromSubmarineType(Submarine sub)
        {
            return sub.Info.Type switch
            {
                SubmarineType.Outpost or SubmarineType.OutpostModule => SpawnAction.SpawnLocationType.Outpost,
                SubmarineType.Wreck => SpawnAction.SpawnLocationType.Wreck,
                SubmarineType.Ruin => SpawnAction.SpawnLocationType.Ruin,
                SubmarineType.BeaconStation => SpawnAction.SpawnLocationType.BeaconStation,
                SubmarineType.Player => SpawnAction.SpawnLocationType.MainSub,
                _ => SpawnAction.SpawnLocationType.Any
            };
        }

        protected virtual Character LoadHuman(HumanPrefab humanPrefab, XElement element, Submarine submarine)
        {
            Identifier[] moduleFlags = element.GetAttributeIdentifierArray("moduleflags", null);
            Identifier[] spawnPointTags = element.GetAttributeIdentifierArray("spawnpointtags", null);
            var spawnPointType = element.GetAttributeEnum("spawnpointtype", SpawnType.Human);
            ISpatialEntity spawnPos = SpawnAction.GetSpawnPos(
                GetSpawnLocationTypeFromSubmarineType(submarine), spawnPointType,
                moduleFlags ?? humanPrefab.GetModuleFlags(),
                spawnPointTags ?? humanPrefab.GetSpawnPointTags(),
                element.GetAttributeBool("asfaraspossible", false));
            spawnPos ??= submarine.GetHulls(alsoFromConnectedSubs: false).GetRandomUnsynced();
            var teamId = element.GetAttributeEnum("teamid", CharacterTeamType.None);
            var originalTeam = Level.Loaded.StartOutpost?.TeamID ?? teamId;
            Character spawnedCharacter = CreateHuman(humanPrefab, characters, characterItems, submarine, originalTeam, spawnPos);
            //consider the NPC to be "originally" from the team of the outpost it spawns in, and change it to the desired (hostile) team afterwards
            //that allows the NPC to fight intruders and otherwise function in the outpost if the mission is configured to spawn the hostile NPCs in a friendly outpost
            if (teamId != originalTeam)
            {
                spawnedCharacter.SetOriginalTeamAndChangeTeam(teamId, processImmediately: true);
            }
            if (element.GetAttribute("color") != null)
            {
                spawnedCharacter.UniqueNameColor = element.GetAttributeColor("color", Color.Red);
            }
            if (submarine.Info is { IsOutpost: true } outPostInfo)
            {
                outPostInfo.AddOutpostNPCIdentifierOrTag(spawnedCharacter, humanPrefab.Identifier);
                foreach (Identifier tag in humanPrefab.GetTags())
                {
                    outPostInfo.AddOutpostNPCIdentifierOrTag(spawnedCharacter, tag);
                }
            }
            if (spawnPos is WayPoint wp)
            {
                spawnedCharacter.GiveIdCardTags(wp);
            }
            InitCharacter(spawnedCharacter, element);
            return spawnedCharacter;
        }

        protected virtual Character LoadMonster(CharacterPrefab monsterPrefab, XElement element, Submarine submarine)
        {
            Identifier[] moduleFlags = element.GetAttributeIdentifierArray("moduleflags", null);
            Identifier[] spawnPointTags = element.GetAttributeIdentifierArray("spawnpointtags", null);
            ISpatialEntity spawnPos = SpawnAction.GetSpawnPos(SpawnAction.SpawnLocationType.Outpost, SpawnType.Enemy, moduleFlags, spawnPointTags, element.GetAttributeBool("asfaraspossible", false));
            spawnPos ??= submarine.GetHulls(alsoFromConnectedSubs: false).GetRandomUnsynced();
            Character spawnedCharacter = Character.Create(monsterPrefab.Identifier, spawnPos.WorldPosition, ToolBox.RandomSeed(8), createNetworkEvent: false);
            characters.Add(spawnedCharacter);
            if (spawnedCharacter.Inventory != null)
            {
                characterItems.Add(spawnedCharacter, spawnedCharacter.Inventory.FindAllItems(recursive: true));
            }
            if (submarine != null && spawnedCharacter.AIController is EnemyAIController enemyAi)
            {
                enemyAi.UnattackableSubmarines.Add(submarine);
                enemyAi.UnattackableSubmarines.Add(Submarine.MainSub);
                foreach (Submarine sub in Submarine.MainSub.DockedTo)
                {
                    enemyAi.UnattackableSubmarines.Add(sub);
                }
            }
            InitCharacter(spawnedCharacter, element);
            return spawnedCharacter;
        }
        
        protected virtual void InitCharacter(Character character, XElement element)
        {
            if (element.GetAttributeBool(Tags.IgnoredByAI.Value, false))
            {
                character.AddAbilityFlag(AbilityFlags.IgnoredByEnemyAI);
            }
            float playDeadProbability = element.GetAttributeFloat("playdeadprobability", -1);
            if (playDeadProbability >= 0)
            {
                character.EvaluatePlayDeadProbability(playDeadProbability);
            }
            float huskProbability = element.GetAttributeFloat("huskprobability", 0);
            if (huskProbability > 0 && Rand.Value() <= huskProbability)
            {
                character.TurnIntoHusk();
            }
            else if (element.GetAttributeBool("corpse", false))
            {
                character.Kill(CauseOfDeathType.Unknown, causeOfDeathAffliction: null, log: false);
            }
        }

        public void Start(Level level)
        {
            state = 0;
#if CLIENT
            shownMessages.Clear();
#endif
            delayedTriggerEvents.Clear();
            foreach (string categoryToShow in Prefab.UnhideEntitySubCategories)
            {
                foreach (MapEntity entityToShow in MapEntity.MapEntityList.Where(me => me.Prefab?.HasSubCategory(categoryToShow) ?? false))
                {
                    entityToShow.IsLayerHidden = false;
                }
            }
            this.level = level;
            TryTriggerEvents(0);
            StartMissionSpecific(level);
        }

        protected virtual void StartMissionSpecific(Level level) { }

        public void Update(float deltaTime)
        {
            for (int i = delayedTriggerEvents.Count - 1; i>=0;i--)
            {
                delayedTriggerEvents[i].Delay -= deltaTime;
                if (delayedTriggerEvents[i].Delay <= 0.0f)
                {
                    TriggerEvent(delayedTriggerEvents[i].TriggerEvent);
                    delayedTriggerEvents.RemoveAt(i);
                }
            }
            UpdateMissionSpecific(deltaTime);
        }

        protected virtual void UpdateMissionSpecific(float deltaTime) { }

        protected void ShowMessage(int missionState)
        {
            ShowMessageProjSpecific(missionState);
        }

        partial void ShowMessageProjSpecific(int missionState);

        protected virtual LocalizedString ModifyMessage(LocalizedString message, bool color = true)
        {
            return message;
        }

        private void TryTriggerEvents(int state)
        {
            foreach (var triggerEvent in Prefab.TriggerEvents)
            {
                if (triggerEvent.State == state)
                {
                    TryTriggerEvent(triggerEvent);
                }
            }
        }

        /// <summary>
        /// Triggers the event or adds it to the delayedTriggerEvents it if it has a delay
        /// </summary>
        private void TryTriggerEvent(MissionPrefab.TriggerEvent trigger)
        {
            if (trigger.CampaignOnly && GameMain.GameSession?.Campaign == null) { return; }
            if (trigger.Delay > 0 || trigger.State == 0)
            {
                if (!delayedTriggerEvents.Any(t => t.TriggerEvent == trigger))
                {
                    delayedTriggerEvents.Add(new DelayedTriggerEvent(trigger, trigger.Delay));
                }
            }
            else
            {
                TriggerEvent(trigger);
            }
        }

        /// <summary>
        /// Triggers the event immediately, ignoring any delays
        /// </summary>
        private void TriggerEvent(MissionPrefab.TriggerEvent trigger)
        {
            if (trigger.CampaignOnly && GameMain.GameSession?.Campaign == null) { return; }
            //clients are not allowed to trigger events, they're handled by the server
            if (GameMain.NetworkMember is { IsClient: true }) { return; }
            EventPrefab eventPrefab = EventPrefab.FindEventPrefab(trigger.EventIdentifier, trigger.EventTag, Prefab.ContentPackage);
            if (eventPrefab == null)
            {
                DebugConsole.ThrowError($"Mission {Prefab.Identifier} failed to trigger an event (identifier: {trigger.EventIdentifier}, tag: {trigger.EventTag}).", contentPackage: Prefab.ContentPackage);
                return;
            }
            if (GameMain.GameSession?.EventManager != null)
            {
                var newEvent = eventPrefab.CreateInstance(GameMain.GameSession.EventManager.RandomSeed);
                GameMain.GameSession.EventManager.ActivateEvent(newEvent);
            }
        }

        /// <summary>
        /// End the mission and give a reward if it was completed successfully
        /// </summary>
        public void End()
        {
            if (GameMain.NetworkMember is not { IsClient: true })
            {
                completed =      
                    !ForceFailure &&
                    DetermineCompleted() && 
                    (completeCheckDataAction == null || completeCheckDataAction.GetSuccess());
            }
            if (completed)
            {
                if (Prefab.LocationTypeChangeOnCompleted != null)
                {
                    ChangeLocationType(Prefab.LocationTypeChangeOnCompleted);
                }
                try
                {
                    GiveReward();
                }
                catch (Exception e)
                {
                    string errorMsg = "Unknown error while giving mission rewards.";
                    DebugConsole.ThrowError(errorMsg, e, contentPackage: Prefab.ContentPackage);
                    GameAnalyticsManager.AddErrorEventOnce("Mission.End:GiveReward", GameAnalyticsManager.ErrorSeverity.Error, errorMsg + "\n" + e.StackTrace);
#if SERVER
                    GameMain.Server?.SendChatMessage(errorMsg + "\n" + e.StackTrace, Networking.ChatMessageType.Error);
#endif
                }
            }

            TimesAttempted++;

            EndMissionSpecific(completed);
            if (ForceFailure)
            {
                failed = true;
            }
        }

        protected abstract bool DetermineCompleted();

        protected virtual void EndMissionSpecific(bool completed) { }

        /// <summary>
        /// Get the final reward, taking talent bonuses into account if the mission has concluded and the talents modified the reward accordingly.
        /// </summary>
        public int GetFinalReward(Submarine sub)
        {
            return finalReward ?? GetReward(sub);
        }

        /// <summary>
        /// Calculates the final reward after talent bonuses have been applied. Note that this triggers talent effects of the type OnGainMissionMoney, 
        /// and should only be called once when the mission is completed!
        /// </summary>
        private void CalculateFinalReward(Submarine sub)
        {
            int reward = GetReward(sub);
            IEnumerable<Character> crewCharacters = GameSession.GetSessionCrewCharacters(CharacterType.Both);
            var missionMoneyGainMultiplier = new AbilityMissionMoneyGainMultiplier(this, 1f);
            CharacterTalent.CheckTalentsForCrew(crewCharacters, AbilityEffectType.OnGainMissionMoney, missionMoneyGainMultiplier);
            crewCharacters.ForEach(c => missionMoneyGainMultiplier.Value += c.GetStatValue(StatTypes.MissionMoneyGainMultiplier));
            finalReward = (int)(reward * missionMoneyGainMultiplier.Value);
        }

        private float CalculateDifficultyXPMultiplier()
        {
            const float minMissionDifficulty = 1;
            const float maxMissionDifficulty = 4;
            const float maxXpBonus = 1.3f;
            float selectedMissionDifficulty = MathUtils.InverseLerp(minMissionDifficulty, maxMissionDifficulty, Prefab.Difficulty.GetValueOrDefault());
            float xpBonusMultiplier = MathHelper.Lerp(1.0f, maxXpBonus, selectedMissionDifficulty);

            return xpBonusMultiplier;
        }

        private void GiveReward()
        {
            if (GameMain.GameSession.GameMode is not CampaignMode campaign) { return; }

            float xpReward = GetBaseReward(Submarine.MainSub) * Prefab.ExperienceMultiplier * campaign.Settings.ExperienceRewardMultiplier;
            float xpGain = xpReward * level.LevelData.Biome.ExperienceFromMissionRewards * CalculateDifficultyXPMultiplier();

            IEnumerable<Character> crewCharacters = GameSession.GetSessionCrewCharacters(CharacterType.Both);

            // use multipliers here so that we can easily add them together without introducing multiplicative XP stacking
            var experienceGainMultiplier = new AbilityMissionExperienceGainMultiplier(this, 1f, character: null);
            crewCharacters.ForEach(c => experienceGainMultiplier.Value += c.GetStatValue(StatTypes.MissionExperienceGainMultiplier));

            DistributeExperienceToCrew(crewCharacters, (int)(xpGain * experienceGainMultiplier.Value));

            CalculateFinalReward(Submarine.MainSub);
#if SERVER
            finalReward = DistributeRewardsToCrew(GameSession.GetSessionCrewCharacters(CharacterType.Player), finalReward.Value);
#endif
            bool isSingleplayerOrServer = GameMain.IsSingleplayer || GameMain.NetworkMember is { IsServer: true };
            if (isSingleplayerOrServer)
            {
                if (finalReward > 0)
                {
                    campaign.Bank.Give(finalReward.Value);
                }

                foreach (Character character in crewCharacters)
                {
                    character.Info.MissionsCompletedSinceDeath++;
                }

                foreach (var reputationReward in ReputationRewards)
                {
                    var reputationGainMultiplier = new AbilityMissionReputationGainMultiplier(this, 1f, character: null);
                    foreach (var c in crewCharacters) { c.CheckTalents(AbilityEffectType.OnCrewGainMissionReputation, reputationGainMultiplier); }
                    float amount = reputationReward.Amount * reputationGainMultiplier.Value;
                    
                    if (reputationReward.FactionIdentifier == "location")
                    {
                        OriginLocation.Reputation?.AddReputation(amount);
                        TryGiveReputationForOpposingFaction(OriginLocation.Faction, reputationReward.AmountForOpposingFaction);
                    }
                    else
                    {
                        Faction faction = campaign.Factions.Find(faction1 => faction1.Prefab.Identifier == reputationReward.FactionIdentifier);
                        if (faction != null)
                        {
                            faction.Reputation.AddReputation(amount);
                            TryGiveReputationForOpposingFaction(faction, reputationReward.AmountForOpposingFaction);
                        }
                    }
                }

                void TryGiveReputationForOpposingFaction(Faction thisFaction, float amount)
                {
                    if (MathUtils.NearlyEqual(amount, 0.0f)) { return; }
                    if (thisFaction?.Prefab != null && 
                        !thisFaction.Prefab.OpposingFaction.IsEmpty)
                    {
                        Faction faction = campaign.Factions.Find(faction1 => faction1.Prefab.Identifier == thisFaction.Prefab.OpposingFaction);
                        faction?.Reputation.AddReputation(amount);
                    }
                }
            }

            if (Prefab.DataRewards != null)
            {
                foreach (var (identifier, value, operation) in Prefab.DataRewards)
                {
                    SetDataAction.PerformOperation(campaign.CampaignMetadata, identifier, value, operation);
                }
            }
        }

        partial void DistributeExperienceToCrew(IEnumerable<Character> crew, int experienceGain);

        public static int GetRewardDistibutionSum(IEnumerable<Character> crew, int rewardDistribution = 0) => crew.Sum(c => c.Wallet.RewardDistribution) + rewardDistribution;

        public static (int Amount, int Percentage, float Sum) GetRewardShare(int rewardDistribution, IEnumerable<Character> crew, Option<int> reward)
        {
            float sum = GetRewardDistibutionSum(crew, rewardDistribution);
            if (MathUtils.NearlyEqual(sum, 0)) { return (0, 0, sum); }

            float rewardWeight = sum > 100 ? rewardDistribution / sum : rewardDistribution / 100f;
            int rewardPercentage = (int)(rewardWeight * 100);

            int amount = reward.TryUnwrap(out var a) ? a : 0;

            return ((int)(amount * rewardWeight), rewardPercentage, sum);
        }

        protected void ChangeLocationType(LocationTypeChange change)
        {
            if (change == null) { throw new ArgumentException(); }
            if (GameMain.GameSession.GameMode is CampaignMode campaign && !IsClient)
            {
                int srcIndex = -1;
                for (int i = 0; i < Locations.Length; i++)
                {
                    if (Locations[i].Type.Identifier == change.CurrentType)
                    {
                        srcIndex = i;
                        break;
                    }
                }
                if (srcIndex == -1) { return; }
                var location = Locations[srcIndex];

                if (location.LocationTypeChangesBlocked) { return; }

                if (change.RequiredDurationRange.X > 0)
                {
                    location.PendingLocationTypeChange = (change, Rand.Range(change.RequiredDurationRange.X, change.RequiredDurationRange.Y), Prefab);
                }
                else
                {
                    location.ChangeType(campaign, LocationType.Prefabs[change.ChangeToType]);
                    location.LocationTypeChangeCooldown = change.CooldownAfterChange;
                }
            }
        }

        public virtual void AdjustLevelData(LevelData levelData) { }

        // putting these here since both escort and pirate missions need them. could be tucked away into another class that they can inherit from (or use composition)
        protected HumanPrefab GetHumanPrefabFromElement(XElement element)
        {
            if (element.Attribute("name") != null)
            {
                DebugConsole.ThrowError($"Error in mission \"{Name}\" - use character identifiers instead of names to configure the characters.",
                    contentPackage: Prefab.ContentPackage);
                return null;
            }

            Identifier characterIdentifier = element.GetAttributeIdentifier("identifier", Identifier.Empty);
            Identifier characterFrom = element.GetAttributeIdentifier("from", Identifier.Empty);
            HumanPrefab humanPrefab = NPCSet.Get(characterFrom, characterIdentifier, contentPackageToLogInError: Prefab.ContentPackage);
            if (humanPrefab == null)
            {
                DebugConsole.ThrowError($"Couldn't spawn character for mission: character prefab \"{characterIdentifier}\" not found in the NPC set \"{characterFrom}\".",
                    contentPackage: Prefab.ContentPackage);
                return null;
            }

            return humanPrefab;
        }

        protected static Character CreateHuman(HumanPrefab humanPrefab, List<Character> characters, Dictionary<Character, List<Item>> characterItems, Submarine submarine, CharacterTeamType teamType, ISpatialEntity positionToStayIn = null, Rand.RandSync humanPrefabRandSync = Rand.RandSync.ServerAndClient)
        {
            var characterInfo = humanPrefab.CreateCharacterInfo(Rand.RandSync.ServerAndClient);
            characterInfo.TeamID = teamType;

            positionToStayIn ??= 
                    WayPoint.GetRandom(SpawnType.Human, characterInfo.Job?.Prefab, submarine) ??
                    WayPoint.GetRandom(SpawnType.Human, null, submarine);

            Character spawnedCharacter = Character.Create(characterInfo.SpeciesName, positionToStayIn.WorldPosition, ToolBox.RandomSeed(8), characterInfo, createNetworkEvent: false);
            spawnedCharacter.HumanPrefab = humanPrefab;
            humanPrefab.InitializeCharacter(spawnedCharacter, positionToStayIn);
            humanPrefab.GiveItems(spawnedCharacter, submarine, positionToStayIn as WayPoint, Rand.RandSync.ServerAndClient, createNetworkEvents: false);
            characters.Add(spawnedCharacter);
            characterItems.Add(spawnedCharacter, spawnedCharacter.Inventory.FindAllItems(recursive: true));

            return spawnedCharacter;
        }

        protected ItemPrefab FindItemPrefab(XElement element)
        {
            ItemPrefab itemPrefab;
            if (element.Attribute("name") != null)
            {
                DebugConsole.ThrowError($"Error in mission \"{Name}\" - use item identifiers instead of names to configure the items", 
                    contentPackage: Prefab.ContentPackage);
                string itemName = element.GetAttributeString("name", "");
                itemPrefab = MapEntityPrefab.Find(itemName) as ItemPrefab;
                if (itemPrefab == null)
                {
                    DebugConsole.ThrowError($"Couldn't spawn item for mission \"{Name}\": item prefab \"{itemName}\" not found", 
                        contentPackage: Prefab.ContentPackage);
                }
            }
            else
            {
                string itemIdentifier = element.GetAttributeString("identifier", "");
                itemPrefab = MapEntityPrefab.Find(null, itemIdentifier) as ItemPrefab;
                if (itemPrefab == null)
                {
                    DebugConsole.ThrowError($"Couldn't spawn item for mission \"{Name}\": item prefab \"{itemIdentifier}\" not found",
                        contentPackage: Prefab.ContentPackage);
                }
            }
            return itemPrefab;
        }

        protected Vector2? GetCargoSpawnPosition(ItemPrefab itemPrefab, out Submarine cargoRoomSub)
        {
            cargoRoomSub = null;

            WayPoint cargoSpawnPos = WayPoint.GetRandom(SpawnType.Cargo, null, Submarine.MainSub, useSyncedRand: true);
            if (cargoSpawnPos == null)
            {
                DebugConsole.ThrowError($"Couldn't spawn items for mission \"{Name}\": no waypoints marked as Cargo were found",
                    contentPackage: Prefab.ContentPackage);
                return null;
            }

            var cargoRoom = cargoSpawnPos.CurrentHull;
            if (cargoRoom == null)
            {
                DebugConsole.ThrowError($"Couldn't spawn items for mission \"{Name}\": waypoints marked as Cargo must be placed inside a room",
                    contentPackage: Prefab.ContentPackage);
                return null;
            }

            cargoRoomSub = cargoRoom.Submarine;

            return new Vector2(
                cargoSpawnPos.Position.X + Rand.Range(-20.0f, 20.0f, Rand.RandSync.ServerAndClient),
                cargoRoom.Rect.Y - cargoRoom.Rect.Height + itemPrefab.Size.Y / 2);
        }

        /// <summary>
        /// Gets a random submarine by tags, filtered by difficulty. Used by missions that force specific submarines (wrecks, beacons, etc.)
        /// </summary>
        /// <param name="tags">Mission tags to match against</param>
        /// <param name="seed">Random seed for selection</param>
        /// <param name="submarineSelector">Function to filter submarines by type (e.g., s => s.IsWreck)</param>
        /// <param name="submarineTypeName">Name of submarine type for error messages (e.g., "wreck", "beacon station")</param>
        /// <returns>Selected submarine, or null if none found</returns>
        protected static SubmarineInfo GetRandomSubmarineByTagsAndDifficulty(
            IEnumerable<Identifier> tags,
            LevelData levelData,
            Func<SubmarineInfo, bool> submarineSelector,
            string submarineTypeName)
        {
            var rand = new MTRandom(ToolBox.StringToInt(levelData.Seed));
            float levelDifficulty = levelData.Difficulty;

            var submarinesWithTags = SubmarineInfo.SavedSubmarines
                .Where(submarineSelector)
                .Where(s =>
                {
                    return s.GetExtraSubmarineInfo is { } extraInfo && (tags.None() || tags.Any(t => extraInfo.MissionTags.Contains(t)));
                })
                .ToList();

            var matchingSubmarines = submarinesWithTags
                .Where(s =>
                {
                    return s.GetExtraSubmarineInfo is { } extraInfo &&
                           levelDifficulty >= extraInfo.MinLevelDifficulty &&
                           levelDifficulty <= extraInfo.MaxLevelDifficulty;
                })
                .ToList();

            if (matchingSubmarines.Count == 0)
            {
                if (submarinesWithTags.Count > 0)
                {
                    DebugConsole.ThrowError($"Found {submarinesWithTags.Count} {submarineTypeName}(s) with matching tags \"{string.Join(", ", tags)}\", but none are suitable for level difficulty {levelDifficulty:F1}.");
                }
                return null;
            }
            return matchingSubmarines[rand.Next(matchingSubmarines.Count)];
        }
    }

    class AbilityMissionMoneyGainMultiplier : AbilityObject, IAbilityValue, IAbilityMission
    {
        public AbilityMissionMoneyGainMultiplier(Mission mission, float moneyGainMultiplier)
        {
            Value = moneyGainMultiplier;
            Mission = mission;
        }
        public float Value { get; set; }
        public Mission Mission { get; set; }
    }

    class AbilityMissionExperienceGainMultiplier : AbilityObject, IAbilityValue, IAbilityMission, IAbilityCharacter
    {
        public AbilityMissionExperienceGainMultiplier(Mission mission, float missionExperienceGainMultiplier, Character character)
        {
            Value = missionExperienceGainMultiplier;
            Mission = mission;
            Character = character;
        }

        public float Value { get; set; }
        public Mission Mission { get; set; }
        public Character Character { get; set; }
    }
    
    class AbilityMissionReputationGainMultiplier : AbilityObject, IAbilityValue, IAbilityMission, IAbilityCharacter
    {
        public AbilityMissionReputationGainMultiplier(Mission mission, float reputationMultiplier, Character character)
        {
            Value = reputationMultiplier;
            Mission = mission;
            Character = character;
        }

        public float Value { get; set; }
        public Mission Mission { get; set; }
        public Character Character { get; set; }
    }

}
