// Assets/_Project/Scripts/Gameplay/LobbyService.cs
using Unity.Netcode;
using UnityEngine;
using TD.Core;
using TD.Levels;
using TD.Net;

namespace TD.Gameplay
{
    /// <summary>
    /// Scene singleton that owns lobby-wide actions: Start Match (host),
    /// Return to Lobby (after a match), and the "host disconnected" broadcast.
    /// Lives in the <c>Lobby</c> scene with a NetworkObject; spawns when the
    /// scene loads.
    /// </summary>
    /// <remarks>
    /// <para><b>What lives here vs. on PlayerMatchState.</b> Per-player state
    /// (race, ready, slot, display name) lives on <see cref="PlayerMatchState"/>
    /// because it carries across scene transitions with the player connection.
    /// Lobby-wide ACTIONS (Start Match, Return to Lobby) live here because they
    /// are scoped to the lobby scene and don't need to persist.</para>
    ///
    /// <para><b>Start Match flow.</b>
    /// <list type="number">
    ///   <item>Host clicks Start in the lobby UI.</item>
    ///   <item>UI calls <see cref="RequestStartMatchRpc"/>; server validates:
    ///         every PlayerMatchState has a race and IsReady.</item>
    ///   <item>Server calls <c>NetworkBootstrap.LoadSceneAsHost(SceneNames.Match)</c>.
    ///         NGO replicates the scene load to every client.</item>
    ///   <item>Match scene loads; <see cref="MatchState"/> spawns there; gameplay
    ///         begins as usual.</item>
    /// </list></para>
    ///
    /// <para><b>Return to Lobby flow.</b> Triggered by the match-end overlay's
    /// "Retry" button. Server calls <see cref="RequestReturnToLobbyRpc"/>;
    /// resets <see cref="PlayerMatchState.IsReady"/> on every player; loads
    /// the Lobby scene. Race picks are preserved so players don't have to
    /// re-select.</para>
    ///
    /// <para><b>Host-leaves behavior (Option A).</b> When the host quits, NGO
    /// shuts down the connection for every client. Clients detect the disconnect
    /// in <see cref="MainMenuRedirectOnDisconnect"/> (subscribed to
    /// <c>NetworkManager.OnClientDisconnectCallback</c> on the local client)
    /// and load the MainMenu scene locally. See roadmap §1.7-Future Steam Lobby
    /// Migration for the Option C upgrade (lobby persists, new host elected)
    /// once Steam SDK is in.</para>
    /// </remarks>
    [RequireComponent(typeof(NetworkObject))]
    public class LobbyService : NetworkBehaviour
    {
        // ----- Singleton --------------------------------------------------

        public static LobbyService Instance { get; private set; }

        // ----- Networked lobby state --------------------------------------

        // Index into MapRegistry.Maps of the currently selected map. Server-write,
        // everyone-read. Default 0 (the first registered map = MapRegistry.Default).
        // UI subscribes to OnValueChanged to refresh the map browser highlight.
        // A stale index (map removed between sessions) is tolerated by MapRegistry.Get
        // returning null; RequestStartMatchRpc validates before loading.
        private readonly NetworkVariable<int> selectedMapIndex =
            new NetworkVariable<int>(0, NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Server);

        /// <summary>
        /// The index of the currently selected map within <see cref="MapRegistry.Maps"/>.
        /// All clients read the same value; only the host can change it via
        /// <see cref="RequestSelectMapRpc"/>.
        /// </summary>
        public int SelectedMapIndex => selectedMapIndex.Value;

        /// <summary>
        /// Exposed for UI subscription to <c>OnValueChanged</c>. Treat as read-only —
        /// mutations must go through the server via <see cref="RequestSelectMapRpc"/>
        /// so the host-only gate is enforced.
        /// </summary>
        public NetworkVariable<int> SelectedMapIndexVar => selectedMapIndex;

        /// <summary>
        /// Convenience: resolves the currently selected <see cref="LevelData"/> via
        /// <see cref="MapRegistry"/>. Returns null if the registry is missing (e.g. the
        /// editor was started directly in the Lobby scene) or the index is stale.
        /// </summary>
        public LevelData SelectedMap =>
            MapRegistry.Instance != null
                ? MapRegistry.Instance.Get(selectedMapIndex.Value)
                : null;

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

        public override void OnNetworkSpawn()
        {
            if (Instance != null && Instance != this)
            {
                Debug.LogError("[LobbyService] Duplicate instance detected. " +
                               "Only one LobbyService should exist per scene.");
                return;
            }
            Instance = this;

            // Entering the lobby always starts everyone un-ready. Race picks
            // are preserved from the previous lobby visit, but ready state
            // resets so each match requires explicit re-readying.
            if (IsServer)
            {
                ResetAllReady();

                // If the current selection is invalid for any reason (registry missing,
                // index stale from a previous session), snap to the default. Index 0
                // is already the default-default; this is a no-op except after the
                // registry's contents change between sessions.
                var registry = MapRegistry.Instance;
                if (registry != null && registry.Get(selectedMapIndex.Value) == null && registry.Count > 0)
                {
                    selectedMapIndex.Value = 0;
                }
            }
        }

        public override void OnNetworkDespawn()
        {
            if (Instance == this) Instance = null;
        }

        // ----- Public RPC API (client → server) --------------------------

