using System.Collections.Generic;
using System.Threading;
using UnityEngine;

public class PlayerController : MonoBehaviour
{
    [Header("Movement")]
    [SerializeField] private float _moveSpeed = 5f;
    private float _moveInputX = 0f;

    [Header("Jump")]
    [SerializeField] private float _jumpForce = 8f;
    [SerializeField] private Transform _groundCheck;
    [SerializeField] private float _groundCheckRadius = 0.1f;
    [SerializeField] private LayerMask _groundLayer;
    private bool _isGrounded;

    [Header("WallSlide")]
    [SerializeField] private Transform _wallCheckLeft;
    [SerializeField] private Transform _wallCheckRight;
    [SerializeField] private float _wallCheckRadius = 0.1f;
    [SerializeField] private float _wallSlideSpeed = 2f;
    [SerializeField] private Vector2 _wallJumpForce = new Vector2(4f, 5f);
    [SerializeField] private float _wallJumpInputLockDuration = 0.15f;
    private bool _isTouchingLeftWall;
    private bool _isTouchingRightWall;
    private bool IsTouchingWall => _isTouchingLeftWall || _isTouchingRightWall;
    private int _wallDirection;
    private float _inputLockTimer;
    private float _facingLockTimer;
    private ActionData _movementLockAction;
    private ActionData _facingLockAction;

    [Header("Motion")]
    [SerializeField] private ComboNode _dashRootNode;
    [SerializeField] private ComboNode _rollRootNode;
    [SerializeField] private ComboNode _backDashRootNode;
    private readonly Dictionary<ActionData, float> _motionCooldownTimers = new();
    private readonly List<ActionData> _motionCooldownKeys = new();

    [Header("Kinematic Physics")]
    [SerializeField] private float _gravity = -25f;
    [SerializeField] private float _maxFallSpeed = 20f;
    [SerializeField] private float _skinWidth = 0.02f;

    [Header("Attack")]
    [SerializeField] private ComboNode _punchRootNode;
    [SerializeField] private ComboNode _kickRootNode;
    [SerializeField] private LayerMask _enemyLayer;
    [SerializeField] private AttackHitbox _attackHitbox;
    [SerializeField] private string _idleAnimationState = "Idle";
    [SerializeField] private float _bufferOpenTime = 0.1f;
    [SerializeField] private float _bufferLifetime = 0.5f;
    private ComboInputType? _pendingInput;
    private float _pendingInputTime = -1f;
    private float _attackCooldownTimer;
    private ComboNode _currentNode;
    private float _comboWindowTimer;
    private CancellationTokenSource _attackCts;
    private CancellationTokenSource _motionCts;
    private CancellationTokenSource _animationSpeedCts;
    private ActionData _lastAttackGizmoData;
    [SerializeField] private float _hitGizmoFadeDuration = 0.5f;
    private ActionData _lastHitData;
    private Vector2 _lastHitCenter;
    private float _lastHitTime = -1f;

    [Header("Debug")]
    [SerializeField] private bool _showAttackDebug = true;
    private float _attackStartTime = -1f;
    private bool _hitFired;

    private readonly List<RaycastHit2D> _castResults = new();
    private Collider2D[] _bodyColliders;
    private Rigidbody2D _rb;
    private Animator _anim;
    private SpriteRenderer _spriteRenderer;

    private void Awake()
    {
        _rb = GetComponent<Rigidbody2D>();
        _anim = GetComponent<Animator>();
        _spriteRenderer = GetComponentInChildren<SpriteRenderer>();
        _bodyColliders = GetComponentsInChildren<Collider2D>();
        EnsureAttackHitbox();
    }

    private void Start()
    {
        InputManager.Instance.OnMove_Event += OnMoveInput;
        InputManager.Instance.OnJump_Event += OnJumpInput;
        InputManager.Instance.OnPunch_Event += OnPunchInput;
        InputManager.Instance.OnKick_Event += OnKickInput;
        InputManager.Instance.OnDash_Event += OnDashInput;
        InputManager.Instance.OnRoll_Event += OnRollInput;
        InputManager.Instance.OnBackDash_Event += OnBackDashInput;
    }

