"""Downloads service — shared validation and routing for artifact downloads."""

import inspect
from collections.abc import Awaitable, Callable
from pathlib import Path
from typing import Any, cast

from ..core.client import NotebookLMClient
from ._compat import TypedDict
from .errors import ServiceError, ValidationError

VALID_ARTIFACT_TYPES = (
    "audio",
    "video",
    "report",
    "mind_map",
    "slide_deck",
    "infographic",
    "data_table",
    "quiz",
    "flashcards",
)

VALID_OUTPUT_FORMATS = ("json", "markdown", "html")

# Types that support async streaming downloads with progress callbacks
STREAMING_TYPES = ("audio", "video", "slide_deck", "infographic")

# Types that support output_format (json/markdown/html)
INTERACTIVE_TYPES = ("quiz", "flashcards")

# Extension map per artifact type (used for default filenames)
DEFAULT_EXTENSIONS = {
    "audio": "m4a",
    "video": "mp4",
    "report": "md",
    "mind_map": "json",
    "slide_deck": "pdf",
    "infographic": "png",
    "data_table": "csv",
    "quiz": "json",  # varies by format
    "flashcards": "json",  # varies by format
}

# Extension map for output formats (quiz/flashcards)
FORMAT_EXTENSIONS = {
    "json": "json",
    "markdown": "md",
    "html": "html",
}


class DownloadResult(TypedDict):
    """Result of a download operation."""

    artifact_type: str
    path: str


# Directories that are always blocked as download targets, regardless of platform.
_BLOCKED_DIRS = {
    ".ssh",
    ".gnupg",
    ".claude",
    ".config",
    ".aws",
    ".kube",
}


_MISMATCHED_AUDIO_EXTENSIONS = {".mp3", ".wav", ".ogg", ".flac", ".aiff", ".wma"}


def validate_audio_extension(output_path: str) -> None:
    """Reject output extensions that don't match NotebookLM's AAC-in-MP4 audio format.

    NotebookLM Studio delivers audio as AAC inside an MP4/M4A container.
    Writing that stream to a `.mp3` (or other incompatible) extension produces
    a file whose bytes don't match the extension, breaking downstream tools.

    Raises ValidationError with a helpful message and ffmpeg workaround.
    """
    suffix = Path(output_path).suffix.lower()
    if suffix in _MISMATCHED_AUDIO_EXTENSIONS:
        raise ValidationError(
            f"NotebookLM delivers AAC audio in an MP4 container; "
            f"cannot honor '{suffix}' suffix.\n"
            f"Re-run with a .m4a or .mp4 suffix, or transcode with ffmpeg:\n"
            f"  nlm download audio <id> -o raw.m4a\n"
            f"  ffmpeg -i raw.m4a -acodec libmp3lame -q:a 2 podcast.mp3",
        )


def validate_output_path(output_path: str) -> None:
    """Validate that output_path is safe and does not escape to sensitive locations.

    Raises ValidationError if the path resolves to a dangerous location.
    """
    resolved = Path(output_path).expanduser().resolve()

    # Block writes into sensitive dotfile directories
    for part in resolved.parts:
        if part in _BLOCKED_DIRS:
            raise ValidationError(
                f"Refusing to write to sensitive directory: {resolved}. "
                f"Choose a different output path."
            )

    # Block overwriting common sensitive files
    _sensitive_files = {
        ".bashrc",
        ".zshrc",
        ".profile",
        ".bash_profile",
        ".gitconfig",
        "authorized_keys",
        "known_hosts",
        "id_rsa",
        "id_ed25519",
    }
    if resolved.name in _sensitive_files:
        raise ValidationError(
            f"Refusing to overwrite sensitive file: {resolved.name}. "
            f"Choose a different output path."
        )


def validate_artifact_type(artifact_type: str) -> None:
    """Validate artifact type. Raises ValidationError if invalid."""
    if artifact_type not in VALID_ARTIFACT_TYPES:
        raise ValidationError(
            f"Unknown artifact type '{artifact_type}'. "
            f"Valid types: {', '.join(VALID_ARTIFACT_TYPES)}",
        )