        /// <summary>
        /// Host-only request to begin the match. Validates that every connected
        /// player has picked a race and is ready AND that the selected map can
        /// accommodate the current lobby's player count, then transitions every
        /// peer to the selected map's scene via NGO scene management.
        /// </summary>
        [Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Everyone)]
        public void RequestStartMatchRpc(RpcParams rpcParams = default)
        {
            // Only the host's local client may start a match. We compare the
            // sender's clientId to the server's clientId (which == the host's
            // local clientId in host mode).
            if (rpcParams.Receive.SenderClientId != NetworkManager.Singleton.LocalClientId)
            {
                Debug.LogWarning("[LobbyService] Non-host client attempted to start the match. Ignored.");
                return;
            }

            if (!AreAllPlayersReady(out string reason))
            {
                Debug.Log($"[LobbyService] Cannot start match: {reason}");
                return;
            }

            // Re-validate the selected map server-side. The UI greys out unselectable
            // maps based on lobby size, but a late join could push us above the map's
            // PlayerCount between the click and the RPC arriving. Defensive check.
            var registry = MapRegistry.Instance;
            if (registry == null)
            {
                Debug.LogError("[LobbyService] Cannot start match: MapRegistry.Instance is null. " +
                               "Make sure MainMenu was loaded before the lobby (the registry " +
                               "DontDestroyOnLoads from there).");
                return;
            }

            var selected = registry.Get(selectedMapIndex.Value);
            if (selected == null)
            {
                Debug.LogError($"[LobbyService] Cannot start match: selected map index " +
                               $"{selectedMapIndex.Value} is not registered.");
                return;
            }

            int playerCount = CountConnectedPlayers();
            if (!MapRegistry.IsSelectableFor(selected, playerCount))
            {
                Debug.Log($"[LobbyService] Cannot start match: map '{selected.MapName}' supports " +
                          $"up to {selected.PlayerCount} players but the lobby has {playerCount}.");
                return;
            }

            if (string.IsNullOrEmpty(selected.SceneName))
            {
                Debug.LogError($"[LobbyService] Cannot start match: '{selected.MapName}' has " +
                               $"empty SceneName (ScenePath='{selected.ScenePath}'). Re-bake the level.");
                return;
            }

            Debug.Log($"[LobbyService] Starting match on '{selected.MapName}' " +
                      $"(index={selectedMapIndex.Value}, scene='{selected.SceneName}', " +
                      $"scenePath='{selected.ScenePath}', players={playerCount}).");
            NetworkBootstrap.LoadSceneAsHost(selected.SceneName);
        }

        /// <summary>
        /// Host-only request to change the selected map. Validates that the requested index
        /// resolves to a real map in <see cref="MapRegistry"/>; if so, writes
        /// <see cref="SelectedMapIndex"/> which replicates to every client.
        /// </summary>
        /// <remarks>
        /// Selectability for the current lobby size is NOT enforced here — players are
        /// allowed to highlight an oversized map (e.g. while waiting for more friends to
        /// join); the actual Start Match call enforces the rule. This keeps the host's
        /// intent visible to everyone without preventing them from "claiming" a future map.
        /// </remarks>
        [Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Everyone)]
        public void RequestSelectMapRpc(int mapIndex, RpcParams rpcParams = default)
        {
            if (rpcParams.Receive.SenderClientId != NetworkManager.Singleton.LocalClientId)
            {
                Debug.LogWarning("[LobbyService] Non-host client attempted to change the map. Ignored.");
                return;
            }

            var registry = MapRegistry.Instance;
            if (registry == null)
            {
                Debug.LogError("[LobbyService] Cannot change map: MapRegistry.Instance is null.");
                return;
            }

            if (registry.Get(mapIndex) == null)
            {
                Debug.LogWarning($"[LobbyService] Rejected map index {mapIndex} — not registered.");
                return;
            }

            selectedMapIndex.Value = mapIndex;
        }

        /// <summary>
        /// Anyone can request a return to the lobby (typically the host clicks
        /// "Retry" after a match). Server-side: resets ready state on every
        /// player and triggers the scene transition. Race picks are preserved.
        /// </summary>
        [Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Everyone)]
        public void RequestReturnToLobbyRpc()
        {
            // We don't gate this on host-only currently — any client retrying is
            // semantically equivalent (the host needs to be willing to keep
            // running). When dedicated server / Steam migration lands this
            // becomes host-only.
            ResetAllReady();
            NetworkBootstrap.LoadSceneAsHost(SceneNames.Lobby);
        }

        // ----- Public queries --------------------------------------------

        /// <summary>
        /// True if every connected player has picked a race AND marked themselves
        /// ready. Returns the reason as <paramref name="reason"/> when false
        /// (for debug logs / future UI tooltips on the disabled Start button).
        /// </summary>
        public static bool AreAllPlayersReady(out string reason)
        {
            int count = 0;
            foreach (var pms in PlayerMatchState.AllPlayers)
            {
                count++;
                if (pms.RaceSelection == RaceId.None)
                {
                    reason = $"Slot {pms.Slot} hasn't picked a race.";
                    return false;
                }
                if (!pms.IsReady)
                {
                    reason = $"Slot {pms.Slot} is not ready.";
                    return false;
                }
            }

            if (count == 0)
            {
                reason = "No players connected.";
                return false;
            }

            reason = string.Empty;
            return true;
        }

        /// <summary>
        /// Current number of connected players (every <see cref="PlayerMatchState"/> in the
        /// static registry). Used by map selectability checks both in the UI and the server-side
        /// Start Match validator.
        /// </summary>
        public static int CountConnectedPlayers()
        {
            int n = 0;
            foreach (var _ in PlayerMatchState.AllPlayers) n++;
            return n;
        }

        // ----- Server helpers --------------------------------------------

        private static void ResetAllReady()
        {
            foreach (var pms in PlayerMatchState.AllPlayers)
                pms.SetReady(false);
        }
    }
}
