using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using common;
using common.GUIUtils;
using eagle.Notifications;
using Net.Eagle0.Eagle.Api;
using Net.Eagle0.Eagle.Common;
using Net.Eagle0.Eagle.Views;
using Net.Eagle0.Shardok.Common;
using TMPro;
using UnityEngine;
using UnityGoDiceInterface;
using Logger = common.Logger;
using Notification = eagle.Notifications.Notification;

namespace eagle {
    using UnitId = Int32;
    using FactionId = Int32;
    using PlayerId = Int32;
    using BattalionId = Int32;
    using HeroId = Int32;
    using ProvinceId = Int32;
    using ShardokGameId = String;
    using GameId = Int64;
    using TokenId = Int64;

    public interface IGameModel {
        FactionId? PlayerId { get; }
        Date CurrentDate { get; }
        IDictionary<HeroId, HeroView> Heroes { get; }
        IDictionary<ProvinceId, ProvinceView> Provinces { get; }
        IDictionary<FactionId, FactionView> ActiveFactions { get; }
        IDictionary<BattalionTypeId, BattalionType> BattalionTypes { get; }

        Dictionary<ProvinceId, OneProvinceAvailableCommands> AvailableCommandsByProvince { get; }
        List<AvailableCommand> AvailableCommandsForProvince(ProvinceId pid);
        ProvinceId SuggestedProvinceId { get; }

        TokenId? CommandToken { get; }
        TokenId LastPostedToken { get; }

        Dictionary<String, ShardokGameModel> ShardokGameModels { get; }

        private bool ShardokGameModelIsRunning(ShardokGameModel sgm) =>
                sgm.GameStatus.State == GameStatus.Types.State.GameRunning
                || sgm.GameStatus.State == GameStatus.Types.State.SetUp;
        Dictionary<String, ShardokGameModel> RunningShardokGameModels =>
                ShardokGameModels.Where(kv => ShardokGameModelIsRunning(kv.Value))
                        .ToDictionary(kv => kv.Key, kv => kv.Value);

        FactionView MaybeDestroyedFaction(FactionId factionId);
        string FactionName(FactionId factionId);

        public List<ProvinceId> ProvincesForFaction(FactionId factionId);

        public List<ChronicleEntry> ChronicleEntries { get; }

        RollFetcher RollFetcher { get; }
    }

    public class GameModelUpdater : IClientConnectionSubscriber {
        private FactionId? PlayerId => _currentModel.PlayerId;
        public Int64? CurrentEagleToken => _currentModel.CommandToken;
        public Int64? CurrentShardokToken(string shardokGameId) {
            if (_currentModel.ShardokGameModels.TryGetValue(shardokGameId, out var sgm)) {
                return sgm.History.Count;
            }
            return null;
        }

        public delegate void ReceiveNotes(
                string title,
                string text,
                string llmId,
                List<ProvinceId> pids,
                List<HeroView> displayedHeroes);

        public Action<IGameModel> UpdateAction { get; set; }
        public ReceiveNotes NoteRecipient { get; set; }

        private void Notify(Notification notification) {
            NoteRecipient(
                    notification.Title,
                    notification.Text,
                    notification.LlmId,
                    notification.ProvinceIds,
                    notification.DisplayedHeroes);
        }

        private readonly ResourceFetcher resourceFetcher;

        public long GameId { get; }

        public int LastUnfilteredResultCount => _lastUnfilteredResultCount;

        public List<IClientConnectionSubscriber.ShardokViewStatus> ShardokViewStatuses =>
                _currentModel.ShardokGameModels
                        .Select(sgm => new IClientConnectionSubscriber.ShardokViewStatus {
                            shardokGameId = sgm.Key,
                            filteredResultCount = sgm.Value.History.Count()
                        })
                        .ToList();

        public List<IClientConnectionSubscriber.StreamingTextStatus> StreamingTextStatuses =>
                ClientTextProvider.Provider.All()
                        .Select(kv => new IClientConnectionSubscriber.StreamingTextStatus {
                            llmIdentifier = kv.Key,
                            knownByteCount = Encoding.UTF8.GetBytes(kv.Value.Text).Length,
                            knownComplete = kv.Value.Completed
                        })
                        .ToList();

