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

// ============================================================================
// PlayerController
// ----------------------------------------------------------------------------
// 플레이어 캐릭터의 모든 동작을 관리하는 중심 컨트롤러.
// - Kinematic Rigidbody2D 기반 (중력/충돌 모두 코드에서 직접 처리)
// - 이동, 점프(2단), 그라운드 파운드
// - 콤보 공격 (Punch/Kick/Grab) — ActionData + ComboNode 그래프로 정의
// - 모션 액션 (Dash/Roll/BackDash) — 단발 실행, 자체 쿨다운
// - 입력 버퍼링: 쿨다운 중에도 다음 콤보 입력 받아서 자동 실행
// - 외부에서 들어오는 데미지 수용 (IDamageable)
// ============================================================================
public class PlayerController : MonoBehaviour,IDamageable
{
    // ─── 좌우 이동 ────────────────────────────────────────────────────────
    [Header("Movement")]
    [SerializeField] private float _moveSpeed = 5f;                  // 이동 속도 (units/sec)
    [SerializeField] private string _walkAnimationState = "Run";     // 걷기/달리기 애니메이션 State 이름
    private float _moveInputX = 0f;                                  // 현재 X 입력값 (-1/0/1)
    private string _activeBaseState;                                 // 현재 재생 중인 locomotion State (중복 Play 방지용)
    private bool _isInActionAnimation;                               // 액션 애니메이션 재생 중인지 (locomotion 잠시 양보)

    // ─── 점프 단계별 애니메이션 ───────────────────────────────────────────
    // 점프를 4단계(Rise/Mid/Fall/Land)로 분리해서 vy에 따라 자동 전환.
    [Header("Jump Animation")]
    [SerializeField] private string _jumpRiseAnimationState = "JumpRise";  // 상승 중 (vy > _jumpMidThreshold)
    [SerializeField] private string _jumpMidAnimationState = "JumpMid";    // 정점 부근 (|vy| <= threshold)
    [SerializeField] private string _jumpFallAnimationState = "JumpFall";  // 낙하 중 (vy < -threshold)
    [SerializeField] private string _landAnimationState = "Land";          // 착지 직후 짧게 재생
    [SerializeField] private float _jumpMidThreshold = 2f;                 // Rise/Mid/Fall 구분 vy 경계값
    [SerializeField] private float _landAnimationDuration = 0.15f;         // Land 애니 지속 시간
    private bool _wasGroundedLastFrame = true;                             // 이전 프레임 grounded 여부 (Land 트리거용)
    private float _landTimer;                                              // Land 애니 남은 시간

    // ─── 점프 (메커니즘) ─────────────────────────────────────────────────
    [Header("Jump")]
    [SerializeField] private float _jumpForce = 8f;                  // 점프 시 vy 값
    [SerializeField] private int _maxJumpCount = 2;                  // 최대 점프 횟수 (지상 + 공중 점프 포함)
    [SerializeField] private float _groundCheckDistance = 0.1f; // GroundCheck 콜라이더 하향 캐스트 거리
    [SerializeField] private float _groundCheckRayInset = 0.02f;     // 벽 옆면 접촉을 피하기 위한 좌우 안쪽 여백
    [SerializeField, Range(0f, 1f)] private float _groundNormalMinY = 0.65f; // 이 값 이상 위를 보는 면만 지면으로 인정
    [SerializeField] private LayerMask _groundLayer;                 // 지면/벽으로 취급할 레이어
    private bool _isGrounded;                                        // 현재 지면 접촉 여부
    private int _jumpsUsed;                                          // 이번 공중 체류 동안 사용한 점프 수

    private float _inputLockTimer;                                   // 이동 입력 잠금 타이머
    private float _facingLockTimer;                                  // 페이싱 잠금 타이머
    private ActionData _movementLockAction;                          // 애니메이션 기반 잠금 시 참조 액션
    private ActionData _facingLockAction;

    // ─── 그라운드 파운드 (공중에서 Grab 키로 발동하는 다이브 슬램) ────────
    [Header("Ground Pound")]
    [SerializeField] private ActionData _groundPoundData;            // 데미지/판정 데이터
    [SerializeField] private string _groundPoundFallAnimationState = "GroundSlamLoop";  // 낙하 중 루프 애니
    [SerializeField] private string _groundPoundImpactAnimationState = "GroundSlamEnd"; // 착지 임팩트 애니
    [SerializeField] private float _groundPoundWindupDuration = 0.15f;                  // 공중 정지(윈드업) 시간
    [SerializeField] private float _groundPoundFallSpeed = 25f;                         // 슬램 낙하 속도 (gravity 우회)
    private bool _isGroundPounding;                                  // 그라운드 파운드 중일 때 ApplyGravity 우회 플래그

    // ─── 모션 액션 (콤보 트리와 별개의 즉발 액션) ─────────────────────────
    [Header("Motion")]
    [SerializeField] private ComboNode _dashRootNode;                // Dash 진입 노드
    [SerializeField] private ComboNode _rollRootNode;                // Roll
    [SerializeField] private ComboNode _backDashRootNode;            // 후방 대시
    [SerializeField] private ComboNode _grabSmashRootNode;           // 잡기 콤보 진입
    private readonly Dictionary<ActionData, float> _motionCooldownTimers = new();  // 액션별 쿨다운 남은 시간
    private readonly List<ActionData> _motionCooldownKeys = new();                 // 매 프레임 순회용 임시 키 리스트

