"""Audio player core module for music playback functionality."""

from __future__ import annotations

import logging
import threading
from dataclasses import dataclass
from enum import Enum
from typing import TYPE_CHECKING, Callable

import numpy as np
import sounddevice as sd
import soundfile as sf

if TYPE_CHECKING:
    from pathlib import Path

logger = logging.getLogger(__name__)


class PlaybackState(Enum):
    """Enumeration of possible playback states."""

    STOPPED = "stopped"
    PLAYING = "playing"
    PAUSED = "paused"


@dataclass
class AudioTrack:
    """Data class representing an audio track."""

    file_path: Path
    title: str
    artist: str
    album: str
    duration: float
    file_size: int


class AudioPlayer:
    """Core audio player class handling music playback operations.

    This class provides thread-safe audio playback functionality using sounddevice.
    It supports play, pause, stop, volume control, and seeking.
    """

    def __init__(self) -> None:
        """Initialize audio player with default settings."""
        self._state: PlaybackState = PlaybackState.STOPPED
        self._current_track: AudioTrack | None = None
        self._volume: float = 0.7
        self._position: float = 0.0
        self._lock = threading.RLock()
        self._progress_callback: Callable[[float], None] | None = None
        self._play_thread: threading.Thread | None = None
        self._should_stop = threading.Event()
        self._audio_stream = None
        self._audio_data = None
        self._sample_rate = None
        self._current_frame = 0

    @property
    def state(self) -> PlaybackState:
        """Get current playback state."""
        with self._lock:
            return self._state

    @property
    def current_track(self) -> AudioTrack | None:
        """Get currently loaded track."""
        with self._lock:
            return self._current_track

    @property
    def volume(self) -> float:
        """Get current volume level (0.0 to 1.0)."""
        with self._lock:
            return self._volume

    @volume.setter
    def volume(self, value: float) -> None:
        """Set volume level with validation."""
        with self._lock:
            self._volume = max(0.0, min(1.0, value))
            # Volume is applied during playback

    @property
    def position(self) -> float:
        """Get current playback position in seconds."""
        with self._lock:
            return self._position

    def _load_audio_file(self, file_path: Path) -> bool:
        """Load audio file into memory for playback.

        Args:
            file_path: Path to audio file

        Returns
        -------
            True if loaded successfully, False otherwise
        """
        try:
            # Load audio file
            self._audio_data, self._sample_rate = sf.read(
                str(file_path),
                dtype="float32",
            )

            # Convert mono to stereo if needed
            if self._audio_data.ndim == 1:
                self._audio_data = np.column_stack([self._audio_data, self._audio_data])

            logger.info(f"Loaded audio file: {file_path.name} ({self._sample_rate}Hz)")
            return True

        except Exception as e:
            logger.exception(f"Failed to load audio file {file_path}: {e}")
            self._audio_data = None
            self._sample_rate = None
            return False

    def set_progress_callback(self, callback: Callable[[float], None]) -> None:
        """Set callback function for progress updates."""
        with self._lock:
            self._progress_callback = callback

    def load_track(self, track: AudioTrack) -> bool:
        """Load an audio track for playback.

        Args:
            track: AudioTrack object containing file information

        Returns
        -------
            True if track loaded successfully, False otherwise
        """
        with self._lock:
            try:
                if not track.file_path.exists():
                    logger.error(f"Audio file not found: {track.file_path}")
                    return False

                # Stop current playback
                self._stop_internal()

                # Load new track
                self._load_audio_file(track.file_path)
                self._current_track = track
                self._state = PlaybackState.STOPPED
                self._position = 0.0
                self._current_frame = 0

                logger.info(f"Loaded track: {track.title} by {track.artist}")
                return True

            except Exception as e:
                logger.exception(f"Failed to load audio file {track.file_path}: {e}")
                return False

    def play(self) -> bool:
        """Start playback of currently loaded track.

        Returns
        -------
            True if playback started successfully, False otherwise
        """
        with self._lock:
            if self._current_track is None:
                logger.warning("No track loaded for playback")
                return False

            try:
                self._should_stop.clear()
                self._play_thread = threading.Thread(
                    target=self._play_audio_stream,
                    daemon=True,
                )
                self._play_thread.start()
                self._state = PlaybackState.PLAYING
                logger.info(f"Started playing: {self._current_track.title}")
                return True

            except Exception as e:
                logger.exception(f"Failed to start playback: {e}")
                return False

    def pause(self) -> None:
        """Pause current playback."""
        with self._lock:
            if self._state == PlaybackState.PLAYING:
                self._should_stop.set()
                if self._play_thread:
                    self._play_thread.join(timeout=1.0)
                self._state = PlaybackState.PAUSED
                logger.info("Playback paused")

    def resume(self) -> bool:
        """Resume paused playback.

        Returns
        -------
            True if resumed successfully, False otherwise
        """
        with self._lock:
            if self._state == PlaybackState.PAUSED:
                try:
                    return self.play()
                except Exception as e:
                    logger.exception(f"Failed to resume playback: {e}")
                    return False
            return False

    def stop(self) -> None:
        """Stop playback and reset position."""
        with self._lock:
            self._stop_internal()
            logger.info("Playback stopped")

    def _play_audio_stream(self) -> None:
        """Play audio using sounddevice stream."""
        if self._audio_data is None or self._sample_rate is None:
            logger.error("No audio data loaded")
            return

        try:
            # Audio callback function
            def audio_callback(outdata, frames, time, status) -> None:
                if status:
                    logger.warning(f"Audio callback status: {status}")

                if self._should_stop.is_set():
                    outdata.fill(0)
                    raise sd.CallbackStop

                # Calculate end frame
                end_frame = self._current_frame + frames
                if end_frame > len(self._audio_data):
                    # End of audio
                    remaining = len(self._audio_data) - self._current_frame
                    if remaining > 0:
                        outdata[:remaining] = self._audio_data[self._current_frame : end_frame] * self._volume
                        outdata[remaining:].fill(0)
                    else:
                        outdata.fill(0)
                    raise sd.CallbackStop
                # Normal playback
                outdata[:] = self._audio_data[self._current_frame : end_frame] * self._volume
                self._current_frame = end_frame

                # Update position
                with self._lock:
                    self._position = self._current_frame / self._sample_rate

                    # Call progress callback if set
                    if self._progress_callback:
                        self._progress_callback(self._position)

            # Create and start audio stream
            with sd.OutputStream(
                samplerate=self._sample_rate,
                channels=self._audio_data.shape[1],
                callback=audio_callback,
                blocksize=1024,
            ) as stream:
                self._audio_stream = stream
                stream.start()

                # 使用非阻塞方式等待播放完成
                while stream.active and not self._should_stop.is_set():
                    import time

                    time.sleep(0.1)

        except Exception as e:
            logger.exception(f"Error during audio playback: {e}")
        finally:
            with self._lock:
                if not self._should_stop.is_set():
                    self._state = PlaybackState.STOPPED
                    self._position = 0.0
                    self._current_frame = 0

    def _stop_internal(self) -> None:
        """Stop playback without logging."""
        self._should_stop.set()
        if self._play_thread:
            self._play_thread.join(timeout=1.0)
        self._state = PlaybackState.STOPPED
        self._position = 0.0
        self._current_frame = 0

    def seek(self, position: float) -> bool:
        """Seek to specific position in current track.

        Args:
            position: Position in seconds

        Returns
        -------
            True if seek successful, False otherwise
        """
        with self._lock:
            if self._current_track is None:
                return False

            try:
                # Calculate frame position
                target_frame = int(position * self._sample_rate)
                if 0 <= target_frame < len(self._audio_data):
                    self._current_frame = target_frame
                    self._position = position
                    logger.info(f"Seeked to position: {position:.2f}s")
                    return True
                logger.warning(f"Seek position out of bounds: {position}")
                return False
            except Exception as e:
                logger.exception(f"Failed to seek to position {position}: {e}")
                return False

    def get_supported_formats(self) -> list[str]:
        """Get list of supported audio file formats.

        Returns
        -------
            List of supported file extensions
        """
        return [".mp3", ".wav", ".ogg", ".flac", ".m4a"]

    def is_supported_format(self, file_path: Path) -> bool:
        """Check if file format is supported.

        Args:
            file_path: Path to audio file

        Returns
        -------
            True if format is supported, False otherwise
        """
        return file_path.suffix.lower() in self.get_supported_formats()

    def cleanup(self) -> None:
        """Clean up audio resources."""
        with self._lock:
            self._stop_internal()
            self._audio_data = None
            self._sample_rate = None
            logger.info("Audio player cleaned up")
