using UnityEngine;
using UnityEngine.UI;
using Subsystems;
using GeoSus.Client;
using System.Collections.Generic;
using System;
using System.Linq;
using TMPro;

namespace Subsystems
{
    /// <summary>
    /// Reads from GameManager_Network.State (the authoritative GameState) and drives
    /// all in-game canvas panels. No business logic lives here.
    /// </summary>
    public class GameManager_UI
    {
        private GameClient _gameClient;
        private GameState  _state => GameManager.Instance?.networkSubsystem?.State;

        // ── Canvas refs (wired by BindClientScene from Client.unity) ──────────
        public Canvas ClientCreateJoinLobby;
        public Canvas ClientInLobby;
        public Canvas ClientLoadingScreen;
        public Canvas ClientGameScreen;

        // ── HUD element refs (resolved once in BindClientScene) ───────────────
        private TMP_Text   _roleText;
        private TMP_Text   _taskListText;
        private TMP_Text   _taskProgressText;
        private Button     _actionButton;
        private TMP_Text   _actionButtonText;
        private TMP_Text   _killCooldownText;
        private GameObject _sabotagePanel;
        private TMP_Text   _sabotageTimerText;
        private GameObject _meetingPanel;
        private TMP_Text   _meetingHeader;
        private TMP_Text   _meetingPhaseLabel;
        private TMP_Text   _meetingPhaseCountdown;
        private Image      _meetingPhaseProgressBar;
        private TMP_Text   _myVoteIndicator;
        private GameObject _meetingScrollGO;
        private Transform  _meetingScrollContent;
        private TMP_Text   _meetingFallbackText;
        private GameObject _voteResultPanel;
        private TMP_Text   _voteResultText;
        private Button     _skipButton;
        private GameObject _gameEndPanel;
        private TMP_Text   _gameEndText;
        private RectTransform _returnToLobbyBtn;
        private TMP_Text   _toastText;
        private GameObject _toastGO;
        private GameObject _reconnectOverlay;

        // ── Internal state ────────────────────────────────────────────────────
        private bool _isDead;
        private bool _commsBlackout;
        private DateTime _sabotageMeltdownDeadline;
        private bool _sabotageTimerActive;
        private volatile bool _lobbyDirty;

        // Meeting vote-row references rebuilt each meeting
        private readonly List<GameObject> _voteRows = new List<GameObject>();
        private string _pendingVoteResultDisplay; // shown after voting
        private Coroutine _meetingCloseCoroutine; // tracked so phase changes can cancel it

        public GameManager_UI(GameClient gameClient) { _gameClient = gameClient; }

        public void NotifyLobbyChanged() => _lobbyDirty = true;
        public bool IsCommsBlackout => _commsBlackout;
        public bool IsPlayerDead    => _isDead;

        // ── Scene binding ─────────────────────────────────────────────────────

        public void BindClientScene(Canvas createJoin, Canvas inLobby, Canvas loading, Canvas game)
        {
            ClientCreateJoinLobby = createJoin;
            ClientInLobby         = inLobby;
            ClientLoadingScreen   = loading;
            ClientGameScreen      = game;

            foreach (var c in new[] { createJoin, inLobby, loading, game })
                EnsureCanvasReady(c);

            if (createJoin) createJoin.gameObject.SetActive(false);
            if (inLobby)    inLobby.gameObject.SetActive(false);
            if (loading)    loading.gameObject.SetActive(false);
            if (game)       game.gameObject.SetActive(false);

            if (game == null) return;
            var t = game.transform;

            _roleText          = FindTMP(t, "Role");
            _taskListText      = FindTMP(t, "TaskList");
            _taskProgressText  = FindTMP(t, "TaskProgress");
            _killCooldownText  = FindTMP(t, "KillCooldown");
            _sabotageTimerText = FindTMP(t, "SabotageTimer");
            _gameEndText       = FindTMP(t, "GameEndText");
            _toastText         = FindTMP(t, "Toast");
            _meetingHeader     = FindTMP(t, "MeetingHeader");
            _meetingPhaseLabel     = FindTMP(t, "MeetingPhaseLabel");
            _meetingPhaseCountdown = FindTMP(t, "MeetingPhaseCountdown");
            _myVoteIndicator       = FindTMP(t, "MyVoteIndicator");
            _meetingFallbackText = FindTMP(t, "MeetingPlayerList");
            _voteResultText    = FindTMP(t, "VoteResult");
            _meetingScrollContent = FindTransform(t, "MeetingContent");
            _meetingScrollGO   = FindTransformGO(t, "_MeetingScroll");

            var progressBarGO = FindTransformGO(t, "MeetingPhaseProgressBar");
            if (progressBarGO != null) _meetingPhaseProgressBar = progressBarGO.GetComponent<Image>();

            var skipGO = FindTransformGO(t, "SkipButton");
            if (skipGO != null) _skipButton = skipGO.GetComponent<Button>();

            var actionGO = t.Find("ActionButton");
            if (actionGO != null)
            {
                _actionButton     = actionGO.GetComponent<Button>();
                _actionButtonText = actionGO.GetComponentInChildren<TMP_Text>();
            }

            _sabotagePanel   = t.Find("SabotagePanel")?.gameObject;
            _meetingPanel    = t.Find("MeetingPanel")?.gameObject;
            _gameEndPanel    = t.Find("GameEndPanel")?.gameObject;
            _voteResultPanel = FindTransformGO(t, "VoteResultPanel");
            _toastGO         = FindTransformGO(t, "Toast");
            _reconnectOverlay = FindTransformGO(t, "ReconnectOverlay");

            var retBtn = FindTransform(t, "ReturnToLobbyButton");
            if (retBtn != null) _returnToLobbyBtn = retBtn as RectTransform;

            if (_meetingPanel) _meetingPanel.SetActive(false);
            if (_gameEndPanel) _gameEndPanel.SetActive(false);
            if (_voteResultPanel) _voteResultPanel.SetActive(false);
            if (_toastGO) _toastGO.SetActive(false);
            if (_reconnectOverlay) _reconnectOverlay.SetActive(false);
        }