        private PersistentClientConnection PersistentConnection { get; }

        public ErrorHandler ErrorHandler;

        private int _lastUnfilteredResultCount = 0;

        private readonly Logger _connectionLogger = Logger.GetLogger("ConnectionLogger");

        private readonly RollFetcher _rollFetcher;

        // State synced with server
        private struct ConcreteGameModel : IGameModel {
            public PlayerId? PlayerId { get; set; }

            public GameStateView GsView { get; set; }

            public int HistoryCount { get; set; }
            public Date CurrentDate => GsView.CurrentDate;
            public IDictionary<HeroId, HeroView> Heroes => GsView.Heroes;
            public IDictionary<ProvinceId, ProvinceView> Provinces => GsView.Provinces;
            public IDictionary<FactionId, FactionView> ActiveFactions => GsView.Factions;
            public IDictionary<FactionId, FactionView> DestroyedFactions =>
                    GsView.DestroyedFactions;
            public IList<ShardokBattleView> ShardokBattles => GsView.OutstandingBattles;
            public IDictionary<BattalionTypeId, BattalionType> BattalionTypes { get; set; }
            public IDictionary<BattalionId, String> BattalionNames => GsView.BattalionNames;

            public TokenId? CommandToken { get; set; }
            public TokenId LastPostedToken { get; set; }

            public String CommandTokenString =>
                    CommandToken.HasValue ? CommandToken.Value.ToString() : "{null}";

            public Dictionary<ProvinceId, OneProvinceAvailableCommands>
                    AvailableCommandsByProvince {
                        get; set;
                    }
            public int SuggestedProvinceId { get; set; }

            public List<ProvinceId> ProvincesForFaction(FactionId factionId) {
                return Provinces
                        .Where(p => p.Value.RulingFactionId.HasValue &&
                                    p.Value.RulingFactionId.Value == factionId)
                        .Select(kv => kv.Key)
                        .ToList();
            }

            public List<ChronicleEntry> ChronicleEntries { get; set; }

            public List<AvailableCommand> AvailableCommandsForProvince(ProvinceId pid) {
                if (AvailableCommandsByProvince.TryGetValue(pid, out var cmds)) {
                    return cmds.Commands.ToList();
                }
                return new List<AvailableCommand>();
            }

            public Dictionary<String, ShardokGameModel> ShardokGameModels { get; set; }

            public FactionView MaybeDestroyedFaction(FactionId factionId) {
                if (ActiveFactions.TryGetValue(factionId, out var factionView)) {
                    return factionView;
                }
                return DestroyedFactions[factionId];
            }
            public string FactionName(FactionId factionId) {
                return DisplayNames.ResolvedFactionString(MaybeDestroyedFaction(factionId), this);
            }

            public void SetDynamicFactionText(
                    TMP_Text textComponent,
                    FactionId factionId,
                    DynamicFactionTextUpdater textUpdater,
                    string template = null,
                    string fallbackText = null) {
                var faction = MaybeDestroyedFaction(factionId);
                textUpdater.SetFactionText(textComponent, faction, this, template, fallbackText);
            }

            public RollFetcher RollFetcher { get; set; }
        }

        private ConcreteGameModel _currentModel;

        public GameModelUpdater(
                GameId eagleGameId,
                FactionId? factionId,
                PersistentClientConnection pers,
                RollFetcher rollFetcher) {
            GameId = eagleGameId;
            PersistentConnection = pers;
            this._rollFetcher = rollFetcher;

            _currentModel.LastPostedToken = -1;
            _currentModel.PlayerId = factionId;
            _currentModel.AvailableCommandsByProvince =
                    new Dictionary<ProvinceId, OneProvinceAvailableCommands>();

            _currentModel.GsView = new GameStateView();
            _currentModel.ShardokGameModels = new Dictionary<ShardokGameId, ShardokGameModel>();

            _currentModel.BattalionTypes = new Dictionary<BattalionTypeId, BattalionType>();

            _currentModel.RollFetcher = rollFetcher;
        }

