using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using common;
using common.GUIUtils;
using eagle0;
using Eagle0.Tutorial;
using Net.Eagle0.Eagle.Api;
using Net.Eagle0.Eagle.Common;
using Net.Eagle0.Eagle.Views;
using Shardok;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
#if UNITY_EDITOR
using UnityEditor;
#endif

namespace eagle {
    using FactionId = Int32;
    using ProvinceId = Int32;

    /// <summary>
    /// Layout mode based on aspect ratio:
    /// - Wide: Ultra-wide screens (ratio > 2.0), factions/armies column on right
    /// - Medium: Standard widescreen (ratio >= 1.35), default layout
    /// - Narrow: iPad-like 4:3 (ratio < 1.35), compact command panels
    /// </summary>
    public enum LayoutMode { Wide, Medium, Narrow }

    public class EagleGameController : MonoBehaviour {
        [Header("Audio")]
        public SoundManager soundManager;

        [Header("Status Labels")]
        public TextMeshProUGUI roundStatusLabel;

        [Header("Main Panels")]
        public ProvinceInfoPanelController provinceInfoPanelController;
        public FreeHeroesTableController freeHeroesTableController;
        public HeroesAndBattalionsPanelController heroesAndBattalionsPanelController;
        public MapController mapController;
        public CommandButtonPanelController commandButtonPanelController;
        public ChronicleCanvasController chronicleCanvasController;
        public ErrorHandler errorHandler;
        public NotificationPanel notificationPanel;
        public GameObject popupCanvas;

        [Header("Factions & Armies (Wide/Narrow)")]
        public GameObject factionsAndMovingArmiesNarrow;
        public GameObject factionsAndMovingArmiesWide;
        public FactionsTableController factionsTableControllerNarrow;
        public MovingArmiesTableController movingArmiesTableControllerNarrow;
        public FactionsTableController factionsTableControllerWide;
        public MovingArmiesTableController movingArmiesTableControllerWide;
        public DominionPanelController dominionPanelControllerNarrow;
        public DominionPanelController dominionPanelControllerWide;

        [Header("Layout Spacers")]
        public GameObject wideSpacer;

        [Header("Clicked Province Panel")]
        public GameObject clickedProvincePanel;
        public GameObject underClickedProvincePanel;

        public ProvinceId? SelectedProvince => mapController.SelectedProvinceId;

        [Header("Buttons")]
        public Button showChronicleButton;
        public Button commitCommandButton;
        public Button gameIdButton;
        public Button nextActiveProvinceButton;

        [Header("Map")]
        public ProvinceIDLoader provinceIDLoader;
        public ProvinceActionAnimator provinceActionAnimator;

        [Header("Battle Progress")]
        public BattleProgressPanel battleProgressPanel;

        [Header("Commands")]
        public MouseHandler mouseHandlerPrefab;
        public GameObject commandPanel;
        private CommandPanelController _commandPanelController;
        public PleaseRecruitMeCommandSelector pleaseRecruitMeCommandSelector;
        public ResolveTruceCommandSelector resolveTruceCommandSelector;
        public ResolveAllianceCommandSelector resolveAllianceCommandSelector;
        public ResolveBreakAllianceCommandSelector resolveBreakAllianceCommandSelector;
        public ResolveInvitationCommandSelector resolveInvitationCommandSelector;
        public ResolveRansomOfferCommandSelector resolveRansomOfferCommandSelector;
        public CommandWarningPanelController commandWarningPanelController;

        private double _aspectRatio;
        private const double WideScreenThreshold = 2.0;
        private const double NarrowScreenThreshold = 1.45;

        private bool IsWideScreen => _aspectRatio > WideScreenThreshold;
        private bool IsNarrowScreen => _aspectRatio < NarrowScreenThreshold;

        /// <summary>
        /// Current layout mode based on aspect ratio. Accessible to CommandSelectors.
        /// </summary>
        public LayoutMode CurrentLayoutMode {
            get {
                if (IsWideScreen) return LayoutMode.Wide;
                if (IsNarrowScreen) return LayoutMode.Narrow;
                return LayoutMode.Medium;
            }
        }

        private int _lastWidth;
        private LayoutMode _lastLayoutMode = LayoutMode.Medium;
        private int _lastHeight;

        public GameModelUpdater ModelUpdater { get; private set; }
        public IGameModel GameModel => Model;
        IGameModel Model { get; set; }

        private DynamicFactionTextUpdater factionTextUpdater = new DynamicFactionTextUpdater();
        private IGameModel _newModel = null;

        public GameObject shardokContainer;

