// Assets/_Project/Scripts/UI/LobbyController.cs
using System.Linq;
using System.Text;
using Unity.Netcode;
using UnityEngine;
using UnityEngine.UIElements;
using TD.Core;
using TD.Gameplay;
using TD.Levels;
using TD.Net;

namespace TD.UI
{
    /// <summary>
    /// Drives the lobby UI. Requires a <see cref="UIDocument"/> on the same
    /// GameObject. Programmatic UI for v1 — UXML can be added later if needed.
    /// </summary>
    /// <remarks>
    /// <para><b>What's displayed.</b> A player list with one row per connected
    /// player showing slot, display name, race pick, and ready ✓. The local
    /// player's row has interactive controls: a race dropdown (currently a
    /// placeholder with just <see cref="RaceId.None"/> until Phase 1.8 fills
    /// the enum) and a Ready toggle button. The host sees a Start Match
    /// button at the bottom, enabled only when every player is ready.</para>
    ///
    /// <para><b>Polling vs reactive.</b> The list is rebuilt every Update from
    /// <see cref="PlayerMatchState.AllPlayers"/>. PlayerMatchState's
    /// <c>NetworkVariable</c>s expose <c>OnValueChanged</c> events we could
    /// hook for reactive updates, but polling is simpler at lobby scale
    /// (max 9 players × a handful of fields each) and avoids tracking
    /// subscription lifecycles across spawn/despawn.</para>
    ///
    /// <para><b>Race picker placeholder.</b> The <see cref="RaceId"/> enum
    /// currently only has <c>None</c>. The picker shows a single "Test Race"
    /// button that calls <c>SubmitRaceRpc(RaceId.None)</c> — temporary, so
    /// ready-up flow can be exercised end-to-end before Phase 1.8 lands real
    /// races. Replace with the actual race-selection UI when
    /// <see cref="RaceDefinition"/> assets exist.</para>
    /// </remarks>
    [RequireComponent(typeof(UIDocument))]
    public class LobbyController : MonoBehaviour
    {
        // ----- Cached UI elements -----------------------------------------

        private VisualElement mapListContainer;
        private VisualElement playerListContainer;
        private Button        startMatchButton;
        private Button        leaveButton;
        private Label         statusLabel;

        // Signature snapshot for the map list, mirroring the player-list pattern.
        // Components: registry count, selected index, current player count, host flag.
        // Without this, the cards rebuild every frame and the Clickable manipulator
        // loses presses (same root cause as the player-list signature).
        private string lastMapListSignature = string.Empty;

        // ----- Race selection overlay ------------------------------------

        [Tooltip("Sibling RaceSelectionOverlay component that owns the race-pick UI. " +
                 "Auto-located on the same GameObject if not assigned.")]
        [SerializeField] private RaceSelectionOverlay raceOverlay;

        // Snapshot of player-list state from the last rebuild. RefreshPlayerList
        // skips rebuilding when this matches the current frame's signature —
        // critical because rebuilding every frame destroys the per-row buttons
        // mid-click, and UI Toolkit's Clickable manipulator needs the same
        // element instance to receive PointerDown AND PointerUp for the action
        // to fire. Pre-fix, clicks on Select Race / Ready / Unready were silently
        // lost because the button was destroyed between press and release.
        private string lastPlayerListSignature = string.Empty;
        private readonly StringBuilder signatureBuffer = new StringBuilder();

        // ----- Lifecycle --------------------------------------------------

        private void Start()
        {
            var doc = GetComponent<UIDocument>();
            var root = doc?.rootVisualElement;
            if (root == null)
            {
                Debug.LogError("[LobbyController] rootVisualElement is null. " +
                               "Check the UIDocument's Panel Settings.");
                return;
            }

            BuildUI(root);

            // Initialize the race overlay with our root so it can install its
            // UI elements on top of the lobby. Hidden by default.
            if (raceOverlay == null) raceOverlay = GetComponent<RaceSelectionOverlay>();
            if (raceOverlay != null) raceOverlay.Initialize(root);
            else Debug.LogWarning("[LobbyController] No RaceSelectionOverlay component found — " +
                                  "race-picker button will be disabled. Add one to this GameObject.");
        }

        private void Update()
        {
            if (playerListContainer == null) return;
            RefreshMapList();
            RefreshPlayerList();
            RefreshStartButton();
        }