        private ShardokGameModel MakeGameModel(ShardokGameId shardokGameId) {
            var battleView =
                    _currentModel.ShardokBattles.First(b => b.ShardokGameId == shardokGameId);

            PlayerId playerId = battleView.MyPlayerId ?? -1;

            var map = HexMapStorage.Instance.WithName(battleView.MapName);

            var players = battleView.PlayerInfos
                                  .Select(pi => new ShardokGameModel.PlayerWithHostility {
                                      playerId = pi.PlayerId,
                                      factionName = _currentModel.FactionName(pi.EagleFactionId),
                                      hostility = pi.Hostility
                                  })
                                  .ToList();

            var defenderProvince = battleView.DefenderProvince;

            // Create the model as soon as we know about the game, store it in a dictionary or
            // something, and then just set up the game when we go to the canvas
            var shardokModel = new ShardokGameModel(
                    eagleGameId: GameId,
                    shardokGameId: battleView.ShardokGameId,
                    playerId: playerId,
                    persistentClientConnection: PersistentConnection,
                    map: map,
                    locationName: _currentModel.Provinces[defenderProvince].Name,
                    month: _currentModel.CurrentDate.Month,
                    players: players,
                    heroNames: _currentModel.Heroes.ToDictionary(
                            kv => kv.Key,
                            kv => ClientTextProvider.Provider.GetTextEntry(kv.Value.NameTextId)
                                          .Text),
                    heroImages: _currentModel.Heroes.ToDictionary(
                            kv => kv.Key,
                            kv => kv.Value.ImagePath),
                    battalionNames: _currentModel.BattalionNames.ToDictionary(
                            kv => kv.Key,
                            kv => kv.Value),
                    rollFetcher: _rollFetcher);

            return shardokModel;
        }

        public void ReceiveGameUpdate(GameUpdate updateItem) {
            if (updateItem.StartingState != null) { HandleStartingState(updateItem.StartingState); }

            switch (updateItem.GameUpdateDetailsCase) {
                case GameUpdate.GameUpdateDetailsOneofCase.ActionResultResponse:
                    _lastUnfilteredResultCount =
                            updateItem.ActionResultResponse.UnfilteredResultCountAfter;
                    if (updateItem.ActionResultResponse.ActionResultViews.Any() ||
                        updateItem.ActionResultResponse.AvailableCommands == null ||
                        updateItem.ActionResultResponse.AvailableCommands.Token !=
                                _currentModel.CommandToken) {
                        HandleUpdates(updateItem.ActionResultResponse.ActionResultViews.ToList());
                        HandleAvailableCommands(updateItem.ActionResultResponse.AvailableCommands);

                        if (UpdateAction != null) UpdateAction.Invoke(_currentModel);
                    }

                    break;

                case GameUpdate.GameUpdateDetailsOneofCase.ShardokActionResultResponse:
                    var specResponse = updateItem.ShardokActionResultResponse.ShardokGameResponses;

                    foreach (var oneResponse in specResponse) {
                        if (!_currentModel.ShardokGameModels.TryGetValue(
                                    oneResponse.ShardokGameId,
                                    out var shardokGameModel)) {
                            shardokGameModel =
                                    MakeGameModel(shardokGameId: oneResponse.ShardokGameId);
                        }

                        shardokGameModel.HandleUpdates(
                                oneResponse.ActionResultViews,
                                oneResponse.NewResultViewCount);
                        shardokGameModel.HandleAvailableCommands(oneResponse.AvailableCommands);

                        if (shardokGameModel.GameStatus.State ==
                                    GameStatus.Types.State.GameRunning ||
                            shardokGameModel.GameStatus.State == GameStatus.Types.State.SetUp) {
                            _currentModel.ShardokGameModels[oneResponse.ShardokGameId] =
                                    shardokGameModel;
                        }
                    }
                    if (UpdateAction != null) UpdateAction.Invoke(_currentModel);
                    break;

                case GameUpdate.GameUpdateDetailsOneofCase.StreamingTextResponse:
                    HandleNewStreamingText(updateItem.StreamingTextResponse);

                    if (UpdateAction != null) UpdateAction.Invoke(_currentModel);
                    break;

                case GameUpdate.GameUpdateDetailsOneofCase.ErrorResponse:
                    ErrorHandler.HandleLog(
                            $"Caught server-side error handling command {updateItem.ErrorResponse.SelectedCommand}:\n{updateItem.ErrorResponse.ErrorString}",
                            "",
                            LogType.Error);
                    break;
            }
        }

