"""Streaming preparation worker.

`prepare_stream_task(track_data, deps)` is the function the stream
executor submits to fetch a track from Soulseek/YouTube/etc and stage
it in the local Stream/ folder for the browser audio player.

1. Reset stream state to 'loading' with the new track info.
2. Clear any prior file from the Stream/ folder (only one stream lives
   there at a time).
3. Spin up a fresh asyncio event loop and `download_orchestrator.download()`
   the track.
4. Poll `download_orchestrator.get_all_downloads()` every 1.5 s to track
   progress, with separate handling for queued vs actively downloading
   states. Queue timeout = 15 s; overall timeout = 60 s.
5. On completion (state ~ 'succeeded' or progress >= 100% AND bytes
   transferred match expected size), find the downloaded file with retry
   logic, move it into Stream/, signal completion to the slskd API, and
   mark stream_state as 'ready' with the file path.
6. On any error/timeout/cancel: stream_state goes to 'error' or
   'stopped' with an explanatory message.
7. Finally: tear down the event loop cleanly.

The original mutated `stream_state` as a module global. Here it's
exposed through the `PrepareStreamDeps` proxy as a Python property so
the lifted body keeps the same `name[key] = value` syntax. The setter
fires only if the function reassigns (currently it only mutates in
place via .update() and key assignment).
"""

from __future__ import annotations

import asyncio
import glob
import logging
import os
import shutil
import time
from dataclasses import dataclass
from typing import Any, Callable

logger = logging.getLogger(__name__)


@dataclass
class PrepareStreamDeps:
    """Bundle of cross-cutting deps the stream-prep worker needs."""
    config_manager: Any
    download_orchestrator: Any
    stream_lock: Any  # threading.Lock
    project_root: str  # absolute path to web_server.py's directory
    docker_resolve_path: Callable[[str], str]
    find_streaming_download_in_all_downloads: Callable
    find_downloaded_file: Callable
    extract_filename: Callable[[str], str]
    cleanup_empty_directories: Callable
    _get_stream_state: Callable[[], dict]
    _set_stream_state: Callable[[dict], None]

    @property
    def stream_state(self) -> dict:
        return self._get_stream_state()

    @stream_state.setter
    def stream_state(self, value: dict) -> None:
        self._set_stream_state(value)


