// Assets/_Project/Scripts/Combat/TowerCombat.cs

using System;
using System.Collections.Generic;
using TD.Core;
using TD.Gameplay;
using TD.Towers;
using Unity.Netcode;
using UnityEngine;
using Object = UnityEngine.Object;

namespace TD.Combat
{
    /// <summary>
    /// Per-tower combat controller. Handles target acquisition, attack timing,
    /// damage application, and projectile spawning.
    /// </summary>
    /// <remarks>
    /// <b>Authority:</b> All combat logic (targeting, damage, projectile spawn) runs
    /// on the server only. Clients receive two signals for visual feedback:
    /// <list type="bullet">
    ///   <item><see cref="replicatedTarget"/> — NetworkVariable clients can read to know
    ///         what the tower is aiming at (drives future rotation/lean visuals).</item>
    ///   <item><see cref="FireClientRpc"/> — one-shot broadcast at each attack, carrying
    ///         the target world position for muzzle flash, tracer FX, etc.</item>
    /// </list>
    ///
    /// <b>Component coupling:</b> Reads all stats from <see cref="TowerInstance.Definition"/>
    /// on the same GameObject. Does not modify TowerInstance.
    ///
    /// <b>Inspector setup required:</b>
    /// <list type="bullet">
    ///   <item>Assign <see cref="enemyLayerMask"/> to the "Enemy" physics layer.</item>
    /// </list>
    /// </remarks>
    [RequireComponent(typeof(TowerInstance))]
    public class TowerCombat : NetworkBehaviour
    {
        [Tooltip("Physics layer(s) that enemies occupy. " +
                 "OverlapSphere uses this to find targets efficiently without " +
                 "touching non-enemy colliders.")]
        [SerializeField] private LayerMask enemyLayerMask;

        // Replicated so all peers know the current aim target.
        // Visual consumers (barrel rotation, etc.) subscribe to OnValueChanged.
        // Default (NetworkObjectId = 0) = no target.
        private readonly NetworkVariable<NetworkObjectReference> replicatedTarget =
            new NetworkVariable<NetworkObjectReference>(
                default,
                NetworkVariableReadPermission.Everyone,
                NetworkVariableWritePermission.Server);

        // Server-local reference — cleared when the target dies or leaves range.
        // Clients should use replicatedTarget.Value instead.
        private EnemyHealth currentTarget;

        // Attack cooldown decrements each server frame. When ≤ 0 and a target
        // is in range, the tower fires and the timer resets to 1 / FireRate.
        private float attackCooldown;

        // Cached on OnNetworkSpawn — avoids GetComponent every Update.
        private TowerInstance towerInstance;

        // Lazily resolved on first attack — the owner's slot isn't guaranteed to be
        // set at spawn time, so we defer the lookup until combat actually begins.
        private PlayerBuffManager ownerBuffManager;
        private bool buffManagerResolved;

        // Shared OverlapSphere result buffer. 32 covers any realistic enemy
        // density; size up if profiling reveals overflow.
        private static readonly Collider[] s_overlapBuffer = new Collider[32];

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

        /// <summary>
        /// Fired locally on ALL peers when the tower acquires a new target.
        /// Driven by <see cref="replicatedTarget"/>.<c>OnValueChanged</c>, and also
        /// fired in <see cref="OnNetworkSpawn"/> for late-joining clients so visual
        /// consumers always initialise from the correct state.
        /// </summary>
        public event Action<NetworkObjectReference> OnTargetAcquired;

        /// <summary>Fired locally on ALL peers when the tower loses its target.</summary>
        public event Action OnTargetLost;

        /// <summary>
        /// Fired locally on ALL peers each time the tower attacks.
        /// Argument is the world-space position of the primary target at the moment
        /// of fire — use it to aim muzzle flash, spawn a tracer, etc.
        /// </summary>
        public event Action<Vector3> OnFire;

        /// <summary>
        /// Fired locally on ALL peers when an <see cref="TargetType.AllInRange"/> tower
        /// attacks, carrying the world positions of every enemy hit this tick. A visual
        /// component (e.g. <c>TeslaArcVisual</c>) subscribes to draw an arc to each.
        /// </summary>
        public event Action<Vector3[]> OnAreaFired;

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