    private void OnDestroy()
    {
        if (InputManager.Instance != null)
        {
            InputManager.Instance.OnMove_Event -= OnMoveInput;
            InputManager.Instance.OnJump_Event -= OnJumpInput;
            InputManager.Instance.OnPunch_Event -= OnPunchInput;
            InputManager.Instance.OnKick_Event -= OnKickInput;
            InputManager.Instance.OnDash_Event -= OnDashInput;
            InputManager.Instance.OnRoll_Event -= OnRollInput;
            InputManager.Instance.OnBackDash_Event -= OnBackDashInput;
        }

        _attackCts?.Cancel();
        _attackCts?.Dispose();
        _motionCts?.Cancel();
        _motionCts?.Dispose();
        _animationSpeedCts?.Cancel();
        _animationSpeedCts?.Dispose();
    }

    private void FixedUpdate()
    {
        // 이동, 벽타기, 점프 판단이 모두 이 값을 쓰므로 충돌 체크를 먼저 갱신한다.
        _isGrounded = Physics2D.OverlapCircle(_groundCheck.position, _groundCheckRadius, _groundLayer);
        _isTouchingLeftWall = Physics2D.OverlapCircle(_wallCheckLeft.position, _wallCheckRadius, _groundLayer);
        _isTouchingRightWall = Physics2D.OverlapCircle(_wallCheckRight.position, _wallCheckRadius, _groundLayer);
        _wallDirection = _isTouchingRightWall ? 1 : (_isTouchingLeftWall ? -1 : 0);

        if (_attackCooldownTimer > 0f)
            _attackCooldownTimer -= Time.fixedDeltaTime;

        UpdateMotionCooldowns();
        ExecuteBufferedInputIfReady();
        TickComboWindow();

        if (!IsMovementLocked())
            _rb.linearVelocity = new Vector2(_moveInputX * _moveSpeed, _rb.linearVelocity.y);

        if (!IsFacingLocked())
            UpdateFacingFromMoveInput();

        ApplyGravity();

        if (IsTouchingWall && !_isGrounded && _rb.linearVelocity.y < -_wallSlideSpeed)
            _rb.linearVelocity = new Vector2(_rb.linearVelocity.x, -_wallSlideSpeed);

        // 플레이어와 적 몸체는 물리 충돌하지 않고, 땅/벽만 캐스트로 이동을 막는다.
        ClampVelocityToGround();
    }

    private void ExecuteBufferedInputIfReady()
    {
        if (_attackCooldownTimer > 0f || !_pendingInput.HasValue) return;

        ComboInputType buffered = _pendingInput.Value;
        bool stillValid = Time.time - _pendingInputTime <= _bufferLifetime;
        _pendingInput = null;
        if (stillValid)
            ExecuteComboInput(buffered);
    }

    private void TickComboWindow()
    {
        if (_comboWindowTimer <= 0f) return;

        _comboWindowTimer -= Time.fixedDeltaTime;
        if (_comboWindowTimer <= 0f)
            _currentNode = null;
    }

    private void OnMoveInput(Vector2 value)
    {
        _moveInputX = value.x == 0f ? 0f : Mathf.Sign(value.x);
        if (_facingLockTimer <= 0f)
            UpdateFacingFromMoveInput();
    }

    private void UpdateFacingFromMoveInput()
    {
        if (_moveInputX != 0f && _spriteRenderer != null)
            _spriteRenderer.flipX = _moveInputX < 0f;
    }

    private void OnJumpInput()
    {
        if (_isGrounded)
        {
            _rb.linearVelocity = new Vector2(_rb.linearVelocity.x, _jumpForce);
        }
        else if (IsTouchingWall)
        {
            _rb.linearVelocity = new Vector2(-_wallDirection * _wallJumpForce.x, _wallJumpForce.y);
            _inputLockTimer = _wallJumpInputLockDuration;
        }
    }

    private void OnPunchInput() => HandleComboInput(ComboInputType.Punch);
    private void OnKickInput() => HandleComboInput(ComboInputType.Kick);
    private void OnDashInput() => ExecuteMotionNode(_dashRootNode);
    private void OnRollInput() => ExecuteMotionNode(_rollRootNode);
    private void OnBackDashInput() => ExecuteMotionNode(_backDashRootNode);

