"""FastAPI 应用入口（ARCH §7）。"""

from __future__ import annotations

from collections.abc import AsyncIterator
from contextlib import asynccontextmanager

from fastapi import FastAPI, Request
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse
from ww_config import get_settings
from ww_core.domain.job_repo import SqlJobRepo
from ww_db import get_sessionmaker
from ww_shared import AppError, ErrorBody, ErrorCode, ErrorEnvelope

from ww_api.logging_config import configure_logging, get_logger
from ww_api.middleware import REQUEST_ID_HEADER, request_id_middleware
from ww_api.routers import (
    chain,
    foreshadow,
    generation,
    health,
    jobs,
    kimi_oauth,
    outline,
    projects,
    rules,
    settings_providers,
    style,
    templates,
    toolbox,
)
from ww_api.security.credentials import _fernet
from ww_api.services.project_deps import seed_stub_user

configure_logging()
log = get_logger("ww.api")


@asynccontextmanager
async def _lifespan(app: FastAPI) -> AsyncIterator[None]:
    # 加密 key 启动校验：缺失/非法则**快速失败**（不带病启动接流量）。`_fernet` 缺/非法抛
    # CredentialKeyError；此处让其冒泡使进程启动失败（P0-2），而非首次凭据读写才 500。
    _fernet(get_settings().credential_enc_key.get_secret_value())
    # 幂等 seed 单用户 stub——所有 owner_id FK 依赖它（见 memory/gotchas）。
    async with get_sessionmaker()() as session:
        await seed_stub_user(session)
    # zombie reaper（M4-d / §7.4 缓解）：进程重启会丢未跑完的 BackgroundTask，把残留
    # status=running 的 job 标 failed（让用户看到失败可重试，而非进度条永转）。自 commit、幂等。
    async with get_sessionmaker()() as session:
        reaped = await SqlJobRepo(session).reap_zombies()
        if reaped:
            log.info("job_zombies_reaped", count=reaped)
    yield


def create_app() -> FastAPI:
    app = FastAPI(title="网文创作工作流 API", version="0.0.0", lifespan=_lifespan)
    # CORS：前端（Next.js）与 API 分端口/跨源，浏览器端 `api.POST/PUT` 需 CORS 放行
    # （RSC 服务端取数同源、无需 CORS，故此前同进程测试从未暴露此缺口）。原型单用户、
    # 本地开发：放行 localhost:3000；可经 env `CORS_ORIGINS`（逗号分隔）覆盖。
    settings = get_settings()
    # `allow_credentials=True` 与通配 origin `"*"` 并存会被浏览器拒（且语义危险）——启动断言
    # cors_origins 不含通配，强制显式白名单（P2 收窄）。
    assert "*" not in settings.cors_origins, (
        "cors_origins 不能含通配 '*'（与 allow_credentials=True 不兼容）；请配置显式来源白名单"
    )
    app.add_middleware(
        CORSMiddleware,
        allow_origins=settings.cors_origins,
        allow_credentials=True,
        # 收窄方法/头白名单（避免 `*` 放行任意方法/头，P2）。
        allow_methods=["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
        allow_headers=["content-type", "authorization", REQUEST_ID_HEADER],
    )
    app.middleware("http")(request_id_middleware)

    @app.exception_handler(AppError)
    async def _app_error_handler(request: Request, exc: AppError) -> JSONResponse:
        request_id = getattr(request.state, "request_id", None)
        log.warning("app_error", code=exc.code, message=exc.message)
        envelope = ErrorEnvelope(
            error=ErrorBody(
                code=exc.code,
                message=exc.message,
                details=exc.details,
                request_id=request_id,
            )
        )
        return JSONResponse(status_code=exc.http_status, content=envelope.model_dump())

    @app.exception_handler(Exception)
    async def _unhandled_error_handler(request: Request, exc: Exception) -> JSONResponse:
        # 兜底：任何未被 AppError 捕获的异常 → 统一 INTERNAL 信封（不回显原始异常给客户端），
        # 但服务端记完整上下文 + request_id 便于端到端排查（P0-2）。
        request_id = getattr(request.state, "request_id", None)
        log.error(
            "unhandled_exception",
            request_id=request_id,
            error_type=type(exc).__name__,
            exc_info=exc,
        )
        envelope = ErrorEnvelope(
            error=ErrorBody(
                code=ErrorCode.INTERNAL,
                message="服务器内部错误",
                request_id=request_id,
            )
        )
        return JSONResponse(status_code=500, content=envelope.model_dump())

    app.include_router(health.router)
    app.include_router(jobs.router)
    app.include_router(projects.router)
    app.include_router(foreshadow.router)
    app.include_router(outline.router)
    app.include_router(rules.router)
    app.include_router(style.router)
    app.include_router(templates.router)
    app.include_router(generation.router)
    app.include_router(generation.skills_router)
    app.include_router(toolbox.router)
    app.include_router(chain.router)
    app.include_router(settings_providers.router)
    app.include_router(kimi_oauth.router)
    return app


app = create_app()
