from __future__ import annotations

import asyncio
from datetime import datetime
from typing import Any

from fastapi.responses import JSONResponse
from redis import Redis

from app.config import config
from app.core.services import COLOMBIA_TZ
from app.migrations.definitions import MIGRATIONS


def pending_migrations(sync_database: Any) -> 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 _service(
    status: str,
    *,
    check_mode: str,
    critical: bool,
    details: dict[str, Any],
) -> dict[str, Any]:
    return {
        "status": status,
        "check_mode": check_mode,
        "critical": critical,
        "details": details,
    }


def _error_details(reason_code: str, *, error_class: str | None = None, message: str | None = None) -> dict[str, Any]:
    details: dict[str, Any] = {
        "reason_code": reason_code,
        "reachable": False,
    }
    if error_class:
        details["error_class"] = error_class
    if message:
        details["message"] = message
    return details


def _resource_service_status(
    *,
    configured: bool | None = None,
    available: bool | None = None,
    loaded: bool | None = None,
    enabled: bool | None = None,
) -> tuple[str, dict[str, Any]]:
    details: dict[str, Any] = {}
    status = "ok"
    compatibility_disabled = enabled is False

    if configured is not None:
        details["configured"] = configured
        if not configured and not compatibility_disabled:
            status = "degraded"
            details["reason_code"] = "not_configured"

    if available is not None:
        details["available"] = available
        if not available and not compatibility_disabled:
            status = "degraded"
            details["reason_code"] = "missing_index"

    if enabled is not None:
        details["enabled"] = enabled
        if compatibility_disabled:
            details["reason_code"] = "compatibility_disabled"

    if loaded is not None:
        details["loaded"] = loaded

    return status, details


def _presence_service_status(present: bool, *, extra_details: dict[str, Any] | None = None) -> tuple[str, dict[str, Any]]:
    details = {"present": present}
    if extra_details:
        details.update(extra_details)
    if present:
        return "ok", details
    details["reason_code"] = "startup_incomplete"
    return "degraded", details


def _safe_dispatcher_name(runtime: Any) -> str:
    return str(getattr(runtime, "dispatcher_name", "") or "unknown")


def _collect_heavy_resource_descriptors(services: Any) -> dict[str, dict[str, Any]]:
    heavy_resources = getattr(services, "heavy_resources", None)
    describe_status = getattr(heavy_resources, "describe_status", None)
    if callable(describe_status):
        descriptors = describe_status()
        if isinstance(descriptors, dict):
            return descriptors
    return {}


def _redis_ping() -> bool:
    client = Redis.from_url(config.REDIS_URL, socket_connect_timeout=1, socket_timeout=1)
    try:
        return bool(client.ping())
    finally:
        client.close()


async def ping_redis() -> bool:
    return await asyncio.to_thread(_redis_ping)


def _app_runtime_service(startup_ready: bool) -> dict[str, Any]:
    return _service(
        "ok" if startup_ready else "degraded",
        check_mode="passive",
        critical=False,
        details={"startup_ready": startup_ready},
    )


def _missing_runtime_services() -> dict[str, dict[str, Any]]:
    return {
        "mongo": _service(
            "error",
            check_mode="active",
            critical=True,
            details=_error_details(
                "startup_incomplete",
                message="El runtime de MongoDB no esta disponible.",
            ),
        ),
        "migrations": _service(
            "error",
            check_mode="active",
            critical=True,
            details=_error_details(
                "startup_incomplete",
                message="El estado de migraciones no esta disponible.",
            ),
        ),
    }


async def _runtime_health_services(runtime: Any) -> dict[str, dict[str, Any]]:
    mongo_service = await _mongo_health_service(runtime)
    migrations_service = _migrations_health_service(runtime)
    return {
        "mongo": mongo_service,
        "migrations": migrations_service,
    }


async def _mongo_health_service(runtime: Any) -> dict[str, Any]:
    try:
        await runtime.ping()
    except Exception as exc:
        return _service(
            "error",
            check_mode="active",
            critical=True,
            details=_error_details(
                "unreachable",
                error_class=exc.__class__.__name__,
                message="No fue posible verificar conectividad con MongoDB.",
            ),
        )
    return _service(
        "ok",
        check_mode="active",
        critical=True,
        details={"reachable": True},
    )


def _migrations_health_service(runtime: Any) -> dict[str, Any]:
    pending = pending_migrations(runtime.sync_database)
    if pending:
        return _service(
            "error",
            check_mode="active",
            critical=True,
            details={
                "reason_code": "pending_migrations",
                "pending_count": len(pending),
            },
        )
    return _service(
        "ok",
        check_mode="active",
        critical=True,
        details={"pending_count": 0},
    )


def _resource_health_services(services: Any) -> dict[str, dict[str, Any]]:
    heavy_resources = _collect_heavy_resource_descriptors(services)
    providers = {
        name: _passive_resource_service(
            heavy_resources.get(name),
            configured_key="configured",
            enabled_key="enabled",
        )
        for name in ("groq_provider", "gemini_provider", "openai_stub", "llm_router")
    }
    resources = {
        name: _passive_resource_service(
            heavy_resources.get(name),
            available_key="available",
        )
        for name in ("cie10_resource", "cups_resource", "soat_resource")
    }
    return {**providers, **resources}