        public override void OnNetworkSpawn()
        {
            towerInstance = GetComponent<TowerInstance>();
            replicatedTarget.OnValueChanged += HandleReplicatedTargetChanged;

            // Late-joining clients receive the NV's current value in the initial
            // sync message but won't get an OnValueChanged callback for it.
            // Fire OnTargetAcquired here so visual consumers initialise correctly
            // regardless of when this client connected.
            if (replicatedTarget.Value.TryGet(out _))
                OnTargetAcquired?.Invoke(replicatedTarget.Value);
        }

        public override void OnNetworkDespawn()
        {
            replicatedTarget.OnValueChanged -= HandleReplicatedTargetChanged;

            // Unsubscribe from the current target's death event to prevent
            // a dangling delegate on the enemy after this tower despawns.
            if (currentTarget != null)
                currentTarget.OnDied -= HandleTargetDied;
        }

        // ----- Server update -----------------------------------------------

        private void Update()
        {
            if (!IsServer) return;

            TowerDefinition def = towerInstance?.Definition;
            if (def == null || def.Range <= 0f || def.FireRate <= 0f) return;

            // Only fire during active gameplay.
            var ms = MatchState.Instance;
            if (ms == null || ms.Phase != MatchPhase.Playing)
            {
                if (currentTarget != null) ClearTarget();
                return;
            }

            ValidateTarget(def);
            if (currentTarget == null)
                AcquireTarget(def);
            if (currentTarget != null)
                TickAttack(def);
        }

        // ----- Target management -------------------------------------------

        private void ValidateTarget(TowerDefinition def)
        {
            if (currentTarget == null) return;

            bool dead        = currentTarget.IsDead;
            bool destroyed   = (currentTarget as Object) == null;
            bool outOfRange  = Vector3.Distance(
                                   transform.position,
                                   currentTarget.transform.position) > def.Range;

            if (dead || destroyed || outOfRange)
                ClearTarget();
        }

        private void AcquireTarget(TowerDefinition def)
        {
            int count = Physics.OverlapSphereNonAlloc(
                transform.position, def.Range, s_overlapBuffer, enemyLayerMask);

            EnemyHealth best      = null;
            float       bestScore = 0f;

            for (int i = 0; i < count; i++)
            {
                var eh = s_overlapBuffer[i].GetComponent<EnemyHealth>();
                if (eh == null || eh.IsDead)            continue;
                if (def.GroundedOnly && eh.IsFlying)    continue;

                float score    = ScoreTarget(eh, def.TargetPriority);
                bool  isBetter = best == null
                    || (def.TargetPriority == TargetPriority.Strongest
                            ? score > bestScore   // higher HP wins
                            : score < bestScore); // lower score wins for Closest/Weakest

                if (isBetter) { best = eh; bestScore = score; }
            }

            if (best != null)
                SetTarget(best);
        }

        // Returns a scalar used to rank candidates.
        // Closest → sqrMagnitude (lower = better).
        // Weakest → CurrentHp (lower = better).
        // Strongest → CurrentHp (higher = better — caller inverts the comparison).
        private float ScoreTarget(EnemyHealth eh, TargetPriority priority)
        {
            return priority switch
            {
                TargetPriority.Closest   => (transform.position - eh.transform.position).sqrMagnitude,
                TargetPriority.Weakest   => eh.CurrentHp,
                TargetPriority.Strongest => eh.CurrentHp,
                _                        => 0f,
            };
        }

        private void SetTarget(EnemyHealth eh)
        {
            currentTarget          =  eh;
            currentTarget.OnDied  += HandleTargetDied;
            replicatedTarget.Value =  new NetworkObjectReference(eh.NetworkObject);
        }

        private void ClearTarget()
        {
            if (currentTarget != null)
                currentTarget.OnDied -= HandleTargetDied;

            currentTarget          = null;
            replicatedTarget.Value = default;
        }

        private void HandleTargetDied(EnemyHealth dead)
        {
            if ((object)dead == (object)currentTarget)
                ClearTarget();
        }

