import asyncio
import logging
import os
from contextlib import asynccontextmanager
from datetime import datetime, timezone
from contextlib import suppress

from fastapi import FastAPI, HTTPException, Response
from fastapi.middleware.cors import CORSMiddleware
from fastapi.openapi.docs import get_swagger_ui_html
from starlette.exceptions import HTTPException as StarletteHTTPException
from sqlalchemy import text
from sqlalchemy.exc import SQLAlchemyError
from fastapi.staticfiles import StaticFiles
from starlette.middleware.gzip import GZipMiddleware
from uvicorn.middleware.proxy_headers import ProxyHeadersMiddleware

from src.api.v1.router import api_router
from src.core.config import settings
from src.core.exception_handlers import register_exception_handlers
from src.db.base import engine
from src.db.session import create_tables



logging.basicConfig(level=logging.INFO)

# if not settings.DEBUG:
#     from raisa_fastapi_protected_api import (
#         AuthorizationMiddleware,
#         OpenEndpoint,
#         ProtectedOAuthSettings,
#         ProtectedRolesSettings,
#         ProtectedSettings,
#         SearchType,
#     )


_AUDIT_CLEANUP_LOCK_KEY = 21987431


async def _cleanup_audit_log_once() -> int:
    if "postgresql" not in settings.DATABASE_URL:
        return 0

    total_deleted = 0
    async with engine.begin() as conn:
        lock_ok = (
            await conn.execute(
                text("SELECT pg_try_advisory_lock(:k)"),
                {"k": _AUDIT_CLEANUP_LOCK_KEY},
            )
        ).scalar_one()
        if not lock_ok:
            return 0
        try:
            while True:
                deleted = (
                    await conn.execute(
                        text(
                            """
                            WITH doomed AS (
                                SELECT id
                                FROM v3.audit_log
                                WHERE event_dt < now() - make_interval(days => :retention_days)
                                ORDER BY id
                                LIMIT :batch_size
                            )
                            DELETE FROM v3.audit_log a
                            USING doomed d
                            WHERE a.id = d.id
                            RETURNING a.id
                            """
                        ),
                        {
                            "retention_days": settings.AUDIT_LOG_RETENTION_DAYS,
                            "batch_size": settings.AUDIT_LOG_CLEANUP_BATCH_SIZE,
                        },
                    )
                ).rowcount
                if not deleted:
                    break
                total_deleted += deleted
        finally:
            await conn.execute(
                text("SELECT pg_advisory_unlock(:k)"),
                {"k": _AUDIT_CLEANUP_LOCK_KEY},
            )
    return total_deleted


async def _audit_cleanup_loop() -> None:
    # Первый прогон сразу после старта, дальше — по интервалу.
    while True:
        try:
            await _cleanup_audit_log_once()
        except Exception:
            pass
        await asyncio.sleep(max(60, settings.AUDIT_LOG_CLEANUP_INTERVAL_SECONDS))


@asynccontextmanager
async def lifespan(app: FastAPI):
    cleanup_task: asyncio.Task | None = None
    if not settings.DEBUG:
        await create_tables()
        # ProtectedSettings(
        #     ProtectedOAuthSettings(JWKS_URI=settings.JWKS_URL),
        #     ProtectedRolesSettings(
        #         APP_NAMESPACE=settings.APP_NAMESPACE,
        #         APP_NAME=settings.APP_NAME,
        #     ),
        # )
        cleanup_task = asyncio.create_task(_audit_cleanup_loop())
    try:
        yield
    finally:
        if cleanup_task:
            cleanup_task.cancel()
            with suppress(asyncio.CancelledError):
                await cleanup_task
        await engine.dispose()


app = FastAPI(
    title="DFiP Budget Planning API",
    version="1.0.0",
    lifespan=lifespan,
    root_path=settings.ROOT_PATH,
)
register_exception_handlers(app)

VERSION = "1.0.0"
UP_TIME = datetime.now(timezone.utc)


@app.get("/healthcheck", status_code=200)
async def healthcheck():
    return {"uptime": UP_TIME, "version": VERSION}


@app.get("/healthcheck2", status_code=200)
async def healthcheck2():
    return {
        "uptime": UP_TIME,
        "version": VERSION,
        "debug": settings.DEBUG,
        "host": settings.HOST,
        "port": settings.PORT,
    }

@app.get("/healthz", status_code=200)
async def healthz():
    return {"status": "ok", "uptime": UP_TIME, "version": VERSION}


