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

from __future__ import annotations

import asyncio
import logging
from collections.abc import AsyncGenerator
from contextlib import asynccontextmanager
from contextlib import contextmanager
from contextlib import suppress
from typing import Protocol
from uuid import UUID

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

from transcription.db import session_scope
from transcription.errors import AppError
from transcription.errors import classify_unexpected_error

from .services import ServiceBundle
from .services.documents import DocumentService
from .services.jobs import JobService
from .services.transcription import TranscriptionService
from .services.workflows import advance_job
from .services.workflows import process_next_queued_job as process_next_queued_job_workflow

logger = logging.getLogger(__name__)


class WorkerNotifier(Protocol):
    """Abstraction for signaling the worker loop about new work."""

    def notify(self) -> None:
        """Signal the worker loop that work may be available."""


class EventWorkerNotifier:
    """Worker notifier backed by an asyncio.Event."""

    def __init__(self, wake_event: asyncio.Event):
        self._wake_event = wake_event

    def notify(self) -> None:
        self._wake_event.set()


class NoopWorkerNotifier:
    """Fallback notifier used when worker signaling is unavailable."""

    def notify(self) -> None:
        return


def resolve_worker_notifier(state: object) -> WorkerNotifier:
    """Resolve notifier from app-like state objects with no-op fallback."""
    notifier = getattr(state, "worker_notifier", None)
    if isinstance(notifier, NoopWorkerNotifier):
        return notifier
    if notifier is None:
        return NoopWorkerNotifier()
    return notifier


@asynccontextmanager
async def worker_consumer_lifespan(
    *,
    session_factory: async_sessionmaker[AsyncSession] | None = None,
    poll_interval_seconds: float = 1.0,
) -> AsyncGenerator[tuple[asyncio.Event, WorkerNotifier]]:
    """Start and stop the worker consumer loop for app lifespan."""
    stop_event = asyncio.Event()
    wake_event = asyncio.Event()
    worker_notifier: WorkerNotifier = EventWorkerNotifier(wake_event)
    worker_task = asyncio.create_task(
        run_worker_loop(
            session_factory=session_factory,
            stop_event=stop_event,
            wake_event=wake_event,
            poll_interval_seconds=poll_interval_seconds,
        )
    )
    worker_notifier.notify()

    try:
        yield stop_event, worker_notifier
    finally:
        stop_event.set()
        worker_notifier.notify()
        try:
            await asyncio.wait_for(worker_task, timeout=2.0)
        except TimeoutError:
            worker_task.cancel()
            with suppress(asyncio.CancelledError):
                await worker_task


async def queue_consumer_loop(queue: asyncio.Queue[UUID], stop_event: asyncio.Event):
    """Main worker loop that consumes jobs from the queue and processes them.

    The queue is for Job UUIDs, and the corresponding documents should already have been uploaded.
    """
    service = JobService()
    while not stop_event.is_set():
        with handle_worker_exceptions():
            async with _get_queue_item(queue) as job_id:
                job = await service.read_job(job_id)
                asyncio.create_task(advance_job(job=job, services=ServiceBundle()))


@contextmanager
def handle_worker_exceptions(operation: str = "worker.loop"):
    """Context manager to log and suppress exceptions in the worker loop."""
    try:
        yield
    except Exception as exc:
        error = exc if isinstance(exc, AppError) else classify_unexpected_error(exc, operation=operation)
        logger.exception(
            "Worker loop exception error_id=%s category=%s",
            error.error_id,
            error.category.value,
        )


@asynccontextmanager
async def _get_queue_item(queue: asyncio.Queue[UUID]) -> AsyncGenerator[UUID]:
    """Context manager to enqueue a job and ensure it is marked done."""
    yield await queue.get()
    queue.task_done()


async def run_worker_loop(
    *,
    session_factory: async_sessionmaker[AsyncSession] | None = None,
    stop_event: asyncio.Event | None = None,
    wake_event: asyncio.Event | None = None,
    poll_interval_seconds: float = 1.0,
) -> None:
    """Run worker loop until stop_event is set.

    If wake_event is provided, signal activity wakes the loop immediately while
    timeout-based wakeups preserve current polling behavior.
    """
    while True:
        if stop_event is not None and stop_event.is_set():
            logger.info("Worker stop event received")
            return

        if wake_event is not None:
            with suppress(TimeoutError):
                await asyncio.wait_for(wake_event.wait(), timeout=poll_interval_seconds)
                wake_event.clear()

        processed_any = False
        while True:
            with handle_worker_exceptions(operation="worker.process_next_queued_job"):
                processed = await process_next_queued_job(session_factory=session_factory)
                if not processed:
                    break
                processed_any = True
                continue

            break

        if wake_event is None and not processed_any:
            await asyncio.sleep(poll_interval_seconds)


async def process_next_queued_job(
    *,
    session: AsyncSession | None = None,
    session_factory: async_sessionmaker[AsyncSession] | 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_factory is None:
        services = ServiceBundle()
    else:
        services = ServiceBundle(
            documents=DocumentService(session_factory=session_factory),
            jobs=JobService(session_factory=session_factory),
            transcriptions=TranscriptionService(session_factory=session_factory),
        )

    if session is None:
        async with session_scope(session_factory=session_factory) as local_session:
            return await process_next_queued_job_workflow(services=services, session=local_session)

    return await process_next_queued_job_workflow(services=services, session=session)