        // ----- Buff multiplier lookup -------------------------------------

        // Resolved lazily on the first attack because the owner slot NetworkVariable
        // may not yet have replicated by the time OnNetworkSpawn runs.
        private PlayerBuffManager GetOwnerBuffManager()
        {
            if (buffManagerResolved) return ownerBuffManager;

            var slot = towerInstance.Owner;
            if (slot == PlayerSlot.None) return null;

            var matchState = PlayerMatchState.GetForSlot(slot);
            if (matchState == null) return null;

            ownerBuffManager = matchState.GetComponent<PlayerBuffManager>();
            buffManagerResolved = true;
            return ownerBuffManager;
        }

        // ----- Attack tick -------------------------------------------------

        private void TickAttack(TowerDefinition def)
        {
            attackCooldown -= Time.deltaTime;
            if (attackCooldown > 0f) return;

            float effectiveFireRate = def.FireRate
                * (GetOwnerBuffManager()?.GetMultiplier(BuffStat.AttackSpeed) ?? 1f);
            attackCooldown = 1f / effectiveFireRate;
            Fire(def);
        }

        private void Fire(TowerDefinition def)
        {
            Vector3 targetPos = currentTarget.transform.position;

            // Resolve the effective combat profile for this shot: the tower's authored
            // stats, overridden by its Paint color (Red = splash, Green = poison DoT,
            // Blue = slow). Unpainted towers fire exactly as authored.
            PaintColor paint = towerInstance.Paint;
            CombatProfile profile = BuildProfile(def, paint);

            if (def.ProjectilePrefab == null)
            {
                // Hitscan — apply damage this frame.
                ApplyDamageToTarget(profile, currentTarget, targetPos);
            }
            else
            {
                SpawnProjectile(def, profile, paint, currentTarget);
            }

            FireClientRpc(targetPos);
        }

        // ----- Paint → combat effect tuning --------------------------------
        //
        // Paint overrides the tower's effect profile (paint takes precedence over any
        // authored effect). Centralized here so the numbers are easy to find and tweak;
        // promote to a ScriptableObject if designers need to tune them without a recompile.
        private const float PaintSplashRadius      = 2.0f;  // world units (tiles)
        private const float PaintPoisonDpsFraction = 0.5f;  // DoT/sec = base Damage × this
        private const float PaintPoisonDuration    = 3.0f;  // seconds
        private const float PaintSlowSpeedRetained = 0.5f;  // Cold magnitude: 0.5 = half speed
        private const float PaintSlowDuration      = 2.0f;  // seconds

        // The firing-relevant stats for a single shot, after applying paint overrides.
        // Mirrors the TowerDefinition fields the firing path reads so the rest of the
        // code is agnostic to whether a value came from the asset or from paint.
        private readonly struct CombatProfile
        {
            public readonly float      Damage;
            public readonly DamageType DamageType;
            public readonly TargetType TargetType;
            public readonly float      SplashRadius;
            public readonly float      SlowFactor;
            public readonly float      DotDamagePerSecond;
            public readonly float      EffectDuration;
            public readonly float      ProjectileSpeed;
            public readonly int        ChainCount;
            public readonly float      ChainRange;
            // The tower's attack range. Carried here so the AllInRange path knows how far
            // to sweep without re-reading the TowerDefinition.
            public readonly float      Range;

            public CombatProfile(float damage, DamageType damageType, TargetType targetType,
                float splashRadius, float slowFactor, float dotDamagePerSecond,
                float effectDuration, float projectileSpeed, int chainCount, float chainRange,
                float range)
            {
                Damage             = damage;
                DamageType         = damageType;
                TargetType         = targetType;
                SplashRadius       = splashRadius;
                SlowFactor         = slowFactor;
                DotDamagePerSecond = dotDamagePerSecond;
                EffectDuration     = effectDuration;
                ProjectileSpeed    = projectileSpeed;
                ChainCount         = chainCount;
                ChainRange         = chainRange;
                Range              = range;
            }
        }