def prepare_stream_task(track_data, deps: PrepareStreamDeps):
    """
    Background streaming task that downloads track to Stream folder and updates global state.
    Enhanced version with robust error handling matching the GUI StreamingThread.
    """
    loop = None
    queue_start_time = None
    actively_downloading = False
    last_progress_sent = 0.0
    
    try:
        logger.info(f"Starting stream preparation for: {track_data.get('filename')}")
        
        # Update state to loading
        with deps.stream_lock:
            deps.stream_state.update({
                "status": "loading",
                "progress": 0,
                "track_info": track_data,
                "file_path": None,
                "error_message": None
            })
        
        # Get paths
        download_path = deps.docker_resolve_path(deps.config_manager.get('soulseek.download_path', './downloads'))
        project_root = deps.project_root
        stream_folder = os.path.join(project_root, 'Stream')
        
        # Ensure Stream directory exists
        os.makedirs(stream_folder, exist_ok=True)
        
        # Clear any existing files in Stream folder (only one file at a time)
        for existing_file in glob.glob(os.path.join(stream_folder, '*')):
            try:
                if os.path.isfile(existing_file):
                    os.remove(existing_file)
                elif os.path.isdir(existing_file):
                    shutil.rmtree(existing_file)
                logger.info(f"Cleared old stream file: {existing_file}")
            except Exception as e:
                logger.error(f"Could not remove existing stream file: {e}")
        
        # Start the download using the same mechanism as regular downloads
        loop = asyncio.new_event_loop()
        asyncio.set_event_loop(loop)
        
        try:
            download_result = loop.run_until_complete(deps.download_orchestrator.download(
                track_data.get('username'),
                track_data.get('filename'),
                track_data.get('size', 0)
            ))
            
            if not download_result:
                with deps.stream_lock:
                    deps.stream_state.update({
                        "status": "error",
                        "error_message": "Failed to initiate download - uploader may be offline"
                    })
                return
            
            logger.info("Download initiated for streaming")
            
            # Enhanced monitoring with queue timeout detection (matching GUI)
            max_wait_time = 60  # Increased timeout
            poll_interval = 1.5  # More frequent polling
            queue_timeout = 15   # Queue timeout like GUI
            wait_count = 0
            
            while wait_count * poll_interval < max_wait_time:
                wait_count += 1
                
                # Check download progress via orchestrator (works for Soulseek and YouTube)
                api_progress = None
                download_state = None
                download_status = None

                try:
                    # Use orchestrator's get_all_downloads() which works for both sources
                    all_downloads = loop.run_until_complete(deps.download_orchestrator.get_all_downloads())
                    download_status = deps.find_streaming_download_in_all_downloads(all_downloads, track_data)
                    
                    if download_status:
                        api_progress = download_status.get('percentComplete', 0)
                        download_state = download_status.get('state', '').lower()
                        original_state = download_status.get('state', '')
                        
                        logger.info(f"API Download - State: {original_state}, Progress: {api_progress:.1f}%")
                        
                        # Track queue state timing (matching GUI logic)
                        is_queued = ('queued' in download_state or 'initializing' in download_state)
                        is_downloading = ('inprogress' in download_state or 'transferring' in download_state)
                        # Verify bytes match before trusting state/progress
                        _stream_expected = download_status.get('size', 0)
                        _stream_transferred = download_status.get('bytesTransferred', 0)
                        _bytes_ok = _stream_expected <= 0 or _stream_transferred >= _stream_expected
                        is_completed = ('succeeded' in download_state or api_progress >= 100) and _bytes_ok
                        
                        # Handle queue state timing
                        if is_queued and queue_start_time is None:
                            queue_start_time = time.time()
                            logger.info(f"Download entered queue state: {original_state}")
                            with deps.stream_lock:
                                deps.stream_state["status"] = "queued"
                        elif is_downloading and not actively_downloading:
                            actively_downloading = True
                            queue_start_time = None  # Reset queue timer
                            logger.info(f"Download started actively downloading: {original_state}")
                            with deps.stream_lock:
                                deps.stream_state["status"] = "loading"
                        
                        # Check for queue timeout (matching GUI)
                        if is_queued and queue_start_time:
                            queue_elapsed = time.time() - queue_start_time
                            if queue_elapsed > queue_timeout:
                                logger.error(f"⏰ Queue timeout after {queue_elapsed:.1f}s - download stuck in queue")
                                with deps.stream_lock:
                                    deps.stream_state.update({
                                        "status": "error",
                                        "error_message": "Queue timeout - uploader not responding. Try another source."
                                    })
                                return
                        
                        # Update progress
                        with deps.stream_lock:
                            if api_progress != last_progress_sent:
                                deps.stream_state["progress"] = api_progress
                                last_progress_sent = api_progress
                        
                        # Check if download is complete
                        if is_completed:
                            logger.info(f"Download completed via API status: {original_state}")

                            # Wait for file to stabilise on disk before moving
                            found_file = deps.find_downloaded_file(download_path, track_data)
                            if found_file:
                                _prev_sz = -1
                                for _sc in range(4):
                                    try:
                                        _cur_sz = os.path.getsize(found_file)
                                    except OSError:
                                        _cur_sz = -1
                                    if _cur_sz == _prev_sz and _cur_sz > 0:
                                        break
                                    _prev_sz = _cur_sz
                                    time.sleep(1.5)

                            # Re-find in case it wasn't found on first try
                            if not found_file:
                                found_file = deps.find_downloaded_file(download_path, track_data)
                            
                            # Retry file search a few times (matching GUI logic)
                            retry_attempts = 5
                            for attempt in range(retry_attempts):
                                if found_file:
                                    break
                                logger.warning(f"File not found yet, attempt {attempt + 1}/{retry_attempts}")
                                time.sleep(1)
                                found_file = deps.find_downloaded_file(download_path, track_data)
                            
                            if found_file:
                                logger.debug(f"Found downloaded file: {found_file}")
                                
                                # Move file to Stream folder
                                original_filename = deps.extract_filename(found_file)
                                stream_path = os.path.join(stream_folder, original_filename)
                                
                                try:
                                    shutil.move(found_file, stream_path)
                                    logger.debug(f"Moved file to stream folder: {stream_path}")
                                    
                                    # Clean up empty directories (matching GUI)
                                    deps.cleanup_empty_directories(download_path, found_file)
                                    
                                    # Update state to ready
                                    with deps.stream_lock:
                                        deps.stream_state.update({
                                            "status": "ready",
                                            "progress": 100,
                                            "file_path": stream_path
                                        })
                                    
                                    # Clean up download from slskd API
                                    try:
                                        download_id = download_status.get('id', '')
                                        if download_id and track_data.get('username'):
                                            success = loop.run_until_complete(
                                                deps.download_orchestrator.signal_download_completion(
                                                    download_id, track_data.get('username'), remove=True)
                                            )
                                            if success:
                                                logger.debug(f"Cleaned up download {download_id} from API")
                                    except Exception as e:
                                        logger.error(f"Error cleaning up download: {e}")
                                    
                                    logger.info(f"Stream file ready for playback: {stream_path}")
                                    return  # Success!
                                    
                                except Exception as e:
                                    logger.error(f"Error moving file to stream folder: {e}")
                                    with deps.stream_lock:
                                        deps.stream_state.update({
                                            "status": "error",
                                            "error_message": f"Failed to prepare stream file: {e}"
                                        })
                                    return
                            else:
                                logger.error("Could not find downloaded file after completion")
                                with deps.stream_lock:
                                    deps.stream_state.update({
                                        "status": "error",
                                        "error_message": "Download completed but file not found"
                                    })
                                return
                    else:
                        # No transfer found in API - may still be initializing
                        logger.debug(f"No transfer found in API yet... (elapsed: {wait_count * poll_interval}s)")
                        
                except Exception as e:
                    logger.error(f"Error checking download progress: {e}")
                    # Continue to next iteration if API call fails
                
                # Wait before next poll
                time.sleep(poll_interval)
            
            # If we get here, download timed out
            logger.warning(f"Download timed out after {max_wait_time}s")
            with deps.stream_lock:
                deps.stream_state.update({
                    "status": "error", 
                    "error_message": "Download timed out - try a different source"
                })
                
        except asyncio.CancelledError:
            logger.warning("Stream task cancelled")
            with deps.stream_lock:
                deps.stream_state.update({
                    "status": "stopped",
                    "error_message": None
                })
        finally:
            if loop:
                try:
                    # Clean up any pending tasks
                    pending = asyncio.all_tasks(loop)
                    if pending:
                        loop.run_until_complete(asyncio.gather(*pending, return_exceptions=True))
                    loop.close()
                except Exception as e:
                    logger.error(f"Error cleaning up streaming event loop: {e}")
            
    except Exception as e:
        logger.error(f"Stream preparation failed: {e}")
        with deps.stream_lock:
            deps.stream_state.update({
                "status": "error",
                "error_message": f"Streaming error: {str(e)}"
            })

