"""Application factory and lifespan wiring for the transcription app."""

from __future__ import annotations

import logging
from contextlib import AsyncExitStack
from contextlib import asynccontextmanager
from datetime import UTC
from datetime import datetime
from datetime import timedelta

from fastapi import FastAPI
from fastapi import status
from fastapi.responses import RedirectResponse
from fastapi.staticfiles import StaticFiles

from .api.errors import register_error_handlers
from .api.health import router as health_router
from .config import Settings
from .config import configure_logging
from .config import get_settings
from .db import create_all
from .db import dispose_database_runtime
from .db import initialize_database_runtime
from .services import ServiceBundle
from .services.jobs import JobService
from .ui import register_pages
from .worker import worker_consumer_lifespan

logger = logging.getLogger(__name__)


@asynccontextmanager
async def _lifespan(app: FastAPI):
    settings = getattr(app.state, "settings", None) or get_settings()
    configure_logging(settings)
    app.state.settings = settings
    app.state.services = ServiceBundle()
    app.state.runtime = initialize_database_runtime(settings=settings)

    if settings.should_bootstrap_schema:
        await create_all(engine=app.state.runtime.engine)

    settings.upload_dir.mkdir(parents=True, exist_ok=True)
    settings.prompt_dir.mkdir(parents=True, exist_ok=True)

    await _recover_stale_processing_jobs(app)

    async with AsyncExitStack() as stack:
        stack.push_async_callback(dispose_database_runtime)
        stop_event, worker_notifier = await stack.enter_async_context(
            worker_consumer_lifespan(
                session_factory=app.state.runtime.session_factory,
                poll_interval_seconds=1.0,
            )
        )
        app.state.worker_stop_event = stop_event
        app.state.worker_notifier = worker_notifier
        yield


async def _recover_stale_processing_jobs(app: FastAPI) -> None:
    """Re-queue stale processing jobs at startup.

    Any job left in PROCESSING longer than the configured provider timeout is
    assumed orphaned and moved back to QUEUED before the worker starts.
    """
    settings = app.state.settings
    stale_before = datetime.now(UTC) - timedelta(seconds=settings.worker_provider_timeout_seconds)
    job_service = JobService(session_factory=app.state.runtime.session_factory)
    recovered = await job_service.requeue_stale_processing_jobs(stale_before=stale_before)
    if recovered > 0:
        logger.warning("Recovered %s stale processing job(s) at startup", recovered)


def create_app(settings: Settings | None = None) -> FastAPI:
    """Create and configure the FastAPI application."""
    app = FastAPI(title="Transcription", lifespan=_lifespan)
    active_settings = settings or get_settings()
    app.state.settings = active_settings
    app.mount(
        "/uploads",
        StaticFiles(directory=active_settings.upload_dir, check_dir=False),
        name="uploads",
    )

    @app.get("/", include_in_schema=False)
    async def root_redirect() -> RedirectResponse:
        return RedirectResponse(url="/ui/homepage", status_code=status.HTTP_307_TEMPORARY_REDIRECT)

    @app.get("/ui", include_in_schema=False)
    async def ui_redirect() -> RedirectResponse:
        return RedirectResponse(url="/ui/homepage", status_code=status.HTTP_307_TEMPORARY_REDIRECT)

    @app.get("/healthz")
    def health() -> dict[str, str]:
        return {"status": "ok"}

    register_error_handlers(app)
    register_pages(app)
    app.include_router(health_router)
    return app