        private static CombatProfile BuildProfile(TowerDefinition def, PaintColor paint)
        {
            // Start from the tower's authored stats.
            float      damage      = def.Damage;
            DamageType damageType  = def.DamageType;
            TargetType targetType  = def.TargetType;
            float      splash      = def.SplashRadius;
            float      slow        = def.SlowFactor;
            float      dot         = def.DotDamagePerSecond;
            float      duration    = def.EffectDuration;

            switch (paint)
            {
                case PaintColor.Red:    // area splash
                    targetType = TargetType.Splash;
                    splash     = PaintSplashRadius;
                    break;

                case PaintColor.Green:  // poison damage-over-time
                    damageType = DamageType.Poison;
                    dot        = def.Damage * PaintPoisonDpsFraction;
                    duration   = PaintPoisonDuration;
                    break;

                case PaintColor.Blue:   // chilling slow
                    damageType = DamageType.Cold;
                    slow       = PaintSlowSpeedRetained;
                    duration   = PaintSlowDuration;
                    break;

                case PaintColor.None:
                default:
                    break;      // fire as authored
            }

            return new CombatProfile(damage, damageType, targetType, splash, slow, dot,
                                     duration, def.ProjectileSpeed, def.ChainCount, def.ChainRange,
                                     def.Range);
        }

        // ----- Damage application ------------------------------------------

        private void ApplyDamageToTarget(in CombatProfile p, EnemyHealth primary, Vector3 primaryPos)
        {
            PlayerSlot owner = towerInstance.Owner;

            switch (p.TargetType)
            {
                case TargetType.Single:
                    HitEnemy(p, primary, owner);
                    break;

                case TargetType.Splash:
                    HitEnemy(p, primary, owner);
                    ApplySplash(p, primary, primaryPos, owner);
                    break;

                case TargetType.Chain:
                    ApplyChain(p, primary, owner);
                    break;

                case TargetType.AllInRange:
                    ApplyAllInRange(p, owner);
                    break;
            }
        }

        // Hits every enemy within the tower's range this tick (Tesla Coil). Broadcasts the
        // hit positions so a visual component can draw an arc to each.
        private void ApplyAllInRange(in CombatProfile p, PlayerSlot owner)
        {
            int count = Physics.OverlapSphereNonAlloc(
                transform.position, p.Range, s_overlapBuffer, enemyLayerMask);

            var hitPositions = new List<Vector3>(count);
            for (int i = 0; i < count; i++)
            {
                var eh = s_overlapBuffer[i].GetComponent<EnemyHealth>();
                if (eh == null || eh.IsDead) continue;
                HitEnemy(p, eh, owner);
                hitPositions.Add(eh.transform.position);
            }

            if (hitPositions.Count > 0)
                AreaFiredClientRpc(hitPositions.ToArray());
        }

        private void ApplySplash(in CombatProfile p, EnemyHealth primary,
                                  Vector3 origin, PlayerSlot owner)
        {
            if (p.SplashRadius <= 0f) return;

            int count = Physics.OverlapSphereNonAlloc(
                origin, p.SplashRadius, s_overlapBuffer, enemyLayerMask);

            for (int i = 0; i < count; i++)
            {
                var eh = s_overlapBuffer[i].GetComponent<EnemyHealth>();
                if (eh == null || eh.IsDead || (object)eh == (object)primary) continue;
                HitEnemy(p, eh, owner);
            }
        }

        private void ApplyChain(in CombatProfile p, EnemyHealth primary, PlayerSlot owner)
        {
            var hitPositions = new List<Vector3> { primary.transform.position };
            var alreadyHit   = new HashSet<EnemyHealth> { primary };

            HitEnemy(p, primary, owner);

            EnemyHealth current = primary;
            for (int jump = 0; jump < p.ChainCount; jump++)
            {
                EnemyHealth next    = null;
                float       bestSqr = float.MaxValue;

                int count = Physics.OverlapSphereNonAlloc(
                    current.transform.position, p.ChainRange, s_overlapBuffer, enemyLayerMask);

                for (int i = 0; i < count; i++)
                {
                    var eh = s_overlapBuffer[i].GetComponent<EnemyHealth>();
                    if (eh == null || eh.IsDead || alreadyHit.Contains(eh)) continue;

                    float sqr = (current.transform.position - eh.transform.position).sqrMagnitude;
                    if (sqr < bestSqr) { next = eh; bestSqr = sqr; }
                }

                if (next == null) break;

                alreadyHit.Add(next);
                hitPositions.Add(next.transform.position);
                HitEnemy(p, next, owner);
                current = next;
            }

            ChainFiredClientRpc(hitPositions.ToArray());
        }