        private KeyValuePair<ProvinceId, OneProvinceAvailableCommands>? NextActiveProvinceKeyPair {
            get {
                if (Model == null || Model.AvailableCommandsByProvince.Count == 0) { return null; }

                if (Model.AvailableCommandsByProvince.TryGetValue(
                            Model.SuggestedProvinceId,
                            out var suggestedCommands)) {
                    return new KeyValuePair<ProvinceId, OneProvinceAvailableCommands>(
                            Model.SuggestedProvinceId,
                            suggestedCommands);
                }

                return Model.AvailableCommandsByProvince.First();
            }
        }

        void Start() {
            gameIdButton.GetComponentInChildren<Text>().text = "";
            mapController.targetedProvincesChangedCallback = MapControllerChangedTarget;
            mapController.hoveredProvinceChangedCallback = MapControllerChangedHover;
            popupCanvas.SetActive(true);
            dominionPanelControllerNarrow.gameObject.SetActive(false);
            showChronicleButton.gameObject.SetActive(false);

            if (errorHandler == null) {
                errorHandler = GameSceneManager.Instance?.GetErrorHandler();
            }
            errorHandler.gameObject.SetActive(true);

            ArrangeLayout();
        }

        private void ArrangeLayout() {
            var newWidth = Screen.width;
            var newHeight = Screen.height;

            if (newWidth == _lastWidth && newHeight == _lastHeight) { return; }

            _lastWidth = newWidth;
            _lastHeight = newHeight;

            _aspectRatio = (float)newWidth / (float)newHeight;

            // Notify command panel if layout mode changed
            if (CurrentLayoutMode != _lastLayoutMode) {
                _lastLayoutMode = CurrentLayoutMode;
                _commandPanelController.UpdateSelectorLayoutMode(CurrentLayoutMode);
            }

            if (IsWideScreen) {
                // Show wide layout containers, hide narrow layout containers
                factionsAndMovingArmiesWide.SetActive(true);
                factionsAndMovingArmiesNarrow.SetActive(false);
                dominionPanelControllerWide.gameObject.SetActive(true);
                dominionPanelControllerNarrow.gameObject.SetActive(false);
                if (wideSpacer != null) wideSpacer.SetActive(true);

                provinceInfoPanelController.WideScreen = true;
            } else {
                // Show narrow layout containers, hide wide layout containers
                factionsAndMovingArmiesWide.SetActive(false);
                factionsAndMovingArmiesNarrow.SetActive(true);
                dominionPanelControllerWide.gameObject.SetActive(false);
                // Narrow dominion panel visibility controlled by user toggle, don't change here
                if (wideSpacer != null) wideSpacer.SetActive(false);

                provinceInfoPanelController.WideScreen = false;
            }

            // Clicked province panel: regular for medium/wide, under for narrow
            if (clickedProvincePanel != null) clickedProvincePanel.SetActive(!IsNarrowScreen);
            if (underClickedProvincePanel != null)
                underClickedProvincePanel.SetActive(IsNarrowScreen);
        }

        public void StopAll() {
            _newModel = null;
            TearDownModelUpdater();
            SwapModel();
            Model = null;

            // Clear stale province state from the previous game so it doesn't
            // get used with the next game's hero/battalion maps.
            provinceInfoPanelController.Province = null;
            freeHeroesTableController.CurrentProvince = null;
            movingArmiesTableControllerNarrow.CurrentProvince = null;
            movingArmiesTableControllerWide.CurrentProvince = null;
            factionsTableControllerNarrow.CurrentProvince = null;
            factionsTableControllerWide.CurrentProvince = null;
            heroesAndBattalionsPanelController.CurrentProvince = null;
            chronicleCanvasController.Entries = new List<ChronicleEntry>();
            SetMusic();

            // Clear game state provider when leaving game
            PersistentUIManager.Instance?.connectionStatusUI?.SetGameStateProvider(null);
        }

        void MapControllerChangedTarget(List<ProvinceId> newTarget) {
            if (_commandPanelController.Selector != null) {
                _commandPanelController.Selector.TargetedProvinces = newTarget;
                freeHeroesTableController.UpdateTables();
                movingArmiesTableControllerNarrow.UpdateTables();
                movingArmiesTableControllerWide.UpdateTables();
                factionsTableControllerNarrow.UpdateTables();
                factionsTableControllerWide.UpdateTables();
                heroesAndBattalionsPanelController.UpdateTables();
            }
        }

        void MapControllerChangedHover(ProvinceId? newHover) {
            dominionPanelControllerNarrow.ProvinceHovered(newHover);
            dominionPanelControllerWide.ProvinceHovered(newHover);
            movingArmiesTableControllerNarrow.ProvinceHovered(newHover);
            movingArmiesTableControllerWide.ProvinceHovered(newHover);
            factionsTableControllerNarrow.ProvinceHovered(newHover);
            factionsTableControllerWide.ProvinceHovered(newHover);
        }

