// Assets/_Project/Scripts/Gameplay/MapRegistry.cs
using System.Collections.Generic;
using UnityEngine;
using TD.Levels;

namespace TD.Gameplay
{
    /// <summary>
    /// Persistent (DontDestroyOnLoad) singleton that holds every <see cref="LevelData"/> available
    /// to the lobby's map browser. Mirrors <see cref="RaceRegistry"/>'s pattern.
    /// </summary>
    /// <remarks>
    /// <para><b>Inspector setup.</b> Place ONE <c>MapRegistry</c> GameObject in the <b>MainMenu
    /// scene</b> only. Drag every <see cref="LevelData"/> asset that should appear in the lobby's
    /// map browser into the <c>Maps</c> array. The first non-null entry becomes the
    /// <see cref="Default"/> map (auto-selected when a lobby first opens). Mark itself
    /// <c>DontDestroyOnLoad</c> on Awake so it survives MainMenu → Lobby → Match transitions and
    /// is reachable from any scene via <see cref="Instance"/>.</para>
    ///
    /// <para><b>Why MainMenu-only?</b> Same reason as <see cref="RaceRegistry"/>: a single
    /// authoritative array prevents the "I updated one and forgot the other" designer trap.
    /// Duplicate instances dropped into Lobby or Match scenes self-destroy on Awake.</para>
    ///
    /// <para><b>Editor-only standalone testing.</b> If you open the Lobby or Match scene directly
    /// from the editor without going through MainMenu, no MapRegistry exists. <see cref="Instance"/>
    /// is null; callers should handle that (the lobby UI shows an empty browser, Quick Start falls
    /// back to a hardcoded scene, etc.). For standalone testing, temporarily add a registry to
    /// whatever scene you're testing — but don't commit it.</para>
    ///
    /// <para><b>Selectability.</b> A map is selectable in a lobby of N players iff
    /// <c>N &lt;= map.PlayerCount</c>. The lobby UI still shows maps it can't currently use, just
    /// greyed out with an explanatory label — so players can see what other maps exist and how
    /// many players they'd need to play them.</para>
    ///
    /// <para><b>Plain MonoBehaviour.</b> Not a NetworkBehaviour — every peer has the same LevelData
    /// assets in the build. Network state tracks only the selected map's index via
    /// <c>LobbyService.SelectedMapIndex</c>.</para>
    /// </remarks>
    public class MapRegistry : MonoBehaviour
    {
        // ----- Singleton -------------------------------------------------

        public static MapRegistry Instance { get; private set; }

        // ----- Inspector --------------------------------------------------

        [Tooltip("All LevelData assets that should appear in the lobby's map browser. Drag each " +
                 "asset into the array. The FIRST non-null entry becomes the default selection " +
                 "when a lobby first opens; order subsequent entries however you want them sorted " +
                 "in the UI. Null entries and assets with empty MapName are skipped with a warning.")]
        [SerializeField] private LevelData[] maps;

        // ----- Public API -------------------------------------------------

        /// <summary>
        /// All registered maps in inspector order. Never returns null entries; entries that failed
        /// validation in Awake are filtered out. Safe to iterate any time after Awake.
        /// </summary>
        public IReadOnlyList<LevelData> Maps => validatedMaps;

        /// <summary>
        /// Total number of valid registered maps. Equivalent to <c>Maps.Count</c> but cheaper
        /// since it doesn't allocate an enumerator.
        /// </summary>
        public int Count => validatedMaps.Count;

        /// <summary>
        /// The map auto-selected when a lobby first opens. Returns the first valid entry in the
        /// inspector array, or null if the registry has no valid maps (which would be a setup
        /// error — Quick Start and lobby will both log and degrade).
        /// </summary>
        public LevelData Default => validatedMaps.Count > 0 ? validatedMaps[0] : null;

        /// <summary>
        /// Returns the map at the given index, or null if the index is out of range. Tolerant of
        /// stale indices that might survive a registry edit (e.g. a NetworkVariable holding an
        /// index whose map was removed).
        /// </summary>
        public LevelData Get(int index)
        {
            if (index < 0 || index >= validatedMaps.Count) return null;
            return validatedMaps[index];
        }

