"""
Session management for RedenLab ML SDK.

The RedenLabSession class owns credentials and upload functionality.
It's the entry point for uploading files and creating reusable Assets.

Audio-specific logic (duration detection, chunking) lives here as private methods,
keeping the soundfile/numpy dependency contained within this module.
"""

import os
import tempfile
from pathlib import Path

from . import api
from .asset import Asset
from .auth import validate_api_key
from .config import get_default_base_url, get_merged_config
from .exceptions import AuthenticationError, ConfigurationError, ValidationError
from .upload import upload_to_presigned_url
from .utils import get_content_type, validate_file_path


class RedenLabSession:
    """
    Session for managing credentials and file uploads.

    A session owns authentication credentials and provides methods to upload
    files, returning Asset objects that can be submitted to any inference client.

    This design enables:
    - One upload, multiple models (reuse Assets across different inference clients)
    - Clear separation between upload and inference concerns
    - Explicit credential management

    Args:
        api_key: API key for authentication (optional, can use env var or config file)
        base_url: API base URL (optional, defaults to production endpoint)
        config_path: Path to config file (optional)

    Example:
        >>> session = RedenLabSession(api_key="sk_live_...")
        >>> asset = session.upload("audio.wav")
        >>>
        >>> # Use asset with multiple models
        >>> intell_result = intell_client.predict(asset)
        >>> natural_result = natural_client.predict(asset)  # No re-upload!
    """

    def __init__(
        self,
        api_key: str | None = None,
        base_url: str | None = None,
        config_path: str | None = None,
    ):
        """
        Initialize session with credentials.

        Raises:
            AuthenticationError: If no API key is found
            ConfigurationError: If configuration is invalid
        """
        # Load configuration from all sources
        config_path_obj: Path | None = Path(config_path) if config_path else None
        config = get_merged_config(
            config_path=config_path_obj,
            api_key=api_key,
            base_url=base_url,
        )

        # Get and validate API key
        self.api_key: str = config.get("api_key")  # type: ignore
        if not self.api_key:
            raise AuthenticationError(
                "No API key found. Please set via:\n"
                "1. RedenLabSession(api_key='...')\n"
                "2. Environment variable: REDENLAB_ML_API_KEY\n"
                "3. Config file: ~/.redenlab-ml/config.yaml"
            )
        validate_api_key(self.api_key)

        # Set base URL (use provided, or from config, or default)
        base_url = config.get("base_url") or get_default_base_url()
        if not base_url or not isinstance(base_url, str):
            raise ConfigurationError(
                "No API base URL configured. Please set via:\n"
                "1. RedenLabSession(base_url='https://...')\n"
                "2. Environment variable: REDENLAB_ML_BASE_URL\n"
                "3. Config file: ~/.redenlab-ml/config.yaml"
            )
        self.base_url: str = base_url

    @staticmethod
    def _get_audio_duration(file_path: str) -> float:
        """
        Get audio file duration in seconds without loading the entire file.

        Args:
            file_path: Path to the audio file

        Returns:
            Duration in seconds

        Raises:
            ValidationError: If file cannot be read
            ImportError: If soundfile is not installed
        """
        try:
            import soundfile as sf
        except ImportError as e:
            raise ImportError(
                "soundfile is required for audio chunking. "
                "Install it with: pip install soundfile"
            ) from e

        try:
            info = sf.info(file_path)
            return float(info.duration)
        except Exception as e:
            raise ValidationError(f"Cannot read audio file duration: {e}") from e

    @staticmethod
    def _split_audio_into_chunks(
        file_path: str, chunk_duration: int = 60, overlap: float = 1.0
    ) -> list[str]:
        """
        Split audio file into chunks with overlap.

        Creates temporary chunk files with naming pattern:
        {base_name}_chunk_{index}_of_{total}.{ext}

        Args:
            file_path: Path to the audio file to split
            chunk_duration: Duration of each chunk in seconds (default: 60)
            overlap: Overlap between chunks in seconds (default: 1.0)

        Returns:
            List of paths to temporary chunk files or empty list if chunking not required

        Raises:
            ValidationError: If file cannot be read or split
            ImportError: If soundfile/numpy is not installed
        """
        try:
            import numpy as np
            import soundfile as sf
        except ImportError as e:
            raise ImportError(
                "soundfile and numpy are required for audio chunking. "
                "Install them with: pip install soundfile numpy"
            ) from e

        # Validate file exists
        path = validate_file_path(file_path)

        try:
            # Read audio file
            data, samplerate = sf.read(str(path))

            # Calculate total duration
            total_duration = len(data) / samplerate

            # If audio is shorter than chunk_duration, no need to split
            if total_duration <= chunk_duration:
                # Return empty list to signal no chunking needed
                return []

            # Calculate chunk parameters
            chunk_samples = int(chunk_duration * samplerate)
            overlap_samples = int(overlap * samplerate)
            step_samples = chunk_samples - overlap_samples

            # Calculate number of chunks
            num_chunks = int(np.ceil((len(data) - overlap_samples) / step_samples))

            # Create temp directory for chunks
            temp_dir = tempfile.mkdtemp(prefix="redenlab_audio_chunks_")

            # Get file info for naming
            base_name = path.stem
            ext = path.suffix

            chunk_files = []

            for i in range(num_chunks):
                # Calculate chunk boundaries
                start = i * step_samples
                end = min(start + chunk_samples, len(data))

                # Extract chunk data
                chunk_data = data[start:end]

                # Create chunk filename with pattern
                chunk_filename = f"{base_name}_chunk_{i}_of_{num_chunks}{ext}"
                chunk_path = os.path.join(temp_dir, chunk_filename)

                # Write chunk to temp file
                sf.write(chunk_path, chunk_data, samplerate)
                chunk_files.append(chunk_path)

            return chunk_files

        except Exception as e:
            raise ValidationError(f"Failed to split audio file: {e}") from e

    @staticmethod
    def _cleanup_chunk_files(chunk_files: list[str]) -> None:
        """
        Clean up temporary chunk files and their parent directory.

        Args:
            chunk_files: List of chunk file paths to clean up

        Note:
            Silently ignores errors during cleanup to avoid disrupting main workflow.
        """
        if not chunk_files:
            return

        try:
            # Get parent directory (should be the temp dir we created)
            temp_dir = os.path.dirname(chunk_files[0])

            # Remove all files
            for chunk_file in chunk_files:
                try:
                    if os.path.exists(chunk_file):
                        os.remove(chunk_file)
                except Exception:
                    pass  # Ignore individual file errors

            # Remove temp directory
            try:
                if os.path.exists(temp_dir):
                    os.rmdir(temp_dir)
            except Exception:
                pass  # Ignore directory removal errors

        except Exception:
            pass  # Silently ignore all cleanup errors

    def upload(self, file_path: str) -> Asset:
        """
        Upload a single audio file to S3.

        This method:
        1. Requests a presigned URL from the backend
        2. Uploads the file to S3 synchronously
        3. Returns an Asset object immediately

        **Important:** After the S3 upload completes, the backend update the upload
        status (upload_pending → uploaded). This typically takes 1-3 seconds.

        The SDK automatically handles this when you use client.predict() - it will retry
        if the upload is still being processed. For manual submit() calls, use
        client._submit_with_upload_wait() to handle upload completion.

        Args:
            file_path: Path to the audio file to upload

        Returns:
            Asset object representing the uploaded file (upload may still be processing)

        Raises:
            ValidationError: If file path is invalid or file type not supported
            AuthenticationError: If API key is invalid
            UploadError: If file upload fails
            APIError: If API communication fails

        Example:
            >>> asset = session.upload("audio.wav")
            >>> # Asset(asset_id="abc-123", filename="audio.wav", ...)
            >>> result = client.predict(asset)  # Handles upload completion automatically
        """
        # Validate file path
        file_path_obj = validate_file_path(file_path)
        content_type = get_content_type(file_path)
        filename = file_path_obj.name

        # Step 1: Request presigned URL
        asset_id, upload_url, expires_in = api.request_presigned_url(
            base_url=self.base_url,
            api_key=self.api_key,
            filename=filename,
            content_type=content_type,
        )

        # Step 2: Upload to S3
        upload_to_presigned_url(
            file_path=str(file_path_obj),
            presigned_url=upload_url,
            content_type=content_type,
        )

        # Step 3: Return Asset handle
        return Asset(
            asset_id=asset_id,
            filename=filename,
            content_type=content_type,
            duration=None,  # Populated by backend Lambda asynchronously
        )

    def upload_chunked(
        self,
        file_path: str,
        chunk_duration: float = 50.0,
        overlap: float = 1.0,
    ) -> list[Asset]:
        """
        Upload audio file with automatic chunking for long files.

        If file duration <= chunk_duration, uploads as a single asset.
        Otherwise:
        1. Uploads the ORIGINAL FILE as a parent record (for investigation/debugging)
        2. Splits file into chunks locally
        3. Uploads each chunk with parent_asset_id referencing the original file

        **Backend tracking:** All chunks are linked to the original file via
        `parent_asset_id` in the backend database. The original file is preserved
        for investigation, while chunks are used for inference.

        Args:
            file_path: Path to the audio file to upload
            chunk_duration: Duration of each chunk in seconds (default: 50)
            overlap: Overlap between chunks in seconds (default: 1.0)

        Returns:
            List of Asset objects representing chunks. Each has parent_asset_id set
            to the original file's asset_id.

        Raises:
            ValidationError: If file path is invalid or file type not supported
            AuthenticationError: If API key is invalid
            UploadError: If file upload fails
            APIError: If API communication fails
            ImportError: If soundfile is not installed (required for chunking)

        Example:
            >>> # Short file - returns single-element list
            >>> chunks = session.upload_chunked("short_audio.wav")
            >>> # [Asset(asset_id="abc", parent_asset_id=None)]

            >>> # Long file - returns multiple chunks linked to parent
            >>> chunks = session.upload_chunked("long_audio_120s.wav")
            >>> # [
            >>> #   Asset(asset_id="chunk1", parent_asset_id="original_file_id", chunk_index=0),
            >>> #   Asset(asset_id="chunk2", parent_asset_id="original_file_id", chunk_index=1),
            >>> # ]
            >>> # Backend also has: Asset(asset_id="original_file_id", parent_asset_id=None)

            >>> # Use chunks on multiple models - no re-upload!
            >>> result1 = intell_client.predict_chunked(chunks)
            >>> result2 = natural_client.predict_chunked(chunks)
        """
        try:
            duration = self._get_audio_duration(file_path)
        except ImportError:
            # soundfile not installed - upload as single asset
            # User will get backend error if file is too long for model
            return [self.upload(file_path)]

        # Short file - no chunking needed
        if duration <= chunk_duration:
            return [self.upload(file_path)]

        # Long file - chunking required
        # Step 1: Upload the ORIGINAL FILE as parent (for investigation/debugging)
        # This ensures backend has access to full file and creates a valid parent record
        parent_asset = self.upload(file_path)
        parent_asset_id = parent_asset.asset_id  # Use original file's asset_id as parent

        # Step 2: Split into chunks locally
        chunk_files = self._split_audio_into_chunks(
            file_path, chunk_duration=int(chunk_duration), overlap=overlap
        )

        if not chunk_files:
            # Splitting returned empty (shouldn't happen, but defensive)
            # Return the parent file we already uploaded
            return [parent_asset]

        # Step 3: Upload each chunk with parent_asset_id reference
        chunks: list[Asset] = []
        seen_indices: set[int] = set()  # Track indices to prevent duplicates

        try:
            for i, chunk_file in enumerate(chunk_files):
                # Validate: no duplicate chunk indices (backend doesn't check this)
                if i in seen_indices:
                    raise ValidationError(f"Duplicate chunk_index {i} detected")
                seen_indices.add(i)

                file_path_obj = Path(chunk_file)
                content_type = get_content_type(chunk_file)
                filename = file_path_obj.name

                # Request presigned URL WITH chunk metadata
                # Backend requires: both parent_asset_id and chunk_index present (co-required)
                asset_id, upload_url, expires_in = api.request_presigned_url(
                    base_url=self.base_url,
                    api_key=self.api_key,
                    filename=filename,
                    content_type=content_type,
                    parent_asset_id=parent_asset_id,  # References original file
                    chunk_index=i,  # Co-required with parent_asset_id
                )

                # Upload to S3
                upload_to_presigned_url(
                    file_path=str(file_path_obj),
                    presigned_url=upload_url,
                    content_type=content_type,
                )

                # Build Asset object
                chunks.append(
                    Asset(
                        asset_id=asset_id,
                        filename=filename,
                        content_type=content_type,
                        duration=None,  # Populated by backend Lambda
                        parent_asset_id=parent_asset_id,
                        chunk_index=i,
                    )
                )

            return chunks
        finally:
            # Always cleanup temp chunk files
            self._cleanup_chunk_files(chunk_files)

    def __repr__(self) -> str:
        """Return string representation of the session."""
        from .auth import mask_api_key

        return (
            f"RedenLabSession(api_key='{mask_api_key(self.api_key)}', base_url='{self.base_url}')"
        )