        // ── Update (called every frame from GameManager.Update) ───────────────

        public void UpdateLobbyUI()
        {
            var lobbyState = _gameClient.CurrentLobbyState;
            if (lobbyState == null) return;

            if (_lobbyDirty)
            {
                _lobbyDirty = false;
                LobbyDisplayUI.RefreshAll(lobbyState);
            }

            if (ClientGameScreen == null) return;

            switch (lobbyState.Phase)
            {
                case GamePhase.Loading:
                    SetCanvases(false, false, true, false);
                    break;
                case GamePhase.Lobby:
                    SetCanvases(false, true, false, false);
                    break;
                case GamePhase.Playing:
                case GamePhase.Meeting:
                case GamePhase.Voting:
                    SetCanvases(false, false, false, true);
                    UpdateGameHUD();
                    break;
                case GamePhase.Ended:
                    SetCanvases(false, false, false, true);
                    break;
            }

            TickToast();
        }

        // ── Game HUD tick ─────────────────────────────────────────────────────

        private void UpdateGameHUD()
        {
            var s = _state;
            if (s == null) return;

            // Role
            if (_roleText != null)
            {
                string ghostSuffix = s.IsDead ? " (GHOST)" : "";
                _roleText.text  = $"{s.MyRole?.ToString() ?? "?"}{ghostSuffix}";
                _roleText.color = s.MyRole == PlayerRole.Impostor ? new Color(0.9f,0.2f,0.2f) : new Color(0.2f,0.8f,1f);
            }

            // Task list with checkmarks
            if (_taskListText != null)
            {
                var sb = new System.Text.StringBuilder();
                foreach (var task in s.MyTasks)
                {
                    bool done = s.MyCompletedTaskIds.Contains(task.TaskId);
                    string mark = done ? "<color=#2DB84B>✓</color>" : "○";
                    sb.AppendLine($"{mark} {task.Name}");
                }
                _taskListText.text = sb.ToString();
            }

            // Global task progress
            if (_taskProgressText != null && s.TotalRequired > 0)
                _taskProgressText.text = $"Tasks: {s.TotalCompleted}/{s.TotalRequired}";

            // Kill cooldown
            if (_killCooldownText != null)
            {
                bool show = s.KillCooldownRemaining > 0;
                _killCooldownText.gameObject.SetActive(show);
                if (show) _killCooldownText.text = $"Kill: {Mathf.CeilToInt(s.KillCooldownRemaining)}s";
            }

            // Sabotage banner - meltdown countdown plus simultaneous-repair coaching
            if (_sabotageTimerActive && _sabotageTimerText != null)
            {
                double remaining = (_sabotageMeltdownDeadline - DateTime.UtcNow).TotalSeconds;
                string head = remaining > 0 ? $"⚠ MELTDOWN: {remaining:F0}s" : "⚠ MELTDOWN!";

                // For multi-station sabotages, surface how many of the required
                // simultaneous repair stations are currently active. This is
                // what makes "you're alone, you need a partner" obvious.
                int required = s.ActiveSabotage?.RequiredSimultaneousRepairs ?? 0;
                if (required > 1)
                {
                    int active = s.ActiveRepairs.Count;
                    head += $"   <size=32>{active}/{required} stations active</size>";
                }
                _sabotageTimerText.text = head;
            }

            // Keep meeting sub-phase strip, countdown, vote gating, tallies and
            // my-vote indicator fresh each frame.
            UpdateMeetingPhaseStrip();
        }

        // ── Kill cooldown helper (called from GameManager) ────────────────────

        // ── Reconnect overlay ─────────────────────────────────────────────────

        /// <summary>
        /// Show a full-screen "Reconnecting..." overlay. Call when the socket
        /// drops mid-game; the server keeps the player slot for ~60s before
        /// removing them so a brief disconnect is recoverable.
        /// </summary>
        public void ShowReconnecting()
        {
            if (_reconnectOverlay) _reconnectOverlay.SetActive(true);
        }

