// Assets/_Project/Scripts/Gameplay/TowerPlacementController.cs
using UnityEngine;
using UnityEngine.InputSystem;
using TD.Core;
using TD.Towers;

namespace TD.Gameplay
{
    /// <summary>
    /// Per-client controller for the tower placement UX. Handles hover raycasts against
    /// the BuildablePlane collider, drives the placement ghost, and dispatches placement
    /// requests to <see cref="TowerPlacementManager"/> via RPC.
    /// </summary>
    /// <remarks>
    /// <para><b>Plain MonoBehaviour.</b> Placement visuals (cursor ghost, color) are
    /// purely cosmetic and local. This component does not need to be a NetworkBehaviour.
    /// All server-authoritative logic lives in <see cref="TowerPlacementManager"/>.</para>
    ///
    /// <para><b>Ghost validity check.</b> The cursor ghost checks ownership, placement
    /// state, and tile occupancy only. It does NOT run a local path check. If the server
    /// rejects because the tower would block the path, the cursor ghost disappears and a
    /// rejection message is shown.</para>
    ///
    /// <para><b>Two ghosts, two systems.</b> The <i>cursor ghost</i> (handled here)
    /// follows the mouse and turns white/red. The <i>build-site visual</i> (in D2,
    /// handled by Builder + BuildSiteVisual) is the green queued ghost or staged
    /// construction visual that appears at a confirmed placement site. They are
    /// distinct prefabs and lifecycles.</para>
    ///
    /// <para><b>Chained queueing.</b> Holding Shift while left-clicking submits the
    /// placement and stays in placement mode for another submission. Releasing Shift
    /// before the click submits and exits placement (single-shot). Right-click always
    /// cancels.</para>
    ///
    /// <para><b>Input System.</b> Uses the New Input System package. Mouse and modifier
    /// state are read directly from <c>Mouse.current</c> and <c>Keyboard.current</c>
    /// each frame. No InputAction asset needed for these placement-specific bindings.</para>
    /// </remarks>
    public class TowerPlacementController : MonoBehaviour
    {
        // ----- Inspector --------------------------------------------------

        [Tooltip("Project-wide placement settings: ghost materials and rejection messages.")]
        [SerializeField] private TowerPlacementSettings settings;

        [Tooltip("Physics layer mask for the BuildablePlane collider. Set this to the " +
                 "'BuildablePlane' layer. Raycasts only hit this layer so stray colliders " +
                 "in the scene don't interfere.")]
        [SerializeField] private LayerMask buildablePlaneLayerMask;

        [Tooltip("Maximum raycast distance from the camera to the buildable plane. " +
                 "Should be at least as large as the camera's far clip plane.")]
        [SerializeField] private float raycastMaxDistance = 500f;

        // ----- Active-placement state -------------------------------------

        // Null when placement mode is inactive.
        private TowerDefinition activeTowerDefinition;
        private int activeTowerTypeId;

        // Set true by a single-shot placement submit; processed at the top of the next
        // Update so that BuilderInputController still sees IsPlacing=true on the same
        // frame as the click and doesn't deselect the builder via its empty-space check.
        private bool pendingCancel;

        // The cursor ghost GameObject: the tower prefab instantiated with transparent materials.
        // Null when placement mode is inactive.
        private GameObject ghostGO;

        // Cached renderers on the ghost, populated when the ghost is created.
        private Renderer[] ghostRenderers = System.Array.Empty<Renderer>();

        // Reused each frame to avoid allocation. Lazily constructed on first use because
        // Unity disallows MaterialPropertyBlock construction in field initializers.
        private MaterialPropertyBlock ghostPropertyBlock;
        private static readonly int ColorPropertyId = Shader.PropertyToID("_Color");

        // The anchor tile computed last frame (used to avoid re-evaluating when the
        // cursor hasn't moved to a new tile).
        private Vector2Int lastAnchor;
        private bool lastAnchorValid;   // false until first raycast succeeds
        private bool lastPlacementValid; // result of the last local validity check

        // ----- Events -----------------------------------------------------

        /// <summary>
        /// Fired on the local client when the server rejects a placement request.
        /// Payload is the human-readable rejection message from
        /// <see cref="TowerPlacementSettings"/>. Subscribe here to display feedback UI.
        /// </summary>
        public static event System.Action<string> OnRejectionMessageReady;

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

        private void OnEnable()
        {
            TowerPlacementManager.OnPlacementRejected += HandlePlacementRejected;
        }

        private void OnDisable()
        {
            TowerPlacementManager.OnPlacementRejected -= HandlePlacementRejected;
            ExitPlacementMode();
        }

