"""
Inference clients for RedenLab ML SDK.

Provides the InferenceClient class and model-specific variants.
Clients operate on Assets (not file paths) and focus solely on inference.
"""

import os
import tempfile
import time
from collections.abc import Callable
from typing import Any

from . import api
from .asset import Asset
from .exceptions import APIError, ConfigurationError, InferenceError, ValidationError
from .payload import get_payload_builder
from .polling import poll_until_complete, poll_with_callback
from .session import RedenLabSession
from .utils import validate_model_name_format, validate_timeout

# Default standard warmup asset ID (pre-uploaded 1-second silence file)
# This asset is used for warming up backends without creating/uploading new files
DEFAULT_WARMUP_ASSET_ID = "287afa05-fc10-4512-9c04-83b8aaec19f0"


class InferenceClient:
    """
    Base inference client for RedenLab ML models.

    This client operates on Assets (not file paths) and focuses solely on:
    - Submitting assets to a model
    - Polling for job completion
    - Checking health and warming up endpoints

    Upload logic lives in RedenLabSession. Chunking and aggregation live in workflows.py.

    Args:
        session: RedenLabSession instance (owns credentials)
        model_name: Name of the model to use
        timeout: Maximum time to wait for inference in seconds (default: 3600)

    Example:
        >>> session = RedenLabSession(api_key="...")
        >>> client = InferenceClient(session, model_name="als-intelligibility-mtpa")
        >>> asset = session.upload("audio.wav")
        >>> result = client.predict(asset)
    """

    # Class attribute to indicate if this model supports health checks
    # Models that use SageMaker endpoints support health checks
    # Models that use AWS managed services (like Transcribe) do not
    _supports_health_check: bool = True

    def __init__(
        self,
        session: RedenLabSession,
        model_name: str,
        timeout: int = 3600,
        chunk_threshold: float = 50.0,
        score_key: str | None = None,
    ):
        """
        Initialize inference client.

        Args:
            session: RedenLabSession instance
            model_name: Name of the model to use (backend model name)
            timeout: Maximum time to wait for inference in seconds (default: 3600)
            chunk_threshold: Duration threshold in seconds for automatic chunking (default: 50.0).
                           Files longer than this will be automatically chunked by run_inference().
            score_key: Key for score aggregation in chunked inference.
                      Set by factory functions (intelligibility_client, naturalness_client).

        Raises:
            ValidationError: If parameters are invalid
        """
        self._session = session
        self._backend_model_name = validate_model_name_format(model_name)
        self._user_model_name = model_name  # Default to backend name
        self.timeout = validate_timeout(timeout)
        self._payload_builder = get_payload_builder(self._backend_model_name)
        self.chunk_threshold = chunk_threshold
        self.score_key = score_key

    @property
    def model_name(self) -> str:
        """
        User-facing model name.

        For clients created via factory functions (intelligibility_client, naturalness_client),
        this returns the user-friendly alias (e.g., 'als-intelligibility-v1').
        For direct InferenceClient instantiation, returns the backend model name.
        """
        return self._user_model_name

    def _submit_with_upload_wait(
        self,
        asset: Asset,
        max_retries: int = 5,
        initial_wait: float = 2.0,
        **model_params: Any,
    ) -> str:
        """
        Submit inference job with retry logic for upload completion.

        After uploading a file, the backend processes the S3 event asynchronously
        to update the upload status from "upload_pending" to "uploaded". This method
        retries submission if we get a 409 error indicating upload is still pending.

        Args:
            asset: Asset to submit
            max_retries: Maximum number of retry attempts (default: 5)
            initial_wait: Initial wait time in seconds, doubles with each retry (default: 2.0)
            **model_params: Model-specific parameters

        Returns:
            Job ID string

        Raises:
            APIError: If submission fails after all retries
            InferenceError: If submission fails for non-upload-pending reasons
        """
        wait_time = initial_wait

        for attempt in range(max_retries):
            try:
                return self.submit(asset, **model_params)
            except APIError as e:
                error_msg = str(e).lower()
                # Check if error is due to upload still pending
                if "upload" in error_msg and "pending" in error_msg:
                    if attempt < max_retries - 1:
                        # Wait and retry
                        time.sleep(wait_time)
                        wait_time *= 2  # Exponential backoff
                        continue
                # Not an upload pending error, or out of retries
                raise

        # Should not reach here, but for safety
        raise APIError(
            f"Failed to submit inference job after {max_retries} attempts. "
            "Upload may still be processing."
        )

    def submit(self, asset: Asset, **model_params: Any) -> str:
        """
        Submit inference job for an uploaded asset. Returns immediately without waiting.

        The job will process asynchronously on the server. Use poll() or get_status()
        to check results later.

        Args:
            asset: Previously uploaded Asset instance
            **model_params: Model-specific parameters (vary by model type)

        Returns:
            job_id: Job identifier string that can be used with poll() or get_status()

        Raises:
            ValidationError: If model_params are invalid for the model
            AuthenticationError: If API key is invalid
            APIError: If API communication fails

        Example:
            >>> asset = session.upload("audio.wav")
            >>> job_id = client.submit(asset)
            >>> print(f"Submitted job: {job_id}")
            >>> # ... do other work ...
            >>> result = client.get_status(job_id)
        """
        # Build payload with asset_id (NEW API CONTRACT)
        payload = self._payload_builder.build(asset_id=asset.asset_id, **model_params)

        # Submit job and capture job_id from response (NEW API CONTRACT)
        response = api.submit_inference_job(
            base_url=self._session.base_url,
            api_key=self._session.api_key,
            model_name=self._backend_model_name,
            payload=payload,
        )

        # Extract job_id from response
        job_id: str = response["job_id"]
        return job_id

    def poll(
        self,
        job_id: str,
        timeout: int | None = None,
        progress_callback: Callable[[dict[str, Any]], None] | None = None,
    ) -> dict[str, Any]:
        """
        Poll for inference job completion. Blocks until job completes, fails, or times out.

        Args:
            job_id: Job identifier (returned from submit())
            timeout: Maximum time to wait in seconds (default: use client timeout)
            progress_callback: Optional callback function that receives status updates
                             during polling. Called with status dict after each check.

        Returns:
            Dictionary containing inference results:
            - job_id: Job identifier
            - status: 'completed'
            - result: Inference result data (model-specific)
            - created_at: Job creation timestamp
            - completed_at: Job completion timestamp

        Raises:
            InferenceError: If inference job fails
            TimeoutError: If inference doesn't complete within timeout
            APIError: If API communication fails

        Example:
            >>> def on_progress(status):
            ...     print(f"Status: {status['status']}")
            >>> result = client.poll(job_id="abc-123", progress_callback=on_progress)
            >>> print(result['result'])
        """
        poll_timeout = timeout if timeout is not None else self.timeout

        if progress_callback:
            result = poll_with_callback(
                get_status_func=lambda: self.get_status(job_id),
                job_id=job_id,
                progress_callback=progress_callback,
                timeout=poll_timeout,
            )
        else:
            result = poll_until_complete(
                get_status_func=lambda: self.get_status(job_id),
                job_id=job_id,
                timeout=poll_timeout,
            )

        return result

    def predict(
        self,
        asset: Asset,
        progress_callback: Callable[[dict[str, Any]], None] | None = None,
        wait_for_upload: bool = True,
        **model_params: Any,
    ) -> dict[str, Any]:
        """
        Run inference on an uploaded asset. Convenience method that submits and polls.

        For chunked inference, use workflows.run_chunked_inference().

        This is equivalent to: client.poll(client.submit(asset))

        Args:
            asset: Previously uploaded Asset instance
            progress_callback: Optional callback function that receives status updates
                             during polling. Called with status dict after each check.
            wait_for_upload: If True, retries submission if upload is still pending
                           (useful when calling predict() right after upload).
                           Default: True
            **model_params: Model-specific parameters

        Returns:
            Dictionary containing inference results:
            - job_id: Job identifier
            - status: 'completed'
            - result: Inference result data (model-specific)
            - created_at: Job creation timestamp
            - completed_at: Job completion timestamp

        Raises:
            ValidationError: If model_params are invalid for the model
            AuthenticationError: If API key is invalid
            InferenceError: If inference job fails
            TimeoutError: If inference doesn't complete within timeout
            APIError: If API communication fails

        Example:
            >>> asset = session.upload("audio.wav")
            >>> result = client.predict(asset)
            >>> print(result['result'])

            >>> # Disable upload wait for assets uploaded long ago
            >>> result = client.predict(old_asset, wait_for_upload=False)
        """
        if wait_for_upload:
            job_id = self._submit_with_upload_wait(asset, **model_params)
        else:
            job_id = self.submit(asset, **model_params)
        return self.poll(job_id, progress_callback=progress_callback)

    def get_status(self, job_id: str) -> dict[str, Any]:
        """
        Get the current status of an inference job.

        Args:
            job_id: Job ID to check

        Returns:
            Dictionary containing job status:
            - job_id: Job identifier
            - status: Current status (upload_pending, processing, completed, failed)
            - result: Inference result (if completed)
            - error: Error message (if failed)
            - created_at: Job creation timestamp
            - completed_at: Job completion timestamp (if completed)

        Raises:
            APIError: If status check fails
            AuthenticationError: If API key is invalid

        Example:
            >>> status = client.get_status(job_id="abc-123")
            >>> print(status['status'])
            'processing'
        """
        return api.get_job_status(
            base_url=self._session.base_url,
            api_key=self._session.api_key,
            job_id=job_id,
            model_name=self._backend_model_name,
        )

    def health(self, timeout: int = 60) -> dict[str, Any]:
        """
        Check backend health and readiness for this model.

        This method pings the /{model_name}/health endpoint to check if the backend
        is ready to serve requests. It's useful for detecting cold starts - if the
        status is "warming_up" or response time is >5 seconds, the backend is waking
        up from sleep, which may cause longer processing times (up to 10+ minutes) for
        subsequent inference requests.

        IMPORTANT: This method only CHECKS status - it does NOT trigger backend warmup.
        To wake up a cold backend, use the warmup() method or submit an actual job.

        Note: Health checks are only available for SageMaker-based models
        (intelligibility, naturalness). Managed services like Transcribe don't
        support health checks as they're always available.

        Args:
            timeout: Maximum time to wait for health check in seconds (default: 60s)

        Returns:
            Dictionary containing:
            - status: Health status ("ready", "warming_up", "not_supported", or error)
            - ready: Boolean indicating if endpoint is ready to serve requests
            - response_time: Response time in seconds (if applicable)
            - is_warming: Boolean indicating if backend is warming up/cold starting
            - warning: Optional warning message if warming up detected

        Raises:
            APIError: If health check fails or times out (only for supported models)
            AuthenticationError: If API key is invalid

        Example:
            >>> health = client.health()
            >>> if health['status'] == 'not_supported':
            ...     print("Health check not supported, proceeding with submission")
            >>> elif not health['ready']:
            ...     print("Backend is cold - use client.warmup() to wake it up")
            ...     client.warmup()  # Wake up the backend
        """
        # Check if this model supports health checks
        if not self._supports_health_check:
            return {
                "status": "not_supported",
                "ready": True,  # Assume ready since it's a managed service
                "is_warming": False,
                "message": (
                    f"Health checks not supported for {self.model_name}. "
                    "This model uses a managed service that is always available."
                ),
            }

        health_result = api.check_health(
            base_url=self._session.base_url,
            api_key=self._session.api_key,
            model_name=self._backend_model_name,
            timeout=timeout,
        )

        # Add user-friendly warning message
        if health_result["is_warming"]:
            if health_result["status"] == "warming_up":
                health_result["warning"] = (
                    "Backend is currently warming up from cold start. "
                    "Initial requests may take 10+ minutes to process. "
                    "Please wait and check again, or proceed knowing processing may be delayed."
                )
            else:
                health_result["warning"] = (
                    f"Backend responded slowly ({health_result['response_time']}s), "
                    "which may indicate a cold start. Initial requests may take 10+ minutes. "
                    "Subsequent requests will be faster once the backend is warm."
                )

        return health_result

    def warmup(
        self,
        timeout: int = 900,
        show_progress: bool = True,
        use_standard_asset: bool = True,
        standard_asset_id: str | None = None,
        force: bool = False,
    ) -> dict[str, Any]:
        """
        Warm up the backend by submitting a dummy inference job.

        By default, checks if the backend is already ready before warming up.
        If already warm, returns immediately without submitting a job.

        By default, this uses a pre-uploaded standard warmup asset for efficiency.
        If the standard asset is unavailable (e.g., deleted by bucket policy), it
        automatically falls back to creating and uploading a new dummy file.

        Note: Only available for SageMaker-based models (intelligibility, naturalness).
        Managed services like Transcribe don't require warmup as they're always available.

        Args:
            timeout: Maximum time to wait for warmup in seconds (default: 900s = 15 min)
            show_progress: Whether to print progress messages (default: True)
            use_standard_asset: Use pre-uploaded standard asset for warmup (default: True).
                              Set to False to always create and upload a new dummy file.
            standard_asset_id: Custom standard asset ID to use (overrides default).
                             Only used if use_standard_asset=True.
            force: Force warmup even if health check shows backend is ready (default: False).
                  Useful if you want to ensure the backend is fully warmed.

        Returns:
            Dictionary containing:
            - status: "success" if warmed up, "already_ready" if no warmup needed,
                     "not_supported" if not applicable
            - warmup_time: Time taken to warm up in seconds (if warmed up)
            - message: Informational message
            - used_standard_asset: Whether the standard asset was used (if warmed up)

        Raises:
            ConfigurationError: If warmup is not supported for this model
            TimeoutError: If warmup doesn't complete within timeout
            InferenceError: If warmup job fails
            APIError: If API communication fails

        Example:
            >>> # Smart warmup - checks health first (default)
            >>> client.warmup()
            Backend is already ready (response time: 1.2s)

            >>> # Force warmup even if appears ready
            >>> client.warmup(force=True)
            Warming up backend (this may take up to 10 minutes)...

            >>> # Force creating new dummy file
            >>> client.warmup(use_standard_asset=False)
        """
        # Check if this model supports warmup (only SageMaker models)
        if not self._supports_health_check:
            return {
                "status": "not_supported",
                "message": (
                    f"Warmup not needed for {self.model_name}. "
                    "This model uses a managed service that is always available."
                ),
            }

        # Check health first unless force=True
        if not force:
            if show_progress:
                print("Checking endpoint health...")

            try:
                health = self.health(timeout=30)

                if health.get("ready") and not health.get("is_warming"):
                    response_time = health.get("response_time", "unknown")
                    if show_progress:
                        print(
                            f"✅ Backend is already ready (response time: {response_time}s)\n"
                            f"   No warmup needed. Use force=True to warmup anyway."
                        )
                    return {
                        "status": "already_ready",
                        "message": f"Backend already ready (response time: {response_time}s)",
                        "health": health,
                    }

                if show_progress and health.get("is_warming"):
                    print("⚠️  Backend is currently warming up. Proceeding with warmup job...")
            except Exception as e:
                # If health check fails, proceed with warmup anyway
                if show_progress:
                    print(f"⚠️  Health check failed ({e}), proceeding with warmup...")

        start_time = time.time()

        if show_progress:
            print(
                f"Warming up backend for {self.model_name} " "(this may take up to 10 minutes)..."
            )

        # Try using standard asset first if enabled
        if use_standard_asset:
            asset_id = standard_asset_id or DEFAULT_WARMUP_ASSET_ID

            if show_progress:
                print(f"Using standard warmup asset ({asset_id})...")

            try:
                # Create Asset object with the standard asset ID
                # Note: filename is just a client-side label; backend knows the real filename
                asset = Asset(
                    asset_id=asset_id,
                    filename="<standard_warmup_asset>",
                    content_type="audio/wav",
                )

                # Try to submit with standard asset (should be already uploaded)
                job_id = self.submit(asset)

                # Poll for completion with progress updates
                def progress_callback(status: dict[str, Any]) -> None:
                    if show_progress:
                        elapsed = int(time.time() - start_time)
                        print(f"⏳ Warming up... ({elapsed}s)")

                # Poll with progress callback
                self.poll(
                    job_id=job_id,
                    timeout=timeout,
                    progress_callback=progress_callback if show_progress else None,
                )

                warmup_time = int(time.time() - start_time)

                if show_progress:
                    print(f"✅ Backend warmed up successfully! (took {warmup_time}s)")

                return {
                    "status": "success",
                    "warmup_time": warmup_time,
                    "message": f"Backend ready. Warmup completed in {warmup_time}s.",
                    "warmup_job_id": job_id,
                    "used_standard_asset": True,
                }

            except (APIError, InferenceError) as e:
                # Standard asset not available - warn and fall back to creating new file
                error_msg = str(e).lower()
                if (
                    "not found" in error_msg
                    or "does not exist" in error_msg
                    or "upload" in error_msg
                ):
                    if show_progress:
                        print(
                            "⚠️  Standard warmup asset not available (may have been deleted).\n"
                            "   Falling back to creating new warmup file..."
                        )
                else:
                    # Unexpected error - re-raise
                    raise

        # Fall back to creating and uploading new dummy file
        temp_fd = None
        temp_path = None
        try:
            # Import soundfile (lazy import to avoid requiring it for all operations)
            try:
                import numpy as np
                import soundfile as sf
            except ImportError as e:
                raise ConfigurationError(
                    "Warmup requires numpy and soundfile packages. "
                    "Install with: pip install numpy soundfile"
                ) from e

            # Create temporary file with identifiable name
            temp_fd, temp_path = tempfile.mkstemp(
                suffix=".wav", prefix="warmup_redenlab_", dir=None  # Use system temp directory
            )

            # Close the file descriptor, we'll write with soundfile
            os.close(temp_fd)
            temp_fd = None

            # Generate 1 second of silence at 16kHz
            sample_rate = 16000
            duration = 1.0
            dummy_audio = np.zeros(int(sample_rate * duration), dtype=np.float32)

            # Write dummy audio to temp file
            sf.write(temp_path, dummy_audio, sample_rate)

            if show_progress:
                print("Creating and uploading warmup file...")

            # Upload the dummy file
            asset = self._session.upload(temp_path)

            if show_progress:
                print("Submitting warmup job (waiting for upload to complete)...")

            # Submit with retries - backend needs time to process S3 event and update status
            job_id = self._submit_with_upload_wait(asset)

            # Poll for completion with progress updates
            def progress_callback(status: dict[str, Any]) -> None:
                if show_progress:
                    elapsed = int(time.time() - start_time)
                    print(f"⏳ Warming up... ({elapsed}s)")

            # Poll with progress callback
            self.poll(
                job_id=job_id,
                timeout=timeout,
                progress_callback=progress_callback if show_progress else None,
            )

            warmup_time = int(time.time() - start_time)

            if show_progress:
                print(f"✅ Backend warmed up successfully! (took {warmup_time}s)")

            return {
                "status": "success",
                "warmup_time": warmup_time,
                "message": f"Backend ready. Warmup completed in {warmup_time}s.",
                "warmup_job_id": job_id,
                "used_standard_asset": False,
            }

        finally:
            # Always cleanup temp file
            if temp_fd is not None:
                try:
                    os.close(temp_fd)
                except OSError:
                    pass

            if temp_path and os.path.exists(temp_path):
                try:
                    os.remove(temp_path)
                except OSError:
                    pass  # Best effort cleanup

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

        parts = [
            f"session=RedenLabSession(api_key='{mask_api_key(self._session.api_key)}')",
            f"model_name='{self.model_name}'",
            f"timeout={self.timeout}",
        ]
        return f"{self.__class__.__name__}({', '.join(parts)})"


