using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Auth;
using common;
using common.GUIUtils;
using common.WhatsNew;
using eagle;
using Grpc.Core;
using Grpc.Core.Interceptors;
using Net.Eagle0.Eagle.Api;
using Net.Eagle0.Eagle.Api.Auth;
using Net.Eagle0.Eagle.Common;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
using Object = System.Object;

public class AuthInterceptor : Interceptor {
    private readonly string headerName = "Authorization";
    private string headerValue;

    public static string HeaderValue(string name, string password) {
        String toEncode = name + ":" + password;
        return Convert.ToBase64String(Encoding.UTF8.GetBytes(toEncode));
    }

    public AuthInterceptor(String name, String password) {
        headerValue = "Basic " + HeaderValue(name, password);
    }
    public override TResponse BlockingUnaryCall<TRequest, TResponse>(
            TRequest request,
            ClientInterceptorContext<TRequest, TResponse> context,
            BlockingUnaryCallContinuation<TRequest, TResponse> continuation) {
        return continuation(request, ContextWithHeaders(context));
    }
    public override AsyncUnaryCall<TResponse> AsyncUnaryCall<TRequest, TResponse>(
            TRequest request,
            ClientInterceptorContext<TRequest, TResponse> context,
            AsyncUnaryCallContinuation<TRequest, TResponse> continuation) {
        return continuation(request, ContextWithHeaders(context));
    }
    public override AsyncServerStreamingCall<TResponse>
    AsyncServerStreamingCall<TRequest, TResponse>(
            TRequest request,
            ClientInterceptorContext<TRequest, TResponse> context,
            AsyncServerStreamingCallContinuation<TRequest, TResponse> continuation) {
        return continuation(request, ContextWithHeaders(context));
    }
    public override AsyncClientStreamingCall<TRequest, TResponse>
    AsyncClientStreamingCall<TRequest, TResponse>(
            ClientInterceptorContext<TRequest, TResponse> context,
            AsyncClientStreamingCallContinuation<TRequest, TResponse> continuation) {
        return continuation(ContextWithHeaders(context));
    }
    public override AsyncDuplexStreamingCall<TRequest, TResponse>
    AsyncDuplexStreamingCall<TRequest, TResponse>(
            ClientInterceptorContext<TRequest, TResponse> context,
            AsyncDuplexStreamingCallContinuation<TRequest, TResponse> continuation) {
        return continuation(ContextWithHeaders(context));
    }

    private ClientInterceptorContext<TRequest, TResponse> ContextWithHeaders<TRequest, TResponse>(
            ClientInterceptorContext<TRequest, TResponse> original)
            where TRequest : class
            where TResponse : class {
        var headers = new Metadata();
        headers.Add(headerName, headerValue);

        return new ClientInterceptorContext<TRequest, TResponse>(
                original.Method,
                original.Host,
                original.Options.WithHeaders(headers));
    }
};