    private void HandleComboInput(ComboInputType input)
    {
        if (_attackCooldownTimer > 0f)
        {
            float elapsed = Time.time - _attackStartTime;
            if (_lastAttackGizmoData != null && elapsed >= _bufferOpenTime)
            {
                _pendingInput = input;
                _pendingInputTime = Time.time;
            }
            return;
        }

        ExecuteComboInput(input);
    }

    private void ExecuteComboInput(ComboInputType input)
    {
        // 콤보 입력 가능 시간이 열려 있으면 현재 노드에서 다음 연계로 이어간다.
        if (_comboWindowTimer > 0f && _currentNode != null)
        {
            foreach (var transition in _currentNode.Transitions)
            {
                if (transition.Trigger != input) continue;
                if (transition.Next == null || transition.Next.Action == null) continue;

                ApplyForwardStep(transition.ForwardStep, transition.ForwardStepDuration);
                PerformAttack(transition.Next.Action, transition.ForwardStep > 0f);
                _currentNode = transition.Next;
                _comboWindowTimer = transition.Next.ComboWindow;
                return;
            }
        }

        // 이어갈 연계가 없으면 입력에 맞는 루트 노드부터 새로 시작한다.
        ComboNode root = input switch
        {
            ComboInputType.Punch => _punchRootNode,
            ComboInputType.Kick => _kickRootNode,
            _ => null
        };
        if (root == null || root.Action == null) return;

        PerformAttack(root.Action);
        _currentNode = root;
        _comboWindowTimer = root.ComboWindow;
    }

    private void ExecuteMotionNode(ComboNode root)
    {
        if (root == null || root.Action == null) return;
        if (IsMotionOnCooldown(root.Action)) return;

        PerformMotion(root.Action);
        _currentNode = root;
        _comboWindowTimer = root.ComboWindow;
    }

    private void UpdateMotionCooldowns()
    {
        if (_motionCooldownTimers.Count == 0) return;

        _motionCooldownKeys.Clear();
        foreach (var pair in _motionCooldownTimers)
            _motionCooldownKeys.Add(pair.Key);

        foreach (var action in _motionCooldownKeys)
        {
            float remaining = _motionCooldownTimers[action] - Time.fixedDeltaTime;
            if (remaining <= 0f)
                _motionCooldownTimers.Remove(action);
            else
                _motionCooldownTimers[action] = remaining;
        }
    }

    private bool IsMotionOnCooldown(ActionData data)
    {
        return data != null && _motionCooldownTimers.ContainsKey(data);
    }

    private void SetMotionCooldown(ActionData data)
    {
        if (data == null || data.Cooldown <= 0f) return;
        _motionCooldownTimers[data] = data.Cooldown;
    }

    private async void PerformAttack(ActionData data, bool preserveHorizontalVelocity = false)
    {
        _attackCts?.Cancel();
        _attackCts?.Dispose();
        _attackCts = CancellationTokenSource.CreateLinkedTokenSource(destroyCancellationToken);
        CancellationToken token = _attackCts.Token;

        _attackCooldownTimer = data.Cooldown;
        _lastAttackGizmoData = data;
        _attackStartTime = Time.time;
        _hitFired = false;

        ClearActionLocks();
        PlayActionAnimation(data);

        if (data.HasMotion)
            ApplyActionVelocity(data);

        LockMovementIfNeeded(data, preserveHorizontalVelocity, true);
        LockFacingIfNeeded(data);

        try
        {
            await HitRoutine(data, token);
        }
        catch (System.OperationCanceledException)
        {
            _attackHitbox?.Deactivate();
        }
    }

    private async void PerformMotion(ActionData data)
    {
        if (data == null || IsMotionOnCooldown(data)) return;

        // 대시/구르기 같은 모션은 공격을 끊고 새로운 콤보 노드가 된다.
        CancelAttack();
        _motionCts?.Cancel();
        _motionCts?.Dispose();
        _motionCts = CancellationTokenSource.CreateLinkedTokenSource(destroyCancellationToken);
        CancellationToken token = _motionCts.Token;

        SetMotionCooldown(data);
        ClearActionLocks();
        FaceMotionDirection(data);
        PlayActionAnimation(data);
        LockMovementIfNeeded(data);
        LockFacingIfNeeded(data);

        bool completed = false;
        try
        {
            await MotionRoutine(data, token);
            completed = true;
        }
        catch (System.OperationCanceledException) { }

        if (completed)
        {
            StopActionVelocity(data);
            ClearActionLocks();
            PlayIdleAnimation();
        }
    }