        // ----- UI construction --------------------------------------------

        private void BuildUI(VisualElement root)
        {
            root.style.flexDirection   = FlexDirection.Column;
            root.style.alignItems      = Align.Center;
            root.style.justifyContent  = Justify.FlexStart;
            root.style.backgroundColor = new Color(0.05f, 0.05f, 0.08f, 1f);
            root.style.width  = Length.Percent(100);
            root.style.height = Length.Percent(100);
            root.style.paddingTop = 40;

            // Title.
            var title = new Label("Lobby");
            title.style.fontSize         = 36;
            title.style.color            = Color.white;
            title.style.unityFontStyleAndWeight = FontStyle.Bold;
            title.style.marginBottom     = 24;
            root.Add(title);

            // Map selection panel — host clicks to change, everyone sees the current pick.
            // Horizontal row of cards rebuilt by RefreshMapList when its signature changes.
            var mapPanelLabel = new Label("Map");
            mapPanelLabel.style.fontSize = 18;
            mapPanelLabel.style.color    = new Color(0.85f, 0.85f, 0.85f);
            mapPanelLabel.style.unityFontStyleAndWeight = FontStyle.Bold;
            mapPanelLabel.style.marginBottom = 6;
            root.Add(mapPanelLabel);

            mapListContainer = new VisualElement();
            mapListContainer.style.flexDirection   = FlexDirection.Row;
            mapListContainer.style.justifyContent  = Justify.Center;
            mapListContainer.style.flexWrap        = Wrap.Wrap;
            mapListContainer.style.minWidth        = 520;
            mapListContainer.style.paddingTop      = 8;
            mapListContainer.style.paddingBottom   = 8;
            mapListContainer.style.paddingLeft     = 8;
            mapListContainer.style.paddingRight    = 8;
            mapListContainer.style.backgroundColor = new Color(0f, 0f, 0f, 0.4f);
            mapListContainer.style.marginBottom    = 24;
            root.Add(mapListContainer);

            // Player list container (rebuilt every Update from AllPlayers).
            playerListContainer = new VisualElement();
            playerListContainer.style.flexDirection    = FlexDirection.Column;
            playerListContainer.style.minWidth         = 520;
            playerListContainer.style.paddingTop       = 12;
            playerListContainer.style.paddingBottom    = 12;
            playerListContainer.style.paddingLeft      = 16;
            playerListContainer.style.paddingRight     = 16;
            playerListContainer.style.backgroundColor  = new Color(0f, 0f, 0f, 0.4f);
            playerListContainer.style.marginBottom     = 24;
            root.Add(playerListContainer);

            // Action row: Start Match (host) + Leave (everyone).
            var actionRow = new VisualElement();
            actionRow.style.flexDirection = FlexDirection.Row;
            actionRow.style.marginTop     = 8;
            root.Add(actionRow);

            startMatchButton = new Button(OnStartMatchClicked) { text = "Start Match" };
            startMatchButton.style.minWidth    = 200;
            startMatchButton.style.height      = 44;
            startMatchButton.style.fontSize    = 18;
            startMatchButton.style.marginRight = 16;
            startMatchButton.SetEnabled(false);
            actionRow.Add(startMatchButton);

            leaveButton = new Button(OnLeaveClicked) { text = "Leave Lobby" };
            leaveButton.style.minWidth = 200;
            leaveButton.style.height   = 44;
            leaveButton.style.fontSize = 18;
            actionRow.Add(leaveButton);

            statusLabel = new Label(string.Empty);
            statusLabel.style.color     = new Color(1f, 0.6f, 0.3f);
            statusLabel.style.marginTop = 16;
            statusLabel.style.minHeight = 20;
            root.Add(statusLabel);
        }

        // ----- Per-frame refresh ------------------------------------------

