// Assets/_Project/Scripts/Gameplay/TowerInstance.cs
using Unity.Netcode;
using UnityEngine;
using TD.Core;
using TD.Towers;
using TD.UI.Minimap;

namespace TD.Gameplay
{
    /// <summary>
    /// Per-tower runtime component. Lives on the tower's NetworkObject prefab root.
    ///
    /// Responsibilities:
    /// <list type="bullet">
    ///   <item>Hold the network-replicated identity of this tower: which
    ///         <see cref="TowerDefinition"/> it is and which <see cref="PlayerSlot"/> owns it.</item>
    ///   <item>On <see cref="OnNetworkSpawn"/>, stamp the tower's footprint into
    ///         <see cref="LevelLoader"/> on every client so local grids stay in sync
    ///         with the server-authoritative state.</item>
    ///   <item>Apply the owner's player color to the tower mesh, so towers are
    ///         visually distinct by zone during testing.</item>
    /// </list>
    /// </remarks>
    /// <remarks>
    /// <para><b>Grid stamping split.</b> The server stamps the footprint in
    /// <c>TowerPlacementManager.ProcessRequest</c> (before <c>NetworkObject.Spawn</c>)
    /// so the path-validity check in the same frame sees the updated grid. Non-host
    /// clients stamp in <see cref="OnNetworkSpawn"/> when NGO replicates the
    /// NetworkObject to them. The server's <see cref="OnNetworkSpawn"/> also runs,
    /// but by then the footprint is already stamped — <see cref="SetWalkable"/> and
    /// <see cref="SetOccupied"/> are idempotent writes, so double-stamping is safe.</para>
    ///
    /// <para><b>Definition reference replication.</b> TowerDefinition assets live in the
    /// project on all clients. We replicate the tower's <c>TowerTypeId</c> (its index in
    /// <see cref="TowerPlacementManager"/>'s catalog) via a <see cref="NetworkVariable{T}"/>,
    /// then resolve the full asset locally with <see cref="TowerPlacementManager.GetDefinition"/>.
    /// This avoids serializing the ScriptableObject over the network and reuses the same
    /// identifier placement and the deck already use — a single source of truth.</para>
    ///
    /// <para><b>Combat.</b> No combat logic here yet. Combat fields live stubbed on
    /// <see cref="TowerDefinition"/>; they will be consumed by a future
    /// <c>TowerCombat</c> component added to the same prefab.</para>
    /// </remarks>
    [RequireComponent(typeof(NetworkObject))]
    public class TowerInstance : NetworkBehaviour, IMinimapEntity, ISelectable
    {
        // ----- Inspector --------------------------------------------------

        [Header("Visuals")]
        [Tooltip("Mesh renderers tinted with the owner's player color (and the Paint " +
                 "tool's color). Drag in only the tower body's renderers to exclude " +
                 "anything with its own color rules (selection rings, range indicators, " +
                 "FX). If left EMPTY, every MeshRenderer under the tower is tinted " +
                 "automatically — fine for most prefabs; populate this only when you need " +
                 "to exclude specific children.")]
        [SerializeField] private MeshRenderer[] tintedRenderers;

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

        // The TowerTypeId (index into TowerPlacementManager's catalog) for this tower.
        // Replicated so every client resolves the full definition locally via the catalog —
        // the same identifier placement and the deck already use. 0 = unset/invalid (the
        // reserved catalog index). Replaces the old replicate-by-name + TowerRegistry path.
        private readonly NetworkVariable<int> definitionTypeId =
            new NetworkVariable<int>(
                0,
                readPerm: NetworkVariableReadPermission.Everyone,
                writePerm: NetworkVariableWritePermission.Server);

        // The footprint anchor (SW corner, world-tile coords). Replicated so
        // clients can stamp the correct tiles in OnNetworkSpawn.
        private readonly NetworkVariable<Vector2Int> anchorTile =
            new NetworkVariable<Vector2Int>(
                default,
                readPerm: NetworkVariableReadPermission.Everyone,
                writePerm: NetworkVariableWritePermission.Server);

        // The PlayerSlot that placed this tower. Replicated for the HUD context
        // panel and for view-selection by non-owning clients.
        private readonly NetworkVariable<PlayerSlot> ownerSlot =
            new NetworkVariable<PlayerSlot>(
                PlayerSlot.None,
                readPerm: NetworkVariableReadPermission.Everyone,
                writePerm: NetworkVariableWritePermission.Server);

        // Paint color applied by the Paint tool. None means "unpainted" — the tower
        // shows its owner color. Set server-side via RequestPaintServerRpc (own-tower
        // only). Replicated so every client re-tints; later iterations will read this
        // to drive projectile behavior.
        private readonly NetworkVariable<PaintColor> paintColor =
            new NetworkVariable<PaintColor>(
                PaintColor.None,
                readPerm: NetworkVariableReadPermission.Everyone,
                writePerm: NetworkVariableWritePermission.Server);