    private async Awaitable MotionRoutine(ActionData data, CancellationToken token)
    {
        float elapsed = 0f;
        float duration = GetActionDuration(data);

        while (ShouldKeepActionPlaying(data, elapsed, duration))
        {
            token.ThrowIfCancellationRequested();
            float normalizedTime = GetActionNormalizedTime(data, elapsed, duration);
            ApplyActionVelocity(data, normalizedTime);

            await Awaitable.NextFrameAsync(token);
            elapsed += Time.deltaTime;
        }
    }

    private void CancelAttack()
    {
        _attackCts?.Cancel();
        _attackCooldownTimer = 0f;
        _pendingInput = null;
        _attackHitbox?.Deactivate();
        ClearActionLocks();
    }

    private async Awaitable HitRoutine(ActionData data, CancellationToken token)
    {
        float attackStartTime = Time.time;

        if (!data.HasHit)
        {
            await WaitForActionEnd(data, token);
            return;
        }

        if (data.HitTiming > 0f)
            await Awaitable.WaitForSecondsAsync(data.HitTiming, token);

        // 타격 가능 시간은 범위 검사 대신 실제 트리거 콜라이더를 켜서 표현한다.
        ActivateAttackHitbox(data);

        float activeTime = Mathf.Max(data.HitDuration, 0.02f);
        await Awaitable.WaitForSecondsAsync(activeTime, token);
        _attackHitbox.Deactivate();

        await WaitForActionEnd(data, token, Time.time - attackStartTime);

        ClearActionLocks();
        PlayIdleAnimation();
    }

    private void ActivateAttackHitbox(ActionData data)
    {
        Vector2 localPosition = GetAttackLocalPosition(data.Offset);
        Vector2 hitVelocity = GetHitVelocity(data.HitVelocity);
        Vector2? hitTargetPosition = GetHitTargetPosition(data);

        _lastHitData = data;
        _lastHitCenter = transform.TransformPoint(localPosition);
        _lastHitTime = Time.time;
        _hitFired = true;

        Vector2 sourcePosition = _rb != null ? _rb.position : (Vector2)transform.position;
        _attackHitbox.Activate(data, localPosition, hitVelocity, sourcePosition, hitTargetPosition, data.CorrectHitTargetY, _groundLayer.value, _enemyLayer);
    }

    private void EnsureAttackHitbox()
    {
        // 인스펙터에서 직접 넣을 수도 있고, 없으면 기본 공격 판정을 자동으로 만든다.
        if (_attackHitbox != null)
        {
            SetAttackHitboxLayer();
            return;
        }

        _attackHitbox = GetComponentInChildren<AttackHitbox>(true);
        if (_attackHitbox != null)
        {
            SetAttackHitboxLayer();
            return;
        }

        GameObject hitboxObject = new GameObject("AttackHitbox");
        hitboxObject.transform.SetParent(transform, false);
        hitboxObject.AddComponent<CircleCollider2D>();
        _attackHitbox = hitboxObject.AddComponent<AttackHitbox>();
        SetAttackHitboxLayer();
    }

    private void SetAttackHitboxLayer()
    {
        // 플레이어와 적의 물리 충돌이 꺼져 있으므로 공격 판정은 플레이어 레이어를 쓰면 안 된다.
        int defaultLayer = LayerMask.NameToLayer("Default");
        if (defaultLayer >= 0)
            _attackHitbox.gameObject.layer = defaultLayer;
    }

    private void ApplyActionVelocity(ActionData data, float normalizedTime = 0f)
    {
        float direction = GetMotionDirection(data);
        float speedMultiplier = data.MotionSpeedCurve != null
            ? data.MotionSpeedCurve.Evaluate(normalizedTime)
            : 1f;

        Vector2 velocity = data.Velocity * speedMultiplier;
        velocity.x *= direction;

        if (data.PreserveYVelocity)
            velocity.y = _rb.linearVelocity.y;

        _rb.linearVelocity = velocity;
    }

    private void StopActionVelocity(ActionData data)
    {
        if (!data.StopHorizontalVelocityOnEnd) return;
        _rb.linearVelocity = new Vector2(0f, _rb.linearVelocity.y);
    }

