using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.InputSystem;

public enum Result { Perfect, Good, Bad, Miss }
public class RhythmManager : MonoBehaviour
{
    [SerializeField] private AudioSource _audioSource;
    [SerializeField] private RhythmChart _currentChart;
    [SerializeField] private RhythmNoteSpawner _spawner;
    [SerializeField] private float _leadTime = 1f;  // 노트가 생성돼 판정선까지 흐르는 시간(초)

    [Header("판정 윈도우 (초, 절대 시간차 기준)")]
    [SerializeField] private float _perfectWindow = 0.05f;
    [SerializeField] private float _goodWindow = 0.10f;
    [SerializeField] private float _badWindow = 0.15f;   // 이 밖이면 입력 무시(헛침)

    [Header("효과음")]
    [SerializeField] private AudioClip _hitSfx;   // 내려칠 때마다 재생 (판정과 무관, 헛침 포함)

    [SerializeField] private GameObject StartButtonObj;

    // 모든 타이밍의 기준. 오디오 클럭(dspTime) 기반이라 리드인 동안 음수(-leadTime→0)로 흐른다
    public float SongTime => (float)(AudioSettings.dspTime - _dspSongStart);

    private List<Note> SongNoteList;
    private int _nextNoteIndex;   // 다음에 소환할 노트 인덱스
    private bool _isPlaying;   //곡이 재생 중인지 (종료 감지용)
    private double _dspSongStart;  // 오디오가 실제 시작되는 dsp 시각 (= SongTime 0 지점)
    private float _clipLength;     // 곡 길이 캐시 (종료 감지용)
    private Func<float> _songTimeProvider;   // 노트에 넘길 시간 제공자 (매 스폰마다 람다 재생성 방지용 캐시)
    private readonly List<RhythmNoteInstance> _activeNotes = new();  // 화면에 떠 있는(아직 처리 안 된) 노트들

    // 점수 상태 (HUD가 읽음). 누적 로직은 RhythmScore가 전담
    public RhythmScore Score { get; } = new();

    public event Action OnSongStarted;               // 곡 시작 (ScoreHud 표시 / ResultHud 숨김)
    public event Action<Result> OnJudged;            // 노트 하나가 판정될 때마다 (HUD 연출용)
    public event Action<RhythmScore> OnScoreChanged; // 점수/콤보가 바뀔 때 (실시간 HUD)
    public event Action<RhythmScore> OnSongFinished; // 곡이 끝났을 때 (결과창)

    private void Awake()
    {
        _songTimeProvider = () => SongTime;   // 한 번만 만들어 모든 노트가 공유
    }

    private void Start()
    {
        ChangeSong();

        InputManager.Instance.OnKey_Left_Event += OnPlayerInput;
        InputManager.Instance.OnKey_Right_Event += OnPlayerInput;
    }

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

        //곡이 끝나면(리드인 지나 곡 길이 도달) 정지·주변음 복구
        if (SongTime >= _clipLength)
        {
            StopSong();
            return;
        }