        private void Update()
        {
            if (pendingCancel)
            {
                pendingCancel = false;
                ExitPlacementMode();
                return;
            }

            if (activeTowerDefinition == null) return;     // idle — nothing to do

            var mouse = Mouse.current;
            if (mouse == null) return;         // no mouse device connected

            // Right-click cancels placement.
            if (mouse.rightButton.wasPressedThisFrame)
            {
                ExitPlacementMode();
                return;
            }

            // Raycast mouse position against the BuildablePlane.
            if (!TryGetBuildablePlaneHit(mouse.position.ReadValue(), out Vector3 hitPoint))
            {
                // Cursor is off the buildable plane — hide the ghost but stay in
                // placement mode so the player can move back onto the plane.
                SetGhostVisible(false);
                lastAnchorValid = false;
                return;
            }

            SetGhostVisible(true);

            // Compute the footprint anchor from the hit point.
            Vector2Int anchor = ComputeAnchor(hitPoint, activeTowerDefinition.FootprintSize);

            // Position the cursor ghost at the footprint center.
            Vector3 ghostPos = GridCoordinates.GetFootprintCenterWorld(anchor, activeTowerDefinition.FootprintSize);
            ghostPos.y = 0.5f; // lift off the plane so the cube base sits flush
            ghostGO.transform.position = ghostPos;

            // Only re-evaluate validity when the anchor tile changes, to avoid
            // re-running grid lookups every frame when the cursor is stationary.
            if (!lastAnchorValid || anchor != lastAnchor)
            {
                lastAnchor = anchor;
                lastAnchorValid = true;
                lastPlacementValid = EvaluateLocalValidity(anchor, activeTowerDefinition);
            }

            // Update ghost color.
            ApplyGhostColor(lastPlacementValid);

            // Left-click to attempt placement.
            if (mouse.leftButton.wasPressedThisFrame)
            {
                bool chained = IsShiftHeld();
                TrySubmitPlacement(anchor, chained);
            }
        }

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

        /// <summary>
        /// Activates placement mode for the given tower type. The ghost appears
        /// immediately under the cursor. Call this from HUD tower buttons.
        /// </summary>
        public void BeginPlacement(TowerDefinition def, int towerTypeId)
        {
            if (def == null)
            {
                Debug.LogError("[TowerPlacementController] BeginPlacement called with null " +
                               "TowerDefinition.");
                return;
            }

            // Cancel any existing placement before starting a new one.
            ExitPlacementMode();

            activeTowerDefinition = def;
            activeTowerTypeId = towerTypeId;

            CreateGhost(def);
        }

        /// <summary>
        /// Cancels placement mode and destroys the ghost. Safe to call when idle.
        /// </summary>
        void ExitPlacementMode()
        {
            activeTowerDefinition = null;
            activeTowerTypeId = 0;
            lastAnchorValid = false;

            DestroyGhost();
        }

        /// <summary>
        /// True when placement mode is currently active.
        /// </summary>
        public bool IsPlacing => activeTowerDefinition != null;

        // ----- Modifier helpers -------------------------------------------

        private static bool IsShiftHeld()
        {
            var kb = Keyboard.current;
            if (kb == null) return false;
            return kb.leftShiftKey.isPressed || kb.rightShiftKey.isPressed;
        }

        // ----- Ghost management -------------------------------------------

        private void CreateGhost(TowerDefinition def)
        {
            if (def.TowerPrefab == null)
            {
                Debug.LogWarning($"[TowerPlacementController] '{def.DisplayName}' has no " +
                                 $"TowerPrefab — ghost cannot be created.");
                return;
            }

            ghostGO = Instantiate(def.TowerPrefab);
            ghostGO.name = $"Ghost_{def.name}";

            // Disable all NetworkObject, TowerInstance, and Collider components on
            // the ghost — it must not participate in networking or physics.
            DisableGhostComponents();

            ghostRenderers = ghostGO.GetComponentsInChildren<Renderer>();

            // Apply ghost valid material to all renderers initially.
            // ApplyGhostColor will update each frame.
            if (settings != null && settings.GhostValidMaterial != null)
            {
                foreach (var rend in ghostRenderers)
                    rend.sharedMaterial = settings.GhostValidMaterial;
            }

            // Start invisible; shown on the first successful raycast.
            SetGhostVisible(false);
        }

        private void DestroyGhost()
        {
            if (ghostGO != null)
            {
                Destroy(ghostGO);
                ghostGO = null;
            }
            ghostRenderers = System.Array.Empty<Renderer>();
        }

        private void SetGhostVisible(bool visible)
        {
            if (ghostGO != null)
                ghostGO.SetActive(visible);
        }

        /// <summary>
        /// Disables components on the ghost that must not run: NetworkObject,
        /// TowerInstance, Colliders, and Rigidbodies. The ghost is purely visual.
        /// </summary>
        private void DisableGhostComponents()
        {
            // NetworkObject — must be disabled so NGO doesn't try to register it.
            var netObj = ghostGO.GetComponent<Unity.Netcode.NetworkObject>();
            if (netObj != null) netObj.enabled = false;

            // TowerInstance — must not stamp grids or fire OnNetworkSpawn.
            var towerInstance = ghostGO.GetComponent<TowerInstance>();
            if (towerInstance != null) towerInstance.enabled = false;

            // Colliders — ghost must not block raycasts or physics queries.
            foreach (var col in ghostGO.GetComponentsInChildren<Collider>())
                col.enabled = false;

            // Rigidbodies — ghost must not fall or interact with physics.
            foreach (var rb in ghostGO.GetComponentsInChildren<Rigidbody>())
                rb.isKinematic = true;
        }