class TranscribeClient(InferenceClient):
    """
    Client for the transcribe model with language_code handling.

    Args:
        session: RedenLabSession instance
        language_code: Language code for transcription (optional, e.g., 'en-US', 'es-ES').
                       Can be overridden per job.
        timeout: Maximum time to wait for inference in seconds (default: 3600)

    Example:
        >>> session = RedenLabSession(api_key="...")
        >>> client = TranscribeClient(session, language_code="en-US")
        >>> asset = session.upload("audio.wav")
        >>> result = client.predict(asset)  # Uses default language
        >>> result = client.predict(asset, language_code="es-ES")  # Override
    """

    # Transcribe uses AWS managed service, no health endpoint
    _supports_health_check = False

    def __init__(
        self,
        session: RedenLabSession,
        language_code: str | None = None,
        timeout: int = 3600,
    ):
        super().__init__(session, model_name="transcribe", timeout=timeout)
        self.language_code = language_code

    def submit(self, asset: Asset, **model_params: Any) -> str:
        """
        Submit transcribe job with language_code handling.

        Args:
            asset: Previously uploaded Asset
            **model_params: May include language_code to override default

        Returns:
            job_id: Job identifier

        Raises:
            ValueError: If no language_code is provided (either here or in __init__)
        """
        # Use provided language_code or instance default
        if "language_code" not in model_params and self.language_code:
            model_params = {**model_params, "language_code": self.language_code}

        return super().submit(asset, **model_params)

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

        parts = [
            f"session=RedenLabSession(api_key='{mask_api_key(self._session.api_key)}')",
            f"timeout={self.timeout}",
        ]
        if self.language_code:
            parts.append(f"language_code='{self.language_code}'")
        return f"TranscribeClient({', '.join(parts)})"