        /// <summary>
        /// Hide the reconnect overlay. Call from OnConnected once the socket
        /// is healthy again.
        /// </summary>
        public void HideReconnecting()
        {
            if (_reconnectOverlay) _reconnectOverlay.SetActive(false);
        }

        public void SetKillCooldownText(string text)
        {
            if (_killCooldownText == null) return;
            bool show = !string.IsNullOrEmpty(text);
            _killCooldownText.gameObject.SetActive(show);
            if (show) _killCooldownText.text = text;
        }

        public void UpdateTaskProgress(int completed, int total)
        {
            if (_taskProgressText != null)
                _taskProgressText.text = $"Tasks: {completed}/{total}";
        }

        // ── Action button ─────────────────────────────────────────────────────

        public void SetActionButton(string label, bool visible, UnityEngine.Events.UnityAction onClick = null)
        {
            if (_actionButton == null) return;
            _actionButton.gameObject.SetActive(visible);
            if (_actionButtonText != null) _actionButtonText.text = label;
            if (onClick != null)
            {
                _actionButton.onClick.RemoveAllListeners();
                _actionButton.onClick.AddListener(onClick);
            }
        }

        // ── P13g: Impostor sabotage menu ──────────────────────────────────────
        // The audit found that the production HUD never had an impostor
        // sabotage trigger - GameManager.StartSabotage exists, the wire path
        // is intact (StartSabotage -> server -> SabotageStarted broadcast +
        // station markers), but no UI ever called it. So sabotages literally
        // never fired in production. This menu fixes that gap with a runtime-
        // built two-button overlay (no scene file change, no prefab needed).

        private GameObject _sabotageMenuRoot;
        private Button _sabotageBlackoutBtn;
        private Button _sabotageMeltdownBtn;

        private void EnsureSabotageMenu()
        {
            if (_sabotageMenuRoot != null || ClientGameScreen == null) return;

            var canvasRT = ClientGameScreen.transform as RectTransform;
            if (canvasRT == null) return;

            // Root container - top-right corner, vertical stack.
            _sabotageMenuRoot = new GameObject("ImpostorSabotageMenu", typeof(RectTransform), typeof(CanvasRenderer));
            var rootRT = _sabotageMenuRoot.GetComponent<RectTransform>();
            rootRT.SetParent(canvasRT, worldPositionStays: false);
            rootRT.anchorMin = new Vector2(1, 1);
            rootRT.anchorMax = new Vector2(1, 1);
            rootRT.pivot     = new Vector2(1, 1);
            rootRT.anchoredPosition = new Vector2(-24, -180); // below the top-right safe-area
            rootRT.sizeDelta        = new Vector2(360, 240);

            _sabotageBlackoutBtn = BuildSabotageOption(rootRT, "📡 BLACKOUT",
                new Color(0.20f, 0.55f, 1.0f), 0, () => GameManager.Instance?.StartSabotage(0));

            _sabotageMeltdownBtn = BuildSabotageOption(rootRT, "☢️ MELTDOWN",
                new Color(1.0f, 0.30f, 0.30f), 1, () => GameManager.Instance?.StartSabotage(1));

            _sabotageMenuRoot.SetActive(false);
        }

        private static Button BuildSabotageOption(RectTransform parent, string label, Color tint, int slot, UnityEngine.Events.UnityAction onClick)
        {
            // Each button: 360w x 110h, stacked vertically with 10px gap.
            var go = new GameObject($"SabBtn_{slot}", typeof(RectTransform), typeof(CanvasRenderer), typeof(Image), typeof(Button));
            var rt = go.GetComponent<RectTransform>();
            rt.SetParent(parent, worldPositionStays: false);
            rt.anchorMin = new Vector2(0, 1);
            rt.anchorMax = new Vector2(1, 1);
            rt.pivot     = new Vector2(0.5f, 1);
            rt.anchoredPosition = new Vector2(0, -slot * 120);
            rt.sizeDelta        = new Vector2(0, 110);

            var img = go.GetComponent<Image>();
            img.color = new Color(tint.r * 0.4f, tint.g * 0.4f, tint.b * 0.4f, 0.92f);

            // Border via outline component
            var outline = go.AddComponent<Outline>();
            outline.effectColor = tint;
            outline.effectDistance = new Vector2(2, -2);

            // Text child
            var txtGO = new GameObject("Label", typeof(RectTransform));
            var txtRT = txtGO.GetComponent<RectTransform>();
            txtRT.SetParent(rt, worldPositionStays: false);
            txtRT.anchorMin = Vector2.zero;
            txtRT.anchorMax = Vector2.one;
            txtRT.offsetMin = Vector2.zero;
            txtRT.offsetMax = Vector2.zero;
            var tmp = txtGO.AddComponent<TextMeshProUGUI>();
            tmp.text = label;
            tmp.alignment = TextAlignmentOptions.Center;
            tmp.fontSize = 36;
            tmp.color = Color.white;
            tmp.fontStyle = FontStyles.Bold;

            var btn = go.GetComponent<Button>();
            btn.onClick.AddListener(onClick);
            return btn;
        }