        void Update() {
            // Process batched streaming text updates (thread-safe, once per frame)
            ClientTextProvider.Provider.ProcessPendingUpdates();

            ArrangeLayout();
            if (_newModel != null) { SwapModel(); }

            if (Model != null) {
                if (_commandPanelController.Selector == null) {
                    if (Model.AvailableCommandsByProvince.Count > 0 &&
                        Model.AvailableCommandsByProvince.ContainsKey(0)) {
                        mapController.ProvincesWithCommands = new List<ProvinceId>();
                    } else {
                        mapController.ProvincesWithCommands =
                                Model.AvailableCommandsByProvince.Keys.ToList();
                    }
                    mapController.TargetedProvinces = new List<ProvinceId>();
                    mapController.TargetedProvincesAsGroup = false;
                } else {
                    mapController.TargetedProvinces =
                            _commandPanelController.Selector.TargetedProvinces;
                    mapController.TargetedProvincesAsGroup =
                            _commandPanelController.Selector.TargetedFactionIds.Count > 0;
                    mapController.ProvincesWithCommands = new List<ProvinceId>();
                }
            }

            if (GameInput.WindowedModeReleased()) {
#if UNITY_EDITOR
                // do nothing
#else
                Screen.fullScreen = !Screen.fullScreen;
#endif
            }
        }

        public void ReloadFactionDisplaySetting() {
            SetHeaderString();
            mapController.ReloadFactionDisplaySetting();
            factionsTableControllerNarrow.UpdateTables();
            factionsTableControllerWide.UpdateTables();
            _commandPanelController.RefreshUI();
        }

        void OnApplicationPause(bool pause) {
            if (ModelUpdater == null) { return; }
            // Don't unsubscribe on pause - this caused a race condition where the subscriber
            // could be lost if pause happened before the async StartListeningForUpdates completed.
            // With MainQueue rate-limiting, keeping the subscription during pause is safe.
            // Reconnects will continue to work, and updates will queue up and be processed on
            // resume.
        }

#if UNITY_EDITOR
        void OnPlayModeStateChanged(PlayModeStateChange state) {}
#endif

        public void SetUpGame(
                long gameId,
                int? playerId,
                PersistentClientConnection persistentClientConnection,
                string environmentName = null) {
            TearDownModelUpdater();

            var newModelUpdater =
                    new GameModelUpdater(gameId, playerId, persistentClientConnection) {
                        NoteRecipient = notification => notificationPanel.AddNote(notification)
                    };
            newModelUpdater.UpdateAction = model => ModelUpdated(newModelUpdater, model);
            ModelUpdater = newModelUpdater;

            commitCommandButton.gameObject.SetActive(false);
            battleProgressPanel.SetGoToBattleAction(GoToBattle);

            _commandPanelController = commandPanel.GetComponent<CommandPanelController>();
            _commandPanelController.Model = Model;

            resolveTruceCommandSelector.submissionAction = sc => PostCommittedCommand(0, sc);
            resolveAllianceCommandSelector.submissionAction = sc => PostCommittedCommand(0, sc);
            resolveBreakAllianceCommandSelector.submissionAction = sc =>
                    PostCommittedCommand(0, sc);
            resolveInvitationCommandSelector.submissionAction = sc => PostCommittedCommand(0, sc);
            resolveRansomOfferCommandSelector.submissionAction = sc => PostCommittedCommand(0, sc);
            pleaseRecruitMeCommandSelector.submissionAction = sc => PostCommittedCommand(0, sc);

            ModelUpdater.ErrorHandler = errorHandler;
            ModelUpdater.ActionResultAction = arv => {
                MainQueue.Q.Enqueue(() => {
                    if (provinceActionAnimator != null)
                        provinceActionAnimator.HandleActionResult(arv);
                });
            };

            // Set up game state provider and environment for connection status UI
            var statusUI = PersistentUIManager.Instance?.connectionStatusUI;
            if (statusUI != null) {
                statusUI.SetGameStateProvider(ModelUpdater);
                statusUI.SetEnvironment(environmentName);
            }

            // Fire-and-forget - subscription is awaited internally and failures are logged
            var modelUpdater = ModelUpdater;
            MainQueue.Q.EnqueueForNextUpdate(() => {
                if (this == null || ModelUpdater != modelUpdater) { return; }
                _ = modelUpdater.StartListeningForUpdates();
            });

            // Initialize tutorial system (onboarding starts after first model update in SwapModel)
            TutorialManager.Instance?.Initialize(this, null);
            // Register command panel for tutorial positioning
            if (TutorialManager.Instance?.TargetRegistry != null && commandPanel != null) {
                var registry = TutorialManager.Instance.TargetRegistry;
                registry.RegisterTarget("CommandPanel", commandPanel.GetComponent<RectTransform>());
                registry.RegisterTarget(
                        "InfoButton",
                        _commandPanelController.infoButton.GetComponent<RectTransform>());
            }
            // Register province info panel fields for dialogue highlight
            if (TutorialManager.Instance?.TargetRegistry != null &&
                provinceInfoPanelController != null) {
                var registry = TutorialManager.Instance.TargetRegistry;
                if (provinceInfoPanelController.SupportField != null) {
                    registry.RegisterTarget(
                            "SupportField",
                            provinceInfoPanelController.SupportField.GetComponent<RectTransform>());
                }
            }

#if UNITY_EDITOR
            EditorApplication.playModeStateChanged += (state) => OnPlayModeStateChanged(state);
#endif
        }