# Factory functions for intelligibility and naturalness clients
# These collapse the old IntelligibilityClient and NaturalnessClient classes
# into simple functions, eliminating 200+ lines of duplicated code

INTELLIGIBILITY_MODELS = {
    "als-intelligibility-v1": "als-intelligibility-mtpa",
    "als-intelligibility-v2": "als-intelligibility-eals",
    "ataxia-intelligibility": "ataxia-intelligibility",
}

NATURALNESS_MODELS = {
    "als-naturalness-v1": "als-naturalness-mtpa",
    "als-naturalness-v2": "als-naturalness-eals",
    "ataxia-naturalness": "ataxia-naturalness",
}


def intelligibility_client(
    session: RedenLabSession,
    model_name: str,
    timeout: int = 3600,
) -> InferenceClient:
    """
    Create an intelligibility inference client with alias mapping.

    This factory function replaces the old IntelligibilityClient class.
    It validates the model name and maps user-facing aliases to backend names.

    Args:
        session: RedenLabSession instance
        model_name: User-facing model name (e.g., 'als-intelligibility-v1')
        timeout: Maximum time to wait for inference in seconds (default: 3600)

    Returns:
        InferenceClient configured for the intelligibility model

    Raises:
        ValidationError: If model_name is not a valid intelligibility model

    Example:
        >>> session = RedenLabSession(api_key="...")
        >>> client = intelligibility_client(session, model_name="als-intelligibility-v1")
        >>> asset = session.upload("audio.wav")
        >>> result = client.predict(asset)
        >>> print(result['result']['intelligibility_score'])
    """
    if model_name not in INTELLIGIBILITY_MODELS:
        raise ValidationError(
            f"Invalid intelligibility model: {model_name}. "
            f"Valid models: {', '.join(INTELLIGIBILITY_MODELS.keys())}"
        )

    backend_model_name = INTELLIGIBILITY_MODELS[model_name]
    client = InferenceClient(session, model_name=backend_model_name, timeout=timeout)

    # Set user-facing model name (the alias)
    client._user_model_name = model_name

    # Set score key for aggregation in chunked inference
    client.score_key = "intelligibility_score"

    return client