        /// <summary>
        /// P13g: show the impostor sabotage menu when the local player is
        /// alive impostor in the Playing phase with no active sabotage and
        /// not in a meeting. Driven from GameManager_Tasks.UpdateProximity.
        /// </summary>
        public void SetSabotageMenuVisible(bool visible)
        {
            if (visible) EnsureSabotageMenu();
            if (_sabotageMenuRoot != null && _sabotageMenuRoot.activeSelf != visible)
                _sabotageMenuRoot.SetActive(visible);
        }

        // ── Player state ──────────────────────────────────────────────────────

        public void OnLocalPlayerDied()
        {
            _isDead = true;
            if (_state != null) _state.IsDead = true;
        }

        // ── Meeting ───────────────────────────────────────────────────────────

        public void ShowMeetingAlert()
        {
            ShowToast("⚠ Meeting called! Head to the meeting point.");
        }

        public void ShowMeetingPanel(List<PlayerInfo> players, MeetingStartedPayload payload)
        {
            if (_meetingPanel == null) return;
            _meetingPanel.SetActive(true);

            if (_meetingHeader != null)
                _meetingHeader.text = payload.Type == MeetingType.BodyReport ? "BODY REPORTED!" : "EMERGENCY MEETING!";

            // Make sure the result subpanel is hidden at start of a fresh meeting,
            // and the scroll list is visible (results phase will swap them).
            if (_voteResultPanel) _voteResultPanel.SetActive(false);
            if (_meetingScrollGO)  _meetingScrollGO.SetActive(true);
            if (_myVoteIndicator)  _myVoteIndicator.text = "";

            BuildMeetingVoteRows(players);
            UpdateMeetingPhaseStrip();
        }

        private void BuildMeetingVoteRows(List<PlayerInfo> players)
        {
            // Clear old rows
            foreach (var r in _voteRows) if (r) UnityEngine.Object.Destroy(r);
            _voteRows.Clear();

            if (_meetingScrollContent == null || players == null)
            {
                // Fall back to text list
                if (_meetingFallbackText != null)
                {
                    _meetingFallbackText.gameObject.SetActive(true);
                    var sb = new System.Text.StringBuilder();
                    foreach (var p in players ?? new List<PlayerInfo>())
                        sb.AppendLine($"{p.DisplayName}  [{p.State}]");
                    _meetingFallbackText.text = sb.ToString();
                }
                return;
            }

            string myId = _gameClient.ClientUuid;
            bool canVote = !_isDead;

            // Dynamic row height: spread the available scroll-area height
            // across however many players we have. Clamps so rows never get
            // tinier than legible (small phone, many players -> 80px) or
            // ridiculously tall (tablet, two players -> 140px).
            float rowH = ComputeVoteRowHeight(players.Count);

            foreach (var player in players)
            {
                bool isMe   = player.ClientUuid == myId;
                bool isAlive = player.State == PlayerState.Alive;
                var row = BuildVoteRow(player, isMe, isAlive, canVote && isAlive && !isMe, rowH);
                row.transform.SetParent(_meetingScrollContent, false);
                _voteRows.Add(row);
            }
        }

        /// <summary>
        /// Compute a per-row height that fills the scroll viewport when there
        /// are few players, and shrinks (until scrolling kicks in) when there
        /// are many. Inputs are CanvasScaler reference coordinates, so the
        /// values are device-independent.
        /// </summary>
        private float ComputeVoteRowHeight(int playerCount)
        {
            if (playerCount <= 0) return 110f;
            // The scroll area occupies y=0.18 to y=0.74 of the canvas (per
            // InGameHUDBuilder.BuildMeetingPanel) and reference height is 1920.
            const float referenceHeight = 1920f;
            const float scrollFraction  = 0.74f - 0.18f; // 0.56
            float available = referenceHeight * scrollFraction;
            float h = available / playerCount;
            return Mathf.Clamp(h, 80f, 140f);
        }

