"""Prompt loading and provider-backed transcription service."""

from __future__ import annotations

import logging
import mimetypes
from contextlib import contextmanager
from pathlib import Path
from uuid import UUID

from sqlalchemy.ext.asyncio import async_sessionmaker
from sqlalchemy.orm import selectinload
from sqlmodel.ext.asyncio.session import AsyncSession

from transcription.config import Settings
from transcription.config import get_settings
from transcription.errors import AppError
from transcription.errors import ErrorCategory
from transcription.models import Transcript
from transcription.providers import ProviderAuthError
from transcription.providers import ProviderError
from transcription.providers import ProviderResponseError
from transcription.providers import TranscriptionProvider
from transcription.providers import TranscriptionResult
from transcription.providers import get_transcription_provider

from .base import ServiceBase

logger = logging.getLogger(__name__)

DEFAULT_PROMPT_FILE = "transcribe_document.md"
SUPPORTED_EXTENSIONS = {".jpg", ".jpeg", ".png", ".tif", ".tiff", ".pdf"}


class PromptLoadError(AppError):
    """Raised when prompt artifacts cannot be loaded safely."""


class TranscriptionError(AppError):
    """Raised when transcription execution fails."""


class TranscriptionNotFoundError(TranscriptionError):
    """Raised when a transcription is not found in the database."""


class TranscriptionService(ServiceBase):
    """Service class for managing transcription operations.

    This is the top-level service that composes functionality from the other services."""

    provider: TranscriptionProvider

    def __init__(self, session_factory: async_sessionmaker[AsyncSession] | None = None):
        super().__init__(session_factory=session_factory)
        self.provider = get_transcription_provider(settings=self.settings)

    async def create_transcript(self, transcript: Transcript) -> Transcript:
        """Create a new transcript in the database."""
        async with self.session_factory() as session:
            session.add(transcript)
            await session.commit()
            await session.refresh(transcript)
            return transcript

    async def read_transcript(self, transcript_id: UUID) -> Transcript:
        """Read an existing transcript from the database."""
        async with self.session_factory() as session:
            transcript = await session.get(
                Transcript,
                transcript_id,
                # Makes the full Job model object available in the return Transcript object
                options=(selectinload(Transcript.job),),  # pyright: ignore[reportArgumentType]
            )
        if transcript is None:
            raise TranscriptionNotFoundError(
                f"Transcript with id {transcript_id} not found",
                category=ErrorCategory.NOT_FOUND,
                suggestion="Verify the transcript id and retry.",
            )
        return transcript

    async def update_transcript(self, transcript: Transcript) -> Transcript:
        """Update an existing transcript in the database."""
        async with self.session_factory() as session:
            await session.merge(transcript)
            await session.commit()
            await session.refresh(transcript)
            return transcript

    async def delete_transcript(self, transcript: Transcript) -> None:
        """Delete a transcript from the database."""
        async with self.session_factory() as session:
            await session.delete(transcript)
            await session.commit()

    async def transcribe_document(
        self,
        image_path: str | Path,
        *,
        prompt_name: str = DEFAULT_PROMPT_FILE,
    ) -> TranscriptionResult:
        """Transcribe a local image using the configured prompt and provider."""
        return transcribe_document_image(
            image_path=image_path,
            prompt_name=prompt_name,
            settings=self.settings,
            provider=self.provider,
        )


def load_prompt_text(*, prompt_name: str = DEFAULT_PROMPT_FILE, settings: Settings | None = None) -> str:
    """Load and validate prompt text from PROMPT_DIR."""
    runtime_settings = settings or get_settings()
    prompt_path = runtime_settings.prompt_dir / prompt_name

    if not prompt_path.exists() or not prompt_path.is_file():
        raise PromptLoadError(
            f"Prompt file not found: {prompt_path}",
            category=ErrorCategory.INFRA_PERSISTENT,
            suggestion="Verify PROMPT_DIR and prompt file configuration, then retry.",
        )

    prompt_text = prompt_path.read_text(encoding="utf-8").strip()
    if not prompt_text:
        raise PromptLoadError(
            f"Prompt file is empty: {prompt_path}",
            category=ErrorCategory.VALIDATION,
            suggestion="Populate the prompt file with valid instructions and retry.",
        )

    logger.info("Loaded prompt artifact: %s", prompt_path)
    return prompt_text


def load_image_payload(image_path: str | Path) -> tuple[bytes, str]:
    """Read image bytes and detect mime type for supported uploads."""
    path = Path(image_path)

    if not path.exists() or not path.is_file():
        raise TranscriptionError(
            f"Image file not found: {path}",
            category=ErrorCategory.NOT_FOUND,
            suggestion="Verify the uploaded file exists and retry from the jobs page.",
        )

    suffix = path.suffix.lower()
    if suffix not in SUPPORTED_EXTENSIONS:
        raise TranscriptionError(
            f"Unsupported file type: {suffix}",
            category=ErrorCategory.USER_INPUT,
            suggestion="Use JPG, JPEG, PNG, TIFF, or PDF files.",
        )

    mime_type, _ = mimetypes.guess_type(path.name)
    if suffix in {".tif", ".tiff"}:
        mime_type = "image/tiff"
    if not mime_type:
        raise TranscriptionError(
            f"Unable to determine MIME type for: {path}",
            category=ErrorCategory.VALIDATION,
            suggestion="Re-save the file in a supported format and retry.",
        )

    return path.read_bytes(), mime_type


def transcribe_document_image(
    image_path: str | Path,
    *,
    prompt_name: str = DEFAULT_PROMPT_FILE,
    settings: Settings | None = None,
    provider: TranscriptionProvider | None = None,
) -> TranscriptionResult:
    """Transcribe a local image using the configured prompt and provider."""
    runtime_settings = settings or get_settings()
    prompt_text = load_prompt_text(prompt_name=prompt_name, settings=runtime_settings)
    image_bytes, mime_type = load_image_payload(image_path)

    adapter = provider or get_transcription_provider(settings=runtime_settings)
    logger.info("Starting transcription for image=%s mime_type=%s", image_path, mime_type)

    with handle_transcription_errors():
        result = adapter.transcribe(
            prompt_text=prompt_text,
            image_bytes=image_bytes,
            mime_type=mime_type,
        )
    logger.info("Transcription completed for image=%s provider=%s", image_path, result.provider)
    return result


@contextmanager
def handle_transcription_errors():
    """Context manager to handle transcription errors."""
    try:
        yield
    except ProviderAuthError as exc:
        raise TranscriptionError(
            "Provider authentication failed",
            category=ErrorCategory.INFRA_PERSISTENT,
            suggestion="Verify provider API credentials and retry.",
        ) from exc
    except ProviderResponseError as exc:
        raise TranscriptionError(
            "Provider returned an invalid response",
            category=ErrorCategory.EXTERNAL_PROVIDER,
            suggestion="Retry once. If it persists, try a different provider model or inspect provider status.",
            retriable=True,
        ) from exc
    except ProviderError as exc:
        raise TranscriptionError(
            "Provider transcription failed",
            category=ErrorCategory.EXTERNAL_PROVIDER,
            suggestion="Retry the transcription from jobs. If repeated, check provider availability.",
            retriable=True,
        ) from exc