def naturalness_client(
    session: RedenLabSession,
    model_name: str,
    timeout: int = 3600,
) -> InferenceClient:
    """
    Create a naturalness inference client with alias mapping.

    This factory function replaces the old NaturalnessClient class.
    It validates the model name and maps user-facing aliases to backend names.

    Args:
        session: RedenLabSession instance
        model_name: User-facing model name (e.g., 'als-naturalness-v1')
        timeout: Maximum time to wait for inference in seconds (default: 3600)

    Returns:
        InferenceClient configured for the naturalness model

    Raises:
        ValidationError: If model_name is not a valid naturalness model

    Example:
        >>> session = RedenLabSession(api_key="...")
        >>> client = naturalness_client(session, model_name="als-naturalness-v1")
        >>> asset = session.upload("audio.wav")
        >>> result = client.predict(asset)
        >>> print(result['result']['naturalness_score'])
    """
    if model_name not in NATURALNESS_MODELS:
        raise ValidationError(
            f"Invalid naturalness model: {model_name}. "
            f"Valid models: {', '.join(NATURALNESS_MODELS.keys())}"
        )

    backend_model_name = NATURALNESS_MODELS[model_name]
    client = InferenceClient(session, model_name=backend_model_name, timeout=timeout)

    # Set user-facing model name (the alias)
    client._user_model_name = model_name

    # Set score key for aggregation in chunked inference
    client.score_key = "naturalness_score"

    return client