    // ─── 키네마틱 물리 (Kinematic Rigidbody2D를 위한 자체 중력/충돌) ──────
    [Header("Kinematic Physics")]
    [SerializeField] private float _gravity = -25f;                  // 중력 가속도 (units/sec^2, 음수)
    [SerializeField] private float _maxFallSpeed = 20f;              // 낙하 최대 속도 클램프
    [SerializeField] private Collider2D _groundCheckCollider;             // 지면 판정 전용 콜라이더 (trigger여도 됨)

    [FormerlySerializedAs("_wallCheckCollider")]
    [SerializeField] private Collider2D _bodyCollider;
    [FormerlySerializedAs("_wallCheckDistance")]
    [SerializeField] private float _collisionSkinWidth = 0.02f;

    // ─── 공격 (펀치/킥/잡기 콤보 시스템) ──────────────────────────────────
    [Header("Attack")]
    [SerializeField] private ComboNode _punchRootNode;               // Punch 입력 시 시작 노드
    [SerializeField] private ComboNode _kickRootNode;                // Kick 입력 시 시작 노드
    [SerializeField] private LayerMask _enemyLayer;                  // 적 레이어 (공격 판정 대상)
    [SerializeField] private AttackHitbox _attackHitbox;             // 자식 Trigger 콜라이더 (활성/비활성으로 hit window 표현)
    [SerializeField] private string _idleAnimationState = "Idle";    // 기본 idle State
    [SerializeField] private float _bufferOpenTime = 0.1f;           // 공격 시작 후 이 시간 지나야 다음 입력 버퍼링
    [SerializeField] private float _bufferLifetime = 0.5f;           // 버퍼된 입력의 유효 시간
    private ComboInputType? _pendingInput;                           // 쿨다운 중에 미리 받은 다음 입력
    private float _pendingInputTime = -1f;                           // 버퍼 입력이 기록된 시각 (lifetime 체크용)
    private float _attackCooldownTimer;                              // 공격 잠금 타이머 (모든 공격이 공유)
    private ComboNode _currentNode;                                  // 현재 콤보 트리 위치
    private float _comboWindowTimer;                                 // 콤보 다음 입력 허용 시간
    private CancellationTokenSource _attackCts;                      // 공격 코루틴(async) 취소 토큰
    private CancellationTokenSource _motionCts;                      // 모션 코루틴 취소 토큰
    private CancellationTokenSource _animationSpeedCts;              // 애니메이션 속도 커브 토큰
    private CancellationTokenSource _actionVelocityCts;              // 액션 속도 커브 토큰
    private bool _isAttackActive;                                    // 공격 진행 중 (이동/페이싱 보조 잠금용)
    private bool _isMotionActive;                                    // 모션 진행 중
    private float _actionDirection = 1f;                             // 액션 시작 시 캐릭터 페이싱 (속도 X 부호)
    private ActionData _lastAttackGizmoData;                         // OnGUI 디버그 패널용 마지막 액션 참조
    [SerializeField] private float _hitGizmoFadeDuration = 0.5f;     // hit 영역 Gizmo 페이드 시간
    private ActionData _lastHitData;                                 // 마지막으로 발화된 hit 액션 (gizmo)
    private Vector2 _lastHitCenter;                                  // hit 발화 위치 (gizmo)
    private float _lastHitTime = -1f;                                // hit 발화 시각 (gizmo)
    private Enemy _lastHitEnemy;                                     // 가장 최근 맞은 적 참조 (잡기 타겟 찾기 최적화용)

    // ─── 디버그 표시 ─────────────────────────────────────────────────────
    [Header("Debug")]
    [SerializeField] private bool _showAttackDebug = true;           // OnGUI에 공격 정보 패널 표시 여부
    private float _attackStartTime = -1f;                            // 액션 시작 시각 (디버그 elapsed 계산)
    private bool _hitFired;                                          // 현재 액션에서 hit이 이미 발화됐는지

    private readonly List<RaycastHit2D> _castResults = new();        // Cast 결과 버퍼 (GC 회피용)
    private Rigidbody2D _rb;
    private Animator _anim;
    private SpriteRenderer _spriteRenderer;                          // 좌우 반전 + flipX 페이싱용
    private Health _health;

    // ─── 무기 시스템 ────────────────────────────────────────────────────
    // PlayerWeaponInventory가 현재 장착 무기 관리. 무기 교체 시 OnWeaponChanged 이벤트 발화.
    // 무장 시 idle/walk 애니메이션과 Punch 콤보 root가 무기 데이터로 교체됨.
    private PlayerWeaponInventory _weaponInventory;

    // 컴포넌트 캐싱 + 자식 AttackHitbox 자동 보장 + hit 이벤트 구독.
    private void Awake()
    {
        _rb = GetComponent<Rigidbody2D>();
        _anim = GetComponent<Animator>();
        _spriteRenderer = GetComponentInChildren<SpriteRenderer>();
        _health = GetComponent<Health>();
        ResolveBodyColliderReference();
        EnsureAttackHitbox();
        // 공격이 enemy를 hit하면 _lastHitEnemy를 기억 → 다음 잡기에서 그 enemy를 우선 타겟팅.
        _attackHitbox.OnHit += OnAttackHit;

        // 무기 인벤토리: 같은 GameObject에 컴포넌트가 없으면 자동 부착.
        _weaponInventory = GetComponent<PlayerWeaponInventory>();
        if (_weaponInventory == null)
            _weaponInventory = gameObject.AddComponent<PlayerWeaponInventory>();
        _weaponInventory.OnWeaponChanged += OnWeaponChanged;
    }