        // ----- Local resolved state -------------------------------------------

        // Resolved on every client in OnNetworkSpawn from definitionTypeId via the catalog.
        // Null if the lookup fails (TypeId not in the catalog on this peer).
        private TowerDefinition resolvedDefinition;

        // ----- Pre-spawn initialization data ----------------------------------
        //
        // Set by InitializeServer (called by TowerPlacementManager BEFORE Spawn).
        // Read by the server's OnNetworkSpawn to populate the NetworkVariables.
        //
        // Why this two-step dance: NGO 2.x disallows writing NetworkVariables
        // before NetworkObject.Spawn() — those writes produce warnings and may
        // not replicate reliably. The supported pattern is to set NVs inside
        // OnNetworkSpawn on the server; NGO captures those writes and includes
        // them in the initial sync message sent to clients, so every client
        // sees correct values on its very first OnNetworkSpawn callback.

        private TowerDefinition pendingDefinition;
        private Vector2Int pendingAnchor;
        private PlayerSlot pendingOwner = PlayerSlot.None;
        private bool hasPendingInit;

        // ----- Public accessors -----------------------------------------------

        /// <summary>The TowerDefinition for this tower, resolved locally. Null until
        /// <see cref="OnNetworkSpawn"/> runs and the definition lookup succeeds.</summary>
        public TowerDefinition Definition => resolvedDefinition;

        /// <summary>The PlayerSlot that placed this tower.</summary>
        public PlayerSlot Owner => ownerSlot.Value;

        /// <summary>The paint color applied to this tower, or <see cref="PaintColor.None"/>
        /// if unpainted (showing its owner color).</summary>
        public PaintColor Paint => paintColor.Value;

        /// <summary>The footprint anchor tile (SW corner, world-tile coords).</summary>
        public Vector2Int AnchorTile => anchorTile.Value;

        // ----- ISelectable ----------------------------------------------------

        // Absolute world-unit margin that the selection ring extends beyond the
        // tower's footprint edges. Tuned for visibility — the tower body sits ON
        // the ring at ground level, so only the area outside the footprint is
        // actually rendered. Too small (was 0.15) and the ring is invisible under
        // anything taller than a paving stone. 0.5 gives a half-tile-wide visible
        // band around the tower at any footprint size.
        private const float SelectionRingPadding = 0.5f;

        /// <summary>Display name shown in the HUD portrait when this tower is selected.</summary>
        public string DisplayName =>
            resolvedDefinition != null ? resolvedDefinition.DisplayName : "Tower";

        public SelectableKind Kind => SelectableKind.Tower;

        public Transform SelectionTransform => transform;

        // Ring radius derived from footprint: max axis * 0.5 * tile size, plus a
        // small padding so the ring is visible outside the tower's edges. Falls
        // back to 1×1 if the definition hasn't resolved yet (transient, harmless —
        // the HUD won't allow selection until OnNetworkSpawn finishes anyway).
        public float SelectionRadius
        {
            get
            {
                Vector2Int fp = resolvedDefinition != null
                    ? resolvedDefinition.FootprintSize
                    : new Vector2Int(1, 1);
                return Mathf.Max(fp.x, fp.y) * 0.5f * GridCoordinates.TILE_SIZE
                       + SelectionRingPadding;
            }
        }

        // ----- Server-only initialization -------------------------------------

        /// <summary>
        /// Called by <c>TowerPlacementManager</c> on the server immediately after
        /// instantiation and before <c>NetworkObject.Spawn</c>. Stores the data that
        /// the server's <see cref="OnNetworkSpawn"/> will copy into the
        /// NetworkVariables. NetworkVariables themselves are NOT written here —
        /// see the comment on the pending-init fields above for why.
        /// </summary>
        public void InitializeServer(TowerDefinition def, Vector2Int anchor, PlayerSlot owner)
        {
            var nm = NetworkManager.Singleton;
            if (nm == null || !nm.IsServer)
            {
                Debug.LogError("[TowerInstance] InitializeServer called when not running " +
                               "as a server. This must only be called by " +
                               "TowerPlacementManager on the server.");
                return;
            }

            pendingDefinition = def;
            pendingAnchor = anchor;
            pendingOwner = owner;
            hasPendingInit = true;

            // Cache the resolved definition on the server immediately — clients
            // resolve from the replicated TowerTypeId via the catalog once it arrives.
            resolvedDefinition = def;
        }

        // ----- NGO lifecycle --------------------------------------------------

