"""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 pathlib import Path
from typing import Callable

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

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.error(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.error(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.error(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.error(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:
        """Internal method to 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):
                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()
                else:
                    # 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.error(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:
        """Internal method to 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
                else:
                    logger.warning(f"Seek position out of bounds: {position}")
                    return False
            except Exception as e:
                logger.error(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")
