"""
Module: src.transcription.whisperx_pipeline
Workorder: WO01
Purpose: WhisperX transcription orchestration for local MVP pipeline.

Dependencies:
    - pathlib: File path handling
    - importlib: Runtime import for optional heavy dependencies

Inputs:
    - normalized_audio_path: Path to normalized audio
    - language_mode: Language mode selection

Outputs:
    - Structured transcription payload for downstream export

Key Functions/Classes:
    - transcribe_with_whisperx: Executes transcription pipeline

Tests: tests/unit/test_placeholder.py
"""
import importlib
import inspect
from pathlib import Path
from typing import Any, Callable, Optional

from src.shared.errors import ProcessingError

ALLOWED_LANGUAGE_MODES = {"auto", "de", "en"}


def _emit_progress(
    progress_callback: Optional[Callable[[float], None]], value: float
) -> None:
    if progress_callback is not None:
        progress_callback(value)


def _resolve_runtime_device(requested_device: str) -> str:
    if requested_device != "mps":
        return requested_device

    try:
        torch = importlib.import_module("torch")
        mps_backend = getattr(torch.backends, "mps", None)
        if bool(mps_backend) and torch.backends.mps.is_available():
            return "mps"
    except ModuleNotFoundError:
        pass

    return "cpu"


def _compute_types_for_device(device: str) -> list[str]:
    if device == "cpu":
        return ["int8", "float32"]
    if device == "mps":
        return ["float16", "int8", "float32"]
    return ["float16", "int8", "float32"]


def _model_candidates_for_device(model_name: str, device: str) -> list[str]:
    if device != "cpu":
        return [model_name]

    candidates = [model_name]
    if model_name not in {"small", "base"}:
        candidates.extend(["small", "base"])
    return candidates


def transcribe_with_whisperx(
    normalized_audio_path: Path,
    language_mode: str,
    initial_prompt: str = "",
    device: str = "mps",
    model_name: str = "medium",
    temperature: float = 0.0,
    progress_callback: Optional[Callable[[float], None]] = None,
) -> dict[str, Any]:
    """Transcribe normalized audio using WhisperX.

    Args:
        normalized_audio_path: Path to normalized audio.
        language_mode: `auto`, `de`, or `en`.
        initial_prompt: Initial prompt for vocabulary boosting.
        device: Runtime device, expected `mps` for Apple Silicon.
        model_name: Whisper model name.
        temperature: Decoding temperature.
        progress_callback: Optional callback for progress updates 0..1.

    Returns:
        Structured transcription dictionary with language and segments.

    Raises:
        ProcessingError: If dependencies or runtime requirements are not met.
    """
    if language_mode not in ALLOWED_LANGUAGE_MODES:
        allowed = ", ".join(sorted(ALLOWED_LANGUAGE_MODES))
        raise ProcessingError(f"Invalid language mode '{language_mode}'. Allowed: {allowed}")

    if not normalized_audio_path.exists():
        raise ProcessingError(f"Normalized audio file not found: {normalized_audio_path}")

    _emit_progress(progress_callback, 0.1)

    try:
        whisperx = importlib.import_module("whisperx")
    except ModuleNotFoundError as error:
        raise ProcessingError(
            "WhisperX is not installed. Install dependencies before running transcription."
        ) from error

    _emit_progress(progress_callback, 0.25)

    language_arg = None if language_mode == "auto" else language_mode

    runtime_device = _resolve_runtime_device(device)
    load_errors: list[str] = []
    model = None

    for candidate_device in [runtime_device, "cpu"] if runtime_device != "cpu" else ["cpu"]:
        for candidate_model in _model_candidates_for_device(model_name, candidate_device):
            for compute_type in _compute_types_for_device(candidate_device):
                try:
                    model = whisperx.load_model(
                        candidate_model,
                        candidate_device,
                        compute_type=compute_type,
                    )
                    runtime_device = candidate_device
                    break
                except Exception as load_error:
                    load_errors.append(
                        "device="
                        f"{candidate_device}, model={candidate_model}, "
                        f"compute_type={compute_type}, error={load_error}"
                    )

            if model is not None:
                break

        if model is not None:
            break

    if model is None:
        details = " | ".join(load_errors[-4:])
        raise ProcessingError(
            "WhisperX model loading failed for all device/compute_type candidates. "
            f"Details: {details}"
        )

    _emit_progress(progress_callback, 0.45)

    transcribe_kwargs: dict[str, Any] = {}
    transcribe_signature = inspect.signature(model.transcribe)
    if "language" in transcribe_signature.parameters:
        transcribe_kwargs["language"] = language_arg
    if "temperature" in transcribe_signature.parameters:
        transcribe_kwargs["temperature"] = temperature
    if "initial_prompt" in transcribe_signature.parameters:
        transcribe_kwargs["initial_prompt"] = initial_prompt or None

    raw_result = model.transcribe(
        str(normalized_audio_path),
        **transcribe_kwargs,
    )
    _emit_progress(progress_callback, 0.8)

    segments: list[dict[str, Any]] = []
    for segment in raw_result.get("segments", []):
        confidence_value = segment.get("confidence")
        if confidence_value is None:
            confidence_value = 1.0

        segments.append(
            {
                "speaker": "Speaker 0",
                "start": float(segment.get("start", 0.0)),
                "end": float(segment.get("end", 0.0)),
                "text": str(segment.get("text", "")).strip(),
                "confidence": float(confidence_value),
            }
        )

    _emit_progress(progress_callback, 1.0)

    return {
        "language": raw_result.get("language", language_mode if language_mode != "auto" else "unknown"),
        "segments": segments,
        "text": " ".join(segment["text"] for segment in segments).strip(),
    }