        SpawnDueNotes();
    }

    // SongTime 기준으로 소환할 때가 된 노트들을 순서대로 생성
    private void SpawnDueNotes()
    {
        // 판정 시각보다 _leadTime 먼저 생성해야 박자에 맞춰 판정선에 도달
        while (_nextNoteIndex < SongNoteList.Count && //총 노트수보다 노트인덱스가 작고
               SongTime >= SongNoteList[_nextNoteIndex].Time - _leadTime) //노래길이가 다음 노트의 노트타임 - 리드타임 보다 작을때까지만
        {
            Note note = SongNoteList[_nextNoteIndex];
            // spawnTime은 실제 프레임 시각이 아니라 이론값(note.Time - _leadTime)으로 줘야 보간이 정확
            RhythmNoteInstance instance = _spawner.SpawnNote(note, note.Time - _leadTime, _songTimeProvider, OnNoteMissed);
            _activeNotes.Add(instance);
            _nextNoteIndex++;
        }
    }

    public void ChangeSong()
    {
        _audioSource.clip = _currentChart.SongClip;
    }

    // 곡 재생 + 채보 로드
    public void StartSong()
    {
        SongNoteList = _currentChart.GenerateNotes();
        _nextNoteIndex = 0;
        _activeNotes.Clear();

        Score.Reset();
        OnSongStarted?.Invoke();         // ScoreHud 표시 / ResultHud 숨김
        OnScoreChanged?.Invoke(Score);   // HUD 초기화(0점)
        StartButtonObj.SetActive(false);

        _clipLength = _audioSource.clip != null ? _audioSource.clip.length : 0f;

        // 리드인: 지금부터 _leadTime 뒤에 오디오 시작.
        // 그 사이 SongTime이 -_leadTime→0으로 흘러 첫 노트(0초 부근)도 충분히 날아온다
        _dspSongStart = AudioSettings.dspTime + _leadTime;
        _audioSource.PlayScheduled(_dspSongStart);

        _isPlaying = true;

        //BGM·환경음을 낮춰 리듬게임 소리만 들리게
        if (SoundManager.Instance != null) SoundManager.Instance.EnterMinigameMode();
    }

    // 곡 정지 + 주변음 복구
    public void StopSong()
    {
        if (!_isPlaying) return;   // 중복 호출 방지(결과창 두 번 뜨는 것 차단)

        _audioSource.Stop();
        _isPlaying = false;

        if (SoundManager.Instance != null) SoundManager.Instance.ExitMinigameMode();

        OnSongFinished?.Invoke(Score);   // 결과창 표시
    }

    // 노트가 판정선을 지나쳐 스스로 Miss 처리될 때 호출 (노트는 이미 자기 파괴됨)
    private void OnNoteMissed(RhythmNoteInstance note)
    {
        _activeNotes.Remove(note);
        ApplyJudge(Result.Miss);
        Debug.Log("[Rhythm] Miss (지나침)");
    }

    public void OnPlayerInput()
    {
        if (!_isPlaying) return;

        // 판정과 무관하게 내려칠 때마다 효과음 (헛침 포함)
        if (_hitSfx != null && SoundManager.Instance != null)
            SoundManager.Instance.PlaySFX(_hitSfx);

        if (_activeNotes.Count == 0) return;

        // 판정선에 가장 가까운(시간차 최소) 노트 탐색
        RhythmNoteInstance target = null;
        float bestDiff = float.MaxValue;
        foreach (RhythmNoteInstance note in _activeNotes)
        {
            float diff = Mathf.Abs(SongTime - note.HitTime);
            if (diff < bestDiff)
            {
                bestDiff = diff;
                target = note;
            }
        }

        Result result = Judge(bestDiff);
        if (result == Result.Miss) return;   // 히트 범위 밖이면 입력 무시 (노트 소비 안 함)

        // 성공 판정 → 히트 이펙트 + 노트 소비
        if (result == Result.Perfect || result == Result.Good)
        {
            // 이펙트는 노트에서 분리돼 따로 재생되므로 노트를 바로 파괴해도 됨
            if (target.TryGetComponent(out RhythmProjectile projectile))
                projectile.Detonate();
        }
        _activeNotes.Remove(target);
        Destroy(target.gameObject);

        ApplyJudge(result);
        Debug.Log($"[Rhythm] {result} (diff {bestDiff:F3}s)");
    }

    // 판정 결과를 점수에 반영하고 이벤트로 알림
    private void ApplyJudge(Result result)
    {
        Score.Apply(result);
        OnJudged?.Invoke(result);
        OnScoreChanged?.Invoke(Score);
    }

    // diff = 절대 시간차(초). 윈도우 안이면 Perfect/Good/Bad, 밖이면 Miss(입력 무시)
    public Result Judge(float diff)
    {
        if (diff <= _perfectWindow) return Result.Perfect;
        if (diff <= _goodWindow) return Result.Good;
        if (diff <= _badWindow) return Result.Bad;
        return Result.Miss;
    }
}