        public void StartListeningForUpdates() { PersistentConnection.Subscribe(this); }

        public void StopListeningForUpdates() { PersistentConnection.Unsubscribe(this); }

        public Task PostCommand(ProvinceId provinceId, SelectedCommand command) {
            _connectionLogger.LogLine(
                    $"Posting command with token {_currentModel.CommandTokenString}");
            if (_currentModel.CommandToken is long token) {
                _currentModel.AvailableCommandsByProvince.Clear();
                _currentModel.CommandToken = null;
                _currentModel.LastPostedToken = token;
                return PersistentConnection.PostEagleCommand(
                        gameId: GameId,
                        token: token,
                        provinceId: provinceId,
                        command: command);
            }

            throw new ArgumentException("No command token available to post command");
        }

        private void HandleStartingState(GameStateView startingState) {
            _currentModel.GsView = startingState;
            _currentModel.BattalionTypes =
                    startingState.BattalionTypes.ToDictionary(bt => bt.TypeId, bt => bt);
            _currentModel.ChronicleEntries = startingState.ChronicleEntries.ToList();
        }

        private void HandleUpdates(List<ActionResultView> results) {
            foreach (ActionResultView entry in results) { HandleNewHistoryEntry(entry); }
        }

        private void HandleAvailableCommands(AvailableCommands availableCommands) {
            if (availableCommands == null || availableCommands.CommandsByProvince.Count == 0) {
                if (_currentModel.CommandToken.HasValue) {
                    _connectionLogger.LogLine(
                            $"Token was {_currentModel.CommandTokenString}, but nulling out since there's no available commands");
                }
                _currentModel.CommandToken = null;
                _currentModel.AvailableCommandsByProvince.Clear();
                return;
            }

            if (availableCommands.Token < _currentModel.LastPostedToken) {
                throw new ArgumentException(
                        $"Received token {availableCommands.Token} that is less than last posted token {_currentModel.LastPostedToken}");
            }

            if (availableCommands.Token == _currentModel.LastPostedToken) {
                // We're receiving a token for a command we already posted. That probably indicates
                // a race, where we're receiving a game update from some other source before our
                // last command was processed.

                if (_currentModel.CommandToken != null ||
                    _currentModel.AvailableCommandsByProvince.Count > 0) {
                    throw new ArgumentException(
                            $"Received token {availableCommands.Token} that matches last posted token, but we already have commands: {_currentModel.AvailableCommandsByProvince}");
                }

                _connectionLogger.LogLine(
                        $"Received token {availableCommands.Token} that matches last posted token; skipping");
                return;
            }

            _connectionLogger.LogLine(
                    $"Updating token from {_currentModel.CommandTokenString} to {availableCommands.Token}");
            if (_currentModel.CommandToken.HasValue &&
                _currentModel.CommandToken.Value != availableCommands.Token) {
                _connectionLogger.LogLine("How are we updating an existing token?");
                _connectionLogger.LogLine(
                        $"Existing commands: {_currentModel.AvailableCommandsByProvince}");
                _connectionLogger.LogLine($"New commands: {availableCommands.CommandsByProvince}");
            } else if (
                    _currentModel.CommandToken.HasValue &&
                    _currentModel.AvailableCommandsByProvince.Count > 0) {
                // If we're in here, we already have commands and the tokens match, so there's
                // nothing to update.
                return;
            }

            _currentModel.CommandToken = availableCommands.Token;

            // does this not clear existing UX if there's a token replacement?
            _currentModel.AvailableCommandsByProvince =
                    new Dictionary<UnitId, OneProvinceAvailableCommands>(
                            availableCommands.CommandsByProvince);
            _currentModel.SuggestedProvinceId = availableCommands.SuggestedProvinceId;
        }