    private void LockMovementIfNeeded(ActionData data, bool preserveHorizontalVelocity = false, bool forceLock = false)
    {
        if (!forceLock && data.CanMoveDuringAction) return;

        // 이동 없는 공격은 직전 프레임의 걷기 속도를 이어받지 않게 한다.
        if (!preserveHorizontalVelocity && !data.HasMotion)
            _rb.linearVelocity = new Vector2(0f, _rb.linearVelocity.y);

        _inputLockTimer = GetActionLockDuration(data);
        _movementLockAction = ShouldUseAnimationLength(data) ? data : null;
    }

    private void LockFacingIfNeeded(ActionData data)
    {
        if (data.CanTurnDuringAction) return;
        _facingLockTimer = GetActionLockDuration(data);
        _facingLockAction = ShouldUseAnimationLength(data) ? data : null;
    }

    private void PlayIdleAnimation()
    {
        if (_anim == null) return;

        _animationSpeedCts?.Cancel();
        _anim.speed = 1f;
        if (!string.IsNullOrEmpty(_idleAnimationState))
            _anim.Play(_idleAnimationState);
    }

    private void PlayActionAnimation(ActionData data)
    {
        if (_anim == null) return;

        _animationSpeedCts?.Cancel();
        _animationSpeedCts?.Dispose();
        _animationSpeedCts = CancellationTokenSource.CreateLinkedTokenSource(destroyCancellationToken);

        _anim.speed = GetAnimationSpeed(data, 0f);
        if (!string.IsNullOrEmpty(data.AnimationState))
        {
            _anim.Play(data.AnimationState);
            _anim.Update(0f);
        }

        ApplyAnimationSpeedCurve(data, _animationSpeedCts.Token);
    }

    private async void ApplyAnimationSpeedCurve(ActionData data, CancellationToken token)
    {
        float duration = GetActionDuration(data);
        float elapsed = 0f;

        try
        {
            while (ShouldKeepActionPlaying(data, elapsed, duration))
            {
                token.ThrowIfCancellationRequested();
                float normalizedTime = GetActionNormalizedTime(data, elapsed, duration);
                _anim.speed = GetAnimationSpeed(data, normalizedTime);

                await Awaitable.NextFrameAsync(token);
                elapsed += Time.deltaTime;
            }
        }
        catch (System.OperationCanceledException) { }
    }

    private float GetAnimationSpeed(ActionData data, float normalizedTime)
    {
        float curveMultiplier = data.AnimationSpeedCurve != null
            ? data.AnimationSpeedCurve.Evaluate(normalizedTime)
            : 1f;

        return Mathf.Max(data.AnimationSpeed * curveMultiplier, 0.01f);
    }

    private float GetMotionDirection(ActionData data)
    {
        if (data.UseInputDirection && _moveInputX != 0f)
            return _moveInputX;

        return _spriteRenderer != null && _spriteRenderer.flipX ? -1f : 1f;
    }

    private void FaceMotionDirection(ActionData data)
    {
        if (_spriteRenderer == null) return;
        if (!data.UseInputDirection || _moveInputX == 0f) return;

        _spriteRenderer.flipX = _moveInputX < 0f;
        _facingLockTimer = 0f;
    }

    private Vector2 GetHitVelocity(Vector2 hitVelocity)
    {
        float facing = _spriteRenderer != null && _spriteRenderer.flipX ? -1f : 1f;
        hitVelocity.x *= facing;
        return hitVelocity;
    }

    private Vector2 GetAttackLocalPosition(Vector2 offset)
    {
        float facing = _spriteRenderer != null && _spriteRenderer.flipX ? -1f : 1f;
        offset.x *= facing;
        return offset;
    }

    private Vector2? GetHitTargetPosition(ActionData data)
    {
        if (!data.UseHitPositionCorrection) return null;

        float facing = _spriteRenderer != null && _spriteRenderer.flipX ? -1f : 1f;
        Vector2 playerPosition = _rb != null ? _rb.position : (Vector2)transform.position;
        Vector2 offset = data.HitTargetOffset;
        offset.x *= facing;
        return playerPosition + offset;
    }