        private void SelectRecommendedCommand(ProvinceId? provinceId) {
            OneProvinceAvailableCommands opac;
            if (provinceId is {} pid &&
                Model.AvailableCommandsByProvince.TryGetValue(pid, out opac)) {
                SelectButtonIndex(opac.SuggestedCommandIndex);  // probably wrong
            } else {
                ClearActiveCommandUi();
            }
        }

        private void ClearActiveCommandUi() {
            commitCommandButton.gameObject.SetActive(false);
            _commandPanelController.SetAvailableCommandAndSelector(null, null);
            mapController.TargetedProvinces = new List<ProvinceId>();
            mapController.TargetedProvincesAsGroup = false;
            commandButtonPanelController.ClearButtonContents();
        }

        public void SelectNextActiveProvince() {
            mapController.SelectedProvinceId = NextActiveProvinceKeyPair?.Key;
        }

        public void SelectButtonIndex(int buttonIndex) {
            var selector =
                    Model.AvailableCommandsForProvince(SelectedProvince.Value)
                            .SelectMany(cmd => _commandPanelController.AppropriateSelectors(cmd))
                            .ElementAt(buttonIndex);

            var newType = selector.CommandType;
            var cmd = Model.AvailableCommandsForProvince(SelectedProvince.Value)
                              .Find(c => c.SealedValueCase == newType);
            var commandIndex =
                    Model.AvailableCommandsForProvince(SelectedProvince.Value).IndexOf(cmd);

            _commandPanelController.SetAvailableCommandAndSelector(cmd, selector);
            mapController.ProvincesWithCommands = new List<ProvinceId>();
            movingArmiesTableControllerNarrow.UpdateTables();
            movingArmiesTableControllerWide.UpdateTables();
            factionsTableControllerNarrow.UpdateTables();
            factionsTableControllerWide.UpdateTables();
            heroesAndBattalionsPanelController.UpdateTables();
            freeHeroesTableController.UpdateUnaffiliatedHeroSelections();
        }

        private void UpdateButtons() {
            if (SelectedProvince.HasValue) {
                commandButtonPanelController.SetButtonContents(
                        Model.AvailableCommandsForProvince(SelectedProvince.Value)
                                .SelectMany(
                                        cmd => _commandPanelController.ButtonContentsForCommand(
                                                cmd))
                                .ToArray());
            } else {
                commandButtonPanelController.ClearButtonContents();
            }
        }

        private void SetNextActiveProvinceButton() {
            nextActiveProvinceButton.gameObject.SetActive(
                    _commandPanelController.Selector == null && Model != null &&
                    Model.AvailableCommandsByProvince.Count > 0);
        }

        public void ProvinceWasSelected(ProvinceId? pid, bool selectRecommendedCommand = true) {
            if (Model == null) { return; }
            if (pid.HasValue) {
                if (selectRecommendedCommand) { SelectRecommendedCommand(pid.Value); }
                var currentProvince = Model.Provinces[pid.Value];
                provinceInfoPanelController.Province = currentProvince;
                freeHeroesTableController.CurrentProvince = currentProvince;
                movingArmiesTableControllerNarrow.CurrentProvince = currentProvince;
                movingArmiesTableControllerWide.CurrentProvince = currentProvince;
                factionsTableControllerNarrow.CurrentProvince = currentProvince;
                factionsTableControllerWide.CurrentProvince = currentProvince;
                heroesAndBattalionsPanelController.CurrentProvince = currentProvince;
            } else {
                if (selectRecommendedCommand) { SelectRecommendedCommand(null); }
                provinceInfoPanelController.Province = null;
                freeHeroesTableController.CurrentProvince = null;
                movingArmiesTableControllerNarrow.CurrentProvince = null;
                movingArmiesTableControllerWide.CurrentProvince = null;
                factionsTableControllerNarrow.CurrentProvince = null;
                factionsTableControllerWide.CurrentProvince = null;
                heroesAndBattalionsPanelController.CurrentProvince = null;
            }

            UpdateButtons();
            SetNextActiveProvinceButton();
            dominionPanelControllerNarrow.ForceUpdate();
            dominionPanelControllerWide.ForceUpdate();
            SetMusic();
        }

