using FishNet.Object.Prediction;
using FishNet.Transporting;
using FishNet.Utility.Template;
using UnityEngine;
using IStalk.Data;
using IStalk.Shared;

namespace IStalk.Runner
{
    [RequireComponent(typeof(CharacterController))]
    public class RunnerController : TickNetworkBehaviour
    {
        [Header("Detection")]
        [SerializeField] private Transform _wallCheckLeft;
        [SerializeField] private Transform _wallCheckRight;
        [SerializeField] private Transform _vaultCheck;
        [SerializeField] private Transform _mantleCheck;
        [SerializeField] private LayerMask _wallLayer;
        [SerializeField] private LayerMask _groundLayer;

        private CharacterController _cc;
        private WeightComponent _weight;
        private RunnerStamina _stamina;
        private RunnerHealth _health;

        private Vector3 _velocity;
        private bool _isGrounded;
        private float _coyoteTimer;
        private float _jumpBufferTimer;
        private bool _isSprinting;
        private bool _isSliding;
        private float _slideTimer;
        private float _dashTimer;
        private float _dashCooldown;

        private MoveState _moveState;
        private float _wallRunTimer;
        private float _wallRunGravity;
        private int _wallRunSide;
        private bool _canWallRun;
        private float _parkourLerpTimer;
        private float _parkourLerpDuration;
        private Vector3 _parkourStart;
        private Vector3 _parkourEnd;

        private bool _dashEnabled;
        private bool _wallRunEnabled;
        private bool _airDashEnabled;
        private bool _slideEnabled;
        private bool _vaultEnabled;

        private float _speedBonus;

        private GlobalRules_SO Rules => GlobalRules_SO.Instance;

        public void EnableAbility(WearableAbilityType ability)
        {
            switch (ability)
            {
                case WearableAbilityType.Dash: _dashEnabled = true; break;
                case WearableAbilityType.WallRun: _wallRunEnabled = true; break;
                case WearableAbilityType.AirDash: _airDashEnabled = true; break;
                case WearableAbilityType.Slide: _slideEnabled = true; break;
                case WearableAbilityType.Vault: _vaultEnabled = true; break;
            }
        }

        public void DisableAbility(WearableAbilityType ability)
        {
            switch (ability)
            {
                case WearableAbilityType.Dash: _dashEnabled = false; break;
                case WearableAbilityType.WallRun: _wallRunEnabled = false; break;
                case WearableAbilityType.AirDash: _airDashEnabled = false; break;
                case WearableAbilityType.Slide: _slideEnabled = false; break;
                case WearableAbilityType.Vault: _vaultEnabled = false; break;
            }
        }

        public void SetSpeedBonus(float bonus) => _speedBonus = bonus;

        public enum MoveState { Normal, WallRunning, Vaulting, Mantling, Sliding, Dashing }

        private struct MoveData : IReplicateData
        {
            public Vector2 Move;
            public bool Jump;
            public bool Sprint;
            public bool Slide;
            public bool Dash;
            public float Yaw;
            private uint _tick;
            public void Dispose() { }
            public uint GetTick() => _tick;
            public void SetTick(uint value) => _tick = value;
        }

        private struct ReconcileData : IReconcileData
        {
            public Vector3 Position;
            public Vector3 Velocity;
            public bool IsGrounded;
            public float CoyoteTimer;
            public byte State;
            public float WallRunTimer;
            public float SlideTimer;
            public float DashTimer;
            private uint _tick;
            public void Dispose() { }
            public uint GetTick() => _tick;
            public void SetTick(uint value) => _tick = value;
        }

        public override void OnStartClient()
        {
            base.OnStartClient();
            _cc = GetComponent<CharacterController>();
            _weight = GetComponent<WeightComponent>();
            _stamina = GetComponent<RunnerStamina>();
            _health = GetComponent<RunnerHealth>();

            if (!IsOwner)
            {
                enabled = false;
                return;
            }
        }

        public override void OnStartServer()
        {
            base.OnStartServer();
            _cc = GetComponent<CharacterController>();
            _weight = GetComponent<WeightComponent>();
            _stamina = GetComponent<RunnerStamina>();
            _health = GetComponent<RunnerHealth>();
        }

        protected override void TimeManager_OnTick()
        {
            PerformReplicate(BuildInput());
        }

        protected override void TimeManager_OnPostTick()
        {
            CreateReconcile();
        }