@app.get("/readyz")
async def readyz(response: Response):
    result = {"status": "ok", "db_status": "ok", "mv_expense_item_tree": "ok"}
    try:
        async with engine.connect() as conn:
            await conn.execute(text("SELECT 1"))
            if "postgresql" in settings.DATABASE_URL:
                my_exists = (
                    await conn.execute(
                        text(
                            """
                            SELECT 1
                            FROM pg_matviews
                            WHERE schemaname = 'v3' 
                              AND matviewname = 'mv_expense_item_tree'
                            """
                        )
                    )
                ).scalar_one_or_none()
                if my_exists is None:
                    result["status"] = "fail"
                    result["mv_expense_item_tree"] = "missing"
                    response.status_code = 503
    except SQLAlchemyError as exc:
        result["status"] = "fail"
        result["db"] = f"error: {exc.__class__.__name__}"
        response.status_code = 503
    return result



# if not settings.DEBUG:
#     app.add_middleware(
#         AuthorizationMiddleware,
#         open_endpoints=[
#             OpenEndpoint(path="", type_search=SearchType.ABSOLUTE),
#             OpenEndpoint(path=settings.ROOT_PATH + "", type_search=SearchType.ABSOLUTE),
#             OpenEndpoint(path="/", type_search=SearchType.ABSOLUTE),
#             OpenEndpoint(path=settings.ROOT_PATH + "/", type_search=SearchType.ABSOLUTE),
#             OpenEndpoint(path="/healthcheck", type_search=SearchType.ABSOLUTE),
#             OpenEndpoint(
#                 path=settings.ROOT_PATH + "/healthcheck",
#                 type_search=SearchType.ABSOLUTE,
#             ),
#             OpenEndpoint(path="/healthz", type_search=SearchType.ABSOLUTE),
#             OpenEndpoint(
#                 path=settings.ROOT_PATH + "/healthz",
#                 type_search=SearchType.ABSOLUTE,
#             ),
#             OpenEndpoint(path="/readyz", type_search=SearchType.ABSOLUTE),
#             OpenEndpoint(
#                 path=settings.ROOT_PATH + "/readyz",
#                 type_search=SearchType.ABSOLUTE,
#             ),
#             OpenEndpoint(path="/openapi.json", type_search=SearchType.START),
#             OpenEndpoint(
#                 path=settings.ROOT_PATH + "/openapi.json",
#                 type_search=SearchType.START,
#             ),
#             OpenEndpoint(path="/docs", type_search=SearchType.START),
#             OpenEndpoint(
#                 path=settings.ROOT_PATH + "/docs",
#                 type_search=SearchType.START,
#             ),
#         ],
#     )

app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)
app.add_middleware(ProxyHeadersMiddleware, trusted_hosts=["*"])
app.add_middleware(GZipMiddleware, minimum_size=1000, compresslevel=1)

os.makedirs("web", exist_ok=True)
app.include_router(api_router, prefix="/api/v1")



@app.get("/docs-local", include_in_schema=False)
async def custom_swagger_ui_html():
    return get_swagger_ui_html(
        openapi_url=f"{settings.ROOT_PATH}{app.openapi_url}",  # Путь к OpenAPI схеме (обычно /openapi.json)
        title=app.title + " - Swagger UI",
        oauth2_redirect_url=app.swagger_ui_oauth2_redirect_url,
        # Указываем пути к локальным файлам
        swagger_js_url=f"{settings.ROOT_PATH}/back-static/swagger-ui-bundle.js",
        swagger_css_url=f"{settings.ROOT_PATH}/back-static/swagger-ui.css",
    )


def sep_static(path, stat_dir):
    sep_path = path.split("/")
    index = sep_path.index(stat_dir)
    return "/".join(sep_path[index:])


class SPAStaticFiles(StaticFiles):
    async def get_response(self, path: str, scope):
        try:
            return await super().get_response(path, scope)
        except (HTTPException, StarletteHTTPException) as ex:
            if ex.status_code == 404:
                if "images" in path:
                    return await super().get_response(sep_static(path, "images"), scope)
                if "static" in path:
                    return await super().get_response(sep_static(path, "static"), scope)
                return await super().get_response("index.html", scope)
            else:
                raise ex


app.mount("/back-static", StaticFiles(directory="back_static"), name="back-static")
app.mount("/", SPAStaticFiles(directory="web", html=True), name="web")

app.mount("/", StaticFiles(directory="web", html=True), name="web")