        private void PrefetchHeadshotForHeroes(List<HeroView> heroViews) {
            ResourceFetcher.headshotFetcher.Prefetch(heroViews.Select(h => h.ImagePath));
        }

        private void PrefetchHeadshots(IGameModel model) {
            var heroList = model.ActiveFactions.Values
                                   .Select(faction => model.Heroes[faction.FactionHeadId])
                                   .ToList();

            foreach (ProvinceView province in model.Provinces.Values) {
                if (province.FullInfo is FullProvinceInfo fpi) {
                    heroList.AddRange(fpi.RulingFactionHeroIds.Select(hid => model.Heroes[hid]));
                    heroList.AddRange(fpi.UnaffiliatedHeroes.Select(uh => uh.HeroId)
                                              .Select(hid => model.Heroes[hid]));
                }
            }

            PrefetchHeadshotForHeroes(heroList);
        }

        private void SetMusic() {
            if (Model == null) {
                soundManager.CurrentMusicType = SoundManager.MusicType.MusicNone;
                return;
            }
            if (SelectedProvince is ProvinceId pid) {
                var province = Model.Provinces[pid];
                if (province.RulingFactionId is FactionId fid && fid == Model.PlayerId &&
                    province.FullInfo.RulerIsVisitingTown) {
                    soundManager.CurrentMusicType = SoundManager.MusicType.MusicVisitTown;
                    return;
                }
                if (Model.AvailableCommandsForProvince(pid).Exists(
                            x => x.HandleCapturedHeroCommand != null)) {
                    soundManager.CurrentMusicType = SoundManager.MusicType.MusicVictory;
                    return;
                }
            }

            if (Model.RunningShardokGameModels.Count > 0) {
                soundManager.CurrentMusicType = SoundManager.MusicType.MusicBattle;
            } else {
                switch (Model.CurrentDate.Month) {
                    case 12:
                    case 1:
                    case 2:
                        soundManager.CurrentMusicType = SoundManager.MusicType.MusicWinter;
                        break;

                    case 3:
                    case 4:
                    case 5:
                        soundManager.CurrentMusicType = SoundManager.MusicType.MusicSpring;
                        break;

                    case 6:
                    case 7:
                    case 8:
                        soundManager.CurrentMusicType = SoundManager.MusicType.MusicSummer;
                        break;

                    case 9:
                    case 10:
                    case 11:
                        soundManager.CurrentMusicType = SoundManager.MusicType.MusicAutumn;
                        break;

                    default:
                        soundManager.CurrentMusicType = SoundManager.MusicType.MusicSpring;
                        break;
                }
            }
        }

        void SetHeaderString() {
            var date = Model.CurrentDate;
            if (date != null) {
                string dateString = DisplayNames.DateString(date);
                if (Model.PlayerId.HasValue) {
                    var faction = Model.MaybeDestroyedFaction(Model.PlayerId.Value);
                    var factionLeaderName = DynamicTextTemplate.Placeholder(
                            DisplayNames.FactionTextPlaceholders.FactionLeaderName);
                    var prestige = DynamicTextTemplate.Placeholder(
                            DisplayNames.FactionTextPlaceholders.Prestige);
                    factionTextUpdater.SetFactionTextWithCustomFormat(
                            roundStatusLabel,
                            faction,
                            Model,
                            $"{dateString} - {factionLeaderName} ({prestige})",
                            $"{dateString} - Observer");
                } else {
                    roundStatusLabel.text = $"{dateString} - Observer";
                }
            }
        }