        private GameObject BuildVoteRow(PlayerInfo player, bool isMe, bool isAlive, bool canVote, float rowH)
        {
            var go = new GameObject($"VoteRow_{player.ClientUuid}");
            var rt = go.AddComponent<RectTransform>();
            rt.sizeDelta = new Vector2(0, rowH);
            var le = go.AddComponent<LayoutElement>();
            le.minHeight = le.preferredHeight = rowH;

            var bg = go.AddComponent<Image>();
            bg.color = isMe ? new Color(0.12f,0.18f,0.30f) : new Color(0.10f,0.12f,0.20f);

            // Dead overlay
            if (!isAlive)
            {
                bg.color = new Color(0.08f,0.08f,0.10f,0.7f);
            }

            // Name label - left 50% (was 65%, gave width back to tally + button)
            var namRT = MakeChild("Name", rt);
            namRT.anchorMin = new Vector2(0,0); namRT.anchorMax = new Vector2(0.50f,1);
            namRT.offsetMin = new Vector2(16,6); namRT.offsetMax = new Vector2(0,-6);
            var namTmp = namRT.gameObject.AddComponent<TextMeshProUGUI>();
            namTmp.text      = (player.IsOwner ? "👑 " : "") + (player.DisplayName ?? "???");
            namTmp.fontSize  = 36;
            namTmp.color     = !isAlive ? Color.gray : (isMe ? Color.white : new Color(0.73f,0.8f,0.88f));
            namTmp.fontStyle = isMe ? FontStyles.Bold : FontStyles.Normal;
            namTmp.alignment = TextAlignmentOptions.MidlineLeft;

            // Tally column - middle 18%, shows live vote count for this player
            var tallyRT = MakeChild("Tally", rt);
            tallyRT.anchorMin = new Vector2(0.50f,0); tallyRT.anchorMax = new Vector2(0.66f,1);
            tallyRT.offsetMin = Vector2.zero; tallyRT.offsetMax = Vector2.zero;
            var tallyTmp = tallyRT.gameObject.AddComponent<TextMeshProUGUI>();
            tallyTmp.text      = "";
            tallyTmp.fontSize  = 30;
            tallyTmp.fontStyle = FontStyles.Bold;
            tallyTmp.color     = new Color(1f,0.72f,0.10f); // C_YELLOW-ish
            tallyTmp.alignment = TextAlignmentOptions.Center;

            // Vote button - right 30% (interactability is updated each frame)
            var voteBtnRT = MakeChild("VoteBtn", rt);
            voteBtnRT.anchorMin = new Vector2(0.68f,0.10f); voteBtnRT.anchorMax = new Vector2(0.95f,0.90f);
            var voteBg = voteBtnRT.gameObject.AddComponent<Image>();
            voteBg.color = canVote ? new Color(0.2f,0.6f,1f) : new Color(0.2f,0.2f,0.2f,0.5f);
            var voteBtn = voteBtnRT.gameObject.AddComponent<Button>();
            voteBtn.targetGraphic = voteBg;
            voteBtn.interactable  = canVote;
            string capturedId = player.ClientUuid;
            voteBtn.onClick.AddListener(() => GameManager.Instance?.CastVote(capturedId));
            var voteTxtRT = MakeChild("Txt", voteBtnRT);
            Stretch(voteTxtRT);
            var voteTmp = voteTxtRT.gameObject.AddComponent<TextMeshProUGUI>();
            voteTmp.text      = isAlive ? "VOTE" : "DEAD";
            voteTmp.fontSize  = 28;
            voteTmp.fontStyle = FontStyles.Bold;
            voteTmp.color     = Color.white;
            voteTmp.alignment = TextAlignmentOptions.Center;

            // Voted-by-this-player checkmark (shown when the row's player has cast a vote)
            var votedRT = MakeChild("VotedTick", rt);
            votedRT.anchorMin = new Vector2(0.95f,0.20f); votedRT.anchorMax = new Vector2(1f,0.80f);
            var vtTmp = votedRT.gameObject.AddComponent<TextMeshProUGUI>();
            vtTmp.text = "✓"; vtTmp.fontSize = 34;
            vtTmp.color = new Color(0.18f,0.75f,0.30f); vtTmp.alignment = TextAlignmentOptions.Center;
            votedRT.gameObject.SetActive(false);

            return go;
        }

