"""FastAPI app factory.

W2-F13/F14: import-time `init_db()` 제거 + `on_event("startup")` → `lifespan` 전환.
W2-F15: default-user bootstrap을 `backend/scripts/bootstrap_default_users.py` CLI로 이관.
  - dev/test 환경: lifespan이 자동 호출 (기존 편의 유지).
  - prod 환경: 자동 호출 생략 — CLI로 명시적 초기화 필요.
W2-F17: insecure default credentials는 prod에서 Settings가 fail-fast로 거부.
  dev/test에서는 경고만 남긴다.
"""
from __future__ import annotations

import logging
from contextlib import asynccontextmanager

from fastapi import FastAPI
from sqlalchemy import text

from app.core.config import has_insecure_defaults, settings
from app.core.database import SessionLocal, init_db
from app.core.errors import AppError, app_error_handler
from app.core.logging_config import setup_logging
from app.i18n.loader import load_strings
from app.startup.default_users import bootstrap_default_users
from app.version import APP_VERSION

logger = logging.getLogger(__name__)


@asynccontextmanager
async def lifespan(app: FastAPI):
    """startup + shutdown. 전역 side effect는 모두 이곳에서만 수행."""
    setup_logging()
    load_strings()

    issues = has_insecure_defaults()
    if issues:
        logger.warning(
            "SECURITY: insecure default credentials in use — %s. "
            "Override via env vars before exposing this server.",
            ", ".join(issues),
        )

    init_db()

    # Codex W2 High 1: stale progress 복구는 env 무관하게 공통 경로.
    # crash/restart 복구는 prod에서도 필요하다 (아니면 'running' row가 영구 stuck).
    db = SessionLocal()
    try:
        from app.core.task_registry import recover_stale_progress
        recovered = recover_stale_progress(db)
        if recovered:
            logger.warning(
                "Recovered %d stale pipeline progress entries on startup", recovered
            )

        # 스텝 락 풀기 — **이 호스트의 죽은 프로세스**가 잡은 락만.
        #
        # 전에는 서버를 죽이면 `step_run` 에 `running` 행이 남고, 락이 소유자의
        # 죽음을 확인할 길이 없어 경과 시간(기본 3600초)을 기다렸다. 2026-08-26
        # 새벽에 그렇게 40분을 잃었다. 이제 다시 뜨는 순간 푼다.
        #
        # ★`owner_pid` 가 **없는 프로세스로 확인될 때만** 푼다. 신원 칸이
        #  NULL 인 옛 행과 다른 호스트의 행은 안 건드린다 (자동 대량 정리 금지).
        if settings.step_lock_startup_reclaim:
            from app.core.step_lock import reclaim_dead_locks_on_startup
            try:
                reclaimed = reclaim_dead_locks_on_startup(db)
                if reclaimed:
                    logger.warning(
                        "기동 시 스텝 락 %d개 풀기 — 소유 프로세스가 없었다", reclaimed
                    )
            except Exception as exc:
                # 풀기 실패로 부팅을 막지 않는다 — 예전 동작(경과 시간 대기)으로
                # 떨어질 뿐이고, 그것은 오늘까지의 동작이다.
                logger.error("기동 시 스텝 락 풀기 실패 (계속 진행): %s", exc)

        # default-user bootstrap은 dev/test에서만 자동. prod는 CLI 강제.
        if settings.environment in ("dev", "test"):
            bootstrap_default_users(db)
        else:
            logger.info(
                "ENVIRONMENT=%s — skipping automatic bootstrap. "
                "Run `backend/scripts/bootstrap_default_users.py` if needed.",
                settings.environment,
            )
    finally:
        db.close()

    # 하트비트 — 이 프로세스가 잡은 락에 「살아있다」를 주기적으로 적는다.
    # 다른 호스트라 PID 를 못 물어볼 때의 판정 근거이자, 운영자가
    # `GET .../locks` 에서 멈춘 스텝을 알아보는 신호다.
    from app.core.step_lock import start_heartbeat, stop_heartbeat
    try:
        start_heartbeat(
            SessionLocal,
            interval_seconds=settings.step_lock_heartbeat_seconds,
        )
    except Exception as exc:
        logger.error("스텝 락 하트비트 시작 실패 (계속 진행): %s", exc)

    yield

    # shutdown — 하트비트를 세운다. 여기까지 온 종료는 **정상 종료**이므로
    # 락은 각 스텝의 finalize 가 이미 내렸거나, 안 내렸다면 다음 기동의
    # 풀기가 가져간다.
    try:
        stop_heartbeat()
    except Exception as exc:
        logger.warning("스텝 락 하트비트 정지 실패: %s", exc)


def create_app() -> FastAPI:
    app = FastAPI(
        title=settings.app_name,
        docs_url="/api/docs",
        openapi_url="/api/openapi.json",
        lifespan=lifespan,
    )

    app.add_exception_handler(AppError, app_error_handler)

    # Register routers
    from app.api.v1.auth import router as auth_router
    from app.api.v1.users import router as users_router
    from app.api.v1.projects import router as projects_router, activity_router
    from app.api.v1.episodes import router as episodes_router
    from app.api.v1.entities import router as entities_router
    from app.api.v1.images import router as images_router
    from app.api.v1.exports import router as exports_router, import_router as project_import_router
    from app.api.v1.operations import router as operations_router
    from app.api.v1.prompts import router as prompts_router
    from app.api.v1.steps import router as steps_router
    from app.api.v1.pipeline import router as pipeline_router
    from app.api.v1.settings import router as settings_router
    from app.api.v1.grounding_fidelity import (
        router as grounding_fidelity_router)
    app.include_router(auth_router)
    app.include_router(users_router)
    app.include_router(projects_router)
    app.include_router(activity_router)
    app.include_router(episodes_router)
    app.include_router(entities_router)
    app.include_router(images_router)
    app.include_router(exports_router)
    app.include_router(project_import_router)
    app.include_router(operations_router)
    app.include_router(prompts_router)
    app.include_router(steps_router)
    app.include_router(pipeline_router)
    app.include_router(settings_router)
    app.include_router(grounding_fidelity_router)

    @app.get("/api/v1/health")
    @app.get("/api/health")  # 외부 모니터링 alias — 404 스팸 방지 (v0.5.20)
    def health_check():
        from app.modules.llm.gemini_key_pool import key_count
        db_ok = False
        try:
            db = SessionLocal()
            db.execute(text("SELECT 1"))
            db_ok = True
            db.close()
        except Exception:
            # 의도적: 헬스체크 DB probe 실패 → 응답에 db: error / status: degraded로 반영. 예외 세부는 의도적으로 노출 안 함.
            pass
        return {
            "status": "ok" if db_ok else "degraded",
            "version": APP_VERSION,
            "db": "ok" if db_ok else "error",
            "gemini_keys": key_count(),
            "openai_key": bool(_active_openai_key()),
        }

    return app


app = create_app()

def _active_openai_key() -> str:
    """활성 키 슬롯 — 1차 필드를 직접 보면 보조 키가 안 쓰인다."""
    from app.core.openai_keys import active_key

    return active_key()