        private void RefreshMapList()
        {
            // Inputs that determine the rendered state of the map row. Anything not in this
            // signature won't trigger a rebuild — match what BuildMapCard reads.
            var registry = MapRegistry.Instance;
            var svc = LobbyService.Instance;
            int registryCount = registry != null ? registry.Count : 0;
            int selectedIndex = svc != null ? svc.SelectedMapIndex : -1;
            int playerCount = LobbyService.CountConnectedPlayers();
            bool isHost = NetworkManager.Singleton != null && NetworkManager.Singleton.IsHost;

            string signature = $"{registryCount}:{selectedIndex}:{playerCount}:{(isHost ? 'H' : 'C')}";
            if (signature == lastMapListSignature) return;
            lastMapListSignature = signature;

            mapListContainer.Clear();

            if (registry == null || registryCount == 0)
            {
                var emptyLabel = new Label("(no maps available — MapRegistry missing)");
                emptyLabel.style.color = new Color(0.7f, 0.5f, 0.5f);
                emptyLabel.style.unityTextAlign = TextAnchor.MiddleCenter;
                mapListContainer.Add(emptyLabel);
                return;
            }

            for (int i = 0; i < registryCount; i++)
            {
                var map = registry.Get(i);
                if (map == null) continue;

                bool isSelected = i == selectedIndex;
                bool fitsLobby = MapRegistry.IsSelectableFor(map, playerCount);
                bool clickable = isHost && fitsLobby;

                mapListContainer.Add(BuildMapCard(map, i, isSelected, fitsLobby, clickable, playerCount));
            }
        }

        private VisualElement BuildMapCard(LevelData map, int index, bool isSelected,
                                           bool fitsLobby, bool clickable, int playerCount)
        {
            var card = new VisualElement();
            card.style.width            = 180;
            card.style.height           = 220;
            card.style.marginRight      = 8;
            card.style.marginLeft       = 8;
            card.style.paddingTop       = 8;
            card.style.paddingBottom    = 8;
            card.style.paddingLeft      = 8;
            card.style.paddingRight     = 8;
            card.style.alignItems       = Align.Center;
            card.style.justifyContent   = Justify.FlexStart;

            // Background reflects selectability: highlighted when selected, dim when oversized.
            Color background = isSelected
                ? new Color(0.20f, 0.32f, 0.55f)
                : new Color(0.12f, 0.12f, 0.16f);
            if (!fitsLobby) background.a *= 0.7f;
            card.style.backgroundColor = background;

            // Border to make the selection visually unambiguous.
            float borderWidth = isSelected ? 3f : 1f;
            Color borderColor = isSelected
                ? new Color(0.45f, 0.70f, 1f)
                : new Color(0.25f, 0.25f, 0.30f);
            card.style.borderTopWidth    = borderWidth;
            card.style.borderBottomWidth = borderWidth;
            card.style.borderLeftWidth   = borderWidth;
            card.style.borderRightWidth  = borderWidth;
            card.style.borderTopColor    = borderColor;
            card.style.borderBottomColor = borderColor;
            card.style.borderLeftColor   = borderColor;
            card.style.borderRightColor  = borderColor;

            // Thumbnail (top of card). Falls back to a neutral placeholder if MapThumbnail isn't set.
            var thumb = new VisualElement();
            thumb.style.width  = 140;
            thumb.style.height = 105;
            thumb.style.marginBottom = 6;
            thumb.style.backgroundColor = new Color(0.05f, 0.05f, 0.08f);
            if (map.MapThumbnail != null)
            {
                thumb.style.backgroundImage = new StyleBackground(map.MapThumbnail);
            }
            // Dim the thumbnail when the map doesn't fit the lobby — reinforces the disabled look.
            if (!fitsLobby)
            {
                thumb.style.opacity = 0.45f;
            }
            card.Add(thumb);

            // Map name.
            var nameLabel = new Label(map.MapName);
            nameLabel.style.color = fitsLobby ? Color.white : new Color(0.7f, 0.6f, 0.6f);
            nameLabel.style.unityFontStyleAndWeight = FontStyle.Bold;
            nameLabel.style.fontSize = 14;
            nameLabel.style.unityTextAlign = TextAnchor.MiddleCenter;
            card.Add(nameLabel);

            // Player count label — tells the user the capacity, and why the card is greyed out
            // when oversized. Live player count is shown alongside the cap so it's obvious why.
            string countText = fitsLobby
                ? $"Up to {map.PlayerCount} players"
                : $"Up to {map.PlayerCount} — needs ≤ {map.PlayerCount} (lobby has {playerCount})";
            var countLabel = new Label(countText);
            countLabel.style.color = fitsLobby
                ? new Color(0.75f, 0.75f, 0.75f)
                : new Color(0.85f, 0.55f, 0.45f);
            countLabel.style.fontSize = 11;
            countLabel.style.unityTextAlign = TextAnchor.MiddleCenter;
            countLabel.style.marginTop = 2;
            countLabel.style.whiteSpace = WhiteSpace.Normal;
            card.Add(countLabel);

            // Make the whole card clickable for host (when the map fits the lobby). Non-hosts
            // see the cards but can't change selection. Capture `index` in a local so the
            // closure doesn't bind to the loop variable.
            if (clickable)
            {
                int capturedIndex = index;
                card.RegisterCallback<ClickEvent>(_ => OnMapCardClicked(capturedIndex));
                // Standard hover affordance to advertise interactivity.
                card.RegisterCallback<MouseEnterEvent>(_ =>
                {
                    if (!isSelected)
                        card.style.backgroundColor = new Color(0.17f, 0.20f, 0.28f);
                });
                card.RegisterCallback<MouseLeaveEvent>(_ =>
                {
                    if (!isSelected)
                        card.style.backgroundColor = new Color(0.12f, 0.12f, 0.16f);
                });
            }

            return card;
        }