        /// <summary>
        /// Per-frame meeting UI update. Computes the meeting sub-phase from the
        /// timestamps in MeetingStartedPayload (server doesn't broadcast a
        /// discrete discussion-end event) and uses it to drive the countdown
        /// label, progress bar, vote-button interactivity, live tallies, and
        /// "Your vote: X" indicator.
        /// </summary>
        private void UpdateMeetingPhaseStrip()
        {
            var s = _state;
            if (s == null) return;
            // Only run if we're actually in a meeting; phase Playing skips the work.
            if (s.Phase != GamePhase.Meeting && s.LastVoteResult == null) return;

            var sub = s.GetMeetingSubPhase();

            // ── Sub-phase label + countdown text + progress bar ───────────────
            string label;
            switch (sub)
            {
                case MeetingSubPhase.Arrival:    label = "ARRIVAL";    break;
                case MeetingSubPhase.Discussion: label = "DISCUSSION"; break;
                case MeetingSubPhase.Voting:     label = "VOTING";     break;
                case MeetingSubPhase.Resolved:   label = "RESULTS";    break;
                default: label = ""; break;
            }
            if (_meetingPhaseLabel != null) _meetingPhaseLabel.text = label;

            if (s.ActiveMeeting != null && sub != MeetingSubPhase.Resolved)
            {
                var deadline = s.GetMeetingSubPhaseDeadline(sub);
                var remaining = (deadline - DateTime.UtcNow).TotalSeconds;
                if (remaining < 0) remaining = 0;

                if (_meetingPhaseCountdown != null)
                {
                    int mins = (int)(remaining / 60);
                    int secs = (int)(remaining % 60);
                    string verb = sub == MeetingSubPhase.Voting   ? "Voting ends in"
                                : sub == MeetingSubPhase.Discussion ? "Voting begins in"
                                                                    : "Arrival ends in";
                    _meetingPhaseCountdown.text = $"{verb} {mins}:{secs:D2}";
                }

                // Progress bar drains over the current sub-phase. The server
                // doesn't tell us when the meeting started, so we can only
                // compute a meaningful fill for Discussion (start = arrival
                // deadline) and Voting (start = discussion end / arrival
                // deadline). Arrival's start time is unknown here; show full.
                if (_meetingPhaseProgressBar != null)
                {
                    if (sub == MeetingSubPhase.Arrival)
                    {
                        _meetingPhaseProgressBar.fillAmount = 1f;
                    }
                    else
                    {
                        DateTime start = sub == MeetingSubPhase.Discussion
                            ? s.ActiveMeeting.ArrivalDeadline
                            : (s.ActiveMeeting.DiscussionEndTime ?? s.ActiveMeeting.ArrivalDeadline);
                        var total = (deadline - start).TotalSeconds;
                        var elapsed = (DateTime.UtcNow - start).TotalSeconds;
                        float fill = total > 0.001
                            ? Mathf.Clamp01(1f - (float)(elapsed / total))
                            : 0f;
                        _meetingPhaseProgressBar.fillAmount = fill;
                    }
                }
            }
            else
            {
                if (_meetingPhaseCountdown != null) _meetingPhaseCountdown.text = "";
                if (_meetingPhaseProgressBar != null) _meetingPhaseProgressBar.fillAmount = 0f;
            }

            // ── Vote button gating + per-row tally / voted-indicator ──────────
            bool votingOpen = sub == MeetingSubPhase.Voting && !_isDead;
            bool iAmArrived = s.ActiveMeeting == null
                || s.ArrivedPlayerIds.Contains(_gameClient.ClientUuid);

            // Skip button mirrors the same gate
            if (_skipButton != null) _skipButton.interactable = votingOpen && iAmArrived;

            foreach (var row in _voteRows)
            {
                if (row == null) continue;
                string rowUuid = row.name.Replace("VoteRow_", "");

                // Voted-tick: this row's player has cast a vote
                var tick = row.transform.Find("VotedTick")?.gameObject;
                if (tick != null) tick.SetActive(s.VotedPlayerIds.Contains(rowUuid));

                // Tally text: how many votes is this row's player receiving?
                var tally = row.transform.Find("Tally")?.GetComponent<TMP_Text>();
                if (tally != null)
                {
                    s.VoteTallies.TryGetValue(rowUuid, out var count);
                    tally.text = count > 0 ? count.ToString() : "";
                }

                // Vote button: gate by sub-phase + arrival + alive + not-self
                var btnGO = row.transform.Find("VoteBtn")?.gameObject;
                if (btnGO != null)
                {
                    var btn = btnGO.GetComponent<Button>();
                    var btnImg = btnGO.GetComponent<Image>();
                    var rowPlayer = s.Players?.FirstOrDefault(p => p.ClientUuid == rowUuid);
                    bool isMe = rowUuid == _gameClient.ClientUuid;
                    bool rowAlive = rowPlayer?.State == PlayerState.Alive;

                    bool canPress = votingOpen && iAmArrived && rowAlive && !isMe;
                    if (btn != null) btn.interactable = canPress;
                    if (btnImg != null)
                        btnImg.color = canPress ? new Color(0.2f,0.6f,1f)
                                                : new Color(0.2f,0.2f,0.2f,0.5f);

                    // Mark the row's button if it's the local player's chosen vote
                    if (s.MyVoteTarget != null && s.MyVoteTarget == rowUuid && btnImg != null)
                        btnImg.color = new Color(0.2f,0.75f,0.30f); // green = your vote
                }
            }

            // ── My vote indicator strip ───────────────────────────────────────
            if (_myVoteIndicator != null)
            {
                if (s.LastVoteResult != null) _myVoteIndicator.text = "";
                else if (!iAmArrived)         _myVoteIndicator.text = "Travel to the meeting point to vote";
                else if (sub == MeetingSubPhase.Discussion) _myVoteIndicator.text = "Discussion - voting opens shortly";
                else if (sub == MeetingSubPhase.Arrival)    _myVoteIndicator.text = "Waiting for players to arrive";
                else if (s.MyVoteTarget == null)           _myVoteIndicator.text = "Cast your vote";
                else if (s.MyVoteTarget == GameState.VoteSkip) _myVoteIndicator.text = "You voted: SKIP";
                else
                {
                    var target = s.Players?.FirstOrDefault(p => p.ClientUuid == s.MyVoteTarget);
                    _myVoteIndicator.text = $"You voted for: {target?.DisplayName ?? s.MyVoteTarget}";
                }
            }
        }

        public void AppendVoteInstruction()
        {
            // no-op - vote instructions are embedded in the row buttons
        }

