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

from __future__ import annotations

import logging
import mimetypes
from collections.abc import Sequence
from contextlib import contextmanager
from pathlib import Path
from uuid import UUID

from sqlalchemy import func
from sqlalchemy.ext.asyncio import async_sessionmaker
from sqlalchemy.orm import selectinload
from sqlmodel import select
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, *, session: AsyncSession | None = None) -> Transcript:
        """Create a new transcript in the database."""
        async with self._session_scope(session) as _session:
            _session.add(transcript)
            await self._finalize(session=_session, caller_session=session, refresh=(transcript,))
            return transcript

    async def read_transcript(self, transcript_id: UUID, *, session: AsyncSession | None = None) -> Transcript:
        """Read an existing transcript from the database."""
        async with self._session_scope(session) 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, *, session: AsyncSession | None = None) -> Transcript:
        """Update an existing transcript in the database."""
        async with self._session_scope(session) as _session:
            merged = await _session.merge(transcript)
            await self._finalize(session=_session, caller_session=session, refresh=(merged,))
            return merged

    async def delete_transcript(self, transcript: Transcript, *, session: AsyncSession | None = None) -> None:
        """Delete a transcript from the database."""
        async with self._session_scope(session) as _session:
            await _session.delete(transcript)
            await self._finalize(session=_session, caller_session=session)

    async def transcribe_document(
        self,
        image_path: str | Path,
        job_id: UUID,
        *,
        prompt_name: str = DEFAULT_PROMPT_FILE,
        session: AsyncSession | None = None,
    ):
        """Transcribe a local image using the configured prompt and provider."""
        result = await transcribe_document_image(
            image_path=image_path,
            prompt_name=prompt_name,
            settings=self.settings,
            provider=self.provider,
        )
        await self.create_transcript_for_job(
            job_id=job_id,
            text=result.text,
            provider=result.provider,
            model=result.model,
            prompt_name=result.prompt_name,
            session=session,
        )

    async def create_transcript_for_job(
        self,
        *,
        job_id: UUID,
        text: str | None,
        error_detail: str | None = None,
        provider: str | None = None,
        model: str | None = None,
        prompt_name: str = DEFAULT_PROMPT_FILE,
        session: AsyncSession | None = None,
    ) -> Transcript:
        """Create a new transcript revision for a job id."""
        async with self._session_scope(session) as _session:
            rev_query = select(func.max(Transcript.revision)).where(Transcript.job_id == job_id)
            rev_result = await _session.exec(rev_query)
            max_revision = -1 if (rev := rev_result.one_or_none()) is None else rev
            next_revision = max_revision + 1

            transcript = Transcript(
                job_id=job_id,
                revision=next_revision,
                provider=provider or self.settings.provider.value,
                model=model or _resolve_transcript_model(provider=self.provider, settings=self.settings),
                prompt_name=prompt_name,
                text=text,
                error_detail=error_detail,
            )
            _session.add(transcript)
            await self._finalize(session=_session, caller_session=session, refresh=(transcript,))
            return transcript

    async def read_latest_transcript_by_job(
        self,
        job_id: UUID,
        *,
        session: AsyncSession | None = None,
    ) -> Transcript | None:
        """Read the latest transcript revision for a job id."""
        async with self._session_scope(session) as _session:
            query = _transcript_job_query(job_id=job_id).limit(1)
            result = await _session.exec(query)
            return result.one_or_none()

    async def list_transcripts_by_job(
        self,
        job_id: UUID,
        *,
        session: AsyncSession | None = None,
    ) -> Sequence[Transcript]:
        """List transcript revisions for a job id in ascending revision order."""
        async with self._session_scope(session) as _session:
            query = _transcript_job_query(job_id=job_id)
            result = await _session.exec(query)
            return result.all()


def _transcript_job_query(job_id: UUID):
    return (
        select(Transcript)
        .where(Transcript.job_id == job_id)
        .options(selectinload(Transcript.job))  # pyright: ignore[reportArgumentType]
        .order_by(Transcript.revision)  # pyright: ignore[reportArgumentType]
    )  # fmt: skip


def _resolve_transcript_model(*, provider: TranscriptionProvider, settings: Settings) -> str:
    provider_model = getattr(provider, "model", None)
    if isinstance(provider_model, str) and provider_model.strip():
        return provider_model

    if settings.provider_model and settings.provider_model.strip():
        return settings.provider_model

    return "unknown"


async 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 = await 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


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


@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
