"""Background worker for queued transcription jobs."""

from __future__ import annotations

import logging
import time
from datetime import datetime, timezone
from threading import Event

from pydantic import ValidationError
from sqlalchemy.engine import Engine
from sqlmodel import Session, select

from transcription.config import Settings, get_settings
from transcription.db import get_session
from transcription.errors import AppError, ErrorCategory, classify_unexpected_error, format_error_detail
from transcription.models import Document, Job, JobStatus, Transcript
from transcription.services.library import add_revision
from transcription.services.transcription import transcribe_document_image

logger = logging.getLogger(__name__)


def process_next_queued_job(*, session: Session | None = None, engine: Engine | None = None) -> bool:
    """Process the next queued job and persist terminal outcome.

    Returns True when a job was processed, False when no queued job exists.
    """
    if session is None:
        with get_session(engine=engine) as local_session:
            return _process_next_queued_job(session=local_session)
    return _process_next_queued_job(session=session)


def _process_next_queued_job(*, session: Session) -> bool:
    job = session.exec(
        select(Job)
        .where(Job.status == JobStatus.QUEUED)
        .order_by(Job.created_at)
    ).first()

    if job is None:
        return False

    logger.info("Picked queued job operation=worker.pick job_id=%s", job.id)
    job.status = JobStatus.PROCESSING
    job.updated_at = datetime.now(timezone.utc)
    session.add(job)
    session.commit()
    session.refresh(job)

    document = session.get(Document, job.document_id)
    if document is None:
        error = AppError(
            "Document not found",
            category=ErrorCategory.NOT_FOUND,
            suggestion="Re-upload the source document and retry processing.",
        )
        _finalize_failed_job(session=session, job=job, error=error)
        logger.error(
            "Job failed operation=worker.process_job job_id=%s error_id=%s category=%s",
            job.id,
            error.error_id,
            error.category.value,
        )
        return True

    try:
        result = transcribe_document_image(document.file_path)
        revision = add_revision(
            job_id=job.id,
            text=result.text,
            source="worker",
            accepted=False,
            session=session,
        )
        job.status = JobStatus.TRANSCRIBED
        job.updated_at = datetime.now(timezone.utc)
        session.add(job)
        session.commit()
        logger.info(
            "Job transcribed operation=worker.process_job job_id=%s document_id=%s provider=%s revision_number=%s",
            job.id,
            document.id,
            result.provider,
            revision.revision_number,
        )
    except Exception as exc:  # noqa: BLE001
        error = exc if isinstance(exc, AppError) else classify_unexpected_error(exc, operation="worker.process_job")
        settings = _get_worker_settings()
        if _should_retry(job=job, error=error, settings=settings):
            _requeue_for_retry(session=session, job=job, error=error, settings=settings)
            logger.warning(
                "Job retried operation=worker.process_job job_id=%s document_id=%s retry_count=%s error_id=%s category=%s",
                job.id,
                document.id,
                job.retry_count,
                error.error_id,
                error.category.value,
            )
        else:
            _finalize_failed_job(session=session, job=job, error=error)
            logger.exception(
                "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 True


def _upsert_transcript(*, session: Session, job_id, text: str | None, error_detail: str | None) -> Transcript:
    transcript = session.exec(select(Transcript).where(Transcript.job_id == job_id)).first()
    if transcript is None:
        transcript = Transcript(job_id=job_id)

    transcript.text = text
    transcript.error_detail = error_detail
    session.add(transcript)
    session.commit()
    session.refresh(transcript)
    return transcript


def _get_worker_settings() -> Settings:
    try:
        return get_settings()
    except ValidationError:
        return Settings(openrouter_api_key="test-key")


def _should_retry(*, job: Job, error: AppError, settings: Settings) -> bool:
    return error.retriable and job.retry_count < settings.worker_max_retries


def _requeue_for_retry(*, session: Session, job: Job, error: AppError, settings: Settings) -> None:
    _upsert_transcript(session=session, job_id=job.id, text=None, error_detail=format_error_detail(error))
    job.retry_count += 1
    job.status = JobStatus.QUEUED
    job.updated_at = datetime.now(timezone.utc)
    session.add(job)
    session.commit()
    if settings.worker_retry_backoff_seconds > 0:
        time.sleep(settings.worker_retry_backoff_seconds)


def _finalize_failed_job(*, session: Session, job: Job, error: AppError) -> None:
    _upsert_transcript(session=session, job_id=job.id, text=None, error_detail=format_error_detail(error))
    job.status = JobStatus.FAILED
    job.updated_at = datetime.now(timezone.utc)
    session.add(job)
    session.commit()


def run_worker_loop(*, engine: Engine | None = None, stop_event: Event | None = None, poll_interval_seconds: float = 1.0) -> None:
    """Run worker polling loop until stop_event is set."""
    while True:
        if stop_event is not None and stop_event.is_set():
            logger.info("Worker stop event received")
            return

        processed = process_next_queued_job(engine=engine)
        if not processed:
            time.sleep(poll_interval_seconds)