        public override void OnNetworkSpawn()
        {
            // Server-only step: now that the NetworkObject is fully spawned,
            // write the pending init values to the NetworkVariables. These writes
            // will be captured in the initial sync message sent to clients, so
            // every client sees correct values on its very first OnNetworkSpawn.
            if (IsServer && hasPendingInit)
            {
                // Resolve the catalog index for this definition and replicate that.
                int typeId = 0;
                var pm = TowerPlacementManager.Instance;
                if (pm == null || !pm.TryGetTypeId(pendingDefinition, out typeId))
                {
                    Debug.LogError($"[TowerInstance] Could not resolve a TowerTypeId for " +
                                   $"'{pendingDefinition?.name}'. Ensure it is in the " +
                                   $"TowerPlacementManager catalog (towerDefinitions).");
                }

                definitionTypeId.Value = typeId;
                anchorTile.Value = pendingAnchor;
                ownerSlot.Value = pendingOwner;

                // Clear the pending data — it's now committed to NetworkVariables.
                hasPendingInit = false;
            }

            // Resolve the TowerDefinition from the (now-available) replicated name.
            // On the server this is already set by InitializeServer; the lookup is
            // redundant but harmless and keeps the code path uniform.
            ResolveDefinition();

            // Stamp the footprint into the local LevelLoader grids.
            // The server already stamped in TowerPlacementManager before Spawn(),
            // but SetWalkable/SetOccupied are idempotent — double-stamping is safe.
            StampFootprint(walkable: false, occupied: true);

            // Apply the tower tint (paint color if painted, else owner color), and
            // re-tint on every client whenever the paint color changes.
            ApplyTint();
            paintColor.OnValueChanged += HandlePaintColorChanged;

            // Register for minimap rendering.
            MinimapEntityRegistry.Register(this);

            // Selection auto-transfer: if a BuildSiteVisual at our anchor is the
            // active local selection, the player was watching this tower complete —
            // hand selection off to the new TowerInstance so the HUD/visualizer
            // transition smoothly. Server's completion order (spawn THEN despawn)
            // means we get here BEFORE the BuildSiteVisual's OnNetworkDespawn,
            // so the old reference is still valid and selected.
            var selState = SelectionState.Instance;
            if (selState != null
                && selState.SelectedObject is BuildSiteVisual bsv
                && bsv.Anchor == anchorTile.Value)
            {
                selState.Select(this);
            }

            if (resolvedDefinition != null)
            {
                Debug.Log($"[TowerInstance] Spawned '{resolvedDefinition.DisplayName}' " +
                          $"for {ownerSlot.Value} at anchor {anchorTile.Value}. " +
                          $"IsServer={IsServer}");
            }
        }

        public override void OnNetworkDespawn()
        {
            paintColor.OnValueChanged -= HandlePaintColorChanged;

            // Un-stamp the footprint when the tower is destroyed (sold, wave end, etc.)
            // so the tiles become walkable and buildable again.
            StampFootprint(walkable: true, occupied: false);

            MinimapEntityRegistry.Deregister(this);

            // Clear local selection if THIS tower was selected. Without this,
            // SelectionState (and any subscriber holding our reference — HUD,
            // SelectionVisualizer) keeps pointing at a soon-to-be-destroyed Unity
            // object and throws MissingReferenceException on the next access.
            if (SelectionState.Instance != null && SelectionState.Instance.IsSelected(this))
                SelectionState.Instance.Clear();
        }

        // ----- Paint tool -----------------------------------------------------

        /// <summary>
        /// Client → server request to paint this tower. The server applies the color
        /// only if the requesting client owns this tower (matching the placement
        /// ownership rule). <see cref="PaintColor.None"/> resets the tower to its owner
        /// color. The change replicates back to every client via
        /// <see cref="paintColor"/>'s OnValueChanged.
        /// </summary>
        [Rpc(SendTo.Server)]
        public void RequestPaintServerRpc(PaintColor color, RpcParams rpcParams = default)
        {
            PlayerSlot senderSlot = PlayerMatchState.SlotForClient(rpcParams.Receive.SenderClientId);
            if (senderSlot == PlayerSlot.None || senderSlot != ownerSlot.Value)
            {
                Debug.Log($"[TowerInstance] Paint rejected: client " +
                          $"{rpcParams.Receive.SenderClientId} ({senderSlot}) does not own " +
                          $"tower owned by {ownerSlot.Value}.");
                return;
            }

            paintColor.Value = color;
        }

        // Re-tint on every client (and the server) when the replicated paint color changes.
        private void HandlePaintColorChanged(PaintColor previous, PaintColor current) => ApplyTint();

        // ----- IMinimapEntity -------------------------------------------------
        //
        // Towers are static, so WorldPosition is cheap (no movement to track). Color reflects
        // the replicated ownerSlot; reads safely on every client because ownerSlot is set in
        // OnNetworkSpawn before this entity is added to the registry.

        Vector3 IMinimapEntity.WorldPosition => transform.position;
        Color IMinimapEntity.MinimapColor => PlayerColors.Get(ownerSlot.Value);
        MinimapIconKind IMinimapEntity.IconKind => MinimapIconKind.Tower;