public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
    public TMP_Dropdown resolutionDropdown;

    [Header("OAuth UI")]
    public GameObject oauthPanel;
    public Button discordLoginButton;
    public Button googleLoginButton;
    public Button githubLoginButton;
    public Button appleLoginButton;
    public Button steamLoginButton;
    public Button twitchLoginButton;
    public TextMeshProUGUI oauthStatusText;

    [Header("Stored Accounts")]
    public GameObject storedAccountsContainer;
    public StoredAccountButton storedAccountButtonPrefab;
    public Sprite discordProviderIcon;
    public Sprite googleProviderIcon;
    public Sprite githubProviderIcon;
    public Sprite appleProviderIcon;
    public Sprite steamProviderIcon;
    public Sprite twitchProviderIcon;

    [Header("Status Display")]
    public TextMeshProUGUI connectionStatusText;

    [Header("Connection Panel Environment")]
    [Tooltip("Connect to QA button in connection panel (editor-only)")]
    public Button connectToQAButton;

    public GameObject connectionPanel;
    public GameObject connectionBackgroundLayer;
    public GameObject gameSelectionPanel;
    public GameObject customBattlePanel;
    public ErrorHandler errorHandler;

    [Header("Lobby Controls")]
    public Button logoutButton;
    public Button customBattleButton;
    public Button cancelCustomBattleButton;
    public Button lobbyConnectToQAButton;
    public TextMeshProUGUI lobbyUserText;
    public TextMeshProUGUI lobbyStatusText;

    public GameObject runningGamesListArea;
    public GameObject runningGamesListItemPrefab;

    public GameObject availableGamesListArea;
    public GameObject availableGamesListItemPrefab;
    public GameObject waitingGamesListItemPrefab;
    public CreateGameItem createGameItem;
    public TMP_Dropdown tutorialPhaseDropdown;
    public Button startTutorialButton;

    public DropGameConfirmationPanel dropGameConfirmationPanel;

    public Canvas connectionCanvas;
    public Canvas eagleCanvas;
    public GameObject shardokContainer;
    public CustomBattleHandler customBattleHandler;

    List<GameInfo> fetchedRunningGames;
    List<AvailableLeader> fetchedNewGameLeaders;
    private Task<EagleGameController> _eagleScenePreloadTask;
    private long _acceptedGameId;
    private EagleConnection eagleConnection;
    private PersistentClientConnection _persistentClientConnection;
    private HttpClient _httpClient;
    private CancellationTokenSource _cancellationTokenSource;
    private readonly Object pendingReplyLock = new Object();
    private int _connectedEnvironmentIndex = -1;  // -1 means not connected
    private bool _isLocalQAConnection = false;
    private List<StoredAccountButton> _storedAccountButtons = new();
    private bool _exitedFullscreenForNewUser = false;  // Track if we exited fullscreen for login
    private FullScreenMode _previousFullScreenMode;    // Store mode to restore after login
    private int _previousWidth;                        // Store resolution to restore after login
    private int _previousHeight;

    private const string InvitationSignInHint =
            "Came from an invitation? Check your browser for a 'Complete Sign-In' button.";
    private const string CheckingStoredSessionText = "Checking saved session...";
    private const string ExpiredStoredSessionText =
            "Session expired. Tap your account to sign in again.";

    const String BaseDomain = "eagle0.net";
    const String EnvironmentKey = "environmentKey";

    // Environment options: index 0 = prod., index 1 = qa., index 2 = (none)
    private static readonly List<string> EnvironmentOptions = new() { "prod.", "qa.", "" };
    private static readonly List<string> EnvironmentDisplayNames =
            new() { "prod.", "qa.", "(none)" };

    /// <summary>
    /// Returns the connected environment name ("prod." or "qa.") or null if not connected.
    /// </summary>
    public string ConnectedEnvironmentName =>
            _isLocalQAConnection ? "QA (local)"
            : _connectedEnvironmentIndex >= 0 &&
                            _connectedEnvironmentIndex < EnvironmentDisplayNames.Count
                    ? EnvironmentDisplayNames[_connectedEnvironmentIndex]
                    : null;

    private string GetUrlFromEnvironment() {
        int envIndex = PlayerPrefs.GetInt(EnvironmentKey, 0);
        string prefix = EnvironmentOptions[envIndex];
        return prefix + BaseDomain;
    }

    /// <summary>
    /// Get full URL with scheme for the selected environment (Eagle server).
    /// Remote servers use HTTPS, could be extended for local HTTP.
    /// </summary>
    private string GetEagleUrl() { return "https://" + GetUrlFromEnvironment(); }

    /// <summary>
    /// Get full URL with scheme for the Go auth service.
    /// Always uses prod auth service - OAuth tokens work across all environments.
    /// </summary>
    private string GetAuthServiceUrl() { return "https://prod.eagle0.net:40033"; }

    public void ReceiveLobbyUpdate(LobbyResponse lobbyResponse) {
        _handleLobbyResponse(lobbyResponse);
    }

    void Start() {
        MainQueue.EnsureExists();
        EnsureOAuthManager();

        var currentResolution = Screen.currentResolution;
        var resolutions = Screen.resolutions;

        resolutionDropdown.ClearOptions();
        resolutionDropdown.AddOptions(resolutions.Select(r => $"{r.width} x {r.height}").ToList());
        resolutionDropdown.value = resolutions.ToList().IndexOf(currentResolution);

        ShowConnectionPanel();

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

        // Initialize OAuth UI
        SetupOAuthUI();

        // Initialize connection panel QA button (editor-only)
        SetupConnectToQAButton();

        // Initialize Lobby UI (logout button, etc.)
        SetupLobbyUI();

        // Initialize status text
        UpdateConnectionStatus();

        // Configure bug report webhook
        BugReportPanelController.SetWebhookUrl(
                "https://discord.com/api/webhooks/1468260773187944448/PdjrzE3CXc1m58VbubUugvJwXDSKdkgaRI4GjgHcxMDW5D4L-JkDaeziLRnCgr972H6J");

        // Try to restore existing OAuth session
        TryRestoreSession();
    }

    private static void EnsureOAuthManager() {
        if (OAuthManager.Instance != null) return;

        if (FindAnyObjectByType<OAuthManager>(FindObjectsInactive.Include) != null) return;

        var oauthManager = new GameObject("OAuth Manager");
        oauthManager.AddComponent<OAuthManager>();
    }

    private void SetupConnectToQAButton() {
        if (connectToQAButton == null) return;

        // Only show QA button in Unity Editor; always use prod in builds
        if (!Application.isEditor) {
            connectToQAButton.gameObject.SetActive(false);
            PlayerPrefs.SetInt(EnvironmentKey, 0);  // Force prod
            return;
        }

        connectToQAButton.onClick.AddListener(OnConnectToQAClicked);
    }

    private void SetupOAuthUI() {
        // Set up OAuth button click handlers for new sign-ins
        if (discordLoginButton != null) {
            discordLoginButton.onClick.AddListener(
                    () => OnOAuthLoginClicked(OAuthProvider.Discord));
        }
        if (googleLoginButton != null) {
            googleLoginButton.onClick.AddListener(() => OnOAuthLoginClicked(OAuthProvider.Google));
        }
        if (githubLoginButton != null) {
            githubLoginButton.onClick.AddListener(() => OnOAuthLoginClicked(OAuthProvider.Github));
        }
        if (appleLoginButton != null) {
            appleLoginButton.onClick.AddListener(() => OnOAuthLoginClicked(OAuthProvider.Apple));
        }
        if (steamLoginButton != null) {
            steamLoginButton.onClick.AddListener(() => OnOAuthLoginClicked(OAuthProvider.Steam));
        }
        if (twitchLoginButton != null) {
            twitchLoginButton.onClick.AddListener(() => OnOAuthLoginClicked(OAuthProvider.Twitch));
        }

        // Subscribe to OAuthManager events
        if (OAuthManager.Instance != null) {
            OAuthManager.Instance.OnLoginSuccess += OnOAuthLoginSuccess;
            OAuthManager.Instance.OnLoginFailed += OnOAuthLoginFailed;
            OAuthManager.Instance.OnLogout += OnOAuthLogout;
        }

        // Show auth panel with stored accounts
        ShowAuthPanel();
    }

    private void ShowAuthPanel() {
        // Show OAuth panel
        if (oauthPanel != null) oauthPanel.SetActive(true);

        // Refresh stored account buttons
        RefreshStoredAccountButtons();

        // Show a sign-in hint for new users who have no stored accounts.
        var accounts = OAuthManager.Instance?.GetStoredAccounts();
        if (accounts == null || accounts.Count == 0) {
            // Show hint for users coming from invitation flow
            SetOAuthStatusText(InvitationSignInHint);
            // Exit fullscreen so user can see browser with the sign-in link
            if (Screen.fullScreen) {
                _exitedFullscreenForNewUser = true;
                _previousFullScreenMode = Screen.fullScreenMode;
                _previousWidth = Screen.width;
                _previousHeight = Screen.height;
                Screen.fullScreen = false;
                Debug.Log(
                        $"[ConnectionHandler] Exited fullscreen for new user sign-in flow (was {_previousWidth}x{_previousHeight})");
            }
        } else {
            SetOAuthStatusText(
                    TokenStorage.HasValidToken ? CheckingStoredSessionText
                                               : ExpiredStoredSessionText);
        }
    }

    private void SetOAuthStatusText(string text) {
        if (oauthStatusText != null) { oauthStatusText.text = text; }
    }

    private static string StoredAccountButtonLabel(StoredAccount account) {
        if (account.HasValidToken) { return account.DisplayName; }
        return $"{account.DisplayName} (sign in again)";
    }

    private void RefreshStoredAccountButtons() {
        foreach (var storedAccountButton in _storedAccountButtons) {
            if (storedAccountButton == null) continue;

            storedAccountButton.gameObject.SetActive(false);
            Destroy(storedAccountButton.gameObject);
        }
        _storedAccountButtons.Clear();

        if (storedAccountsContainer == null || storedAccountButtonPrefab == null) return;

        // Get stored accounts
        var accounts = OAuthManager.Instance?.GetStoredAccounts();
        if (accounts == null || accounts.Count == 0) return;

        // Create button for each stored account
        foreach (var account in accounts) {
            var buttonObj =
                    Instantiate(storedAccountButtonPrefab, storedAccountsContainer.transform);
            buttonObj.transform.localScale = Vector3.one;

            // Set button text (just display name, icon shows provider)
            buttonObj.label.text = StoredAccountButtonLabel(account);

            var providerImage = buttonObj.icon;
            if (providerImage != null) {
                var providerLower = account.Provider.ToLowerInvariant();
                providerImage.sprite = providerLower switch { "google" => googleProviderIcon,
                                                              "github" => githubProviderIcon,
                                                              "apple" => appleProviderIcon,
                                                              "steam" => steamProviderIcon,
                                                              "twitch" => twitchProviderIcon,
                                                              _ => discordProviderIcon };
            }

            // Set click handler for sign-in
            var button = buttonObj.GetComponent<Button>();
            if (button != null) {
                var accountCopy = account;  // Capture for lambda
                button.onClick.AddListener(() => OnStoredAccountClicked(accountCopy));
            }

            // Set click handler for delete button
            if (buttonObj.deleteButton != null) {
                var accountCopy = account;  // Capture for lambda
                buttonObj.deleteButton.onClick.AddListener(
                        () => OnDeleteStoredAccount(accountCopy));
            }

            _storedAccountButtons.Add(buttonObj);
        }
    }

    private async void OnStoredAccountClicked(StoredAccount account) {
        // Configure OAuthManager with URLs
        OAuthManager.Instance.SetServerUrls(GetAuthServiceUrl(), GetEagleUrl());

        if (oauthStatusText != null) {
            oauthStatusText.text = $"Connecting as {account.DisplayName}...";
        }

        var success = await OAuthManager.Instance.ConnectWithStoredAccountAsync(account);
        if (success) {
            // OnLoginSuccess will handle connection to lobby
        } else {
            // Token refresh failed — auto re-authenticate with the same provider
            var provider = ParseOAuthProvider(account.Provider);
            if (oauthStatusText != null) {
                oauthStatusText.text =
                        $"Session expired. Signing in again with {account.Provider}...";
            }
            try {
                await OAuthManager.Instance.LoginAsync(provider);
            } catch (Exception ex) {
                Debug.LogWarning($"[ConnectionHandler] Re-auth failed: {ex.Message}");
                if (oauthStatusText != null) {
                    oauthStatusText.text = $"Sign-in failed: {ex.Message}";
                }
            }
        }
    }

    private static OAuthProvider ParseOAuthProvider(string provider) {
        return provider?.ToLowerInvariant() switch {
            "discord" => OAuthProvider.Discord,
            "google" => OAuthProvider.Google,
            "github" => OAuthProvider.Github,
            "apple" => OAuthProvider.Apple,
            "steam" => OAuthProvider.Steam,
            "twitch" => OAuthProvider.Twitch,
            _ => throw new ArgumentException($"Unknown OAuth provider: {provider}")
        };
    }

    private void OnDeleteStoredAccount(StoredAccount account) {
        Debug.Log($"[ConnectionHandler] Deleting stored account: {account.DisplayName}");
        OAuthManager.Instance?.RemoveStoredAccount(account);
        RefreshStoredAccountButtons();
    }

    private static bool IsAdmin => TokenStorage.IsAdmin;

    private static readonly List<string> TutorialPhaseNames =
            new() { "Opening Battle", "Expansion", "Mobilization", "Eagle Appears" };

    private static readonly List<TutorialPhase> TutorialPhaseValues = new() {
        TutorialPhase.OpeningBattle,
        TutorialPhase.Expansion,
        TutorialPhase.Mobilization,
        TutorialPhase.EagleAppears
    };

    private void SetupLobbyUI() {
        customBattleButton.onClick.AddListener(CustomBattle);
        cancelCustomBattleButton.onClick.AddListener(CancelCustomBattle);

        // Set up tutorial phase dropdown + start button (admins + editor)
        bool showTutorial = IsAdmin || Application.isEditor;
        if (tutorialPhaseDropdown != null) {
            tutorialPhaseDropdown.gameObject.SetActive(showTutorial);
            tutorialPhaseDropdown.ClearOptions();
            tutorialPhaseDropdown.AddOptions(TutorialPhaseNames);
        }
        if (startTutorialButton != null) {
            startTutorialButton.gameObject.SetActive(showTutorial);
            startTutorialButton.onClick.AddListener(StartTutorialFromDropdown);
        }

        // Set up QA button (only in Unity Editor)
        if (lobbyConnectToQAButton != null) {
            if (!Application.isEditor) {
                lobbyConnectToQAButton.gameObject.SetActive(false);
            } else {
                lobbyConnectToQAButton.onClick.AddListener(OnLobbyConnectToQAClicked);
            }
        }
    }

    private void UpdateLobbyStatusDisplays() {
        lobbyUserText.text = _isLocalQAConnection ? "editor" : TokenStorage.DisplayName ?? "";
    }

    private void OnConnectToQAClicked() {
        Debug.Log("[ConnectionHandler] Connect to QA clicked");
        _isLocalQAConnection = true;

        SetLobbyActive(true);
        connectionCanvas.enabled = true;

        _createLocalQAConnection();

        if (_persistentClientConnection == null) {
            Debug.LogError("[ConnectionHandler] QA connection creation failed");
            return;
        }

        try {
            RequestMaps();
            StartListeningForLobbyUpdates();
        } catch (Exception e) {
            MainQueue.Q.Enqueue(() => { errorHandler.Add(e); });
        }
    }

    private void OnLobbyConnectToQAClicked() {
        if (_isLocalQAConnection) return;

        Debug.Log("[ConnectionHandler] Lobby: switching to QA (local)");

        // Disconnect from current environment
        _persistentClientConnection?.Dispose();
        eagleConnection?.Dispose();
        _httpClient?.Dispose();

        _persistentClientConnection = null;
        eagleConnection = null;
        _httpClient = null;

        _isLocalQAConnection = true;

        // Clear current game lists while reconnecting
        foreach (Transform row in runningGamesListArea.transform) { Destroy(row.gameObject); }
        foreach (Transform row in availableGamesListArea.transform) { Destroy(row.gameObject); }

        if (lobbyStatusText != null) { lobbyStatusText.text = "Connecting to QA..."; }

        _createLocalQAConnection();
        RequestMaps();
        StartListeningForLobbyUpdates();
    }

    private void _createLocalQAConnection() {
        // Dispose existing connections before creating new ones
        _httpClient?.Dispose();
        _persistentClientConnection?.Dispose();
        eagleConnection?.Dispose();
        _cancellationTokenSource?.Dispose();

        _cancellationTokenSource = new CancellationTokenSource();

        eagleConnection = EagleConnection.CreateInsecure("http://localhost:40032");

        // Plain HttpClient with no auth for local QA — ResourceFetcher still uses prod assets
        _httpClient = new HttpClient();

        _persistentClientConnection = new PersistentClientConnection(
                eagleConnection.EagleGrpcClient,
                eagleConnection.credentials,
                EagleConnection.EagleCancellationToken);
        _persistentClientConnection.OnChannelDead = () => {
            MainQueue.Q.Enqueue(() => {
                Debug.Log("[ConnectionHandler] QA channel dead, recreating connection");
                _createLocalQAConnection();
            });
        };
        _persistentClientConnection.OnCommandError = (errorMessage) => {
            MainQueue.Q.Enqueue(() => {
                Debug.LogError($"[ConnectionHandler] Command rejected by server: {errorMessage}");
            });
        };
        _ = _persistentClientConnection.Connect();

        PersistentClientConnection.Current = _persistentClientConnection;
        errorHandler.PersistentClientConnection = _persistentClientConnection;
    }

    public async void OnLogoutClicked() {
        Debug.Log("[ConnectionHandler] Logout button clicked");

        // Clear OAuth tokens
        if (OAuthManager.Instance != null) { await OAuthManager.Instance.LogoutAsync(); }

        // Disconnect from server
        _persistentClientConnection?.Dispose();
        eagleConnection?.Dispose();
        _httpClient?.Dispose();
        _cancellationTokenSource?.Dispose();

        _persistentClientConnection = null;
        eagleConnection = null;
        _httpClient = null;
        _cancellationTokenSource = null;
        _connectedEnvironmentIndex = -1;
        _isLocalQAConnection = false;

        // Return to connection screen
        ShowConnectionPanel();

        // Re-initialize the auth UI
        ShowAuthPanel();
        UpdateConnectionStatus();
    }

    private async void TryRestoreSession() {
        if (OAuthManager.Instance == null) return;

        // Configure OAuthManager with auth service and Eagle URLs
        OAuthManager.Instance.SetServerUrls(GetAuthServiceUrl(), GetEagleUrl());

        SetOAuthStatusText(CheckingStoredSessionText);

        // TryRestoreSessionAsync() fires OnLoginSuccess if successful, which triggers
        // ConnectWithOAuth() via the OnOAuthLoginSuccess handler. If restore failed,
        // show an appropriate message.
        var restored = await OAuthManager.Instance.TryRestoreSessionAsync();
        if (!restored) {
            var accounts = OAuthManager.Instance.GetStoredAccounts();
            SetOAuthStatusText(
                    accounts != null && accounts.Count > 0 ? ExpiredStoredSessionText : "");
            RefreshStoredAccountButtons();
        }
    }

    private async void OnOAuthLoginClicked(OAuthProvider provider) {
        // Configure OAuthManager with auth service and Eagle URLs (user may have changed dropdown)
        OAuthManager.Instance.SetServerUrls(GetAuthServiceUrl(), GetEagleUrl());

        if (oauthStatusText != null) {
            oauthStatusText.text = $"Opening browser for {provider} login...";
        }

        try {
            await OAuthManager.Instance.LoginAsync(provider);
            // OnLoginSuccess or OnNewUserNeedsDisplayName will be called
        } catch (Exception ex) {
            Debug.LogWarning($"OAuth login failed: {ex.Message}");
            if (oauthStatusText != null) { oauthStatusText.text = $"Login failed: {ex.Message}"; }
        }
    }

    private void OnOAuthLoginSuccess(UserInfo user) {
        MainQueue.Q.Enqueue(() => {
            if (oauthStatusText != null) { oauthStatusText.text = $"Welcome, {user.DisplayName}!"; }
            // Restore fullscreen if we exited it for the new user sign-in flow
            if (_exitedFullscreenForNewUser) {
                _exitedFullscreenForNewUser = false;
                Screen.SetResolution(_previousWidth, _previousHeight, _previousFullScreenMode);
                Debug.Log(
                        $"[ConnectionHandler] Restored fullscreen: {_previousWidth}x{_previousHeight} {_previousFullScreenMode}");
            }

            // Re-evaluate admin-gated tutorial controls now that IsAdmin is up to date
            bool showTutorial = IsAdmin || Application.isEditor;
            if (tutorialPhaseDropdown != null) {
                tutorialPhaseDropdown.gameObject.SetActive(showTutorial);
            }
            if (startTutorialButton != null) {
                startTutorialButton.gameObject.SetActive(showTutorial);
            }

            // Show what's new for returning users
            WhatsNewManager.Instance?.CheckAndShow();

            ConnectWithOAuth();
        });
    }

    private void OnOAuthLoginFailed(string error) {
        MainQueue.Q.Enqueue(() => {
            if (oauthStatusText != null) { oauthStatusText.text = $"Login failed: {error}"; }
        });
    }

    private void OnOAuthLogout() {
        MainQueue.Q.Enqueue(() => {
            ShowAuthPanel();
            if (oauthStatusText != null) { oauthStatusText.text = ""; }
        });
    }

    private void ConnectWithOAuth() { _internalConnectEagle(); }

    private float _statusUpdateTimer = 0f;
    private const float StatusUpdateInterval = 0.5f;

    void Update() {
        // Only update status periodically and when connection panel is visible
        if (!connectionPanel.activeInHierarchy) return;

        _statusUpdateTimer += Time.deltaTime;
        if (_statusUpdateTimer >= StatusUpdateInterval) {
            _statusUpdateTimer = 0f;
            UpdateConnectionStatus();
        }
    }

    private void UpdateConnectionStatus() {
        if (connectionStatusText == null) return;

        // No connection yet - show nothing
        if (_persistentClientConnection == null) {
            connectionStatusText.text = "";
            return;
        }

        var circuitState = _persistentClientConnection.CircuitBreaker.CurrentState;
        var connState = _persistentClientConnection.CurrentState;

        // Circuit breaker takes precedence
        if (circuitState == eagle.ConnectionCircuitBreaker.State.Open) {
            var nextTest = _persistentClientConnection.CircuitBreaker.NextTestAttempt;
            if (nextTest.HasValue) {
                var timeUntilTest = nextTest.Value - DateTime.UtcNow;
                if (timeUntilTest.TotalSeconds > 0) {
                    int seconds = Math.Min(10, (int)Math.Ceiling(timeUntilTest.TotalSeconds));
                    connectionStatusText.text =
                            $"<color=red>●</color> Server unavailable. Retry in {seconds}s";
                    return;
                }
            }
            connectionStatusText.text = "<color=red>●</color> Server unavailable";
            return;
        }

        if (circuitState == eagle.ConnectionCircuitBreaker.State.HalfOpen) {
            connectionStatusText.text = "<color=yellow>●</color> Testing connection...";
            return;
        }

        // Normal connection states
        connectionStatusText.text = connState switch {
            eagle.ConnectionState.Connected => "<color=green>●</color> Connected",
            eagle.ConnectionState.Connecting => "<color=yellow>●</color> Connecting...",
            eagle.ConnectionState.Disconnected => "<color=red>●</color> Disconnected",
            eagle.ConnectionState.Reconnecting => GetReconnectingText(),
            eagle.ConnectionState.SubscriptionPending => "<color=yellow>●</color> Subscribing...",
            _ => ""
        };
    }

    private string GetReconnectingText() {
        var nextAttempt = _persistentClientConnection?.NextReconnectAttempt;
        if (!nextAttempt.HasValue) return "<color=yellow>●</color> Reconnecting...";

        var timeUntilRetry = nextAttempt.Value - DateTime.UtcNow;
        if (timeUntilRetry.TotalSeconds <= 0) { return "<color=yellow>●</color> Reconnecting..."; }

        int seconds = Math.Min(10, (int)Math.Ceiling(timeUntilRetry.TotalSeconds));
        return $"<color=orange>●</color> Retry in {seconds}s";
    }

    public void ConnectEagle() { _internalConnectEagle(); }

    public void CustomBattle() { _internalCustomBattle(); }

    public void CancelCustomBattle() {
        ShowLobbyPanel();
        StartListeningForLobbyUpdates();
    }

    public void OnApplicationQuit() { Dispose(); }

    public void Dispose() {
        // Cancel any ongoing operations using cancellation token
        _cancellationTokenSource?.Cancel();

        _cancellationTokenSource?.Dispose();
        _cancellationTokenSource = null;

        _httpClient?.Dispose();
        _httpClient = null;
        _persistentClientConnection?.Dispose();
        _persistentClientConnection = null;
        eagleConnection?.Dispose();
        eagleConnection = null;

        _connectedEnvironmentIndex = -1;
        _isLocalQAConnection = false;
    }

    public void OnResolutionChanged(int dropdownIndex) {
        var newResolution = Screen.resolutions[dropdownIndex];
        Screen.SetResolution(
                newResolution.width,
                newResolution.height,
                FullScreenMode.FullScreenWindow,
                newResolution.refreshRateRatio);
    }

    private void _handleLobbyResponse(LobbyResponse lobbyResponse) {
        fetchedRunningGames = new List<GameInfo>();
        fetchedRunningGames.AddRange(lobbyResponse.RunningGames);

        fetchedNewGameLeaders = new List<AvailableLeader>();
        fetchedNewGameLeaders.AddRange(lobbyResponse.NewGameOptions.AvailableLeaders);

        MainQueue.Q.Enqueue(() => {
            // Skip lobby UI updates if we've already left the connection UI.
            // The lobby response may arrive after the game has started (e.g., when creating a new
            // game), and the connection canvas is inactive, causing GetComponentInParent to fail.
            // This check must be inside the queue callback since the state may change between
            // when the response arrives and when this callback executes.
            if (!IsConnectionModeActive()) return;

            ResourceFetcher.headshotFetcher.Prefetch(
                    fetchedNewGameLeaders.Select(a => a.ImagePath));

            ShowLobbyPanel();

            // Update lobby status displays
            UpdateLobbyStatusDisplays();

            // Set up running games table
            foreach (Transform row in runningGamesListArea.transform) { Destroy(row.gameObject); }
            foreach (var runningGame in lobbyResponse.RunningGames
                             .OrderBy(g => g.CreatedTimestampMillis)
                             .ThenBy(g => g.GameId)) {
                var listItem = Instantiate(
                        runningGamesListItemPrefab,
                        runningGamesListArea.transform,
                        false);
                var runningGameItem = listItem.GetComponent<RunningGameItem>();
                runningGameItem.SetRunningGame(
                        runningGame.GameId,
                        runningGame.Leader,
                        runningGame.LastPlayedTimestampMillis,
                        runningGame.GameType);
                runningGameItem.GoCallback = this.SelectEagleGame;
                runningGameItem.DropCallback = this.DropGame;
            }

            // Set up available/waiting games table
            foreach (Transform row in availableGamesListArea.transform) { Destroy(row.gameObject); }

            foreach (var waitingGame in lobbyResponse.WaitingGames) {
                var listItem = Instantiate(
                        waitingGamesListItemPrefab,
                        availableGamesListArea.transform,
                        false);
                listItem.GetComponent<WaitingGameItem>().SetWaitingGame(
                        waitingGame.GameId,
                        string.Format(
                                "{0} / {1}",
                                waitingGame.CurrentHumanPlayerCount,
                                waitingGame.MaxHumanPlayerCount),
                        waitingGame.SelectedLeader);
            }

            createGameItem.SetCreationOptions(fetchedNewGameLeaders);
            createGameItem.CreateCallback = this.CreateGame;

            foreach (var availableGame in lobbyResponse.AvailableGames) {
                var listItem = Instantiate(
                        availableGamesListItemPrefab,
                        availableGamesListArea.transform,
                        false);
                var availableGameItem = listItem.GetComponent<AvailableGameItem>();
                availableGameItem.SetAvailableNewGame(
                        availableGame.GameId,
                        string.Format(
                                "{0} / {1}",
                                availableGame.CurrentHumanPlayerCount,
                                availableGame.MaxHumanPlayerCount),
                        availableGame.AvailableLeaders.ToList(),
                        availableGame.MaxHumanPlayerCount);
                availableGameItem.JoinCallback = this.JoinGame;
                availableGameItem.DropCallback = this.DropGame;
            }

            // Clear loading indicator now that lobby is populated
            if (lobbyStatusText != null) { lobbyStatusText.text = ""; }
        });
    }

    private void StartListeningForLobbyUpdates() {
        lock (pendingReplyLock) {
            _persistentClientConnection.SetLobbySubscriber(this);
            _ = _persistentClientConnection.SendUpdateStreamRequestAsync(
                    new UpdateStreamRequest { EnterLobbyRequest = new EnterLobbyRequest() });
        }
    }

    private void _createConnection() {
        _connectedEnvironmentIndex = PlayerPrefs.GetInt(EnvironmentKey, 0);

        // Dispose existing connections before creating new ones
        _httpClient?.Dispose();
        _persistentClientConnection?.Dispose();
        eagleConnection?.Dispose();
        _cancellationTokenSource?.Dispose();

        // Initialize cancellation token for thread management
        _cancellationTokenSource = new CancellationTokenSource();

        string url = GetUrlFromEnvironment();

        // OAuth JWT-based connection
        if (!TokenStorage.HasValidToken) {
            Debug.LogError("[ConnectionHandler] No valid token available for connection");
            return;
        }

        eagleConnection = EagleConnection.CreateWithJwt(url);

        _httpClient = new HttpClient();
        _httpClient.DefaultRequestHeaders.Authorization =
                new AuthenticationHeaderValue("Bearer", TokenStorage.AccessToken);

        _persistentClientConnection = new PersistentClientConnection(
                eagleConnection.EagleGrpcClient,
                eagleConnection.credentials,
                EagleConnection.EagleCancellationToken,
                TryRefreshAuthenticationOnMainThreadAsync);
        _persistentClientConnection.OnChannelDead = () => {
            // Queue to main thread to avoid threading issues
            MainQueue.Q.Enqueue(() => {
                Debug.Log("[ConnectionHandler] Channel dead, recreating connection");
                _createConnection();
            });
        };
        _persistentClientConnection.OnCommandError = (errorMessage) => {
            // Queue to main thread to display error via ErrorHandler
            MainQueue.Q.Enqueue(() => {
                Debug.LogError($"[ConnectionHandler] Command rejected by server: {errorMessage}");
            });
        };
        _ = _persistentClientConnection.Connect();

        PersistentClientConnection.Current = _persistentClientConnection;
        errorHandler.PersistentClientConnection = _persistentClientConnection;
        ResourceFetcher.SetUpConnection(_httpClient);
    }

    private Task<bool> TryRefreshAuthenticationOnMainThreadAsync() {
        var completion =
                new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);

        MainQueue.Q.Enqueue(async () => {
            try {
                if (OAuthManager.Instance == null) {
                    completion.TrySetResult(false);
                    return;
                }

                await OAuthManager.Instance.EnsureValidTokenAsync();
                completion.TrySetResult(TokenStorage.HasValidToken);
            } catch (Exception ex) {
                Debug.LogWarning($"[ConnectionHandler] Session refresh failed: {ex.Message}");
                completion.TrySetResult(false);
            }
        });

        return completion.Task;
    }

    private void _internalCustomBattle() {
        // Connection already exists when called from lobby
        _persistentClientConnection.SetLobbySubscriber(this);

        SetCustomBattleActive(true);

        ShowCustomBattlePanel();
    }

    private void RequestMaps() {
        var mapNames = HexMapStorage.Instance.MapNames();
        var mapInfos = mapNames.Select(mapName => {
                                   var existingMap = HexMapStorage.Instance.WithName(mapName);
                                   return new HexMapsRequest.Types.OneMapRequestInfo {
                                       MapName = mapName,
                                       MapHash = HexMapStorage.Instance.HashForName(mapName)
                                   };
                               })
                               .ToList();

        var hexMapsRequest = new HexMapsRequest();
        hexMapsRequest.MapInfos.AddRange(mapInfos);

        _ = _persistentClientConnection.SendUpdateStreamRequestAsync(
                new UpdateStreamRequest { HexMapsRequest = hexMapsRequest });
    }

    private void _internalConnectEagle() {
        SetLobbyActive(true);
        connectionCanvas.enabled = true;

        _createConnection();

        // If connection creation failed (e.g., no valid token), don't proceed
        if (_persistentClientConnection == null) {
            Debug.LogError("[ConnectionHandler] Connection creation failed, cannot proceed");
            return;
        }

        try {
            RequestMaps();
            StartListeningForLobbyUpdates();
        } catch (RpcException e) {
            MainQueue.Q.Enqueue(() => { errorHandler.Add(e); });
        } catch (Exception e) {
            MainQueue.Q.Enqueue(() => { errorHandler.Add(e); });
        }
    }

    private void JoinGame(long gameId, string leaderName) {
        var game = _internalJoinEagleGame(gameId, leaderName);
    }

    private void DropGame(long gameId) {
        if (dropGameConfirmationPanel != null) {
            dropGameConfirmationPanel.OnConfirm = ConfirmDropGame;
            dropGameConfirmationPanel.Show(gameId, "Are you sure you want to leave this game?");
        } else {
            // Fallback if no confirmation panel is configured
            ConfirmDropGame(gameId);
        }
    }

    private void ConfirmDropGame(long gameId) { _ = _internalDropGame(gameId); }

    private void CreateGame(string leaderNameTextId, int humanPlayerCount, int totalPlayerCount) {
        var game = _internalCreateGame(leaderNameTextId, humanPlayerCount, totalPlayerCount);
    }

    public void StartTutorialFromDropdown() {
        int index = tutorialPhaseDropdown != null ? tutorialPhaseDropdown.value : 0;
        var phase = index >= 0 && index < TutorialPhaseValues.Count ? TutorialPhaseValues[index]
                                                                    : TutorialPhase.OpeningBattle;
        CreateTutorialGame(phase);
    }

    public void CreateTutorialGame(TutorialPhase startPhase) {
        var game = _internalCreateGame(
                desiredLeaderTextId: "",
                humanPlayerCount: 1,
                totalPlayerCount: 2,
                tutorialStartPhase: startPhase);
    }

    public void QuitButtonClicked() { Application.Quit(); }

    public void HandleJoinGameResponse(JoinGameResponse response) {
        try {
            switch (response.Result) {
                case JoinGameResult.SuccessJoinGameResult: break;

                default:
                    // FIXME: display something
                    break;
            }
        } catch (Exception e) { Console.WriteLine(e.ToString()); }
    }

    public void HandleCreateGameResponse(CreateGameResponse response) {
        try {
            switch (response.Result) {
                case JoinGameResult.SuccessJoinGameResult: SelectEagleGame(response.GameId); break;

                default:
                    Debug.LogError($"Game creation failed: {response.Result}");
                    _ = ResetGameCreation();
                    break;
            }
        } catch (Exception e) { Console.WriteLine(e.ToString()); }
    }

    public void HandleCreateGameAcceptedResponse(CreateGameAcceptedResponse response) {
        _acceptedGameId = response.GameId;
        createGameItem.SetCreating(true);
        if (_eagleScenePreloadTask == null) { _eagleScenePreloadTask = PreloadEagleScene(); }
    }

    public void HandleCustomBattleResponse(CustomBattleResponse response) {
        try {
            customBattleHandler.HandleCustomBattleResponse(response);
            customBattleHandler.SetActive(eagleConnection, _persistentClientConnection);
        } catch (Exception e) { Console.WriteLine(e.ToString()); }
    }

    private async Task<bool> _internalCreateGame(
            string desiredLeaderTextId,
            int humanPlayerCount,
            int totalPlayerCount,
            TutorialPhase tutorialStartPhase = TutorialPhase.Unknown) {
        try {
            return await _persistentClientConnection.SendUpdateStreamRequestAsync(
                    new UpdateStreamRequest {
                        CreateGameRequest =
                                new CreateGameRequest {
                                    DesiredLeaderTextId = desiredLeaderTextId,
                                    TotalPlayerCount = totalPlayerCount,
                                    HumanPlayerCount = humanPlayerCount,
                                    TutorialStartPhase = tutorialStartPhase
                                }
                    });
        } catch (WebException e) {
            Debug.Log(String.Format("Caught exception in BeginGetResponse {0}", e.Message));
            SetLobbyActive(true);
            return false;
        }
    }

    private async Task<bool> _internalJoinEagleGame(long gameId, string leaderNameTextId) {
        try {
            return await _persistentClientConnection.SendUpdateStreamRequestAsync(
                    new UpdateStreamRequest {
                        JoinGameRequest =
                                new JoinGameRequest {
                                    GameId = gameId,
                                    DesiredLeaderTextId = leaderNameTextId
                                }
                    });
        } catch (WebException e) {
            Debug.Log(String.Format("Caught exception in BeginGetResponse {0}", e.Message));
            SetLobbyActive(true);
            return false;
        }
    }

    private async Task<bool> _internalDropGame(long gameId) {
        try {
            return await _persistentClientConnection.SendUpdateStreamRequestAsync(
                    new UpdateStreamRequest {
                        DropGameRequest = new DropGameRequest { GameId = gameId }
                    });
        } catch (WebException e) {
            Debug.Log(String.Format("Caught exception in DropGame {0}", e.Message));
            return false;
        }
    }

    private void SetLobbyActive(bool active) {
        runningGamesListArea.SetActive(active);
        availableGamesListArea.SetActive(active);
    }

    private void SetCustomBattleActive(bool active) {
        customBattleHandler.SetActive(eagleConnection, _persistentClientConnection);
    }

    private bool IsConnectionModeActive() {
        return connectionCanvas.gameObject.activeSelf && connectionCanvas.enabled;
    }

    private void ShowConnectionPanel() {
        connectionCanvas.gameObject.SetActive(true);
        connectionCanvas.enabled = true;
        HideGamePanels();

        connectionPanel.gameObject.SetActive(true);
        if (connectionBackgroundLayer != null) { connectionBackgroundLayer.SetActive(true); }
        gameSelectionPanel.gameObject.SetActive(false);
        customBattlePanel.gameObject.SetActive(false);
    }

    private void ShowLobbyPanel() {
        connectionCanvas.gameObject.SetActive(true);
        connectionCanvas.enabled = true;
        HideGamePanels();

        connectionPanel.gameObject.SetActive(false);
        if (connectionBackgroundLayer != null) { connectionBackgroundLayer.SetActive(false); }
        gameSelectionPanel.gameObject.SetActive(true);
        customBattlePanel.gameObject.SetActive(false);
    }

    private void ShowCustomBattlePanel() {
        connectionCanvas.gameObject.SetActive(true);
        connectionCanvas.enabled = true;
        HideGamePanels();

        connectionPanel.gameObject.SetActive(false);
        if (connectionBackgroundLayer != null) { connectionBackgroundLayer.SetActive(false); }
        gameSelectionPanel.gameObject.SetActive(false);
        customBattlePanel.gameObject.SetActive(true);
    }

    private void HideGamePanels() {
        var loadedEagleCanvas =
                eagleCanvas != null ? eagleCanvas : GameSceneManager.Instance?.GetEagleCanvas();
        if (loadedEagleCanvas != null) { loadedEagleCanvas.gameObject.SetActive(false); }

        var loadedShardokContainer = shardokContainer != null
                                             ? shardokContainer
                                             : GameSceneManager.Instance?.GetShardokContainer();
        if (loadedShardokContainer != null) { loadedShardokContainer.SetActive(false); }
    }

    private void ShowEaglePanel(Canvas loadedEagleCanvas) {
        connectionCanvas.enabled = false;
        connectionCanvas.gameObject.SetActive(false);
        var loadedShardokContainer = shardokContainer != null
                                             ? shardokContainer
                                             : GameSceneManager.Instance?.GetShardokContainer();
        if (loadedShardokContainer != null) { loadedShardokContainer.SetActive(false); }
        loadedEagleCanvas.gameObject.SetActive(true);
    }

    private async void SelectEagleGame(long gameId) {
        var gameInfo = fetchedRunningGames.Find(gi => gi.GameId == gameId);

        if (gameInfo != null) {
            SetLobbyActive(false);
            var selectedPersistentConnection = _persistentClientConnection;
            var selectedEnvironmentName = ConnectedEnvironmentName;
            var eagleController = _acceptedGameId == gameId && _eagleScenePreloadTask != null
                                          ? await _eagleScenePreloadTask
                                  : GameSceneManager.Instance == null
                                          ? eagleCanvas?.GetComponent<EagleGameController>()
                                          : await GameSceneManager.Instance.TransitionToEagle();
            _eagleScenePreloadTask = null;
            _acceptedGameId = 0;
            if (eagleController == null) {
                Debug.LogError(
                        "Cannot select Eagle game because the Eagle scene did not provide an " +
                        "EagleGameController.");
                SetLobbyActive(true);
                ShowLobbyPanel();
                return;
            }

            eagleCanvas = eagleController.GetComponent<Canvas>();
            if (eagleCanvas == null) {
                Debug.LogError(
                        "Cannot select Eagle game because EagleGameController is not attached to " +
                        "a Canvas.");
                SetLobbyActive(true);
                ShowLobbyPanel();
                return;
            }

            eagleController.SetUpGame(
                    gameInfo.GameId,
                    gameInfo.FactionId,
                    selectedPersistentConnection,
                    selectedEnvironmentName);
            ShowEaglePanel(eagleCanvas);
            createGameItem.SetCreating(false);
        }
    }

    private async Task<EagleGameController> PreloadEagleScene() {
        var eagleController = GameSceneManager.Instance == null
                                      ? eagleCanvas?.GetComponent<EagleGameController>()
                                      : await GameSceneManager.Instance.TransitionToEagle();
        var loadedCanvas = eagleController?.GetComponent<Canvas>();
        if (loadedCanvas != null) { loadedCanvas.gameObject.SetActive(false); }
        return eagleController;
    }

    private async Task ResetGameCreation() {
        createGameItem.SetCreating(false);
        _acceptedGameId = 0;
        var preloadTask = _eagleScenePreloadTask;
        _eagleScenePreloadTask = null;
        if (preloadTask != null) {
            try {
                await preloadTask;
            } catch (Exception e) { Debug.LogException(e); }
        }
        if (GameSceneManager.Instance != null) {
            await GameSceneManager.Instance.UnloadEagleScene();
        }
    }
}