        private void OnMapCardClicked(int index)
        {
            var svc = LobbyService.Instance;
            if (svc == null)
            {
                Debug.LogError("[LobbyController] LobbyService.Instance is null — cannot change map.");
                return;
            }
            // Don't bother round-tripping if we're already on this map.
            if (svc.SelectedMapIndex == index) return;
            svc.RequestSelectMapRpc(index);
        }

        private void RefreshPlayerList()
        {
            // Sort by slot for stable ordering. AllPlayers is keyed by clientId
            // which may not be slot-ordered.
            var players = PlayerMatchState.AllPlayers.OrderBy(p => (int)p.Slot).ToList();

            // Skip rebuild when the player-relevant state hasn't changed.
            // Without this guard the per-row buttons are destroyed every frame,
            // which loses clicks (see lastPlayerListSignature comment above).
            string signature = ComputePlayerListSignature(players);
            if (signature == lastPlayerListSignature) return;
            lastPlayerListSignature = signature;

            playerListContainer.Clear();

            ulong localId = NetworkManager.Singleton != null
                ? NetworkManager.Singleton.LocalClientId
                : ulong.MaxValue;

            foreach (var pms in players)
            {
                bool isLocal = pms.OwnerClientId == localId;
                playerListContainer.Add(BuildPlayerRow(pms, isLocal));
            }

            if (players.Count == 0)
                playerListContainer.Add(new Label("(no players connected)") { style = { color = Color.gray } });
        }

        // Compact signature of every field the player rows depend on. When this
        // changes we rebuild; when it's identical we leave the existing rows
        // (and their button event handlers) intact for click handling.
        private string ComputePlayerListSignature(System.Collections.Generic.List<PlayerMatchState> players)
        {
            signatureBuffer.Clear();
            foreach (var pms in players)
            {
                signatureBuffer.Append(pms.OwnerClientId);
                signatureBuffer.Append(':');
                signatureBuffer.Append((int)pms.Slot);
                signatureBuffer.Append(':');
                signatureBuffer.Append((int)pms.RaceSelection);
                signatureBuffer.Append(':');
                signatureBuffer.Append(pms.IsReady ? '1' : '0');
                signatureBuffer.Append(';');
            }
            return signatureBuffer.ToString();
        }