    private string GetActionName(ActionData data)
    {
        if (data == null) return string.Empty;
        return string.IsNullOrEmpty(data.ActionName) ? data.name : data.ActionName;
    }

    private void ApplyForwardStep(float distance, float duration)
    {
        if (distance <= 0f) return;

        float safeDuration = Mathf.Max(duration, 0.02f);
        float facing = _spriteRenderer != null && _spriteRenderer.flipX ? -1f : 1f;
        float vx = facing * (distance / safeDuration);

        _rb.linearVelocity = new Vector2(vx, _rb.linearVelocity.y);
        _inputLockTimer = safeDuration;
        _movementLockAction = null;
    }

    private bool IsActionAnimationComplete(ActionData data)
    {
        if (!HasActionAnimation(data)) return false;

        AnimatorStateInfo stateInfo = _anim.GetCurrentAnimatorStateInfo(0);
        return stateInfo.IsName(data.AnimationState) && stateInfo.normalizedTime >= 1f;
    }

    private bool IsActionAnimationLooping(ActionData data)
    {
        if (!HasActionAnimation(data)) return false;

        AnimatorStateInfo stateInfo = _anim.GetCurrentAnimatorStateInfo(0);
        if (!stateInfo.IsName(data.AnimationState)) return false;
        if (stateInfo.loop) return true;

        AnimatorClipInfo[] clipInfos = _anim.GetCurrentAnimatorClipInfo(0);
        for (int i = 0; i < clipInfos.Length; i++)
        {
            AnimationClip clip = clipInfos[i].clip;
            if (clip != null && clip.isLooping)
                return true;
        }

        return false;
    }

    private bool HasActionAnimation(ActionData data)
    {
        return _anim != null && data != null && !string.IsNullOrEmpty(data.AnimationState);
    }

    private bool ShouldUseAnimationLength(ActionData data)
    {
        return HasActionAnimation(data) && !IsActionAnimationLooping(data);
    }

    private bool IsMovementLocked()
    {
        return IsTimerOrActionLockActive(ref _inputLockTimer, ref _movementLockAction);
    }

    private bool IsFacingLocked()
    {
        return IsTimerOrActionLockActive(ref _facingLockTimer, ref _facingLockAction);
    }

    private bool IsTimerOrActionLockActive(ref float timer, ref ActionData action)
    {
        if (timer > 0f)
        {
            timer -= Time.fixedDeltaTime;
            return true;
        }

        if (action == null)
            return false;

        if (!HasActionAnimation(action))
        {
            action = null;
            return false;
        }

        if (!IsActionAnimationActive(action))
        {
            action = null;
            return false;
        }

        if (IsActionAnimationComplete(action))
        {
            action = null;
            return false;
        }

        return true;
    }

    private bool IsActionAnimationActive(ActionData data)
    {
        if (!HasActionAnimation(data)) return false;

        AnimatorStateInfo stateInfo = _anim.GetCurrentAnimatorStateInfo(0);
        return stateInfo.IsName(data.AnimationState);
    }

    private void ClearActionLocks()
    {
        _inputLockTimer = 0f;
        _facingLockTimer = 0f;
        _movementLockAction = null;
        _facingLockAction = null;
    }

    private float GetActionDuration(ActionData data)
    {
        return Mathf.Max(data.MotionDuration, data.HitTiming + data.HitDuration, 0.01f);
    }

    private float GetActionLockDuration(ActionData data)
    {
        return ShouldUseAnimationLength(data) ? 0f : Mathf.Max(data.MotionDuration, 0.02f);
    }

    private bool ShouldKeepActionPlaying(ActionData data, float elapsed, float duration)
    {
        if (!ShouldUseAnimationLength(data))
            return elapsed < duration;

        return !IsActionAnimationComplete(data);
    }

    private float GetActionNormalizedTime(ActionData data, float elapsed, float duration)
    {
        if (!HasActionAnimation(data))
            return Mathf.Clamp01(elapsed / duration);

        AnimatorStateInfo stateInfo = _anim.GetCurrentAnimatorStateInfo(0);
        if (!stateInfo.IsName(data.AnimationState))
            return 0f;

        return Mathf.Clamp01(stateInfo.normalizedTime);
    }