        /// <summary>
        /// Returns the index of the given map in <see cref="Maps"/>, or -1 if it isn't registered.
        /// Use this when you have a LevelData reference and need to sync the selection over the
        /// network as an integer.
        /// </summary>
        public int IndexOf(LevelData map)
        {
            if (map == null) return -1;
            for (int i = 0; i < validatedMaps.Count; i++)
            {
                if (validatedMaps[i] == map) return i;
            }
            return -1;
        }

        /// <summary>
        /// True if a lobby with <paramref name="playerCount"/> players is allowed to start a match
        /// on <paramref name="map"/>. Currently the rule is just "lobby size must fit"; if minimum
        /// player counts become a thing later, gate them here.
        /// </summary>
        public static bool IsSelectableFor(LevelData map, int playerCount)
        {
            if (map == null) return false;
            if (playerCount < 1) return false;
            return playerCount <= map.PlayerCount;
        }

        /// <summary>
        /// True if any registered map's <see cref="LevelData.SceneName"/> matches the given
        /// scene name. Used by components on the persistent Player Prefab
        /// (e.g. <c>PlayerBuilderSpawner</c>) to recognize "we just entered a match scene"
        /// without hardcoding individual scene names. Empty/null inputs return false.
        /// </summary>
        public bool ContainsScene(string sceneName)
        {
            if (string.IsNullOrEmpty(sceneName)) return false;
            for (int i = 0; i < validatedMaps.Count; i++)
            {
                if (validatedMaps[i].SceneName == sceneName) return true;
            }
            return false;
        }

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

        // Validated subset of the inspector array; built once in Awake and never mutated.
        // Holding a separate list lets us silently filter out null/invalid entries without
        // mutating the inspector array (which would surprise designers).
        private readonly List<LevelData> validatedMaps = new List<LevelData>();

        private void Awake()
        {
            // Persistent-singleton pattern: first instance wins, duplicates self-destroy.
            if (Instance != null && Instance != this)
            {
                Debug.LogWarning(
                    $"[MapRegistry] Persistent instance already exists. " +
                    $"Destroying duplicate in scene '{gameObject.scene.name}'. " +
                    $"Keep MapRegistry in the MainMenu scene only.");
                Destroy(gameObject);
                return;
            }
            Instance = this;
            DontDestroyOnLoad(gameObject);
            BuildLookup();
        }

        private void OnDestroy()
        {
            if (Instance == this) Instance = null;
        }

        // ----- Private ----------------------------------------------------

        private void BuildLookup()
        {
            validatedMaps.Clear();

            if (maps == null || maps.Length == 0)
            {
                Debug.LogWarning("[MapRegistry] No LevelData assets assigned. Drag assets into " +
                                 "the Maps array. Lobby map browser will be empty.");
                return;
            }

            var seen = new HashSet<LevelData>();
            foreach (var map in maps)
            {
                if (map == null) continue;

                // Duplicates are likely an authoring mistake (designer dragged the same asset
                // twice); keep the first occurrence and warn.
                if (!seen.Add(map))
                {
                    Debug.LogWarning($"[MapRegistry] Duplicate LevelData '{map.name}' in Maps " +
                                     $"array. Keeping first occurrence only.");
                    continue;
                }

                if (string.IsNullOrEmpty(map.MapName))
                {
                    Debug.LogWarning($"[MapRegistry] '{map.name}' has empty MapName — skipping. " +
                                     "Set MapName on the LevelData asset.");
                    continue;
                }

                if (string.IsNullOrEmpty(map.ScenePath))
                {
                    Debug.LogWarning($"[MapRegistry] '{map.name}' has empty ScenePath — skipping. " +
                                     "Bake the level so ScenePath gets populated.");
                    continue;
                }

                if (map.PlayerCount < 1)
                {
                    Debug.LogWarning($"[MapRegistry] '{map.name}' has PlayerCount={map.PlayerCount} " +
                                     "(must be >= 1) — skipping.");
                    continue;
                }

                validatedMaps.Add(map);
            }

            Debug.Log($"[MapRegistry] Registered {validatedMaps.Count} map(s). " +
                      $"Default = '{(Default != null ? Default.MapName : "<none>")}'.");
        }
    }
}