        private VisualElement BuildPlayerRow(PlayerMatchState pms, bool isLocal)
        {
            var row = new VisualElement();
            row.style.flexDirection    = FlexDirection.Row;
            row.style.alignItems       = Align.Center;
            row.style.paddingTop       = 6;
            row.style.paddingBottom    = 6;
            row.style.paddingLeft      = 8;
            row.style.paddingRight     = 8;
            row.style.marginBottom     = 4;
            row.style.backgroundColor  = isLocal
                ? new Color(0.18f, 0.22f, 0.30f)
                : new Color(0.10f, 0.10f, 0.12f);

            // Slot label.
            var slotLabel = new Label($"P{(int)pms.Slot}");
            slotLabel.style.minWidth  = 40;
            slotLabel.style.color     = Color.white;
            slotLabel.style.unityFontStyleAndWeight = FontStyle.Bold;
            row.Add(slotLabel);

            // Display name.
            var nameLabel = new Label(string.IsNullOrEmpty(pms.DisplayName)
                ? $"Player {(int)pms.Slot}"
                : pms.DisplayName);
            nameLabel.style.minWidth = 140;
            nameLabel.style.color    = Color.white;
            row.Add(nameLabel);

            // Race indicator.
            var raceLabel = new Label($"Race: {pms.RaceSelection}");
            raceLabel.style.minWidth = 160;
            raceLabel.style.color    = pms.RaceSelection == RaceId.None
                ? new Color(0.7f, 0.5f, 0.5f)
                : new Color(0.85f, 0.85f, 0.85f);
            row.Add(raceLabel);

            // Ready indicator.
            var readyLabel = new Label(pms.IsReady ? "READY ✓" : "not ready");
            readyLabel.style.minWidth = 100;
            readyLabel.style.color    = pms.IsReady
                ? new Color(0.3f, 0.85f, 0.3f)
                : new Color(0.6f, 0.6f, 0.6f);
            readyLabel.style.unityFontStyleAndWeight = FontStyle.Bold;
            row.Add(readyLabel);

            // Local-only controls.
            if (isLocal)
            {
                // Race selection — opens the overlay that lets the player browse
                // the 4x4 race grid and pick one. The overlay handles submission
                // (PlayerMatchState.SubmitRaceRpc) directly, so we just open it.
                var pickRaceBtn = new Button(OpenRaceOverlay)
                {
                    text = "Select Race"
                };
                pickRaceBtn.style.minWidth    = 130;
                pickRaceBtn.style.height      = 28;
                pickRaceBtn.style.marginLeft  = 12;
                pickRaceBtn.SetEnabled(raceOverlay != null);
                row.Add(pickRaceBtn);

                var readyBtn = new Button(() => pms.SubmitReadyRpc(!pms.IsReady))
                {
                    text = pms.IsReady ? "Unready" : "Ready"
                };
                readyBtn.style.minWidth    = 100;
                readyBtn.style.height      = 28;
                readyBtn.style.marginLeft  = 8;
                row.Add(readyBtn);
            }

            return row;
        }

        private void RefreshStartButton()
        {
            var nm = NetworkManager.Singleton;
            bool isHost = nm != null && nm.IsHost;

            startMatchButton.style.display = isHost ? DisplayStyle.Flex : DisplayStyle.None;
            if (!isHost) return;

            // Two gates: readiness (existing) and map selectability (new). Show whichever
            // failure is preventing the start so the host knows what to fix. Readiness
            // is checked first because it's the more common blocker.
            bool readyOk = LobbyService.AreAllPlayersReady(out string readyReason);
            string reason = readyOk ? string.Empty : readyReason;

            bool mapOk = true;
            if (readyOk)
            {
                var svc = LobbyService.Instance;
                var selected = svc != null ? svc.SelectedMap : null;
                int playerCount = LobbyService.CountConnectedPlayers();
                if (selected == null)
                {
                    mapOk = false;
                    reason = "No map selected.";
                }
                else if (!MapRegistry.IsSelectableFor(selected, playerCount))
                {
                    mapOk = false;
                    reason = $"'{selected.MapName}' supports up to {selected.PlayerCount} " +
                             $"players; lobby has {playerCount}.";
                }
            }

            bool canStart = readyOk && mapOk;
            startMatchButton.SetEnabled(canStart);
            statusLabel.text = canStart ? string.Empty : reason;
        }

        // ----- Button handlers --------------------------------------------

        private void OpenRaceOverlay()
        {
            if (raceOverlay == null)
            {
                Debug.LogWarning("[LobbyController] Race overlay not assigned.");
                return;
            }
            raceOverlay.Show();
        }

        private void OnStartMatchClicked()
        {
            var svc = LobbyService.Instance;
            if (svc == null)
            {
                Debug.LogError("[LobbyController] LobbyService.Instance is null. " +
                               "Scene setup is incomplete.");
                return;
            }
            svc.RequestStartMatchRpc();
        }

        private void OnLeaveClicked()
        {
            // Disconnect drops our connection. SessionFlow will route the
            // local peer back to the MainMenu scene when the disconnect
            // callback fires.
            NetworkBootstrap.Disconnect();
        }
    }
}
