from __future__ import annotations

import miniaudio
import time
import threading
import numpy as np
from pathlib import Path
import logging

from models.track import Track
from models.playback import PlaybackState

logger = logging.getLogger(__name__)

SAMPLE_RATE = 44100
NUM_CHANNELS = 2
AUDIO_FORMAT = miniaudio.SampleFormat.SIGNED16
DEFAULT_VOLUME = 0.1
VOLUME_STEP = 0.05
RESTART_TRACK_THRESHOLD = 3.0


class AudioPlayer:
    """Service for audio playback management."""
    
    def __init__(self):
        try:
            logger.info("Initializing audio player...")
            
            self._current_track: Track | None = None
            self._playlist: list[Track] = []
            self._current_index: int = -1
            self._volume: float = DEFAULT_VOLUME
            self._muted: bool = False
            self._volume_before_mute: float = DEFAULT_VOLUME
            self._state: PlaybackState = PlaybackState.STOPPED
            self._start_time: float = 0
            self._pause_position: float = 0
            self._track_ended_naturally: bool = False
            
            self._device: miniaudio.PlaybackDevice | None = None
            self._stream_generator = None
            self._stop_playback = threading.Event()
            self._pause_event = threading.Event()
            
            self._audio_buffer_lock = threading.Lock()
            self._latest_audio_buffer: np.ndarray | None = None
            
            logger.info("Audio player initialized successfully")
            
        except Exception as e:
            error_msg = str(e) if str(e) else "Unknown audio error"
            logger.critical(f"Failed to initialize audio device: {error_msg}")
            raise RuntimeError(
                f"🔇 No audio device detected\n\n"
                f"SIGPLAY needs an audio output device to play music.\n"
                f"Please check that:\n"
                f"  • Your audio device is connected and powered on\n"
                f"  • Audio drivers are properly installed\n"
                f"  • The device is not being used exclusively by another app\n\n"
                f"Technical details: {error_msg}"
            ) from e
    
    def play(self, track: Track) -> None:
        """Load and play an audio file.
        
        Args:
            track: Track object to play.
            
        Raises:
            FileNotFoundError: If audio file doesn't exist.
            RuntimeError: If file cannot be loaded or played.
        """
        file_path = Path(track.file_path)
        
        if not file_path.exists():
            logger.error(f"Audio file not found: {file_path}")
            self._state = PlaybackState.STOPPED
            self._current_track = None
            raise FileNotFoundError(
                f"Audio file not found: {file_path.name}\n\n"
                f"The file may have been moved or deleted."
            )
        
        try:
            self.stop()
            
            logger.info(f"Loading track: {track.title} by {track.artist}")
            
            self._stream_generator = miniaudio.stream_file(
                str(file_path),
                output_format=AUDIO_FORMAT,
                nchannels=NUM_CHANNELS,
                sample_rate=SAMPLE_RATE
            )
            
            def audio_generator():
                num_frames = yield b''
                
                while not self._stop_playback.is_set():
                    if self._pause_event.is_set():
                        num_frames = yield b'\x00' * (num_frames * 2 * 2)
                        continue
                    
                    try:
                        audio_data = self._stream_generator.send(num_frames)
                        
                        audio_array = np.frombuffer(audio_data, dtype=np.int16)
                        
                        with self._audio_buffer_lock:
                            self._latest_audio_buffer = audio_array.copy()
                        
                        volume_adjusted = (audio_array * self._volume).astype(np.int16)
                        
                        num_frames = yield volume_adjusted.tobytes()
                        
                    except StopIteration:
                        logger.debug("Reached end of audio file")
                        self._state = PlaybackState.STOPPED
                        self._track_ended_naturally = True
                        num_frames = yield b'\x00' * (num_frames * 2 * 2)
                        break
            
            self._device = miniaudio.PlaybackDevice(
                sample_rate=SAMPLE_RATE,
                nchannels=NUM_CHANNELS,
                output_format=AUDIO_FORMAT
            )
            
            gen = audio_generator()
            next(gen)
            self._device.start(gen)
            
            self._current_track = track
            self._state = PlaybackState.PLAYING
            self._start_time = time.time()
            self._pause_position = 0
            self._track_ended_naturally = False
            
            if self._playlist and track in self._playlist:
                self._current_index = self._playlist.index(track)
            
            self._stop_playback.clear()
            self._pause_event.clear()
            
            logger.debug(f"Successfully started playback of {track.title}")
            
        except miniaudio.MiniaudioError as e:
            logger.error(f"miniaudio error loading {file_path}: {e}")
            self._state = PlaybackState.STOPPED
            self._current_track = None
            raise RuntimeError(
                f"Cannot play audio file: {file_path.name}\n\n"
                f"The file may be corrupted or in an unsupported format.\n"
                f"Technical details: {e}"
            ) from e
        except Exception as e:
            logger.error(f"Unexpected error playing {file_path}: {type(e).__name__}: {e}")
            self._state = PlaybackState.STOPPED
            self._current_track = None
            raise RuntimeError(
                f"Failed to play {file_path.name}: {type(e).__name__}"
            ) from e
    
    def pause(self) -> None:
        """Pause playback."""
        if self._state == PlaybackState.PLAYING:
            self._pause_event.set()
            self._state = PlaybackState.PAUSED
            self._pause_position = time.time() - self._start_time
    
    def resume(self) -> None:
        """Resume playback from paused state."""
        if self._state == PlaybackState.PAUSED:
            self._pause_event.clear()
            self._state = PlaybackState.PLAYING
            self._start_time = time.time() - self._pause_position
    
    def stop(self) -> None:
        """Stop playback and reset position."""
        self._stop_playback.set()
        self._track_ended_naturally = False
        
        if self._device:
            try:
                self._device.stop()
                self._device.close()
            except Exception as e:
                logger.debug(f"Error stopping device: {e}")
            self._device = None
        
        self._stream_generator = None
        self._state = PlaybackState.STOPPED
        self._start_time = 0
        self._pause_position = 0
    
    def next_track(self) -> None:
        """Skip to next track in playlist.
        
        Automatically skips corrupted files and continues to next valid track.
        """
        if not self._playlist:
            logger.debug("No playlist set, cannot skip to next track")
            return
        
        if self._current_index < len(self._playlist) - 1:
            self._current_index += 1
            next_track = self._playlist[self._current_index]
            
            try:
                self.play(next_track)
            except (FileNotFoundError, RuntimeError) as e:
                logger.warning(f"Failed to play next track, skipping: {e}")
                self.next_track()
        else:
            logger.debug("Reached end of playlist")
            self.stop()
    
    def previous_track(self) -> None:
        """Skip to previous track in playlist.
        
        Restarts current track if position > 3 seconds, otherwise goes to previous.
        Automatically skips corrupted files.
        """
        if not self._playlist:
            logger.debug("No playlist set, cannot skip to previous track")
            return
        
        current_position = self.get_position()
        
        if current_position > RESTART_TRACK_THRESHOLD:
            current_track = self._playlist[self._current_index]
            try:
                self.play(current_track)
            except (FileNotFoundError, RuntimeError) as e:
                logger.warning(f"Failed to restart track: {e}")
        elif self._current_index > 0:
            self._current_index -= 1
            prev_track = self._playlist[self._current_index]
            try:
                self.play(prev_track)
            except (FileNotFoundError, RuntimeError) as e:
                logger.warning(f"Failed to play previous track, skipping: {e}")
                self.previous_track()
    
    def set_volume(self, level: float) -> None:
        """Set volume level (0.0 to 1.0)."""
        self._volume = max(0.0, min(1.0, level))
        if self._volume > 0:
            self._muted = False
    
    def increase_volume(self, amount: float = VOLUME_STEP) -> None:
        """Increase volume by specified amount."""
        if self._muted:
            self.unmute()
        else:
            self.set_volume(self._volume + amount)
    
    def decrease_volume(self, amount: float = VOLUME_STEP) -> None:
        """Decrease volume by specified amount."""
        if self._muted:
            self.unmute()
        self.set_volume(self._volume - amount)
    
    def toggle_mute(self) -> None:
        """Toggle mute state."""
        if self._muted:
            self.unmute()
        else:
            self.mute()
    
    def mute(self) -> None:
        """Mute audio."""
        if not self._muted:
            self._volume_before_mute = self._volume
            self._volume = 0.0
            self._muted = True
    
    def unmute(self) -> None:
        """Unmute audio."""
        if self._muted:
            self._volume = self._volume_before_mute
            self._muted = False
    
    def is_muted(self) -> bool:
        """Check if audio is muted."""
        return self._muted
    
    def get_state(self) -> PlaybackState:
        """Return current playback state."""
        return self._state
    
    def get_current_track(self) -> Track | None:
        """Return currently playing track or None."""
        return self._current_track
    
    def get_position(self) -> float:
        """Return current playback position in seconds."""
        if self._state == PlaybackState.STOPPED:
            return 0.0
        elif self._state == PlaybackState.PAUSED:
            return self._pause_position
        elif self._state == PlaybackState.PLAYING:
            return time.time() - self._start_time
        return 0.0
    
    def get_volume(self) -> float:
        """Return current volume level (0.0 to 1.0)."""
        return self._volume
    
    def is_playing(self) -> bool:
        """Check if currently playing."""
        return self._state == PlaybackState.PLAYING
    
    def set_playlist(self, tracks: list[Track], start_index: int = 0) -> None:
        """Set current playlist and starting track index."""
        self._playlist = tracks
        self._current_index = max(0, min(start_index, len(tracks) - 1)) if tracks else -1
    
    def get_latest_audio_buffer(self) -> np.ndarray | None:
        """Get the most recent audio buffer.
        
        Returns:
            Numpy array of audio samples or None if no audio playing
        """
        with self._audio_buffer_lock:
            return self._latest_audio_buffer.copy() if self._latest_audio_buffer is not None else None
    
    def track_ended_naturally(self) -> bool:
        """Check if track ended naturally (not manually stopped)."""
        return self._track_ended_naturally