        public override void CreateReconcile()
        {
            var rd = new ReconcileData
            {
                Position = transform.position,
                Velocity = _velocity,
                IsGrounded = _isGrounded,
                CoyoteTimer = _coyoteTimer,
                State = (byte)_moveState,
                WallRunTimer = _wallRunTimer,
                SlideTimer = _slideTimer,
                DashTimer = _dashTimer
            };
            PerformReconcile(rd);
        }

        private MoveData BuildInput()
        {
            if (!IsOwner) return default;

            return new MoveData
            {
                Move = new Vector2(Input.GetAxisRaw("Horizontal"), Input.GetAxisRaw("Vertical")),
                Jump = Input.GetKey(KeyCode.Space),
                Sprint = Input.GetKey(KeyCode.LeftShift),
                Slide = Input.GetKey(KeyCode.LeftControl),
                Dash = Input.GetKey(KeyCode.E),
                Yaw = transform.eulerAngles.y
            };
        }

        [Replicate]
        private void PerformReplicate(MoveData md, ReplicateState state = ReplicateState.Invalid, Channel channel = Channel.Unreliable)
        {
            float dt = (float)TimeManager.TickDelta;

            if (_health != null && _health.IsDown)
            {
                _velocity = Vector3.zero;
                return;
            }

            switch (_moveState)
            {
                case MoveState.Normal:
                    TickNormal(md, dt);
                    break;
                case MoveState.WallRunning:
                    TickWallRun(md, dt);
                    break;
                case MoveState.Sliding:
                    TickSlide(md, dt);
                    break;
                case MoveState.Dashing:
                    TickDash(dt);
                    break;
                case MoveState.Vaulting:
                case MoveState.Mantling:
                    TickParkourLerp(dt);
                    break;
            }

            _cc.Move(_velocity * dt);
        }

        private void TickNormal(MoveData md, float dt)
        {
            _isGrounded = _cc.isGrounded;

            if (_isGrounded)
            {
                _coyoteTimer = Rules.coyoteTime;
                _canWallRun = true;
                if (_velocity.y < 0) _velocity.y = -2f;
            }
            else
            {
                _coyoteTimer -= dt;
            }

            if (md.Jump)
                _jumpBufferTimer = Rules.jumpBuffer;
            else
                _jumpBufferTimer -= dt;

            _dashCooldown -= dt;
            bool canSprint = _stamina == null || _stamina.CanSprint;
            _isSprinting = md.Sprint && _isGrounded && canSprint;
            float speedMult = _weight != null ? Rules.GetSpeedMultiplier(_weight.CurrentTier) : 1f;
            float healthMult = _health != null ? _health.SlowMultiplier : 1f;
            float speed = Rules.baseRunSpeed * speedMult * healthMult * (1f + _speedBonus);
            if (_isSprinting)
            {
                speed *= Rules.sprintMultiplier;
                if (IsServerStarted && _stamina != null) _stamina.DrainSprint(dt);
            }

            if (_slideEnabled && md.Slide && _isSprinting && _isGrounded)
            {
                EnterSlide(md.Yaw);
                return;
            }

            if ((_dashEnabled || _airDashEnabled) && md.Dash && _dashCooldown <= 0 && !_isGrounded && (_stamina == null || _stamina.CanDash))
            {
                if (IsServerStarted && _stamina != null && !_stamina.TryDash()) { }
                else { EnterDash(md); return; }
            }

            if (_wallRunEnabled && !_isGrounded && _canWallRun && md.Move.y > 0.1f)
            {
                if (TryEnterWallRun(md))
                    return;
            }

            if (_vaultEnabled && !_isGrounded && md.Move.y > 0.1f)
            {
                if (TryVault(md.Yaw))
                    return;
                if (TryMantle(md.Yaw))
                    return;
            }

            Vector3 moveDir = Quaternion.Euler(0, md.Yaw, 0) * new Vector3(md.Move.x, 0, md.Move.y).normalized;
            _velocity.x = moveDir.x * speed;
            _velocity.z = moveDir.z * speed;

            if (_jumpBufferTimer > 0 && _coyoteTimer > 0)
            {
                _velocity.y = Rules.jumpForce;
                _jumpBufferTimer = 0;
                _coyoteTimer = 0;
            }

            _velocity.y += Rules.gravity * dt;
        }