def _passive_resource_service(
    descriptor: dict[str, Any] | None,
    *,
    configured_key: str | None = None,
    available_key: str | None = None,
    enabled_key: str | None = None,
) -> dict[str, Any]:
    descriptor = dict(descriptor or {})
    status, details = _resource_service_status(
        configured=descriptor.get(configured_key) if configured_key else None,
        available=descriptor.get(available_key) if available_key else None,
        loaded=descriptor.get("loaded"),
        enabled=descriptor.get(enabled_key) if enabled_key else None,
    )
    return _service(
        status,
        check_mode="passive",
        critical=False,
        details=details,
    )


async def _dispatcher_health_service(runtime: Any, *, label: str) -> dict[str, Any]:
    dispatcher_name = _safe_dispatcher_name(runtime)
    critical = dispatcher_name == "celery"
    if runtime is None:
        return _service(
            "degraded",
            check_mode="passive",
            critical=critical,
            details={"reason_code": "startup_incomplete", "dispatcher_name": "unknown"},
        )
    if dispatcher_name != "celery":
        return _service(
            "ok",
            check_mode="passive",
            critical=False,
            details={"dispatcher_name": dispatcher_name},
        )
    return await _celery_dispatcher_health_service(dispatcher_name, label=label)


async def _celery_dispatcher_health_service(dispatcher_name: str, *, label: str) -> dict[str, Any]:
    try:
        redis_ok = await ping_redis()
    except Exception as exc:
        details = _error_details(
            "unreachable",
            error_class=exc.__class__.__name__,
            message=f"No fue posible verificar conectividad con el broker del dispatcher {label}.",
        )
        details["dispatcher_name"] = dispatcher_name
        return _service(
            "error",
            check_mode="active",
            critical=True,
            details=details,
        )

    details = {
        "dispatcher_name": dispatcher_name,
        "broker_reachable": redis_ok,
    }
    if not redis_ok:
        details["reason_code"] = "unreachable"
    return _service(
        "ok" if redis_ok else "error",
        check_mode="active",
        critical=True,
        details=details,
    )


def _presence_health_services(services: Any) -> dict[str, dict[str, Any]]:
    service_names = (
        "clinical_document_service",
        "case_epicrisis_service",
        "rda_service",
        "rips_service",
        "batch_runtime",
        "individual_ingestion_runtime",
    )
    return {name: _presence_service_entry(services, name) for name in service_names}


def _presence_service_entry(services: Any, name: str) -> dict[str, Any]:
    target = getattr(services, name, None)
    extra_details = None
    if name in {"batch_runtime", "individual_ingestion_runtime"} and target is not None:
        extra_details = {"dispatcher_name": _safe_dispatcher_name(target)}
    status, details = _presence_service_status(target is not None, extra_details=extra_details)
    return _service(
        status,
        check_mode="passive",
        critical=False,
        details=details,
    )


def _summarize_health(service_map: dict[str, dict[str, Any]]) -> tuple[str, dict[str, int]]:
    summary = {"ok": 0, "degraded": 0, "error": 0}
    overall_status = "ok"
    for result in service_map.values():
        status = str(result["status"])
        summary[status] += 1
        if status == "error" and bool(result["critical"]):
            overall_status = "error"
        elif status in {"error", "degraded"} and overall_status != "error":
            overall_status = "degraded"
    return overall_status, summary


async def build_health_report(app: Any) -> dict[str, Any]:
    runtime = getattr(app.state, "mongo_runtime", None)
    services = getattr(app.state, "services", None)
    startup_ready = bool(getattr(app.state, "startup_ready", False))
    checked_at = datetime.now(COLOMBIA_TZ).isoformat()

    service_map: dict[str, dict[str, Any]] = {
        "app_runtime": _app_runtime_service(startup_ready),
    }
    service_map.update(_missing_runtime_services() if runtime is None else await _runtime_health_services(runtime))
    service_map.update(_resource_health_services(services))
    service_map["batch_dispatcher"] = await _dispatcher_health_service(
        getattr(services, "batch_runtime", None),
        label="batch",
    )
    service_map["individual_ingestion_dispatcher"] = await _dispatcher_health_service(
        getattr(services, "individual_ingestion_runtime", None),
        label="individual",
    )
    service_map.update(_presence_health_services(services))

    overall_status, summary = _summarize_health(service_map)
    return {
        "status": overall_status,
        "checked_at": checked_at,
        "summary": summary,
        "services": service_map,
    }


async def build_ready_response(app: Any) -> JSONResponse | dict[str, Any]:
    runtime = getattr(app.state, "mongo_runtime", None)
    services = getattr(app.state, "services", None)
    if runtime is None or services is None or not getattr(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(),
    }
