import asyncio
import logging

from sqlmodel.ext.asyncio.session import AsyncSession

from ..config import Settings
from ..config import get_settings
from ..errors import AppError
from ..errors import classify_unexpected_error
from ..errors import format_error_detail
from ..models import Job
from ..models import JobStatus
from ..providers import TranscriptionResult
from . import ServiceBundle
from .transcription import DEFAULT_PROMPT_FILE
from .transcription import transcribe_document_image

logger = logging.getLogger(__name__)


async def advance_job(
    job: Job,
    services: ServiceBundle,
    settings: Settings | None = None,
    session: AsyncSession | None = None,
) -> Job | None:
    """Advance a single job by lifecycle status."""
    settings = settings or get_settings()
    match job.status:
        case JobStatus.QUEUED:
            return await process_queued_job(job=job, services=services, session=session)
        case JobStatus.FAILED:
            if job.retry_count < settings.worker_max_retries:
                return await services.jobs.update_job_state(
                    job_id=job.id,
                    status=JobStatus.QUEUED,
                    retry_count_increment=1,
                    session=session,
                )
            else:
                logger.error(f"Job {job.id} has failed and reached max retries.")
                return
        case _:
            return


async def process_queued_job(
    *,
    job: Job,
    services: ServiceBundle,
    session: AsyncSession | None = None,
) -> Job | None:
    """Process one complete transcription attempt for a queued job."""
    if job.status != JobStatus.QUEUED:
        logger.warning(f"Job {job.id} is not queued. Current status: {job.status}")
        return

    # Transaction A: claim job for processing.
    if session is None:
        job = await services.jobs.mark_job_status(job.id, JobStatus.PROCESSING)
    else:
        # If we're sharing the session, need to make sure setting the Job to PROCESSING is committed before we start
        # the transcription, otherwise other workers may see the job as still QUEUED and try to process it.
        job = await services.jobs.mark_job_status(job.id, JobStatus.PROCESSING, session=session)
        await session.commit()

    document = job.document
    assert document is not None, (
        f"Job {job.id} has no associated document or the document failed to be loaded by the job service."
    )

    try:
        result = await transcribe_document_image(document.file_path)
        job = await _finalize_transcribed(job=job, services=services, result=result, session=session)
        logger.info(
            "Job transcribed operation=worker.process_job job_id=%s document_id=%s provider=%s",
            job.id,
            document.id,
            result.provider,
        )
    except Exception as exc:  # noqa: BLE001
        match exc:
            case AppError() as error:
                pass
            case _:
                error = classify_unexpected_error(exc, operation="worker.process_job")

        job = await _finalize_failed(job=job, services=services, error=error, session=session)
        logger.error(
            "Job failed operation=worker.process_job job_id=%s document_id=%s error_id=%s category=%s",
            job.id,
            document.id,
            error.error_id,
            error.category.value,
        )
    return job


async def process_next_queued_job(
    *,
    services: ServiceBundle,
    settings: Settings | None = None,
    session: AsyncSession | None = None,
) -> bool:
    """Process the next queued job if one exists."""
    job = await services.jobs.read_next_queued_job(session=session)
    if job is None:
        return False

    await advance_job(job=job, services=services, settings=settings, session=session)
    return True


async def _finalize_transcribed(
    *,
    job: Job,
    services: ServiceBundle,
    result: TranscriptionResult,
    session: AsyncSession | None = None,
) -> Job:
    """Transaction B: transcript + TRANSCRIBED in one commit."""
    if session is None:
        async with services.jobs._session_scope() as local_session:
            await services.transcriptions.create_transcript_for_job(
                job_id=job.id,
                text=result.text,
                error_detail=None,
                provider=result.provider,
                model=result.model,
                prompt_name=result.prompt_name,
                session=local_session,
            )
            updated_job = await services.jobs.mark_job_status(
                job.id,
                JobStatus.TRANSCRIBED,
                session=local_session,
            )
            await local_session.commit()
            return updated_job

    await services.transcriptions.create_transcript_for_job(
        job_id=job.id,
        text=result.text,
        error_detail=None,
        provider=result.provider,
        model=result.model,
        prompt_name=result.prompt_name,
        session=session,
    )
    updated_job = await services.jobs.mark_job_status(
        job.id,
        JobStatus.TRANSCRIBED,
        session=session,
    )
    await session.commit()
    return updated_job


async def _finalize_retry(
    *,
    job: Job,
    services: ServiceBundle,
    error: AppError,
    settings: Settings,
    session: AsyncSession | None = None,
) -> Job:
    """Transaction C: transcript error + QUEUED + retry increment in one commit."""
    if session is None:
        async with services.jobs._session_scope() as local_session:
            await services.transcriptions.create_transcript_for_job(
                job_id=job.id,
                text=None,
                error_detail=format_error_detail(error),
                prompt_name=DEFAULT_PROMPT_FILE,
                session=local_session,
            )
            updated_job = await services.jobs.update_job_state(
                job_id=job.id,
                status=JobStatus.QUEUED,
                retry_count_increment=1,
                session=local_session,
            )
            await local_session.commit()
    else:
        await services.transcriptions.create_transcript_for_job(
            job_id=job.id,
            text=None,
            error_detail=format_error_detail(error),
            prompt_name=DEFAULT_PROMPT_FILE,
            session=session,
        )
        updated_job = await services.jobs.update_job_state(
            job_id=job.id,
            status=JobStatus.QUEUED,
            retry_count_increment=1,
            session=session,
        )
        await session.commit()

    if settings.worker_retry_backoff_seconds > 0:
        await asyncio.sleep(settings.worker_retry_backoff_seconds)
    return updated_job


async def _finalize_failed(
    *,
    job: Job,
    services: ServiceBundle,
    error: AppError,
    session: AsyncSession | None = None,
) -> Job:
    """Transaction B: transcript error + FAILED in one commit."""
    if session is None:
        async with services.jobs._session_scope() as local_session:
            await services.transcriptions.create_transcript_for_job(
                job_id=job.id,
                text=None,
                error_detail=format_error_detail(error),
                prompt_name=DEFAULT_PROMPT_FILE,
                session=local_session,
            )
            updated_job = await services.jobs.mark_job_status(
                job.id,
                JobStatus.FAILED,
                session=local_session,
            )
            await local_session.commit()
            return updated_job

    await services.transcriptions.create_transcript_for_job(
        job_id=job.id,
        text=None,
        error_detail=format_error_detail(error),
        prompt_name=DEFAULT_PROMPT_FILE,
        session=session,
    )
    updated_job = await services.jobs.mark_job_status(
        job.id,
        JobStatus.FAILED,
        session=session,
    )
    await session.commit()
    return updated_job