        // Tower footprint in world units. Uses the larger axis if the footprint isn't square,
        // so an Nx1 tower still occupies its full long-side on the minimap.
        // Falls back to one tile if the definition hasn't resolved yet (transient, harmless).
        float IMinimapEntity.MinimapWorldSize
        {
            get
            {
                if (resolvedDefinition == null) return GridCoordinates.TILE_SIZE;
                int extent = Mathf.Max(
                    resolvedDefinition.FootprintSize.x,
                    resolvedDefinition.FootprintSize.y);
                return extent * GridCoordinates.TILE_SIZE;
            }
        }

        // ----- Private helpers ------------------------------------------------

        private void ResolveDefinition()
        {
            // Already resolved (server path via InitializeServer).
            if (resolvedDefinition != null) return;

            int typeId = definitionTypeId.Value;
            resolvedDefinition = TowerPlacementManager.GetDefinition(typeId);
            if (resolvedDefinition == null)
            {
                Debug.LogError($"[TowerInstance] NetworkObject {NetworkObjectId}: " +
                               $"no TowerDefinition for TypeId {typeId}. Is the " +
                               $"TowerPlacementManager catalog populated on this peer?");
            }
        }

        private void StampFootprint(bool walkable, bool occupied)
        {
            var loader = LevelLoader.Instance;
            if (loader == null || !loader.IsLoaded)
            {
                Debug.LogWarning($"[TowerInstance] NetworkObject {NetworkObjectId}: " +
                                 $"LevelLoader not available during footprint stamp. " +
                                 $"Grids may be out of sync.");
                return;
            }

            // Determine footprint size from the resolved definition, falling back to
            // 2×2 if the definition hasn't resolved yet (shouldn't happen, but defensive).
            Vector2Int footprintSize = resolvedDefinition != null
                ? resolvedDefinition.FootprintSize
                : new Vector2Int(2, 2);

            foreach (var tile in GridCoordinates.GetFootprintTiles(anchorTile.Value, footprintSize))
            {
                loader.SetWalkable(tile, walkable);
                loader.SetOccupied(tile, occupied);
            }
        }

        // Reused per-instance across color updates to avoid per-call GC allocation.
        // MaterialPropertyBlock is not thread-safe but all rendering runs on the
        // main thread, so a single instance per TowerInstance is fine.
        private MaterialPropertyBlock colorPropertyBlock;
        private static readonly int ColorPropertyId = Shader.PropertyToID("_Color");
        // URP Lit uses _BaseColor, not _Color. Writing both ensures the tint applies
        // regardless of which shader the prefab uses; unknown property writes are
        // silently ignored.
        private static readonly int BaseColorPropertyId = Shader.PropertyToID("_BaseColor");

        private void ApplyTint()
        {
            // Paint color takes precedence when set; otherwise fall back to the owner
            // color. Paint.None means "unpainted" → show owner color.
            Color tint = paintColor.Value != PaintColor.None
                ? PaintColors.Get(paintColor.Value)
                : PlayerColors.Get(ownerSlot.Value);
            tint.a = 1f;

            // MaterialPropertyBlock sets per-renderer properties without allocating
            // a new Material object. Safe to reuse across calls on the same instance.
            // All Unity standard/URP shaders expose _Color or _BaseColor, so writing
            // both lets the tint apply regardless of which shader the prefab uses.
            colorPropertyBlock ??= new MaterialPropertyBlock();
            colorPropertyBlock.SetColor(ColorPropertyId, tint);
            colorPropertyBlock.SetColor(BaseColorPropertyId, tint);

            foreach (var rend in ResolveTintRenderers())
            {
                if (rend == null) continue;
                rend.SetPropertyBlock(colorPropertyBlock);
            }
        }

        // Renderers actually tinted. Prefer the inspector-assigned list (lets a prefab
        // exclude decorative children, FX, etc.). When that list is empty — the common
        // case for imported models nobody has hand-wired — fall back to every MeshRenderer
        // under the tower so owner-color and paint Just Work without per-prefab setup.
        // Cached after the first resolve.
        private MeshRenderer[] resolvedTintRenderers;

        private MeshRenderer[] ResolveTintRenderers()
        {
            if (tintedRenderers != null && tintedRenderers.Length > 0)
                return tintedRenderers;

            if (resolvedTintRenderers == null)
            {
                resolvedTintRenderers = GetComponentsInChildren<MeshRenderer>(includeInactive: true);
                if (resolvedTintRenderers.Length == 0)
                    Debug.LogWarning($"[TowerInstance] '{name}' has no MeshRenderers to tint — " +
                                     $"owner color and paint will have no visible effect.");
            }
            return resolvedTintRenderers;
        }
    }
}