// Assets/_Project/Scripts/Gameplay/PlayerMatchState.cs
using System.Collections.Generic;
using Unity.Collections;
using Unity.Netcode;
using UnityEngine;
using TD.Core;

namespace TD.Gameplay
{
    /// <summary>
    /// Per-player match state. One instance is spawned per connected client (via
    /// <see cref="NetworkManager.PlayerPrefab"/>) and owned by that client.
    ///
    /// Mirrors the <see cref="PlayerGoldManager"/> pattern: static registry keyed by
    /// <c>OwnerClientId</c>, server-authoritative <c>NetworkVariable</c>s, and a
    /// <c>Local</c> convenience accessor.
    ///
    /// The server assigns a <see cref="PlayerSlot"/> on spawn using the next free slot
    /// (Player1..Player9 in connect order). All five former STUB mappings
    /// (Builder, TowerPlacementManager, TowerPlacementController, CameraController,
    /// PlayerBuilderSpawner) delegate here instead.
    /// </summary>
    public class PlayerMatchState : NetworkBehaviour
    {
        // --- Static registry ---------------------------------------------

        private static readonly Dictionary<ulong, PlayerMatchState> s_byClientId
            = new Dictionary<ulong, PlayerMatchState>();

        // Tracks which slots are currently occupied so the server can assign the next free one.
        private static readonly HashSet<PlayerSlot> s_assignedSlots = new HashSet<PlayerSlot>();

        /// <summary>
        /// Returns the <see cref="PlayerMatchState"/> for the given client, or null if not spawned.
        /// Safe to call on server or client.
        /// </summary>
        public static PlayerMatchState GetForClient(ulong clientId)
        {
            s_byClientId.TryGetValue(clientId, out var state);
            return state;
        }

        /// <summary>
        /// Returns the <see cref="PlayerSlot"/> for the given client, or <see cref="PlayerSlot.None"/>
        /// if the client has no spawned <see cref="PlayerMatchState"/>.
        /// Convenience wrapper over <see cref="GetForClient"/>.
        /// </summary>
        public static PlayerSlot SlotForClient(ulong clientId)
            => GetForClient(clientId)?.Slot ?? PlayerSlot.None;

        /// <summary>
        /// Returns the <see cref="PlayerMatchState"/> whose assigned slot matches
        /// <paramref name="slot"/>, or null if no connected client holds that slot.
        /// O(n) over connected players (max 9) — acceptable for server-side use.
        /// Used by <c>WaveManager</c> to resolve kill-gold recipients.
        /// </summary>
        public static PlayerMatchState GetForSlot(PlayerSlot slot)
        {
            foreach (var pms in s_byClientId.Values)
                if (pms.Slot == slot) return pms;
            return null;
        }

        /// <summary>
        /// The local client's own state. Null on a dedicated server or before the
        /// local player has spawned.
        /// </summary>
        public static PlayerMatchState Local
        {
            get
            {
                var nm = NetworkManager.Singleton;
                if (nm == null || !nm.IsClient) return null;
                return GetForClient(nm.LocalClientId);
            }
        }

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

        private readonly NetworkVariable<PlayerSlot> slot = new NetworkVariable<PlayerSlot>(
            value: PlayerSlot.None,
            readPerm: NetworkVariableReadPermission.Everyone,
            writePerm: NetworkVariableWritePermission.Server
        );

        private readonly NetworkVariable<FixedString32Bytes> displayName = new NetworkVariable<FixedString32Bytes>(
            value: default,
            readPerm: NetworkVariableReadPermission.Everyone,
            writePerm: NetworkVariableWritePermission.Server
        );

        // Race chosen by this player. Written by the lobby system in response to
        // SetRaceRpc; carries over scene transitions because PlayerMatchState is
        // the player prefab (owned by the connection, persists across scenes).
        private readonly NetworkVariable<RaceId> raceSelection = new NetworkVariable<RaceId>(
            value: RaceId.None,
            readPerm: NetworkVariableReadPermission.Everyone,
            writePerm: NetworkVariableWritePermission.Server
        );

        // Lobby ready-state. Cleared back to false on scene transitions out of
        // the lobby (the new lobby session starts everyone un-ready).
        private readonly NetworkVariable<bool> isReady = new NetworkVariable<bool>(
            value: false,
            readPerm: NetworkVariableReadPermission.Everyone,
            writePerm: NetworkVariableWritePermission.Server
        );