def validate_output_format(output_format: str) -> None:
    """Validate output format for interactive types. Raises ValidationError if invalid."""
    if output_format not in VALID_OUTPUT_FORMATS:
        raise ValidationError(
            f"Invalid output format '{output_format}'. "
            f"Valid formats: {', '.join(VALID_OUTPUT_FORMATS)}",
        )


def get_default_extension(artifact_type: str, output_format: str = "json") -> str:
    """Get default file extension for an artifact type.

    For interactive types (quiz/flashcards), depends on output_format.
    """
    if artifact_type in INTERACTIVE_TYPES:
        return FORMAT_EXTENSIONS.get(output_format, "json")
    return DEFAULT_EXTENSIONS.get(artifact_type, "bin")


def download_sync(
    client: NotebookLMClient,
    notebook_id: str,
    artifact_type: str,
    output_path: str,
    artifact_id: str | None = None,
    output_format: str = "json",
) -> DownloadResult:
    """Download a non-streaming artifact synchronously.

    For: report, mind_map, data_table, quiz, flashcards.

    Args:
        client: Authenticated NotebookLM client
        notebook_id: Notebook UUID
        artifact_type: Type of artifact
        output_path: Path to save file
        artifact_id: Specific artifact ID (optional)
        output_format: For quiz/flashcards: json|markdown|html

    Returns:
        DownloadResult with artifact_type and path

    Raises:
        ValidationError: If artifact_type or output_format is invalid
        ServiceError: If the download fails
    """
    validate_artifact_type(artifact_type)
    validate_output_path(output_path)

    if artifact_type == "audio":
        validate_audio_extension(output_path)

    if artifact_type in INTERACTIVE_TYPES:
        validate_output_format(output_format)

    try:
        saved_path = _dispatch_sync(
            client,
            notebook_id,
            artifact_type,
            output_path,
            artifact_id,
            output_format,
        )
    except (ValidationError, ServiceError):
        raise
    except Exception as e:
        raise ServiceError(
            f"Failed to download {artifact_type}: {e}",
            user_message=f"Download failed for {artifact_type}.",
        ) from e

    if not saved_path:
        raise ServiceError(
            f"Download returned no path for {artifact_type}",
            user_message=f"{artifact_type} is not ready or does not exist.",
        )

    return {"artifact_type": artifact_type, "path": saved_path}


async def download_async(
    client: NotebookLMClient,
    notebook_id: str,
    artifact_type: str,
    output_path: str,
    artifact_id: str | None = None,
    output_format: str = "json",
    progress_callback: Callable[[int, int], None] | None = None,
    slide_deck_format: str = "pdf",
) -> DownloadResult:
    """Download a streaming artifact asynchronously.

    For: audio, video, slide_deck, infographic, quiz, flashcards.

    Args:
        client: Authenticated NotebookLM client
        notebook_id: Notebook UUID
        artifact_type: Type of artifact
        output_path: Path to save file
        artifact_id: Specific artifact ID (optional)
        output_format: For quiz/flashcards: json|markdown|html
        progress_callback: Called with (current, total) for progress tracking
        slide_deck_format: For slide_deck only: "pdf" (default) or "pptx"

    Returns:
        DownloadResult with artifact_type and path

    Raises:
        ValidationError: If artifact_type or output_format is invalid
        ServiceError: If the download fails
    """
    validate_artifact_type(artifact_type)
    validate_output_path(output_path)

    if artifact_type == "audio":
        validate_audio_extension(output_path)

    if artifact_type in INTERACTIVE_TYPES:
        validate_output_format(output_format)

    try:
        saved_path = await _dispatch_async(
            client,
            notebook_id,
            artifact_type,
            output_path,
            artifact_id,
            output_format,
            progress_callback,
            slide_deck_format=slide_deck_format,
        )
    except (ValidationError, ServiceError):
        raise
    except Exception as e:
        raise ServiceError(
            f"Failed to download {artifact_type}: {e}",
            user_message=f"Download failed for {artifact_type}.",
        ) from e

    if not saved_path:
        raise ServiceError(
            f"Download returned no path for {artifact_type}",
            user_message=f"{artifact_type} is not ready or does not exist.",
        )

    return {"artifact_type": artifact_type, "path": saved_path}


