from __future__ import annotations

import asyncio
import logging
from contextlib import asynccontextmanager
from pathlib import Path
from urllib.parse import quote, urlparse

from fastapi import FastAPI, Request
from fastapi.exception_handlers import http_exception_handler
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse, RedirectResponse
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from jose import JWTError, jwt
from starlette.exceptions import HTTPException as StarletteHTTPException

from app.config import config
from app.core.logging import (
    bind_log_context,
    clear_log_context,
    configure_logging,
    generate_correlation_id,
    get_audit_logger,
    get_logger,
)
from app.core.mongo_runtime import clear_mongo_runtime, init_mongo_runtime
from app.core.services import build_services
from app.migrations.definitions import MIGRATIONS
from app.routes import (
    admin,
    batches,
    cases,
    dashboard,
    documents,
    history,
    individual_ingestion,
    pages,
    profile,
    rda,
    rips,
    soat,
    users,
)


BASE_DIR = Path(__file__).resolve().parent.parent
STATIC_DIR = BASE_DIR / "static"
TEMPLATES = Jinja2Templates(directory=str(BASE_DIR / "web"))


def _logger():
    return get_logger(__name__)


def _audit():
    return get_audit_logger()


@asynccontextmanager
async def app_lifespan(app: FastAPI):
    configure_logging()
    logging.getLogger("passlib.handlers.bcrypt").setLevel(logging.ERROR)
    runtime = init_mongo_runtime()
    try:
        await runtime.ping()
        services = build_services(BASE_DIR, mongo_runtime=runtime)
        app.state.mongo_runtime = runtime
        app.state.services = services
        app.state.startup_ready = True
        yield
    finally:
        app.state.startup_ready = False
        clear_mongo_runtime(runtime)


def _resolve_username_from_cookie(request: Request) -> str:
    access_token = str(request.cookies.get("access_token") or "").strip()
    if not access_token:
        return ""
    token = access_token.replace("Bearer ", "", 1)
    try:
        payload = jwt.decode(token, config.SECRET_KEY or "cidti40", algorithms=["HS256"])
    except JWTError:
        return ""
    return str(payload.get("sub") or "").strip()


def _is_fetch_or_api_request(request: Request) -> bool:
    headers = request.headers
    accept = headers.get("accept", "").lower()
    x_requested_with = headers.get("x-requested-with", "").lower()
    sec_fetch_mode = headers.get("sec-fetch-mode", "").lower()
    path = request.url.path

    if x_requested_with in {"xmlhttprequest", "fetch"}:
        return True
    if path.startswith("/api/"):
        return True
    if "application/json" in accept:
        return True
    return bool(sec_fetch_mode and sec_fetch_mode != "navigate" and "text/html" not in accept)


def _resolve_next_path(request: Request, *, prefer_referer: bool = False) -> str:
    if prefer_referer:
        referer = request.headers.get("referer", "").strip()
        if referer:
            parsed_referer = urlparse(referer)
            same_origin = (not parsed_referer.scheme or parsed_referer.scheme == request.url.scheme) and (
                not parsed_referer.netloc or parsed_referer.netloc == request.url.netloc
            )
            if same_origin and parsed_referer.path and not parsed_referer.path.startswith("/login"):
                referer_path = parsed_referer.path
                if parsed_referer.query:
                    referer_path = f"{referer_path}?{parsed_referer.query}"
                return referer_path

    next_path = request.url.path
    if request.url.query:
        next_path = f"{next_path}?{request.url.query}"
    return next_path


def _build_login_url(request: Request, *, prefer_referer: bool = False) -> str:
    next_path = _resolve_next_path(request, prefer_referer=prefer_referer)
    return f"/login?next={quote(next_path, safe='')}"


def _http_outcome_from_status_code(status_code: int) -> str:
    if status_code >= 500:
        return "error"
    if status_code >= 400:
        return "warning"
    return "success"


def _attach_request_headers(request: Request, response):
    request_id = getattr(request.state, "request_id", "")
    trace_id = getattr(request.state, "trace_id", "")
    if request_id:
        response.headers["X-Request-ID"] = request_id
    if trace_id:
        response.headers["X-Trace-ID"] = trace_id
    return response