        private void TickWallRun(MoveData md, float dt)
        {
            _wallRunTimer -= dt;
            _wallRunGravity += Mathf.Abs(Rules.gravity) * 0.3f * dt;
            _velocity.y = Mathf.Max(_velocity.y - _wallRunGravity * dt, Rules.gravity * 0.5f * dt);

            if (IsServerStarted && _stamina != null) _stamina.DrainWallRun(dt);
            if (_stamina != null && !_stamina.CanSprint)
            {
                _moveState = MoveState.Normal;
                _canWallRun = false;
                return;
            }

            float speed = Rules.baseRunSpeed * 0.9f;
            Vector3 wallForward = GetWallRunForward(md.Yaw);
            _velocity.x = wallForward.x * speed;
            _velocity.z = wallForward.z * speed;

            bool wallStillPresent = CheckWallPresent(_wallRunSide);

            if (_wallRunTimer <= 0 || md.Jump || !wallStillPresent || _cc.isGrounded)
            {
                if (md.Jump && !_cc.isGrounded)
                    WallJump(md.Yaw);
                _moveState = MoveState.Normal;
                _canWallRun = false;
                return;
            }
        }

        private void TickSlide(MoveData md, float dt)
        {
            _slideTimer -= dt;
            _velocity.y += Rules.gravity * dt;

            float decel = Rules.slideSpeed * dt * 2f;
            float hSpeed = new Vector2(_velocity.x, _velocity.z).magnitude;
            hSpeed = Mathf.Max(0, hSpeed - decel);

            Vector3 dir = new Vector3(_velocity.x, 0, _velocity.z).normalized;
            _velocity.x = dir.x * hSpeed;
            _velocity.z = dir.z * hSpeed;

            if (_slideTimer <= 0 || md.Jump || hSpeed < 1f)
            {
                _moveState = MoveState.Normal;
                _isSliding = false;
                if (md.Jump && _cc.isGrounded)
                {
                    _velocity.y = Rules.jumpForce * 0.8f;
                    Vector3 boost = Quaternion.Euler(0, md.Yaw, 0) * Vector3.forward * 3f;
                    _velocity.x += boost.x;
                    _velocity.z += boost.z;
                }
            }
        }

        private void TickDash(float dt)
        {
            _dashTimer -= dt;
            _velocity.y = 0;

            if (_dashTimer <= 0)
            {
                _moveState = MoveState.Normal;
                _velocity.y = -2f;
            }
        }

        private void TickParkourLerp(float dt)
        {
            _parkourLerpTimer += dt;
            float t = Mathf.Clamp01(_parkourLerpTimer / _parkourLerpDuration);
            t = t * t * (3f - 2f * t);

            Vector3 target = Vector3.Lerp(_parkourStart, _parkourEnd, t);
            _velocity = (target - transform.position) / Mathf.Max(dt, 0.001f);

            if (t >= 1f)
            {
                _moveState = MoveState.Normal;
                _velocity = Vector3.zero;
                _velocity.y = -2f;
            }
        }

        private void EnterSlide(float yaw)
        {
            _moveState = MoveState.Sliding;
            _isSliding = true;
            _slideTimer = 0.8f;
            Vector3 slideDir = Quaternion.Euler(0, yaw, 0) * Vector3.forward;
            _velocity = slideDir * Rules.slideSpeed;
        }

        private void EnterDash(MoveData md)
        {
            _moveState = MoveState.Dashing;
            float dashDist = Rules.airDashDistance;
            Vector3 dashDir = Quaternion.Euler(0, md.Yaw, 0) * new Vector3(md.Move.x, 0, md.Move.y).normalized;
            if (dashDir.sqrMagnitude < 0.01f)
                dashDir = Quaternion.Euler(0, md.Yaw, 0) * Vector3.forward;
            _velocity = dashDir * (dashDist / Rules.dashDuration);
            _dashTimer = Rules.dashDuration;
            _dashCooldown = 1.5f;
        }

        private bool TryEnterWallRun(MoveData md)
        {
            float speed = new Vector2(_velocity.x, _velocity.z).magnitude;
            if (speed < Rules.wallRunMinSpeed) return false;

            if (CheckWallPresent(-1))
            {
                StartWallRun(-1, md.Yaw);
                return true;
            }
            if (CheckWallPresent(1))
            {
                StartWallRun(1, md.Yaw);
                return true;
            }
            return false;
        }

        private void StartWallRun(int side, float yaw)
        {
            _moveState = MoveState.WallRunning;
            _wallRunSide = side;
            _wallRunTimer = 1.2f;
            _wallRunGravity = 0f;
            _velocity.y = Mathf.Max(_velocity.y, 2f);
        }