def _dispatch_sync(
    client: NotebookLMClient,
    notebook_id: str,
    artifact_type: str,
    output_path: str,
    artifact_id: str | None,
    output_format: str,
) -> str:
    """Route to the correct synchronous client method."""
    if artifact_type == "report":
        return client.download_report(notebook_id, output_path, artifact_id)
    elif artifact_type == "mind_map":
        return client.download_mind_map(notebook_id, output_path, artifact_id)
    elif artifact_type == "data_table":
        return client.download_data_table(notebook_id, output_path, artifact_id)
    else:
        raise ValidationError(
            f"Artifact type '{artifact_type}' requires async download. "
            f"Use download_async() instead.",
        )


async def _resolve_download_result(result: str | Awaitable[str]) -> str:
    """Await async download results but also accept synchronous implementations."""
    if inspect.isawaitable(result):
        return await result
    return result


def _get_download_method(
    client: NotebookLMClient,
    async_name: str,
    sync_name: str,
) -> Callable[..., Any]:
    """Prefer explicit async client aliases when the concrete client class provides them."""
    if getattr(type(client), async_name, None) is not None:
        return cast(Callable[..., Any], getattr(client, async_name))
    return cast(Callable[..., Any], getattr(client, sync_name))


async def _dispatch_async(
    client: NotebookLMClient,
    notebook_id: str,
    artifact_type: str,
    output_path: str,
    artifact_id: str | None,
    output_format: str,
    progress_callback: Callable[[int, int], None] | None,
    slide_deck_format: str = "pdf",
) -> str:
    """Route to the correct async client method."""
    # Non-streaming types (sync client methods callable from async context)
    if artifact_type == "report":
        return await _resolve_download_result(
            client.download_report(notebook_id, output_path, artifact_id)
        )
    elif artifact_type == "mind_map":
        return await _resolve_download_result(
            client.download_mind_map(notebook_id, output_path, artifact_id)
        )
    elif artifact_type == "data_table":
        return await _resolve_download_result(
            client.download_data_table(notebook_id, output_path, artifact_id)
        )
    # Streaming types (async client methods)
    elif artifact_type == "audio":
        download_audio = _get_download_method(client, "download_audio_async", "download_audio")
        return await _resolve_download_result(
            download_audio(
                notebook_id,
                output_path,
                artifact_id,
                progress_callback=progress_callback,
            )
        )
    elif artifact_type == "video":
        download_video = _get_download_method(client, "download_video_async", "download_video")
        return await _resolve_download_result(
            download_video(
                notebook_id,
                output_path,
                artifact_id,
                progress_callback=progress_callback,
            )
        )
    elif artifact_type == "slide_deck":
        download_slide_deck = _get_download_method(
            client, "download_slide_deck_async", "download_slide_deck"
        )
        return await _resolve_download_result(
            download_slide_deck(
                notebook_id,
                output_path,
                artifact_id,
                progress_callback=progress_callback,
                file_format=slide_deck_format,
            )
        )
    elif artifact_type == "infographic":
        download_infographic = _get_download_method(
            client, "download_infographic_async", "download_infographic"
        )
        return await _resolve_download_result(
            download_infographic(
                notebook_id,
                output_path,
                artifact_id,
                progress_callback=progress_callback,
            )
        )
    elif artifact_type == "quiz":
        download_quiz = _get_download_method(client, "download_quiz_async", "download_quiz")
        return await _resolve_download_result(
            download_quiz(
                notebook_id,
                output_path,
                artifact_id,
                output_format,
            )
        )
    elif artifact_type == "flashcards":
        download_flashcards = _get_download_method(
            client, "download_flashcards_async", "download_flashcards"
        )
        return await _resolve_download_result(
            download_flashcards(
                notebook_id,
                output_path,
                artifact_id,
                output_format,
            )
        )
    else:
        raise ValidationError(
            f"Artifact type '{artifact_type}' is not supported for async download.",
        )