        public void ShowVoteResult(VotingClosedPayload payload, List<PlayerInfo> players)
        {
            // Swap scroll list out, result subpanel in. They occupy the same
            // anchor region (0.18-0.74) so the result text replaces the vote
            // rows rather than overlapping them.
            if (_meetingScrollGO != null) _meetingScrollGO.SetActive(false);
            if (_voteResultPanel != null) _voteResultPanel.SetActive(true);
            // Skip + my-vote strips are no longer relevant once voting ended.
            if (_skipButton != null) _skipButton.gameObject.SetActive(false);
            if (_myVoteIndicator != null) _myVoteIndicator.text = "";

            if (_voteResultText != null)
            {
                // Build a compact tally summary alongside the headline.
                var sb = new System.Text.StringBuilder();
                if (payload.WasTie)
                    sb.AppendLine("⚖ TIE — nobody ejected.");
                else if (string.IsNullOrEmpty(payload.EjectedPlayerId))
                    sb.AppendLine("Nobody ejected (skip).");
                else
                {
                    var ej = players?.Find(p => p.ClientUuid == payload.EjectedPlayerId);
                    sb.AppendLine($"🚪 {ej?.DisplayName ?? payload.EjectedPlayerId} ejected!");
                }

                if (payload.VoteCounts != null && payload.VoteCounts.Count > 0)
                {
                    sb.AppendLine();
                    foreach (var kv in payload.VoteCounts.OrderByDescending(p => p.Value))
                    {
                        if (kv.Value <= 0) continue;
                        string name = kv.Key == GameState.VoteSkip
                            ? "(skip)"
                            : (players?.Find(p => p.ClientUuid == kv.Key)?.DisplayName ?? kv.Key);
                        sb.AppendLine($"<size=24>{name}: {kv.Value}</size>");
                    }
                }
                _voteResultText.text = sb.ToString();
            }

            // Auto-close meeting panel after 5 s. Track the handle so we can
            // cancel it if the game ends or returns to lobby before it fires
            // (otherwise the coroutine fires mid-GameEndPanel and hides nothing
            // useful while the meeting overlay sits visibly stacked on top).
            CancelMeetingAutoClose();
            var gm = GameManager.Instance;
            if (gm != null) _meetingCloseCoroutine = gm.StartCoroutine(CloseMeetingAfterDelay(5f));
        }

        /// <summary>
        /// Hide the meeting/vote panels immediately and cancel any pending
        /// auto-close coroutine. Resets internal toggles (skip/result/scroll
        /// visibility) so the next meeting starts from a clean state. Safe to
        /// call from any phase transition.
        /// </summary>
        public void HideMeetingPanel()
        {
            CancelMeetingAutoClose();
            if (_meetingPanel) _meetingPanel.SetActive(false);
            if (_voteResultPanel) _voteResultPanel.SetActive(false);
            if (_meetingScrollGO) _meetingScrollGO.SetActive(true);
            if (_skipButton) _skipButton.gameObject.SetActive(true);
            if (_myVoteIndicator) _myVoteIndicator.text = "";
            if (_meetingPhaseLabel) _meetingPhaseLabel.text = "";
            if (_meetingPhaseCountdown) _meetingPhaseCountdown.text = "";
            if (_meetingPhaseProgressBar) _meetingPhaseProgressBar.fillAmount = 0f;
        }

        private void CancelMeetingAutoClose()
        {
            if (_meetingCloseCoroutine != null)
            {
                var gm = GameManager.Instance;
                if (gm != null) gm.StopCoroutine(_meetingCloseCoroutine);
                _meetingCloseCoroutine = null;
            }
        }

        private System.Collections.IEnumerator CloseMeetingAfterDelay(float delay)
        {
            yield return new UnityEngine.WaitForSeconds(delay);
            // Use HideMeetingPanel so we restore the scroll/skip/indicator
            // state for the next meeting, not just hide the root panel.
            HideMeetingPanel();
            _meetingCloseCoroutine = null;
        }

        // ── Sabotage ──────────────────────────────────────────────────────────

        public void ShowSabotageTimer(DateTime deadline)
        {
            _sabotageMeltdownDeadline = deadline;
            _sabotageTimerActive      = true;
            if (_sabotagePanel) _sabotagePanel.SetActive(true);
            if (_sabotageTimerText) _sabotageTimerText.gameObject.SetActive(true);
        }

        public void HideSabotageTimer()
        {
            _sabotageTimerActive = false;
            if (_sabotagePanel) _sabotagePanel.SetActive(false);
            SetCommsBlackout(false);
        }

        /// <summary>
        /// Set the comms-blackout flag and (when active) raise the sabotage
        /// banner with a clear "comms down" message. The flag is read by
        /// GameManager_Tasks.UpdateProximity to suppress the REPORT/EMERGENCY
        /// action button while comms are jammed - this gives the player the
        /// visible reason why those buttons disappeared.
        /// </summary>
        public void SetCommsBlackout(bool active)
        {
            _commsBlackout = active;
            if (active)
            {
                if (_sabotagePanel) _sabotagePanel.SetActive(true);
                if (_sabotageTimerText)
                {
                    _sabotageTimerText.gameObject.SetActive(true);
                    _sabotageTimerText.text = "📡 COMMS DOWN — reports & meetings disabled";
                }
            }
            else if (!_sabotageTimerActive)
            {
                // Only tear the banner down if no meltdown timer is using it.
                if (_sabotagePanel) _sabotagePanel.SetActive(false);
            }
        }