async def _resolve_error_page_user(request: Request) -> dict[str, str] | None:
    username = getattr(request.state, "username", "") or _resolve_username_from_cookie(request)
    if not username:
        return None

    try:
        runtime = getattr(request.app.state, "mongo_runtime", None)
        if runtime is None:
            return {"username": username, "role": ""}
        user_data = await runtime.async_database["users"].find_one({"username": username})
    except Exception:
        return {"username": username, "role": ""}

    if not user_data:
        return {"username": username, "role": ""}
    return {
        "username": str(user_data.get("username") or username),
        "role": str(user_data.get("role") or ""),
    }


def _request_path_with_query(request: Request) -> str:
    path = request.url.path
    if request.url.query:
        return f"{path}?{request.url.query}"
    return path


async def _render_error_page(
    request: Request,
    *,
    status_code: int,
    title: str,
    message: str,
    detail: str | None = None,
):
    user = await _resolve_error_page_user(request)
    is_authenticated = bool(user and user.get("username"))
    primary_action_href = "/dashboard" if is_authenticated else "/login"
    primary_action_label = "Volver al dashboard" if is_authenticated else "Ir al inicio de sesión"
    secondary_action_href = None
    secondary_action_label = None

    if status_code == 500:
        primary_action_href = (
            _request_path_with_query(request) if request.method == "GET" else _resolve_next_path(request, prefer_referer=True)
        )
        primary_action_label = "Reintentar"
        secondary_action_href = "/dashboard" if is_authenticated else "/login"
        secondary_action_label = "Volver al dashboard" if is_authenticated else "Ir al inicio de sesión"

    response = TEMPLATES.TemplateResponse(
        request,
        f"{status_code}.html",
        {
            "request": request,
            "title": title,
            "message": message,
            "detail": detail,
            "primary_action_href": primary_action_href,
            "primary_action_label": primary_action_label,
            "secondary_action_href": secondary_action_href,
            "secondary_action_label": secondary_action_label,
            "support_request_id": (
                getattr(request.state, "request_id", None) if user and user.get("role") == "admin" else None
            ),
        },
        status_code=status_code,
    )
    return _attach_request_headers(request, response)


def _pending_migrations(sync_database) -> list[str]:
    applied = {
        str(item.get("version"))
        for item in sync_database["schema_migrations"].find({}, {"version": 1})
        if item.get("version")
    }
    return [migration.version for migration in MIGRATIONS if migration.version not in applied]


def _register_routers(app: FastAPI) -> None:
    app.include_router(users.router)
    app.include_router(dashboard.router)
    app.include_router(cases.router)
    app.include_router(admin.router)
    app.include_router(profile.router)
    app.include_router(pages.router)
    app.include_router(documents.router)
    app.include_router(history.router)
    app.include_router(soat.router)
    app.include_router(batches.router)
    app.include_router(individual_ingestion.router)
    app.include_router(rda.router)
    app.include_router(rips.router)