        private void UpdateHero(HeroViewDiff ch) {
            var hero = _currentModel.Heroes[ch.Id];

            if (ch.NewNameTextId is {} newNameTextId) hero.NameTextId = newNameTextId;
            if (ch.ClearFactionId) hero.FactionId = null;
            if (ch.NewFactionId is {} newFactionId) hero.FactionId = newFactionId;
            if (ch.NewVigor is {} newVigor) hero.Vigor = newVigor;
            if (ch.NewLoyalty is {} newLoyalty) hero.Loyalty = newLoyalty;
            if (ch.BecameFactionLeader is true) hero.Loyalty = null;
            if (ch.NewProfession is {} newProfession) hero.Profession = newProfession.Profession;

            if (ch.NewStrength is {} newStrength) hero.Strength = newStrength;
            if (ch.NewStrengthXp is {} newStrengthXp) hero.StrengthXp = newStrengthXp;
            if (ch.NewAgility is {} newAgility) hero.Agility = newAgility;
            if (ch.NewAgilityXp is {} newAgilityXp) hero.AgilityXp = newAgilityXp;
            if (ch.NewCharisma is {} newCharisma) hero.Charisma = newCharisma;
            if (ch.NewCharismaXp is {} newCharismaXp) hero.CharismaXp = newCharismaXp;
            if (ch.NewWisdom is {} newWisdom) hero.Wisdom = newWisdom;
            if (ch.NewWisdomXp is {} newWisdomXp) hero.WisdomXp = newWisdomXp;

            if (ch.NewConstitution is {} newConstitution) hero.Constitution = newConstitution;
            if (ch.NewConstitutionXp is {} newConstitutionXp)
                hero.ConstitutionXp = newConstitutionXp;

            if (ch.NewIntegrity is {} newIntegrity) hero.Integrity = newIntegrity;
            if (ch.NewAmbition is {} newAmbition) hero.Ambition = newAmbition;
            if (ch.NewGregariousness is {} newGregariousness)
                hero.Gregariousness = newGregariousness;
            if (ch.NewBravery is {} newBravery) hero.Bravery = newBravery;

            if (ch.NewArcheryCapable is {} newArcheryValue) hero.ArcheryCapable = newArcheryValue;
            if (ch.NewStartFireCapable is {} newStartFire) hero.StartFireCapable = newStartFire;

            if (ch.NewImagePath is {} newImagePath) hero.ImagePath = newImagePath;

            if (ch.NewSortKeys.Any()) {
                hero.SortKeys.Clear();
                hero.SortKeys.AddRange(ch.NewSortKeys);
            }

            if (ch.NewBackstoryTextId is {} newBackstoryTextId)
                hero.BackstoryTextId = newBackstoryTextId;
            if (ch.NewPronounGender is {} newPronounGender)
                hero.PronounGender = newPronounGender.PronounGender;
        }

        private void UpdateFaction(FactionViewDiff cf) {
            var faction = _currentModel.ActiveFactions[cf.Id];

            if (cf.NewPrestige is int prestige) faction.Prestige = prestige;
            if (cf.NewFactionHeadId is HeroId hid) faction.FactionHeadId = hid;
            foreach (var addedLeader in cf.AddedLeaders) { faction.Leaders.Add(addedLeader); }
            foreach (var removedLeader in cf.RemovedLeaders) {
                faction.Leaders.Remove(removedLeader);
            }

            var relationshipMap = faction.FactionRelationships.ToDictionary(
                    fac => fac.TargetFactionId,
                    fac => fac);
            foreach (var cr in cf.ChangedFactionRelationships) {
                relationshipMap[cr.TargetFactionId] = cr;
            }
            faction.FactionRelationships.Clear();
            faction.FactionRelationships.Add(relationshipMap.Values);

            faction.IncomingDiplomacyOffers.Add(cf.AddedIncomingDiplomacyOffers);
            foreach (var removedOffer in cf.RemovedIncomingDiplomacyOffers) {
                faction.IncomingDiplomacyOffers.Remove(removedOffer);
            }

            faction.FocusProvinceId = cf.NewFocusProvinceId;
        }