    private async Awaitable WaitForActionEnd(ActionData data, CancellationToken token, float alreadyElapsed = 0f)
    {
        if (!ShouldUseAnimationLength(data))
        {
            float remaining = GetActionDuration(data) - alreadyElapsed;
            if (remaining > 0f)
                await Awaitable.WaitForSecondsAsync(remaining, token);
            return;
        }

        while (!IsActionAnimationComplete(data))
        {
            token.ThrowIfCancellationRequested();
            await Awaitable.NextFrameAsync(token);
        }
    }

    private void ApplyGravity()
    {
        float vy = _rb.linearVelocity.y;

        if (_isGrounded && vy <= 0f)
        {
            if (vy != 0f)
                _rb.linearVelocity = new Vector2(_rb.linearVelocity.x, 0f);
            return;
        }

        float newY = Mathf.Max(vy + _gravity * Time.fixedDeltaTime, -_maxFallSpeed);
        _rb.linearVelocity = new Vector2(_rb.linearVelocity.x, newY);
    }

    private void ClampVelocityToGround()
    {
        int solidMask = _groundLayer.value;
        if (solidMask == 0) return;

        Vector2 velocity = _rb.linearVelocity;
        bool changed = false;

        if (Mathf.Abs(velocity.x) > 0.001f)
        {
            float sign = Mathf.Sign(velocity.x);
            float maxDist = Mathf.Abs(velocity.x * Time.fixedDeltaTime);
            float hitDist = GetClosestHitDistance(new Vector2(sign, 0f), maxDist + _skinWidth, solidMask);
            if (hitDist < maxDist + _skinWidth)
            {
                float allowed = Mathf.Max(hitDist - _skinWidth, 0f);
                velocity.x = sign * (allowed / Time.fixedDeltaTime);
                changed = true;
            }
        }

        if (Mathf.Abs(velocity.y) > 0.001f)
        {
            float sign = Mathf.Sign(velocity.y);
            float maxDist = Mathf.Abs(velocity.y * Time.fixedDeltaTime);
            float hitDist = GetClosestHitDistance(new Vector2(0f, sign), maxDist + _skinWidth, solidMask);
            if (hitDist < maxDist + _skinWidth)
            {
                float allowed = Mathf.Max(hitDist - _skinWidth, 0f);
                velocity.y = sign * (allowed / Time.fixedDeltaTime);
                changed = true;
            }
        }

        if (changed)
            _rb.linearVelocity = velocity;
    }

    private float GetClosestHitDistance(Vector2 direction, float distance, int layerMask)
    {
        if (_bodyColliders == null || _bodyColliders.Length == 0)
            _bodyColliders = GetComponentsInChildren<Collider2D>();

        ContactFilter2D filter = new ContactFilter2D
        {
            useLayerMask = true,
            layerMask = layerMask,
            useTriggers = false
        };

        // 빠른 이동이 지형을 뚫지 않도록 트리거가 아닌 몸체 콜라이더를 전부 캐스트한다.
        float closest = float.PositiveInfinity;
        for (int i = 0; i < _bodyColliders.Length; i++)
        {
            Collider2D bodyCollider = _bodyColliders[i];
            if (bodyCollider == null || bodyCollider.isTrigger) continue;

            _castResults.Clear();
            int hitCount = bodyCollider.Cast(direction, filter, _castResults, distance);
            for (int j = 0; j < hitCount; j++)
            {
                if (_castResults[j].distance < closest)
                    closest = _castResults[j].distance;
            }
        }

        return closest;
    }

    private void OnDrawGizmos()
    {
        if (_lastHitData == null || _lastHitTime < 0f) return;

        float since = Time.time - _lastHitTime;
        if (since < 0f || since > _hitGizmoFadeDuration) return;

        float t = Mathf.Clamp01(since / _hitGizmoFadeDuration);
        float alpha = Mathf.Lerp(0.85f, 0f, t);
        float radius = _lastHitData.Radius;

        Gizmos.color = new Color(1f, 0.2f, 0.2f, alpha * 0.35f);
        Gizmos.DrawSphere(_lastHitCenter, radius);

        Gizmos.color = new Color(1f, 0f, 0f, alpha);
        Gizmos.DrawWireSphere(_lastHitCenter, radius);
    }