    private void ResolveBodyColliderReference()
    {
        if (_bodyCollider != null && _bodyCollider.enabled) return;

        Collider2D[] colliders = GetComponentsInChildren<Collider2D>(true);
        for (int i = 0; i < colliders.Length; i++)
        {
            Collider2D candidate = colliders[i];
            if (candidate == null || !candidate.enabled || candidate.isTrigger) continue;
            if (candidate == _groundCheckCollider) continue;
            if (candidate.GetComponent<AttackHitbox>() != null) continue;

            _bodyCollider = candidate;
            return;
        }
    }

    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;
        InputManager.Instance.OnGrabSmash_Event += OnGrabSmashInput;
        InputManager.Instance.OnWeaponSlot1_Event += OnWeaponSlot1Input;
        InputManager.Instance.OnWeaponSlot2_Event += OnWeaponSlot2Input;
        InputManager.Instance.OnWeaponSlot3_Event += OnWeaponSlot3Input;
    }

    // 이벤트 구독 해제 + 진행 중인 async 작업 모두 취소.
    // 토큰을 명시적으로 dispose하지 않으면 await 중 GC 누수 위험.
    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;
            InputManager.Instance.OnGrabSmash_Event -= OnGrabSmashInput;
            InputManager.Instance.OnWeaponSlot1_Event -= OnWeaponSlot1Input;
            InputManager.Instance.OnWeaponSlot2_Event -= OnWeaponSlot2Input;
            InputManager.Instance.OnWeaponSlot3_Event -= OnWeaponSlot3Input;
        }

        CancelAndDispose(ref _attackCts);
        CancelAndDispose(ref _motionCts);
        CancelAndDispose(ref _animationSpeedCts);
        CancelAndDispose(ref _actionVelocityCts);

        if (_attackHitbox != null)
            _attackHitbox.OnHit -= OnAttackHit;
        if (_weaponInventory != null)
            _weaponInventory.OnWeaponChanged -= OnWeaponChanged;
    }

    // ─── 무기 슬롯 입력 핸들러 ───────────────────────────────────────────
    // Key 1 → 맨손, Key 2 → 첫 픽업 무기, Key 3 → 두 번째 픽업 무기.
    private void OnWeaponSlot1Input() => _weaponInventory.EquipUnarmed();
    private void OnWeaponSlot2Input() => _weaponInventory.EquipSlot(0);
    private void OnWeaponSlot3Input() => _weaponInventory.EquipSlot(1);

    // 무기 교체 이벤트 콜백.
    // 콤보 상태를 리셋하고 locomotion 애니메이션을 즉시 갱신.
    private void OnWeaponChanged(WeaponData weapon)
    {
        _currentNode = null;
        _comboWindowTimer = 0f;
        _pendingInput = null;
        _activeBaseState = null;  // 강제 재평가
        PlayLocomotionState();
    }

    // 매 물리 프레임의 메인 흐름:
    //   1) 충돌 상태 갱신 (지면)
    //   2) 점프 카운트 리셋 (지면 + 낙하 중일 때만)
    //   3) 쿨다운/콤보 윈도우/버퍼 처리
    //   4) 좌우 이동 입력을 velocity로 반영 (잠금/액션 중일 땐 스킵)
    //   5) 페이싱 갱신
    //   6) 자체 중력 적용
    //   7) Locomotion 애니메이션 갱신 (Idle/Walk/Jump/Land)
    private void FixedUpdate()
    {
        _isGrounded = CheckGrounded();

        // 지면에 닿고 점프 중이 아니면(상승 중 아님) 점프 카운트 리셋.
        if (_isGrounded && _rb.linearVelocity.y <= 0f)
            _jumpsUsed = 0;

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

        UpdateMotionCooldowns();
        ExecuteBufferedInputIfReady();  // 쿨다운 끝나면 저장된 입력 실행
        TickComboWindow();              // 콤보 윈도우 카운트다운

        // 입력 잠금 + 액션 중이 아닐 때만 좌우 입력으로 velocity 갱신.
        if (!IsMovementLocked() && !IsActionActive())
            _rb.linearVelocity = new Vector2(_moveInputX * _moveSpeed, _rb.linearVelocity.y);

        if (!IsFacingLocked() && !IsActionActive())
            UpdateFacingFromMoveInput();

        //중력적용
        ApplyGravity();
        ResolveKinematicCollisions();

        UpdateLocomotionAnimation();
        
    }

    // 쿨다운 직전에 버퍼된 콤보 입력을, 쿨다운이 풀리는 순간 자동 발화.
    // 입력 후 _bufferLifetime이 지났으면 폐기 (오래된 입력은 의도가 아닐 가능성 큼).
    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);
    }

    // 콤보 윈도우 카운트다운. 0에 도달하면 현재 노드를 리셋 → 다음 입력은 root부터 새로 시작.
    private void TickComboWindow()
    {
        if (_comboWindowTimer <= 0f) return;

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

    // InputManager의 OnMove_Event 콜백.
    // 키보드라 -1/0/1로 정규화 (디지털 입력). 아날로그 패드면 Sign 빼고 그대로 사용.
    private void OnMoveInput(Vector2 value)
    {
        _moveInputX = value.x == 0f ? 0f : Mathf.Sign(value.x);
        if (_facingLockTimer <= 0f && !IsActionActive())
            UpdateFacingFromMoveInput();
    }

    // SpriteRenderer.flipX로 좌우 반전 (transform.localScale이 아닌 이유: 자식 콜라이더 위치가 따라 뒤집히면 곤란).
    private void UpdateFacingFromMoveInput()
    {
        if (_moveInputX != 0f && _spriteRenderer != null)
            _spriteRenderer.flipX = _moveInputX < 0f;
    }

    // 점프 우선순위: 지상 점프 > 공중(2단) 점프
    private void OnJumpInput()
    {
        if (_isGrounded)
        {
            PerformJump();
        }
        else if (_jumpsUsed < _maxJumpCount)
        {
            // 공중 점프 (2단/3단). _maxJumpCount=2면 지상점프 + 공중 1회 가능.
            PerformJump();
        }
    }

    private void PerformJump()
    {
        _rb.linearVelocity = new Vector2(_rb.linearVelocity.x, _jumpForce);
        _jumpsUsed++;
    }

    // ─── 입력 핸들러: 콤보(공격) 라우팅 ──────────────────────────────────
    // Punch/Kick은 그대로 콤보 시스템으로.
    // Grab은 공중이면 그라운드 파운드, 지상이면 콤보 잡기로 분기.
    // Dash/Roll/BackDash는 콤보가 아닌 단발 모션.
    private void OnPunchInput() => HandleComboInput(ComboInputType.Punch);
    private void OnKickInput() => HandleComboInput(ComboInputType.Kick);
    private void OnGrabSmashInput()
    {
        if (!_isGrounded && _groundPoundData != null)
        {
            PerformGroundPound();
            return;
        }
        HandleComboInput(ComboInputType.Grab);
    }
    private void OnDashInput() => ExecuteMotionNode(_dashRootNode);
    private void OnRollInput() => ExecuteMotionNode(_rollRootNode);
    private void OnBackDashInput() => ExecuteMotionNode(_backDashRootNode);

    // 콤보 입력 라우팅 — 3가지 경우를 처리:
    //   1) 모션 중이면 트랜지션 가능 여부 확인 (예: 대시 중 펀치)
    //   2) 쿨다운 중이면 _bufferOpenTime 이후 들어온 입력만 버퍼링
    //   3) 그 외에는 즉시 ExecuteComboInput으로 발화
    private void HandleComboInput(ComboInputType input)
    {
        if (_isMotionActive && !CanTransitionFromCurrentNode(input))
            return;

        if (_attackCooldownTimer > 0f)
        {
            float elapsed = Time.time - _attackStartTime;
            // _bufferOpenTime 전엔 너무 일찍 누른 입력으로 간주하고 무시.
            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;
                if (!CanPerformAction(transition.Next.Action)) continue;

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

        // 이어갈 연계가 없으면 입력에 맞는 루트 노드부터 새로 시작한다.
        // Punch 입력은 무기 장착 시 무기의 AttackRootNode로 분기 (맨손이면 기본 _punchRootNode).
        ComboNode root = input switch
        {
            ComboInputType.Punch => GetPunchRootNode(),
            ComboInputType.Kick => _kickRootNode,
            ComboInputType.Grab => _grabSmashRootNode,
            _ => null
        };
        if (root == null || root.Action == null) return;
        if (!CanPerformAction(root.Action)) return;

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

    // 무장 시 무기 데이터의 AttackRootNode 사용 (비어있으면 기본 _punchRootNode 폴백).
    private ComboNode GetPunchRootNode()
    {
        WeaponData equipped = _weaponInventory != null ? _weaponInventory.CurrentWeapon : null;
        if (equipped != null && equipped.AttackRootNode != null)
            return equipped.AttackRootNode;
        return _punchRootNode;
    }

    private bool CanPerformAction(ActionData data)
    {
        if (data == null) return false;
        // 잡기 액션은 사정 거리 안에 적이 있어야만 실행. 없으면 입력 자체를 캔슬.
        if (data.IsGrab && FindGrabTarget(data) == null) return false;
        return true;
    }

    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)
    {
        CancelMotion();
        CancelAndDispose(ref _attackCts);
        _attackCts = CancellationTokenSource.CreateLinkedTokenSource(destroyCancellationToken);
        CancellationTokenSource currentAttackCts = _attackCts;
        CancellationToken token = _attackCts.Token;

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

        ClearActionLocks();
        CaptureActionDirection(data);
        PlayActionAnimation(data);

        PlayActionVelocity(data);

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

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

        if (_attackCts == currentAttackCts)
            _isAttackActive = false;
    }

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

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

        SetMotionCooldown(data);
        ClearActionLocks();
        CaptureActionDirection(data);
        _isMotionActive = true;
        FaceMotionDirection(data);
        PlayActionAnimation(data);
        PlayActionVelocity(data);
        LockMovementIfNeeded(data, false, true);
        LockFacingIfNeeded(data);

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

        if (completed)
        {
            StopActionVelocity(data);
            _isMotionActive = false;
            ClearActionLocks();
            PlayIdleAnimation();
        }
    }

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

        while (ShouldKeepActionPlaying(data, elapsed, duration))
        {
            token.ThrowIfCancellationRequested();
            await Awaitable.NextFrameAsync(token);
            elapsed += Time.deltaTime;
        }
    }

    private async void PerformGroundPound()
    {
        if (_groundPoundData == null) return;

        CancelAttack();
        CancelAndDispose(ref _motionCts);
        _motionCts = CancellationTokenSource.CreateLinkedTokenSource(destroyCancellationToken);
        CancellationToken token = _motionCts.Token;

        _isMotionActive = true;
        _isGroundPounding = true;
        _attackStartTime = Time.time;
        _lastAttackGizmoData = _groundPoundData;
        _hitFired = false;

        ClearActionLocks();
        CaptureActionDirection(_groundPoundData);
        FaceMotionDirection(_groundPoundData);
        PlayActionAnimation(_groundPoundData);
        LockMovementIfNeeded(_groundPoundData);
        LockFacingIfNeeded(_groundPoundData);

        float fallSpeed = _groundPoundFallSpeed > 0f
            ? _groundPoundFallSpeed
            : Mathf.Max(Mathf.Abs(_groundPoundData.Velocity.y), 25f);

        try
        {
            // 1) 윈드업: 짧게 공중 정지 후 슬램. ApplyGravity는 _isGroundPounding 플래그로 우회.
            float windupElapsed = 0f;
            while (windupElapsed < _groundPoundWindupDuration && !_isGrounded)
            {
                token.ThrowIfCancellationRequested();
                _rb.linearVelocity = Vector2.zero;
                windupElapsed += Time.deltaTime;
                await Awaitable.NextFrameAsync(token);
            }

            // 2) 낙하 애니메이션으로 전환 (루프). 윈드업이 끝났을 때만 실행.
            if (_anim != null && !string.IsNullOrEmpty(_groundPoundFallAnimationState) && !_isGrounded)
            {
                CancelAndDispose(ref _animationSpeedCts);
                _anim.speed = 1f;
                _anim.Play(_groundPoundFallAnimationState);
                _anim.Update(0f);
            }

            // 3) 지면 닿을 때까지 강제 낙하.
            while (!_isGrounded)
            {
                token.ThrowIfCancellationRequested();
                _rb.linearVelocity = new Vector2(0f, -fallSpeed);
                await Awaitable.NextFrameAsync(token);
            }

            // 4) 착지 순간 임팩트 애니메이션으로 전환 (낙하 거리 불일치 해소)
            if (_anim != null && !string.IsNullOrEmpty(_groundPoundImpactAnimationState))
            {
                CancelAndDispose(ref _animationSpeedCts);
                _anim.speed = 1f;
                _anim.Play(_groundPoundImpactAnimationState);
                _anim.Update(0f);
            }

            // 착지 임팩트
            if (_groundPoundData.HasHit && _attackHitbox != null)
            {
                ActivateAttackHitbox(_groundPoundData);

                float hitDuration = Mathf.Max(_groundPoundData.HitDuration, 0.05f);
                await Awaitable.WaitForSecondsAsync(hitDuration, token);

                _attackHitbox.Deactivate();
            }

            // 후딜
            float recovery = Mathf.Max(_groundPoundData.MotionDuration, 0.15f);
            await Awaitable.WaitForSecondsAsync(recovery, token);

            PlayIdleAnimation();
        }
        catch (System.OperationCanceledException)
        {
            _attackHitbox?.Deactivate();
        }
        finally
        {
            _isGroundPounding = false;
            _isMotionActive = false;
            ClearActionLocks();
            // 그라운드 파운드 직후 Land 애니메이션 중복 재생 방지
            _wasGroundedLastFrame = _isGrounded;
        }
    }

    private void CancelAttack()
    {
        _isAttackActive = false;
        CancelAndDispose(ref _attackCts);
        CancelAndDispose(ref _actionVelocityCts);
        _attackCooldownTimer = 0f;
        _pendingInput = null;
        _attackHitbox?.Deactivate();
        ClearActionLocks();
    }

    private void CancelMotion()
    {
        _isMotionActive = false;
        CancelAndDispose(ref _motionCts);
        CancelAndDispose(ref _actionVelocityCts);
    }

    private bool IsActionActive()
    {
        return _isAttackActive || _isMotionActive || _isGroundPounding;
    }

    private bool CanTransitionFromCurrentNode(ComboInputType input)
    {
        if (_comboWindowTimer <= 0f || _currentNode == null) return false;

        foreach (var transition in _currentNode.Transitions)
        {
            if (transition.Trigger == input && transition.Next != null && transition.Next.Action != null)
                return true;
        }

        return false;
    }

    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 async Awaitable GrabRoutine(ActionData data, CancellationToken token)
    {
        Enemy target = FindGrabTarget(data);
        if (target == null)
        {
            await WaitForActionEnd(data, token);
            ClearActionLocks();
            PlayIdleAnimation();
            return;
        }

        target.BeginGrab(data.GrabbedAnimationState, _groundLayer.value);

        try
        {
            float elapsed = 0f;
            float duration = GetActionDuration(data);
            while (ShouldKeepActionPlaying(data, elapsed, duration))
            {
                token.ThrowIfCancellationRequested();
                float normalizedTime = GetActionNormalizedTime(data, elapsed, duration);
                target.UpdateGrabPosition(GetGrabWorldPosition(data, normalizedTime));

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

            target.EndGrab();
            target.TakeDamage(data.Damage, GetHitVelocity(data.HitVelocity), data.HitReactionAnimationState);
        }
        catch (System.OperationCanceledException)
        {
            target.EndGrab();
            throw;
        }

        ClearActionLocks();
        PlayIdleAnimation();
    }

    private Enemy FindGrabTarget(ActionData data)
    {
        Vector2 playerPosition = _rb != null ? _rb.position : (Vector2)transform.position;
        float grabRangeSqr = data.GrabRange * data.GrabRange;

        if (_lastHitEnemy != null && _lastHitEnemy.isActiveAndEnabled &&
            GetEnemySqrDistance(_lastHitEnemy, playerPosition) <= grabRangeSqr)
        {
            return _lastHitEnemy;
        }

        Collider2D[] hits = Physics2D.OverlapCircleAll(transform.position, data.GrabSearchRadius, _enemyLayer);
        Enemy closest = null;
        float closestSqrDistance = float.PositiveInfinity;

        foreach (var hit in hits)
        {
            Enemy enemy = hit.GetComponentInParent<Enemy>();
            if (enemy == null) continue;

            float sqrDistance = GetEnemySqrDistance(enemy, playerPosition);
            if (sqrDistance > grabRangeSqr) continue;
            if (sqrDistance >= closestSqrDistance) continue;

            closest = enemy;
            closestSqrDistance = sqrDistance;
        }

        return closest;
    }

    private float GetEnemySqrDistance(Enemy enemy, Vector2 playerPosition)
    {
        if (enemy == null) return float.PositiveInfinity;
        return ((Vector2)enemy.transform.position - playerPosition).sqrMagnitude;
    }

    private Vector2 GetGrabWorldPosition(ActionData data, float normalizedTime)
    {
        float facing = _spriteRenderer != null && _spriteRenderer.flipX ? -1f : 1f;
        Vector2 playerPosition = _rb != null ? _rb.position : (Vector2)transform.position;
        Vector2 offset = data.GrabOffset;
        float offsetXMultiplier = data.GrabOffsetXCurve != null ? data.GrabOffsetXCurve.Evaluate(normalizedTime) : 1f;
        float offsetYMultiplier = data.GrabOffsetYCurve != null ? data.GrabOffsetYCurve.Evaluate(normalizedTime) : 1f;
        offset.x *= facing * offsetXMultiplier;
        offset.y *= offsetYMultiplier;
        return playerPosition + offset;
    }

    private void OnAttackHit(IDamageable target)
    {
        if (target is Enemy enemy)
            _lastHitEnemy = enemy;
    }

    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);

        SpawnHitEffect(data);
    }

    // hit 발동 시점에 이펙트 프리팹 생성.
    // 위치는 HitEffectOffset으로 별도 지정 (hit 영역 Offset과 독립).
    // 페이싱에 맞춰 좌우 반전하고, 설정된 수명만큼만 유지.
    private void SpawnHitEffect(ActionData data)
    {
        if (data.HitEffectPrefab == null) return;

        Vector2 localPosition = GetAttackLocalPosition(data.HitEffectOffset);
        Vector3 worldPosition = transform.TransformPoint(localPosition);
        GameObject effect = Instantiate(data.HitEffectPrefab, worldPosition, Quaternion.identity);

        // 캐릭터가 왼쪽을 보면 이펙트도 X 반전.
        if (_spriteRenderer != null && _spriteRenderer.flipX)
        {
            Vector3 scale = effect.transform.localScale;
            scale.x *= -1f;
            effect.transform.localScale = scale;
        }

        // 플레이어 자식으로 붙이면 캐릭터 따라 움직임 (검광처럼 캐릭터에 붙는 이펙트).
        if (data.HitEffectAttachToPlayer)
            effect.transform.SetParent(transform);

        // 0보다 크면 자동 파괴. 0이면 프리팹이 스스로 정리한다고 가정 (파티클 Stop Action 등).
        if (data.HitEffectLifetime > 0f)
            Destroy(effect, data.HitEffectLifetime);
    }

    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.Velocity == Vector2.zero)
            _rb.linearVelocity = new Vector2(0f, _rb.linearVelocity.y);

        if (forceLock)
        {
            // 액션 길이 전체를 보장: 애니메이션 길이와 무관하게 MotionDuration만큼 잠금.
            _inputLockTimer = Mathf.Max(data.MotionDuration, 0.02f);
            _movementLockAction = null;
        }
        else
        {
            _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;

        CancelAndDispose(ref _animationSpeedCts);
        CancelAndDispose(ref _actionVelocityCts);
        _anim.speed = 1f;
        _isInActionAnimation = false;
        _activeBaseState = null;
        PlayLocomotionState();
    }

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

        string desired = ChooseLocomotionState();
        if (string.IsNullOrEmpty(desired)) return;
        if (desired == _activeBaseState) return;

        _anim.Play(desired);
        _activeBaseState = desired;
    }

    private string ChooseLocomotionState()
    {
        if (_landTimer > 0f && !string.IsNullOrEmpty(_landAnimationState))
            return _landAnimationState;

        float vy = _rb.linearVelocity.y;
        bool inAir = !_isGrounded || vy > _jumpMidThreshold;
        if (inAir)
        {
            if (vy > _jumpMidThreshold && !string.IsNullOrEmpty(_jumpRiseAnimationState))
                return _jumpRiseAnimationState;
            if (vy < -_jumpMidThreshold && !string.IsNullOrEmpty(_jumpFallAnimationState))
                return _jumpFallAnimationState;
            if (!string.IsNullOrEmpty(_jumpMidAnimationState))
                return _jumpMidAnimationState;
        }

        // 무기 장착 시 무기 데이터의 idle/walk를 우선 사용 (비어있으면 기본값으로 폴백).
        WeaponData equipped = _weaponInventory != null ? _weaponInventory.CurrentWeapon : null;
        string walk = equipped != null && !string.IsNullOrEmpty(equipped.WalkAnimationState)
            ? equipped.WalkAnimationState
            : _walkAnimationState;
        string idle = equipped != null && !string.IsNullOrEmpty(equipped.IdleAnimationState)
            ? equipped.IdleAnimationState
            : _idleAnimationState;

        if (_moveInputX != 0f && !string.IsNullOrEmpty(walk))
            return walk;

        return idle;
    }

    private void UpdateLocomotionAnimation()
    {
        if (_isInActionAnimation) return;

        if (_isGrounded && !_wasGroundedLastFrame && !string.IsNullOrEmpty(_landAnimationState))
            _landTimer = _landAnimationDuration;
        _wasGroundedLastFrame = _isGrounded;

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

        PlayLocomotionState();
    }

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

        CancelAndDispose(ref _animationSpeedCts);
        _animationSpeedCts = CancellationTokenSource.CreateLinkedTokenSource(destroyCancellationToken);

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

        ApplyAnimationSpeedCurve(data, _animationSpeedCts.Token);
    }

    private void PlayActionVelocity(ActionData data)
    {
        CancelAndDispose(ref _actionVelocityCts);

        if (data.Velocity == Vector2.zero) return;

        _actionVelocityCts = CancellationTokenSource.CreateLinkedTokenSource(destroyCancellationToken);
        ApplyActionVelocityCurve(data, _actionVelocityCts.Token);
    }

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

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

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

    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)
    {
        return _actionDirection;
    }

    private void FaceMotionDirection(ActionData data)
    {
        if (_spriteRenderer == null) return;

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

    private void CaptureActionDirection(ActionData data)
    {
        if (data != null && data.UseInputDirection && _moveInputX != 0f)
            _actionDirection = _moveInputX;
        else
            _actionDirection = _spriteRenderer != null && _spriteRenderer.flipX ? -1f : 1f;
    }

    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 void CancelAndDispose(ref CancellationTokenSource cts)
    {
        if (cts == null) return;

        try
        {
            cts.Cancel();
        }
        catch (System.ObjectDisposedException) { }

        cts.Dispose();
        cts = 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 (!ShouldUseAnimationLength(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()
    {
        // 그라운드 파운드는 _maxFallSpeed에 막히지 않도록 자체 속도를 그대로 유지한다.
        if (_isGroundPounding) return;

        float vy = _rb.linearVelocity.y;

        if (_isGrounded && vy <= 0f)
        {
            float groundedFallSpeed = Mathf.Max(_gravity * Time.fixedDeltaTime, -_maxFallSpeed);
            _rb.linearVelocity = new Vector2(_rb.linearVelocity.x, groundedFallSpeed);
            return;
        }

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

    private bool CheckGrounded()
    {
        return TryGetGroundHit(Mathf.Max(_groundCheckDistance, 0f), out _);
    }

    private void ResolveKinematicCollisions()
    {
        int solidMask = _groundLayer.value;
        if (solidMask == 0) return;
        if (_bodyCollider == null || !_bodyCollider.enabled) return;

        float dt = Time.fixedDeltaTime;
        if (dt <= 0f) return;

        Vector2 velocity = _rb.linearVelocity;

        velocity.x = ResolveAxisVelocity(velocity.x, Vector2.right * Mathf.Sign(velocity.x), solidMask, dt, out _);

        float originalY = velocity.y;
        velocity.y = ResolveAxisVelocity(velocity.y, Vector2.up * Mathf.Sign(velocity.y), solidMask, dt, out bool blockedY);

        if (blockedY && originalY <= 0f)
        {
            _isGrounded = true;
            _jumpsUsed = 0;
        }

        _rb.linearVelocity = velocity;
    }

    private float ResolveAxisVelocity(float axisVelocity, Vector2 direction, int solidMask, float dt, out bool blocked)
    {
        blocked = false;

        if (Mathf.Abs(axisVelocity) <= 0.001f) return axisVelocity;

        float skinWidth = Mathf.Max(_collisionSkinWidth, 0f);
        float moveDistance = Mathf.Abs(axisVelocity) * dt;
        float castDistance = moveDistance + skinWidth;

        if (!TryGetBodyHit(direction, castDistance, solidMask, out RaycastHit2D hit))
            return axisVelocity;

        blocked = true;
        float allowedDistance = Mathf.Max(hit.distance - skinWidth, 0f);
        return Mathf.Sign(axisVelocity) * (allowedDistance / dt);
    }

    private bool TryGetBodyHit(Vector2 direction, float castDistance, int solidMask, out RaycastHit2D closestHit)
    {
        closestHit = default;

        ContactFilter2D filter = CreateSolidContactFilter(solidMask);
        float closestDistance = float.PositiveInfinity;
        bool hasHit = false;

        _castResults.Clear();
        int hitCount = _bodyCollider.Cast(direction, filter, _castResults, castDistance);
        for (int i = 0; i < hitCount; i++)
        {
            RaycastHit2D hit = _castResults[i];
            if (!IsBodyBlockingHit(direction, hit)) continue;
            if (hit.distance >= closestDistance) continue;

            closestDistance = hit.distance;
            closestHit = hit;
            hasHit = true;
        }

        return hasHit;
    }

    private bool IsBodyBlockingHit(Vector2 direction, RaycastHit2D hit)
    {
        if (Mathf.Abs(direction.x) > 0f)
        {
            return direction.x > 0f
                ? hit.normal.x < -0.1f
                : hit.normal.x > 0.1f;
        }

        if (direction.y > 0f)
            return hit.normal.y < -0.1f;

        return hit.normal.y >= _groundNormalMinY;
    }

    private bool TryGetGroundHit(float checkDistance, out RaycastHit2D closestHit)
    {
        closestHit = default;

        int solidMask = _groundLayer.value;
        if (solidMask == 0) return false;
        if (_groundCheckCollider == null || !_groundCheckCollider.enabled) return false;

        ContactFilter2D filter = CreateSolidContactFilter(solidMask);
        Bounds bounds = _groundCheckCollider.bounds;
        int rayCount = 3;
        float inset = Mathf.Min(Mathf.Max(_groundCheckRayInset, 0f), bounds.extents.x);
        float leftX = bounds.min.x + inset;
        float rightX = bounds.max.x - inset;
        float originY = bounds.min.y;
        float closestDistance = float.PositiveInfinity;
        bool hasHit = false;

        for (int i = 0; i < rayCount; i++)
        {
            float t = rayCount == 1 ? 0.5f : i / (float)(rayCount - 1);
            Vector2 origin = new Vector2(Mathf.Lerp(leftX, rightX, t), originY);

            _castResults.Clear();
            int hitCount = Physics2D.Raycast(origin, Vector2.down, filter, _castResults, checkDistance);
            for (int j = 0; j < hitCount; j++)
            {
                RaycastHit2D hit = _castResults[j];
                if (hit.normal.y < _groundNormalMinY) continue;
                if (hit.distance >= closestDistance) continue;

                closestDistance = hit.distance;
                closestHit = hit;
                hasHit = true;
            }
        }

        return hasHit;
    }

    private static ContactFilter2D CreateSolidContactFilter(int layerMask)
    {
        return new ContactFilter2D
        {
            useLayerMask = true,
            layerMask = layerMask,
            useTriggers = false
        };
    }

    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);

        Color fillColor = new Color(1f, 0.2f, 0.2f, alpha * 0.35f);
        Color wireColor = new Color(1f, 0f, 0f, alpha);

        if (_lastHitData.Shape == HitShape.Box)
        {
            Vector3 size = new Vector3(_lastHitData.HitSize.x, _lastHitData.HitSize.y, 0.01f);
            Gizmos.color = fillColor;
            Gizmos.DrawCube(_lastHitCenter, size);
            Gizmos.color = wireColor;
            Gizmos.DrawWireCube(_lastHitCenter, size);
        }
        else
        {
            float radius = _lastHitData.Radius;
            Gizmos.color = fillColor;
            Gizmos.DrawSphere(_lastHitCenter, radius);
            Gizmos.color = wireColor;
            Gizmos.DrawWireSphere(_lastHitCenter, radius);
        }
    }

    private void OnDrawGizmosSelected()
    {
        if (_groundCheckCollider == null || !_groundCheckCollider.enabled) return;

        Gizmos.color = _isGrounded ? Color.green : Color.red;
        Bounds bounds = _groundCheckCollider.bounds;
        int rayCount = Mathf.Max(3, 1);
        float inset = Mathf.Min(Mathf.Max(_groundCheckRayInset, 0f), bounds.extents.x);
        float leftX = bounds.min.x + inset;
        float rightX = bounds.max.x - inset;
        float bottomY = bounds.min.y;
        float checkY = bottomY - Mathf.Max(_groundCheckDistance, 0f);

        for (int i = 0; i < rayCount; i++)
        {
            float t = rayCount == 1 ? 0.5f : i / (float)(rayCount - 1);
            float x = Mathf.Lerp(leftX, rightX, t);
            Gizmos.DrawLine(new Vector3(x, bottomY, 0f), new Vector3(x, checkY, 0f));
        }
    }

    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;
    }

    public void TakeDamage(int amount, Vector2 hitVelocity = default, string hitReactionAnimationState = null, Vector2? hitTargetPosition = null, bool correctHitTargetY = false, int hitPositionSolidMask = 0, float hitPositionCorrectionDuration = 0)
    {
        if (_health == null || _health.IsDead) return;

        _health.TakeDamage(amount);
        Debug.Log($"{name} 피격: -{amount} (HP: {_health.CurrentHealth}/{_health.MaxHealth})");
    }
}