        void SwapModel() {
            if (_newModel == Model) { return; }

            showChronicleButton.gameObject.SetActive(
                    _newModel != null && _newModel.ChronicleEntries.Count > 1);

            if (_newModel != null &&
                _newModel.ChronicleEntries.Count != chronicleCanvasController.Entries.Count) {
                chronicleCanvasController.Entries = _newModel.ChronicleEntries;
            }

            var hasExistingCommands = Model != null && Model.AvailableCommandsByProvince.Any();
            var hasIncomingCommands =
                    _newModel != null && _newModel.AvailableCommandsByProvince.Any();
            var commandTokenChanged = Model != null && _newModel != null &&
                                      Model.CommandToken != _newModel.CommandToken;
            var strategicUpdateChanged =
                    Model != null && _newModel != null &&
                    Model.StrategicUpdateRevision != _newModel.StrategicUpdateRevision;
            var availableCommandsChanged =
                    Model != null && _newModel != null && !SameAvailableCommands(Model, _newModel);

            // Preserve the user's selection across non-strategic refreshes. A result-bearing
            // strategic update must still flow through even when its command token is unchanged:
            // automatic results can change visible state without changing available commands.
            if (hasExistingCommands && hasIncomingCommands && !commandTokenChanged &&
                !strategicUpdateChanged) {
                mapController.UpdateBattleEffects(_newModel);
                UpdateBattleProgressDisplay(_newModel);
                return;
            }

            var oldModel = Model;
            var activeCommandStillAvailable = CommandStillAvailable(
                    _newModel,
                    SelectedProvince,
                    _commandPanelController.AvailableCommand);
            Model = _newModel;

            if (availableCommandsChanged && !commandTokenChanged) {
                LogCommandTokenInvariantViolation(oldModel, _newModel);
            }

            provinceInfoPanelController.Model = _newModel;
            freeHeroesTableController.Model = _newModel;
            movingArmiesTableControllerNarrow.Model = _newModel;
            movingArmiesTableControllerWide.Model = _newModel;
            factionsTableControllerNarrow.Model = _newModel;
            factionsTableControllerWide.Model = _newModel;
            heroesAndBattalionsPanelController.Model = _newModel;
            _commandPanelController.Model = _newModel;
            dominionPanelControllerNarrow.Model = _newModel;
            dominionPanelControllerWide.Model = _newModel;
            mapController.Model = _newModel;

            if (activeCommandStillAvailable) {
                _commandPanelController.Selector.UpdateModel(Model);
            }

            if (Model == null) {
                battleProgressPanel.gameObject.SetActive(false);
                return;
            }

            if (oldModel == null) {
                // Initialize province ID map from server-provided dimensions and hash
                if (Model.EagleMap != null) { provinceIDLoader.Initialize(Model.EagleMap); }
            }

            // Give server-authored tutorial dialogue the latest model for entity resolution.
            TutorialManager.Instance?.OnModelUpdated(Model, oldModel);

            PrefetchHeadshots(Model);

            SetMusic();

            if (!activeCommandStillAvailable &&
                (oldModel == null || commandTokenChanged || availableCommandsChanged) &&
                Model.AvailableCommandsByProvince.Count > 0) {
                if (oldModel != null && oldModel.AvailableCommandsByProvince.Count > 0 &&
                    (commandTokenChanged || availableCommandsChanged)) {
                    ClearActiveCommandUi();
                }

                var firstCommand = Model.AvailableCommandsByProvince.First().Value.Commands.First();
                switch (firstCommand.SealedValueCase) {
                    case AvailableCommand.SealedValueOneofCase.ResolveTruceOfferCommand:
                        resolveTruceCommandSelector.Model = Model;
                        resolveTruceCommandSelector.AvailableCommand = firstCommand;
                        ClearActiveCommandUi();
                        mapController.SelectedProvinceId = null;
                        break;

                    case AvailableCommand.SealedValueOneofCase.ResolveAllianceOfferCommand:
                        resolveAllianceCommandSelector.Model = Model;
                        resolveAllianceCommandSelector.AvailableCommand = firstCommand;
                        ClearActiveCommandUi();
                        mapController.SelectedProvinceId = null;
                        break;

                    case AvailableCommand.SealedValueOneofCase.ResolveBreakAllianceCommand:
                        resolveBreakAllianceCommandSelector.Model = Model;
                        resolveBreakAllianceCommandSelector.AvailableCommand = firstCommand;
                        ClearActiveCommandUi();
                        mapController.SelectedProvinceId = null;
                        break;

                    case AvailableCommand.SealedValueOneofCase.ResolveInvitationCommand:
                        resolveInvitationCommandSelector.Model = Model;
                        resolveInvitationCommandSelector.AvailableCommand = firstCommand;
                        ClearActiveCommandUi();
                        mapController.SelectedProvinceId = null;
                        break;

                    case AvailableCommand.SealedValueOneofCase.ResolveRansomOfferCommand:
                        resolveRansomOfferCommandSelector.Model = Model;
                        resolveRansomOfferCommandSelector.AvailableCommand = firstCommand;
                        ClearActiveCommandUi();
                        mapController.SelectedProvinceId = null;
                        break;

                    case AvailableCommand.SealedValueOneofCase.PleaseRecruitMeCommand:
                        pleaseRecruitMeCommandSelector.Model = Model;
                        pleaseRecruitMeCommandSelector.AvailableCommand = firstCommand;
                        ClearActiveCommandUi();
                        mapController.SelectedProvinceId = null;
                        break;

                    default:
                        // most command types will be here
                        var newlySelectedProvinceId = NextActiveProvinceKeyPair?.Key;
                        mapController.SelectedProvinceId = newlySelectedProvinceId;
                        break;
                }

                // Ensure notifications appear on top of blocking commands
                if (notificationPanel.HasInfo) { notificationPanel.transform.SetAsLastSibling(); }
            }

            freeHeroesTableController.UpdateTables();
            movingArmiesTableControllerNarrow.UpdateTables();
            movingArmiesTableControllerWide.UpdateTables();
            factionsTableControllerNarrow.UpdateTables();
            factionsTableControllerWide.UpdateTables();
            heroesAndBattalionsPanelController.UpdateTables();

            Canvas.ForceUpdateCanvases();
            SetHeaderString();

            UpdateBattleProgressDisplay(Model);

            _newModel = null;
            SetNextActiveProvinceButton();

            if (Model.AvailableCommandsByProvince.Count > 0 &&
                Model.AvailableCommandsByProvince.ContainsKey(0)) {
                mapController.ProvincesWithCommands = new List<ProvinceId>();
            } else {
                mapController.ProvincesWithCommands =
                        Model.AvailableCommandsByProvince.Keys.ToList();
            }
        }