        // ── Game end ──────────────────────────────────────────────────────────

        public void ShowGameEndPanel(GameEndedPayload payload, string myUuid)
        {
            if (_gameEndPanel) _gameEndPanel.SetActive(true);
            if (_gameEndText != null)
            {
                bool won = payload.Winners?.Contains(myUuid) ?? false;
                string title     = won ? "<color=#FFB800>🏆 VICTORY</color>" : "<color=#C43232>💔 DEFEAT</color>";
                string faction   = payload.WinningFaction == "Impostor" ? "Impostors win!" : "Crew wins!";

                // Non-owners can't actually return to lobby themselves; tell
                // them who they're waiting on so the panel doesn't read as
                // "tap leave or stare at the wall." If we can't find an
                // owner record, fall back to a generic message.
                string waitMessage = "";
                if (!_gameClient.IsOwner)
                {
                    var s = _state;
                    var host = s?.Players?.Find(p => p.IsOwner);
                    string hostName = host?.DisplayName ?? "the host";
                    waitMessage = $"\n\n<size=32>Waiting for {hostName} to return to lobby...</size>";
                }
                _gameEndText.text = $"{title}\n{faction}\n<size=38>{payload.Reason}</size>{waitMessage}";
            }

            // Show "Return to Lobby" only for the host
            if (_returnToLobbyBtn != null)
                _returnToLobbyBtn.gameObject.SetActive(_gameClient.IsOwner);
        }

        // ── Toast ─────────────────────────────────────────────────────────────

        public void ShowToast(string message)
        {
            if (_state != null) { _state.ToastMessage = message; _state.ToastExpiry = UnityEngine.Time.time + 4f; }
            if (_toastGO == null) return;
            _toastGO.SetActive(true);
            if (_toastText != null) _toastText.text = message;
        }

        private void TickToast()
        {
            var s = _state;
            if (_toastGO == null) return;

            if (s != null && !string.IsNullOrEmpty(s.ToastMessage) && UnityEngine.Time.time < s.ToastExpiry)
            {
                _toastGO.SetActive(true);
                if (_toastText != null) _toastText.text = s.ToastMessage;
            }
            else
            {
                _toastGO.SetActive(false);
            }
        }

        // ── Canvas switching ──────────────────────────────────────────────────

        private void SetCanvases(bool createJoin, bool inLobby, bool loading, bool game)
        {
            EnsureCanvasReady(ClientCreateJoinLobby);
            EnsureCanvasReady(ClientInLobby);
            EnsureCanvasReady(ClientLoadingScreen);
            EnsureCanvasReady(ClientGameScreen);

            if (ClientCreateJoinLobby) ClientCreateJoinLobby.gameObject.SetActive(createJoin);
            if (ClientInLobby)         ClientInLobby.gameObject.SetActive(inLobby);
            if (ClientLoadingScreen)   ClientLoadingScreen.gameObject.SetActive(loading);
            if (ClientGameScreen)      ClientGameScreen.gameObject.SetActive(game);
        }

        // ── Utilities ─────────────────────────────────────────────────────────

        private static void EnsureCanvasReady(Canvas canvas)
        {
            if (canvas == null) return;
            if (!canvas.enabled) canvas.enabled = true;
            var t = canvas.transform;
            if (t != null)
            {
                var s = t.localScale;
                if (Mathf.Abs(s.x) < 0.001f || Mathf.Abs(s.y) < 0.001f || Mathf.Abs(s.z) < 0.001f)
                    t.localScale = Vector3.one;
            }
        }

        private static TMP_Text FindTMP(Transform root, string name)
        {
            if (root == null) return null;
            foreach (var tmp in root.GetComponentsInChildren<TMP_Text>(true))
                if (tmp != null && tmp.name == name) return tmp;
            return null;
        }

        private static Transform FindTransform(Transform root, string name)
        {
            if (root == null) return null;
            foreach (Transform t in root.GetComponentsInChildren<Transform>(true))
                if (t.name == name) return t;
            return null;
        }

        private static GameObject FindTransformGO(Transform root, string name)
            => FindTransform(root, name)?.gameObject;

        private static RectTransform MakeChild(string name, RectTransform parent)
        {
            var go = new GameObject(name);
            var rt = go.AddComponent<RectTransform>();
            rt.SetParent(parent, false);
            rt.localScale = Vector3.one;
            return rt;
        }

        private static void Stretch(RectTransform rt)
        {
            rt.anchorMin = Vector2.zero; rt.anchorMax = Vector2.one;
            rt.offsetMin = Vector2.zero; rt.offsetMax = Vector2.zero;
        }
    }
}
