"""The application factory: `uvicorn orders_api.main:create_app --factory`."""
import asyncio
import logging
from contextlib import asynccontextmanager
from dataclasses import dataclass

import httpx
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from fastapi.routing import APIRoute
from sqlalchemy.ext.asyncio import async_sessionmaker

from .config import Settings
from .db import Base, make_engine
from .errors import install_error_handlers
from .events import OrderEvents
from .observability import RequestContextMiddleware, configure_logging
from .outbox import relay_forever
from .ratelimit import SlidingWindowLimiter
from .routers import auth, health, orders, products
from .security import hash_password
from .seed import seed

log = logging.getLogger("orders.app")


@dataclass
class HealthState:
    draining: bool = False


def _operation_id(route: APIRoute) -> str:
    return f"{route.tags[0]}-{route.name}" if route.tags else route.name


def create_app(settings: Settings | None = None, *,
               webhook_transport: httpx.AsyncBaseTransport | None = None) -> FastAPI:
    settings = settings or Settings()

    @asynccontextmanager
    async def lifespan(app: FastAPI):
        configure_logging(settings.log_level)
        engine = make_engine(settings.database_url)
        if settings.create_tables:
            async with engine.begin() as conn:
                await conn.run_sync(Base.metadata.create_all)
        sessions = async_sessionmaker(engine, expire_on_commit=False)
        if settings.seed_demo_data:
            await seed(sessions, settings)
        http = httpx.AsyncClient(transport=webhook_transport, timeout=httpx.Timeout(5.0, connect=2.0))
        health = HealthState()
        stop = asyncio.Event()
        relay = (asyncio.create_task(relay_forever(sessions, http, settings, stop), name="outbox-relay")
                 if settings.webhook_url and settings.outbox_relay_enabled else None)
        log.info("started", extra={"fields": {"database": engine.url.render_as_string(hide_password=True),
                                              "outbox_relay": relay is not None}})
        try:
            yield {"settings": settings, "sessions": sessions, "http": http, "health": health,
                   "limiter": SlidingWindowLimiter(), "events": OrderEvents(),
                   "dummy_hash": hash_password("timing-equaliser", settings.bcrypt_rounds)}
        finally:
            health.draining = True
            stop.set()
            if relay:
                await relay                                   # finish the current delivery pass
            await http.aclose()
            await engine.dispose()
            log.info("stopped")

    app = FastAPI(title="Brew & Bean Orders API", version="1.0.0", lifespan=lifespan,
                  description="Orders for the Brew & Bean coffee bars. Amounts are in minor units (paise). "
                              "Errors are RFC 9457 problem details.",
                  generate_unique_id_function=_operation_id,
                  docs_url="/docs" if settings.show_docs else None, redoc_url=None,
                  openapi_url="/openapi.json" if settings.show_docs else None)
    install_error_handlers(app)
    app.add_middleware(CORSMiddleware, allow_origins=settings.cors_origins, allow_credentials=False,
                       allow_methods=["GET", "POST", "PATCH"],
                       allow_headers=["Authorization", "Content-Type", "Idempotency-Key", "If-Match",
                                      "If-None-Match", "X-Request-ID"],
                       expose_headers=["ETag", "Location", "Retry-After", "X-Request-ID", "Idempotent-Replayed"])
    app.add_middleware(RequestContextMiddleware)             # added last = outermost: sees every response
    for module in (auth, products, orders, health):
        app.include_router(module.router)
    return app