        private static bool SameAvailableCommands(IGameModel a, IGameModel b) {
            if (a.AvailableCommandsByProvince.Count != b.AvailableCommandsByProvince.Count) {
                return false;
            }

            foreach (var kv in a.AvailableCommandsByProvince) {
                if (!b.AvailableCommandsByProvince.TryGetValue(kv.Key, out var commands) ||
                    !kv.Value.Equals(commands)) {
                    return false;
                }
            }

            return true;
        }

        private static bool
        CommandStillAvailable(IGameModel model, ProvinceId? provinceId, AvailableCommand command) {
            return model != null && provinceId is ProvinceId pid && command != null &&
                   model.AvailableCommandsByProvince.TryGetValue(pid, out var commands) &&
                   commands.Commands.Contains(command);
        }

        private static void LogCommandTokenInvariantViolation(
                IGameModel oldModel,
                IGameModel newModel) {
            string str = "Available commands changed without command token advancement. Token " +
                         oldModel.CommandToken;

            var oldProvinces = oldModel.AvailableCommandsByProvince.Keys.ToHashSet();
            var newProvinces = newModel.AvailableCommandsByProvince.Keys.ToHashSet();

            var oldNotNew = oldProvinces.Except(newProvinces).ToList();
            if (oldNotNew.Any()) {
                str += $"\nWe used to have commands for provinces {string.Join(", ", oldNotNew)} but now we don't";
            }

            var newNotOld = newProvinces.Except(oldProvinces).ToList();
            if (newNotOld.Any()) {
                str += $"\nWe now have commands for provinces {string.Join(", ", newNotOld)} where we didn't before";
            }

            foreach (var kv in oldModel.AvailableCommandsByProvince) {
                if (!newModel.AvailableCommandsByProvince.TryGetValue(
                            kv.Key,
                            out var newCommands)) {
                    continue;
                }

                var oldCommands = kv.Value;
                if (oldCommands.Commands.Count != newCommands.Commands.Count) {
                    str += $"\nWe had {oldCommands.Commands.Count} but now we have {newCommands.Commands.Count}";
                }

                foreach (var cmd in oldCommands.Commands) {
                    if (!newCommands.Commands.Contains(cmd)) {
                        str += $"\nWe had {cmd} but now we don't";
                    }
                }

                foreach (var cmd in newCommands.Commands) {
                    if (!oldCommands.Commands.Contains(cmd)) {
                        str += $"\nWe have {cmd}, we didn't before";
                    }
                }
            }

            Debug.LogError(str);
        }

        private void UpdateBattleProgressDisplay(IGameModel model) {
            var hasBattles = model.ShardokBattles.Any();
            commandPanel.SetActive(!hasBattles);
            battleProgressPanel.gameObject.SetActive(hasBattles);

            if (hasBattles) {
                battleProgressPanel.UpdateBattles(model.ShardokBattles, model, model.BattleDay);
            }
        }