        private bool CheckWallPresent(int side)
        {
            Transform check = side < 0 ? _wallCheckLeft : _wallCheckRight;
            if (check == null) return false;
            return Physics.Raycast(check.position, check.right * side, 0.6f, _wallLayer);
        }

        private Vector3 GetWallRunForward(float yaw)
        {
            Vector3 forward = Quaternion.Euler(0, yaw, 0) * Vector3.forward;
            Transform check = _wallRunSide < 0 ? _wallCheckLeft : _wallCheckRight;
            if (check != null && Physics.Raycast(check.position, check.right * _wallRunSide, out RaycastHit hit, 0.8f, _wallLayer))
            {
                Vector3 wallNormal = hit.normal;
                forward = Vector3.Cross(wallNormal, Vector3.up) * _wallRunSide;
            }
            return forward.normalized;
        }

        private void WallJump(float yaw)
        {
            Vector3 jumpDir = Vector3.up * Rules.jumpForce * 0.8f;
            Vector3 awayFromWall = (_wallRunSide < 0 ? Vector3.right : Vector3.left);
            awayFromWall = Quaternion.Euler(0, yaw, 0) * awayFromWall;

            _velocity = awayFromWall * Rules.baseRunSpeed * 0.6f;
            _velocity.y = Rules.jumpForce * 0.9f;
        }

        private bool TryVault(float yaw)
        {
            if (_vaultCheck == null) return false;

            Vector3 forward = Quaternion.Euler(0, yaw, 0) * Vector3.forward;
            Vector3 origin = _vaultCheck.position;

            if (!Physics.Raycast(origin, forward, out RaycastHit hit, 1.2f, _wallLayer))
                return false;

            if (hit.collider.bounds.size.y > 1.5f)
                return false;

            Vector3 topCheck = hit.point + Vector3.up * 1.6f;
            if (Physics.Raycast(topCheck, Vector3.down, out RaycastHit topHit, 1.5f, _wallLayer | _groundLayer))
            {
                Vector3 landPos = hit.point + forward * 1.2f;
                landPos.y = topHit.point.y + 0.1f;

                if (!Physics.Raycast(landPos + Vector3.up * 2f, Vector3.down, 3f, _wallLayer))
                    return false;

                _moveState = MoveState.Vaulting;
                _parkourStart = transform.position;
                _parkourEnd = landPos;
                _parkourLerpTimer = 0;
                _parkourLerpDuration = 0.35f;
                return true;
            }
            return false;
        }

        private bool TryMantle(float yaw)
        {
            if (_mantleCheck == null) return false;

            Vector3 forward = Quaternion.Euler(0, yaw, 0) * Vector3.forward;
            Vector3 origin = _mantleCheck.position;

            if (!Physics.Raycast(origin, forward, out RaycastHit hit, 0.8f, _wallLayer))
                return false;

            Vector3 ledgeCheck = hit.point + Vector3.up * 2.5f + forward * 0.3f;
            if (Physics.Raycast(ledgeCheck, Vector3.down, out RaycastHit ledgeHit, 2.5f, _wallLayer | _groundLayer))
            {
                float ledgeHeight = ledgeHit.point.y - transform.position.y;
                if (ledgeHeight > 1.5f && ledgeHeight < 3.5f)
                {
                    Vector3 landPos = ledgeHit.point + forward * 0.5f;
                    landPos.y = ledgeHit.point.y + 0.1f;

                    _moveState = MoveState.Mantling;
                    _parkourStart = transform.position;
                    _parkourEnd = landPos;
                    _parkourLerpTimer = 0;
                    _parkourLerpDuration = 0.5f;
                    return true;
                }
            }
            return false;
        }

        [Reconcile]
        private void PerformReconcile(ReconcileData rd, Channel channel = Channel.Unreliable)
        {
            transform.position = rd.Position;
            _velocity = rd.Velocity;
            _isGrounded = rd.IsGrounded;
            _coyoteTimer = rd.CoyoteTimer;
            _moveState = (MoveState)rd.State;
            _wallRunTimer = rd.WallRunTimer;
            _slideTimer = rd.SlideTimer;
            _dashTimer = rd.DashTimer;
        }

        public MoveState GetMoveState() => _moveState;
        public bool IsWallRunning => _moveState == MoveState.WallRunning;
        public int WallRunSide => _wallRunSide;
        public bool IsSliding => _moveState == MoveState.Sliding;

        public void GrantExtraDash()
        {
            _dashCooldown = 0;
        }
    }
}