        /// <summary>This player's assigned slot in the match. Authoritative once spawned.</summary>
        public PlayerSlot Slot => slot.Value;

        /// <summary>Display name. Stub until a lobby/name system provides it.</summary>
        public string DisplayName => displayName.Value.ToString();

        /// <summary>Race chosen by this player. <see cref="RaceId.None"/> until selected in the lobby.</summary>
        public RaceId RaceSelection => raceSelection.Value;

        /// <summary>True if this player has marked themselves ready in the lobby.</summary>
        public bool IsReady => isReady.Value;

        /// <summary>Server-only. Sets this player's race selection.</summary>
        public void SetRaceSelection(RaceId race)
        {
            if (!IsServer) { Debug.LogWarning("[PlayerMatchState] SetRaceSelection called on a client — ignored."); return; }
            raceSelection.Value = race;
        }

        /// <summary>Server-only. Sets this player's ready state.</summary>
        public void SetReady(bool ready)
        {
            if (!IsServer) { Debug.LogWarning("[PlayerMatchState] SetReady called on a client — ignored."); return; }
            isReady.Value = ready;
        }

        // ----- Client → Server lobby RPCs --------------------------------

        /// <summary>
        /// Client → server: submit a race selection. Owner-only — players can
        /// only change their own race. Validates that the caller is the owner
        /// of this PlayerMatchState; ignored otherwise.
        /// </summary>
        [Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Owner)]
        public void SubmitRaceRpc(RaceId race)
        {
            // Reset ready state when race changes — players shouldn't be "ready"
            // with a stale pick.
            if (raceSelection.Value != race)
            {
                raceSelection.Value = race;
                isReady.Value       = false;
            }
        }

        /// <summary>
        /// Client → server: submit ready state. Owner-only. A player can only
        /// ready up if they've picked a race; the lobby UI gates this already
        /// but the server enforces it as a safety net.
        /// </summary>
        [Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Owner)]
        public void SubmitReadyRpc(bool ready)
        {
            if (ready && raceSelection.Value == RaceId.None)
            {
                Debug.Log($"[PlayerMatchState] Slot {Slot} tried to ready up without a race. Ignored.");
                return;
            }
            isReady.Value = ready;
        }

        /// <summary>
        /// Server-side helper: enumerate every currently-spawned PlayerMatchState.
        /// Used by <c>LobbyService</c> to evaluate "are all players ready" and
        /// by <c>LobbyController</c> on every peer to render the player list.
        /// </summary>
        public static IEnumerable<PlayerMatchState> AllPlayers => s_byClientId.Values;

        /// <summary>
        /// Server-only. Fires on the server immediately after the slot is assigned in
        /// <see cref="OnNetworkSpawn"/>. Sibling components (e.g. PlayerBuilderSpawner)
        /// subscribe to defer work that requires a valid slot.
        /// </summary>
        public event System.Action<PlayerSlot> SlotReady;

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

        public override void OnNetworkSpawn()
        {
            s_byClientId[OwnerClientId] = this;

            if (IsServer)
            {
                PlayerSlot assigned = AllocateNextSlot();
                slot.Value = assigned;
                displayName.Value = new FixedString32Bytes($"Player {(int)assigned}");
                s_assignedSlots.Add(assigned);

                Debug.Log($"[PlayerMatchState] Assigned slot {assigned} to client {OwnerClientId}.");
                SlotReady?.Invoke(assigned);
            }
        }

        public override void OnNetworkDespawn()
        {
            if (s_byClientId.TryGetValue(OwnerClientId, out var registered) && registered == this)
                s_byClientId.Remove(OwnerClientId);

            if (IsServer)
                s_assignedSlots.Remove(slot.Value);
        }

        // --- Slot allocation (server-only) -------------------------------

        private static PlayerSlot AllocateNextSlot()
        {
            for (int i = 1; i <= 9; i++)
            {
                var candidate = (PlayerSlot)i;
                if (!s_assignedSlots.Contains(candidate))
                    return candidate;
            }

            Debug.LogError("[PlayerMatchState] No free PlayerSlot (already 9 players). " +
                           "Returning None — this client will be unable to play.");
            return PlayerSlot.None;
        }
    }
}