def create_app() -> FastAPI:
    app = FastAPI(title="EpicrisisIA - MVP", lifespan=app_lifespan)
    app.state.startup_ready = False
    app.add_middleware(
        CORSMiddleware,
        allow_origins=["*"],
        allow_credentials=True,
        allow_methods=["*"],
        allow_headers=["*"],
    )
    _register_routers(app)
    app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static")

    @app.middleware("http")
    async def request_context_middleware(request: Request, call_next):
        request_id = request.headers.get("x-request-id") or generate_correlation_id()
        trace_id = request.headers.get("x-trace-id") or request_id
        username = _resolve_username_from_cookie(request)
        bind_log_context(
            request_id=request_id,
            trace_id=trace_id,
            username=username,
            method=request.method,
            route=request.url.path,
        )
        request.state.request_id = request_id
        request.state.trace_id = trace_id
        request.state.username = username
        started_at = asyncio.get_running_loop().time()
        try:
            response = await call_next(request)
            outcome = _http_outcome_from_status_code(response.status_code)
            _audit().http_event(
                action=f"{request.method} {request.url.path}",
                outcome=outcome,
                service="fastapi",
                resource={
                    "path": request.url.path,
                    "method": request.method,
                    "remote_addr": request.client.host if request.client else "",
                    "user_agent": request.headers.get("user-agent", ""),
                    "status_code": response.status_code,
                },
                metrics={
                    "duration_ms": round((asyncio.get_running_loop().time() - started_at) * 1000, 3),
                },
            )
            response.headers["X-Request-ID"] = request_id
            response.headers["X-Trace-ID"] = trace_id
            return response
        except Exception as exc:
            _audit().http_event(
                action=f"{request.method} {request.url.path}",
                outcome="error",
                service="fastapi",
                resource={
                    "path": request.url.path,
                    "method": request.method,
                    "remote_addr": request.client.host if request.client else "",
                    "user_agent": request.headers.get("user-agent", ""),
                },
                metrics={
                    "duration_ms": round((asyncio.get_running_loop().time() - started_at) * 1000, 3),
                },
                error={"class": exc.__class__.__name__, "message": str(exc)},
            )
            _logger().exception("Unhandled request error for %s %s", request.method, request.url.path)
            raise
        finally:
            clear_log_context()

    @app.exception_handler(StarletteHTTPException)
    async def http_exception_with_auth_redirect(request: Request, exc: StarletteHTTPException):
        is_auth_expired = (
            exc.status_code == 401
            and str(exc.detail) == "Not authenticated"
            and (exc.headers or {}).get("X-Auth-Error") == "auth_expired"
        )
        is_fetch_or_api = _is_fetch_or_api_request(request)
        if is_auth_expired:
            login_url = _build_login_url(request, prefer_referer=is_fetch_or_api)
            if is_fetch_or_api:
                response = JSONResponse(
                    status_code=401,
                    content={
                        "detail": "Not authenticated",
                        "code": "auth_expired",
                        "login_url": login_url,
                    },
                )
            else:
                response = RedirectResponse(url=login_url, status_code=303)
            response.delete_cookie("access_token")
            return _attach_request_headers(request, response)

        if exc.status_code == 404 and not is_fetch_or_api:
            return await _render_error_page(
                request,
                status_code=404,
                title="Página no encontrada",
                message="La ruta solicitada no está disponible o ya no existe.",
                detail=None,
            )
        if exc.status_code == 500:
            if is_fetch_or_api:
                response = JSONResponse(
                    status_code=500,
                    content={"detail": "Error interno del servidor", "code": "internal_error"},
                )
                return _attach_request_headers(request, response)
            return await _render_error_page(
                request,
                status_code=500,
                title="Error interno del servidor",
                message="No fue posible completar la operación. Puedes reintentar o volver al flujo principal.",
                detail=None,
            )

        return _attach_request_headers(request, await http_exception_handler(request, exc))

    @app.exception_handler(Exception)
    async def unhandled_exception_handler(request: Request, _exc: Exception):
        if _is_fetch_or_api_request(request):
            response = JSONResponse(
                status_code=500,
                content={"detail": "Error interno del servidor", "code": "internal_error"},
            )
            return _attach_request_headers(request, response)
        return await _render_error_page(
            request,
            status_code=500,
            title="Error interno del servidor",
            message="No fue posible completar la operación. Puedes reintentar o volver al flujo principal.",
            detail=None,
        )

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

    @app.get("/ready")
    async def ready(request: Request):
        runtime = getattr(request.app.state, "mongo_runtime", None)
        services = getattr(request.app.state, "services", None)
        if runtime is None or services is None or not getattr(request.app.state, "startup_ready", False):
            return JSONResponse(
                status_code=503,
                content={"status": "not_ready", "reason": "startup_incomplete"},
            )
        try:
            await runtime.ping()
        except Exception as exc:
            return JSONResponse(
                status_code=503,
                content={
                    "status": "not_ready",
                    "reason": "mongo_unreachable",
                    "error": exc.__class__.__name__,
                },
            )
        pending = _pending_migrations(runtime.sync_database)
        if pending:
            return JSONResponse(
                status_code=503,
                content={
                    "status": "not_ready",
                    "reason": "pending_migrations",
                    "pending_migrations": pending,
                },
            )
        return {
            "status": "ready",
            "resources": services.resources_status(),
        }

    return app


app = create_app()