    private void OnDrawGizmosSelected()
    {
        if (_groundCheck != null)
        {
            Gizmos.color = _isGrounded ? Color.green : Color.red;
            Gizmos.DrawWireSphere(_groundCheck.position, _groundCheckRadius);
        }
        if (_wallCheckLeft != null)
        {
            Gizmos.color = _isTouchingLeftWall ? Color.green : Color.red;
            Gizmos.DrawWireSphere(_wallCheckLeft.position, _wallCheckRadius);
        }
        if (_wallCheckRight != null)
        {
            Gizmos.color = _isTouchingRightWall ? Color.green : Color.red;
            Gizmos.DrawWireSphere(_wallCheckRight.position, _wallCheckRadius);
        }
    }

    private void OnGUI()
    {
        if (!_showAttackDebug || _lastAttackGizmoData == null) return;

        ActionData data = _lastAttackGizmoData;
        float elapsed = Time.time - _attackStartTime;

        string status = _hitFired
            ? (elapsed < data.HitTiming + Mathf.Max(data.HitDuration, 0.01f) ? "HIT" : "DONE")
            : "WINDUP";
        string cooldownText = _attackCooldownTimer > 0f
            ? $"{_attackCooldownTimer:F3}s left"
            : "READY";

        string bufferText = _pendingInput.HasValue
            ? $"<color=#ffcc00>BUFFERED: {_pendingInput.Value}</color>"
            : (elapsed >= _bufferOpenTime && _attackCooldownTimer > 0f ? "ready to buffer" : "-");

        string info =
            $"<b>{GetActionName(data)}</b>  [{status}]\n" +
            $"Elapsed     : {elapsed:F3} s\n" +
            $"HitTiming   : {data.HitTiming:F3} s\n" +
            $"HitDuration : {data.HitDuration:F3} s\n" +
            $"Cooldown    : {data.Cooldown:F3} s  ({cooldownText})\n" +
            $"ComboWindow : {_comboWindowTimer:F3} s\n" +
            $"Buffer      : {bufferText}";

        GUIStyle style = new GUIStyle(GUI.skin.box)
        {
            fontSize = 26,
            alignment = TextAnchor.UpperLeft,
            richText = true,
            padding = new RectOffset(16, 16, 12, 12),
            normal = { textColor = Color.white }
        };

        GUI.Box(new Rect(10, 10, 520, 282), info, style);

        DrawTimelineBar(data, elapsed);
    }

    private void DrawTimelineBar(ActionData data, float elapsed)
    {
        float barX = 10f;
        float barY = 302f;
        float barW = 520f;
        float barH = 36f;
        float totalTime = Mathf.Max(
            data.HitTiming + Mathf.Max(data.HitDuration, 0.05f),
            data.Cooldown,
            data.MotionDuration,
            0.3f);

        GUI.color = new Color(0f, 0f, 0f, 0.6f);
        GUI.DrawTexture(new Rect(barX, barY, barW, barH), Texture2D.whiteTexture);

        float hitX = barX + (data.HitTiming / totalTime) * barW;
        float hitEndX = barX + ((data.HitTiming + data.HitDuration) / totalTime) * barW;
        GUI.color = new Color(1f, 0.3f, 0.3f, 0.5f);
        GUI.DrawTexture(new Rect(hitX, barY, Mathf.Max(hitEndX - hitX, 4f), barH), Texture2D.whiteTexture);

        float bufferX = barX + Mathf.Clamp01(_bufferOpenTime / totalTime) * barW;
        GUI.color = new Color(0.2f, 1f, 0.4f, 0.9f);
        GUI.DrawTexture(new Rect(bufferX - 2f, barY, 4f, barH), Texture2D.whiteTexture);

        float cdEndX = barX + Mathf.Clamp01(data.Cooldown / totalTime) * barW;
        GUI.color = new Color(0.3f, 0.7f, 1f, 0.9f);
        GUI.DrawTexture(new Rect(cdEndX - 2f, barY, 4f, barH), Texture2D.whiteTexture);

        float cursorX = barX + Mathf.Clamp01(elapsed / totalTime) * barW;
        GUI.color = Color.yellow;
        GUI.DrawTexture(new Rect(cursorX - 2f, barY - 4f, 4f, barH + 8f), Texture2D.whiteTexture);

        GUI.color = Color.white;
    }
}