        private void UpdateProvince(ProvinceViewDiff cp) {
            if (!_currentModel.Provinces.TryGetValue(cp.Id, out var province))
                province = new ProvinceView();

            if (cp.ClearRulingFactionId) { province.RulingFactionId = null; }
            if (cp.NewRulingFactionId is {} newRulingFactionId)
                province.RulingFactionId = newRulingFactionId;

            if (cp.ClearFullInfo) { province.FullInfo = null; }

            if (cp.EventsReplacement != null) {
                province.KnownEvents.Clear();
                province.KnownEvents.Add(cp.EventsReplacement.NewEvents);
            }

            province.KnownIncomingArmies.Clear();
            province.KnownIncomingArmies.AddRange(cp.NewIncomingArmies);

            if (cp.NewFullInfo != null) {
                province.FullInfo ??= new FullProvinceInfo();

                var nfi = cp.NewFullInfo;

                if (nfi.ClearRulingHero) province.FullInfo.RulingHeroId = null;
                if (nfi.NewRulingHeroId is {} newRulingHeroId)
                    province.FullInfo.RulingHeroId = newRulingHeroId;

                if (nfi.OrderedRulingFactionHeroIds != null) {
                    province.FullInfo.RulingFactionHeroIds.Clear();
                    province.FullInfo.RulingFactionHeroIds.Add(
                            nfi.OrderedRulingFactionHeroIds.RulingFactionHeroIds);
                }

                var removedBattalions = province.FullInfo.Battalions
                                                .Where(b => nfi.RemovedBattalionIds.Contains(b.Id))
                                                .ToList();
                foreach (var removed in removedBattalions) {
                    province.FullInfo.Battalions.Remove(removed);
                }
                province.FullInfo.Battalions.Add(nfi.AddedBattalions);

                foreach (var bn in nfi.AddedBattalions) {
                    _currentModel.BattalionNames[bn.Id] = bn.Name;
                }

                if (nfi.ClearDefendingArmy) province.FullInfo.DefendingArmy = null;
                if (nfi.NewDefendingArmy != null)
                    province.FullInfo.DefendingArmy = nfi.NewDefendingArmy;

                if (nfi.Gold is int newGold) province.FullInfo.Gold = newGold;
                if (nfi.Food is {} newFood) province.FullInfo.Food = newFood;

                if (nfi.Economy is {} newEconomy) province.FullInfo.Economy = newEconomy;
                if (nfi.Agriculture is {} newAgriculture)
                    province.FullInfo.Agriculture = newAgriculture;
                if (nfi.Infrastructure is {} newInfrastructure)
                    province.FullInfo.Infrastructure = newInfrastructure;

                if (nfi.EconomyDevastation is {} newEconomyDevastation)
                    province.FullInfo.EconomyDevastation = newEconomyDevastation;
                if (nfi.AgricultureDevastation is {} newAgricultureDevastation)
                    province.FullInfo.AgricultureDevastation = newAgricultureDevastation;
                if (nfi.InfrastructureDevastation is {} newInfrastructureDevastation)
                    province.FullInfo.InfrastructureDevastation = newInfrastructureDevastation;

                if (nfi.NewPriceIndex is double newPriceIndex)
                    province.FullInfo.PriceIndex = newPriceIndex;

                if (nfi.Support != null) province.FullInfo.Support = nfi.Support;

                if (nfi.SetRulerIsTraveling is {} newRulerIsTraveling)
                    province.FullInfo.RulerIsTraveling = newRulerIsTraveling;

                if (nfi.NewProvinceOrders != null)
                    province.FullInfo.ProvinceOrders = nfi.NewProvinceOrders.Orders;

                if (nfi.FoodConsumption is {} newFoodConsumption)
                    province.FullInfo.FoodConsumption = newFoodConsumption;

                if (nfi.GoldCap is {} newGoldCap) province.FullInfo.GoldCap = newGoldCap;
                if (nfi.FoodCap is {} newFoodCap) province.FullInfo.FoodCap = newFoodCap;

                if (nfi.HexMapName != null) province.FullInfo.HexMapName = nfi.HexMapName;

                if (nfi.CastleCount != null) province.FullInfo.CastleCount = nfi.CastleCount.Value;
                if (nfi.HeroCap is int newCap) province.FullInfo.HeroCap = newCap;

                foreach (HeroId removed in nfi.RemovedUnaffiliatedHeroIds) {
                    for (int i = 0; i < province.FullInfo.UnaffiliatedHeroes.Count; i++) {
                        if (province.FullInfo.UnaffiliatedHeroes[i].HeroId == removed) {
                            province.FullInfo.UnaffiliatedHeroes.RemoveAt(i);
                            break;
                        }
                    }
                }
                province.FullInfo.UnaffiliatedHeroes.Add(nfi.AddedUnaffiliatedHeroes);
            }

            if (cp.AsOf != null) { province.AsOf = cp.AsOf; }
        }