def sylber_time_client(
    session: RedenLabSession,
    timeout: int = 3600,
) -> InferenceClient:
    """
    Create a sylber-time inference client.

    Args:
        session: RedenLabSession instance
        timeout: Maximum time to wait for inference in seconds (default: 3600)

    Returns:
        InferenceClient configured for the sylber-time model

    Example:
        >>> session = RedenLabSession(api_key="...")
        >>> client = sylber_time_client(session)
        >>> asset = session.upload("audio.wav")
        >>> result = client.predict(asset)
        >>> print(result['result'])
    """
    client = InferenceClient(session, model_name="sylber-time", timeout=timeout)

    # Set user-facing model name
    client._user_model_name = "sylber-time"

    return client


def speaker_counter_client(
    session: RedenLabSession,
    timeout: int = 3600,
) -> InferenceClient:
    """
    Create a speaker counter (diarization) inference client.

    This model performs speaker diarization to identify and count unique speakers
    in an audio file, along with their speaking segments and timestamps.

    Args:
        session: RedenLabSession instance
        timeout: Maximum time to wait for inference in seconds (default: 3600)

    Returns:
        InferenceClient configured for the speaker-counter model

    Example:
        >>> session = RedenLabSession(api_key="...")
        >>> client = speaker_counter_client(session)
        >>> asset = session.upload("audio.wav")
        >>> result = client.predict(asset)
        >>> print(f"Speaker count: {result['result']['speaker_count']}")
        >>> for segment in result['result']['segments']:
        ...     print(f"  {segment['speaker']}: {segment['start_time']}-{segment['end_time']}s")
    """
    client = InferenceClient(session, model_name="speaker_counter", timeout=timeout)

    # Set user-facing model name
    client._user_model_name = "speaker_counter"
    client.score_key = "speaker_count"

    return client