        private void HitEnemy(in CombatProfile p, EnemyHealth target, PlayerSlot owner)
        {
            var multiplier = GetOwnerBuffManager()?.GetMultiplier(BuffStat.Damage) ?? 1f;
            float effectiveDamage = p.Damage * multiplier;
            Debug.Log("multiplier: " + multiplier);
            Debug.Log("Effective damage: " + effectiveDamage);
            target.TakeDamage(effectiveDamage, p.DamageType, owner);
            ApplyStatusEffect(p, target, owner);
        }

        private void ApplyStatusEffect(in CombatProfile p, EnemyHealth target, PlayerSlot owner)
        {
            if (p.EffectDuration <= 0f) return;

            float magnitude = p.DamageType switch
            {
                DamageType.Cold   => p.SlowFactor,
                DamageType.Fire   => p.DotDamagePerSecond,
                DamageType.Poison => p.DotDamagePerSecond,
                _                 => 0f,
            };

            if (magnitude <= 0f) return;

            target.GetComponent<EnemyStatus>()
                  ?.ApplyEffect(p.DamageType, magnitude, p.EffectDuration, owner);
        }

        // ----- Projectile spawning -----------------------------------------

        private void SpawnProjectile(TowerDefinition def, in CombatProfile p,
                                     PaintColor tint, EnemyHealth target)
        {
            var go = Instantiate(def.ProjectilePrefab, transform.position, Quaternion.identity);

            var proj = go.GetComponent<Projectile>();
            if (proj == null)
            {
                Debug.LogError($"[TowerCombat] ProjectilePrefab '{def.ProjectilePrefab.name}' " +
                               $"has no Projectile component. The prefab must have Projectile, " +
                               $"NetworkObject, and NetworkTransform at its root.");
                Destroy(go);
                return;
            }

            float effectiveDamage = def.Damage
                * (GetOwnerBuffManager()?.GetMultiplier(BuffStat.Damage) ?? 1f);
            proj.InitializeServer(
                target,
                effectiveDamage,
                p.DamageType,
                p.TargetType,
                p.SplashRadius,
                p.SlowFactor,
                p.DotDamagePerSecond,
                p.EffectDuration,
                p.ProjectileSpeed,
                enemyLayerMask,
                towerInstance.Owner,
                tint);

            go.GetComponent<NetworkObject>().Spawn();
        }

        // ----- ClientRpcs --------------------------------------------------

        [ClientRpc]
        private void FireClientRpc(Vector3 targetPos)
        {
            // Visual consumers subscribe to OnFire for muzzle flash, tracers, etc.
            OnFire?.Invoke(targetPos);
        }

        [ClientRpc]
        private void ChainFiredClientRpc(Vector3[] hitPositions)
        {
            // STUB: lightning-arc visual between hitPositions goes here.
            // A future visual component will subscribe to an OnChainFired event
            // and draw a line renderer through these world positions.
        }

        [ClientRpc]
        private void AreaFiredClientRpc(Vector3[] hitPositions)
        {
            // Visual consumers (TeslaArcVisual) subscribe to OnAreaFired to draw an
            // electrical arc from the tower's emitter to each hit enemy.
            OnAreaFired?.Invoke(hitPositions);
        }

        // ----- NV callback (fires on all peers) ----------------------------

        private void HandleReplicatedTargetChanged(
            NetworkObjectReference prev, NetworkObjectReference next)
        {
            bool hadTarget = prev.TryGet(out _);
            bool hasTarget = next.TryGet(out _);

            if (hasTarget)
                OnTargetAcquired?.Invoke(next);
            else if (hadTarget)
                OnTargetLost?.Invoke();
        }
    }
}