        void ModelUpdated(GameModelUpdater modelUpdater, IGameModel nm) {
            MainQueue.Q.Enqueue(() => {
                if (ModelUpdater != modelUpdater) { return; }
                _newModel = nm;
            });
        }

        private void PostCommittedCommand(ProvinceId provinceId, SelectedCommand selectedCommand) {
            TutorialManager.Instance?.DialogueManager?.OnPlayerAction();
            ModelUpdater.PostCommand(provinceId: provinceId, command: selectedCommand)
                    .ContinueWith(response => {
                        if (response.IsFaulted) { errorHandler.Add(response.Exception); }
                    });
        }

        private void DoCommitCommand() {
            PostCommittedCommand(
                    provinceId: mapController.SelectedProvinceId.GetValueOrDefault(),
                    _commandPanelController.SelectedCommand);
            ClearActiveCommandUi();
            freeHeroesTableController.UpdateTables();
            movingArmiesTableControllerNarrow.UpdateTables();
            movingArmiesTableControllerWide.UpdateTables();
            factionsTableControllerNarrow.UpdateTables();
            factionsTableControllerWide.UpdateTables();
            heroesAndBattalionsPanelController.UpdateTables();
        }

        public void ChronicleButtonClicked() {
            chronicleCanvasController.gameObject.SetActive(true);
        }

        public void CommitCommandButtonClicked() {
            commandWarningPanelController.ClearWarning();
            DoCommitCommand();
        }

        public async void GoToBattle() {
            // End any active dialogue before transitioning to battle
            TutorialManager.Instance?.DialogueManager?.EndDialogueWithoutCompletion();
            TutorialManager.Instance?.DialogueManager?.OnPlayerAction();

            if (Model == null) { throw new InvalidOperationException("GoToBattle: Model is null"); }

            ShardokGameModel selectedModel;

            var runningBattles = Model.RunningShardokGameModels.Values;
            var myBattles = runningBattles.Where(sgm => sgm.PlayerId != -1);
            if (myBattles.Any()) {
                selectedModel = myBattles.First();
            } else if (runningBattles.Any()) {
                selectedModel = runningBattles.First();
            } else {
                throw new InvalidOperationException(
                        "GoToBattle: No battles in RunningShardokGameModels");
            }

            if (!Model.ShardokGameModels.TryGetValue(
                        selectedModel.ShardokGameId,
                        out var shardokModel)) {
                Debug.LogWarning(
                        $"GoToBattle: Shardok model {selectedModel.ShardokGameId} is not ready");
                return;
            }
            await ShowShardokBattle(shardokModel);
        }

        public async void GoToBattle(string shardokGameId) {
            TutorialManager.Instance?.DialogueManager?.EndDialogueWithoutCompletion();
            TutorialManager.Instance?.DialogueManager?.OnPlayerAction();
            battleProgressPanel.gameObject.SetActive(false);

            if (Model == null) { throw new InvalidOperationException("GoToBattle: Model is null"); }

            if (!Model.ShardokGameModels.TryGetValue(shardokGameId, out var shardokModel)) {
                Debug.LogWarning($"GoToBattle: Shardok model {shardokGameId} is not ready");
                battleProgressPanel.gameObject.SetActive(true);
                return;
            }
            await ShowShardokBattle(shardokModel);
        }

        private async System.Threading.Tasks.Task ShowShardokBattle(ShardokGameModel shardokModel) {
            shardokContainer = GameSceneManager.Instance == null
                                       ? shardokContainer
                                       : await GameSceneManager.Instance.TransitionToShardok();

            if (shardokContainer == null) {
                throw new InvalidOperationException("GoToBattle: shardokContainer is null");
            }

            shardokContainer.SetActive(true);
            var shardokController = shardokContainer.GetComponent<ShardokGameController>();
            if (shardokController == null) {
                throw new InvalidOperationException(
                        "GoToBattle: shardokContainer missing ShardokGameController component");
            }

            PersistentUIManager.Instance?.connectionStatusUI?.SetShardokGameStateProvider(
                    shardokModel);
            shardokController.SetUpGame(shardokModel);
            gameObject.SetActive(false);
        }

        public void GameIdButtonClicked() {
            var textField = gameIdButton.GetComponentInChildren<Text>();
            textField.text = textField.text == "" ? ModelUpdater.GameId.ToString("X") : "";
        }

        private void OnDestroy() {
            TearDownModelUpdater();

            factionTextUpdater?.Dispose();
        }

        private void TearDownModelUpdater() {
            var modelUpdater = ModelUpdater;
            ModelUpdater = null;
            modelUpdater?.Dispose();
        }
    }
}