        private void UpdateChronicleEntry(ChronicleEntry newEntry) {
            _currentModel.ChronicleEntries ??= new List<ChronicleEntry>();

            var existingIndex =
                    _currentModel.ChronicleEntries.FindIndex(ce => ce.Date.Equals(newEntry.Date));

            if (existingIndex == -1) {
                _currentModel.ChronicleEntries.Add(newEntry);
            } else {
                _currentModel.ChronicleEntries[existingIndex] = newEntry;
            }

            _currentModel.ChronicleEntries =
                    _currentModel.ChronicleEntries.OrderBy(ce => ce.Date.Year)
                            .ThenBy(ce => ce.Date.Month)
                            .ToList();
        }

        private void ApplyGameStateViewDiff(GameStateViewDiff entry) {
            if (entry == null) return;

            if (entry.NewRoundId is {} newRoundId) {
                _currentModel.GsView.CurrentRoundId = newRoundId;
            }
            if (entry.NewPhase is {} newPhase) {
                _currentModel.GsView.CurrentPhase = newPhase.Value;
            }

            if (entry.NewDate is {} newDate) {
                _currentModel.GsView.CurrentDate = newDate;
                _currentModel.ShardokGameModels.Clear();
            }

            foreach (ProvinceView province in entry.NewProvinces)
                _currentModel.Provinces[province.Id] = province;

            foreach (ProvinceViewDiff cp in entry.ChangedProvinces) UpdateProvince(cp);

            foreach (HeroView newHero in entry.NewHeroes)
                _currentModel.Heroes[newHero.Id] = newHero;

            foreach (HeroViewDiff ch in entry.ChangedHeroes) UpdateHero(ch);

            foreach (FactionView newFaction in entry.NewFactions)
                _currentModel.ActiveFactions[newFaction.Id] = newFaction;

            foreach (FactionViewDiff cf in entry.ChangedFactions) UpdateFaction(cf);

            foreach (FactionId fid in entry.RemovedFactionIds) {
                _currentModel.DestroyedFactions[fid] = new FactionView {
                    Id = fid,
                    Name = _currentModel.ActiveFactions[fid].Name,
                    FactionHeadId = _currentModel.ActiveFactions[fid].FactionHeadId
                };
                _currentModel.ActiveFactions.Remove(fid);
            }

            foreach (ShardokBattleView bv in entry.NewBattles) _currentModel.ShardokBattles.Add(bv);

            foreach (string rb in entry.RemovedBattleIds) {
                for (int i = 0; i < _currentModel.ShardokBattles.Count; i++) {
                    if (_currentModel.ShardokBattles[i].ShardokGameId == rb) {
                        _currentModel.ShardokBattles.RemoveAt(i);
                        break;
                    }
                }
            }

            foreach (BattalionType bt in entry.NewBattalionTypes) {
                _currentModel.BattalionTypes[bt.TypeId] = bt;
            }

            foreach (var bn in entry.NewBattalionNames) {
                _currentModel.BattalionNames[bn.Key] = bn.Value;
            }

            foreach (var ce in entry.UpdatedChronicleEntries) { UpdateChronicleEntry(ce); }
        }

        private void HandleNewHistoryEntry(ActionResultView arv) {
            _currentModel.HistoryCount++;

            MaybeSendNotification(arv);
            ApplyGameStateViewDiff(arv.GameStateDiff);
        }

        private void HandleNewStreamingText(StreamingTextResponse str) {
            if (str == null) return;

            ClientTextProvider.Provider.HandleNewStreamingText(
                    str.LlmIdentifier,
                    str.NewText,
                    str.StartingByteCount,
                    str.Completed);
        }

        private void MaybeSendNotification(ActionResultView entry) {
            if (!PlayerId.HasValue) { return; }

            foreach (var result in NotificationDispatcher.GenerateNotifications(
                             entry,
                             _currentModel)) {
                Notify(result);
            }
        }
    }
}