        /// <summary>
        /// Sets the ghost material color using <see cref="MaterialPropertyBlock"/>.
        /// Switches between valid (white) and invalid (red) materials.
        /// </summary>
        private void ApplyGhostColor(bool valid)
        {
            if (settings == null) return;

            Material ghostMat = valid ? settings.GhostValidMaterial : settings.GhostInvalidMaterial;
            if (ghostMat == null) return;

            foreach (var rend in ghostRenderers)
            {
                rend.sharedMaterial = ghostMat;
            }
        }

        // ----- Raycasting -------------------------------------------------

        private bool TryGetBuildablePlaneHit(Vector2 screenPos, out Vector3 hitPoint)
        {
            hitPoint = Vector3.zero;

            var cam = Camera.main;
            if (cam == null) return false;

            Ray ray = cam.ScreenPointToRay(new Vector3(screenPos.x, screenPos.y, 0f));
            if (Physics.Raycast(ray, out RaycastHit hit, raycastMaxDistance, buildablePlaneLayerMask))
            {
                hitPoint = hit.point;
                return true;
            }
            return false;
        }

        // ----- Anchor computation -----------------------------------------

        /// <summary>
        /// Converts a world hit point to the footprint anchor tile (SW corner) such
        /// that the footprint center is as close as possible to the hit point.
        /// </summary>
        private static Vector2Int ComputeAnchor(Vector3 hitPoint, Vector2Int footprintSize)
        {
            float t = GridCoordinates.TILE_SIZE;
            float halfW = (footprintSize.x - 1) * 0.5f;
            float halfH = (footprintSize.y - 1) * 0.5f;

            int anchorX = Mathf.RoundToInt(hitPoint.x / t - halfW);
            int anchorY = Mathf.RoundToInt(hitPoint.z / t - halfH);
            return new Vector2Int(anchorX, anchorY);
        }

        // ----- Local validity check ---------------------------------------

        /// <summary>
        /// Evaluates the local (client-side) placement validity. Checks ownership,
        /// placement state, and occupancy. Does NOT check gold or run a path BFS —
        /// those are server-side only.
        /// </summary>
        private bool EvaluateLocalValidity(Vector2Int anchor, TowerDefinition def)
        {
            var loader = LevelLoader.Instance;
            if (loader == null || !loader.IsLoaded) return false;

            PlayerSlot localSlot = GetLocalPlayerSlot();

            foreach (var tile in GridCoordinates.GetFootprintTiles(anchor, def.FootprintSize))
            {
                if (loader.GetOwner(tile) != localSlot) return false;
                if (loader.GetPlacement(tile) != PlacementState.Buildable) return false;
                if (loader.IsOccupied(tile)) return false;
            }

            return true;
        }

        // ----- Placement submission ---------------------------------------

        private void TrySubmitPlacement(Vector2Int anchor, bool chained)
        {
            var manager = TowerPlacementManager.Instance;
            if (manager == null)
            {
                Debug.LogWarning("[TowerPlacementController] No TowerPlacementManager in " +
                                 "scene. Cannot submit placement request.");
                return;
            }

            // Send the RPC regardless of local validity state — the server is
            // authoritative. The local check drives the ghost color only.
            manager.RequestPlaceTowerRpc(anchor.x, anchor.y, activeTowerTypeId);

            if (chained)
            {
                // Stay in placement mode. The server will stamp occupancy on success,
                // so when EvaluateLocalValidity runs next frame it will correctly
                // report the just-clicked tile as occupied (cursor turns red there).
                // Force a re-evaluation by invalidating the cached anchor.
                lastAnchorValid = false;
                return;
            }

            // Single-shot: defer the cancel to the next frame so BuilderInputController
            // still sees IsPlacing=true this frame and doesn't deselect the builder.
            pendingCancel = true;
        }

        // ----- Rejection feedback -----------------------------------------

        private void HandlePlacementRejected(PlacementRejectionReason reason)
        {
            if (settings == null)
            {
                Debug.LogWarning($"[TowerPlacementController] Placement rejected: {reason} " +
                                 $"(no TowerPlacementSettings assigned — cannot show message).");
                return;
            }

            string message = settings.GetRejectionMessage(reason);

            Debug.Log($"[TowerPlacementController] Placement rejected: {reason} → \"{message}\"");

            // Fire the event so HUD components can display the message on screen.
            OnRejectionMessageReady?.Invoke(message);
        }

        // ----- Player slot ------------------------------------------------

        private static PlayerSlot GetLocalPlayerSlot()
            => PlayerMatchState.Local?.Slot ?? PlayerSlot.None;
    }
}