"""Infrastructure endpoint handlers (health, help, config, node-types).

Health endpoints follow Kubernetes conventions:
    /healthz  - Liveness: is the process alive? (always 200 if reachable)
    /readyz   - Readiness: can the server handle traffic? (checks dependencies)
    /health   - Legacy combined health check (kept for backwards compatibility)
"""

from __future__ import annotations

import logging
import time
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from server.runtime import Runtime

import aiohttp
from aiohttp import web

from core.pathways.nodes.registry import NodeTypeRegistry
from core.tools import get_tool_registry
from persistence import FileStudioStore
from server.config import AlbusConfig, DeploymentConfig, get_config
from server.transport.schemas import ConfigValidateResponse, HealthResponse, ReadyResponse
from server.transport.utils import get_request_id, get_runtime, json_response
from stdlib.llm.routing import get_runtime_model_config, set_runtime_model_config

logger = logging.getLogger(__name__)

# Server start time — set once on first import.
_SERVER_START_TIME: float = time.monotonic()


# =============================================================================
# SHARED DEPENDENCY CHECKER — single source of truth for readyz + health
# =============================================================================


def _check_dependencies(request: web.Request) -> tuple[dict[str, str], Runtime | None]:
    """Run all dependency checks and return ``(checks_dict, runtime_or_None)``.

    Both ``/readyz`` and ``/health`` delegate here so the logic is never duplicated.
    """
    checks: dict[str, str] = {}
    config = get_config()

    # 1. Runtime
    runtime = None
    try:
        runtime = get_runtime(request)
        checks["runtime"] = "ok"
    except Exception:
        checks["runtime"] = "not_initialised"

    # 2. Pathway VM
    vm = getattr(runtime, "pathway_vm", None) if runtime else None
    checks["pathway_vm"] = "ok" if vm is not None else "unavailable"

    # 3. LLM provider — always use the canonical ``has_any_provider`` check
    checks["llm"] = "ok" if config.llm.has_any_provider else "not_configured"

    # 4. Persistence (file-backed OR database)
    has_persistence = bool(config.persistence.database_url)
    if not has_persistence and runtime is not None and vm is not None:
        try:
            store = vm.ctx.services.studio_store
            if isinstance(store, FileStudioStore):
                has_persistence = store.has_persistence
        except Exception:
            logger.debug("Failed to check file-backed persistence status", exc_info=True)

    checks["persistence"] = "ok" if has_persistence else "not_configured"

    return checks, runtime


async def handle_healthz(request: web.Request) -> web.Response:
    """GET /api/v1/healthz - Liveness probe.

    Returns 200 if the process is alive.  No dependency checks.
    K8s uses this to decide whether to *restart* the pod.
    """
    resp = HealthResponse(
        status="ok",
        uptime_seconds=round(time.monotonic() - _SERVER_START_TIME, 1),
    )
    return json_response(request, resp.model_dump(mode="json"))


async def handle_readyz(request: web.Request) -> web.Response:
    """GET /api/v1/readyz - Readiness probe.

    Returns 200 only when the server can actually serve requests:
    - Runtime is initialised
    - Pathway VM is available
    - At least one LLM provider is configured

    K8s uses this to decide whether to *route traffic* to the pod.
    """
    request_id = get_request_id(request)
    checks, _ = _check_dependencies(request)

    # Readiness requires all core deps to be healthy.
    ready = all(
        checks.get(dep) == "ok"
        for dep in ("runtime", "pathway_vm", "llm")
    )

    resp = ReadyResponse(
        status="ready" if ready else "not_ready",
        checks=checks,
        request_id=request_id,
    )
    return json_response(request, resp.model_dump(mode="json"), status=200 if ready else 503)


async def handle_root(_: web.Request) -> web.Response:
    """GET / or GET /api/v1 - API info and endpoint discovery."""
    # Note: root is often viewed in a browser/terminal; keep it pretty-able.
    # We don't need request_id here since this is discovery metadata.
    #
    # (We accept `_` as request parameter for compatibility with router signature.)
    request = _  # rename for readability
    return json_response(
        request,
        {
            "name": "AlbusOS",
            "version": "1.0",
            "api_version": "v1",
            "description": "A runtime for AI agents. Build pathways, wire tools, run agents.",
            "base_url": "/api/v1",
            "endpoints": {
                "health": {"GET /api/v1/health": "Health check with dependency status"},
                "config": {
                    "GET /api/v1/config/validate": "Validate configuration",
                    "GET /api/v1/config/models": "Get model routing config",
                    "PATCH /api/v1/config/models": "Update model routing (runtime)",
                },
                "skills": {
                    "GET /api/v1/skills": "List all skills",
                    "GET /api/v1/skills/{id}": "Get skill details",
                },
                "tools": {
                    "GET /api/v1/tools": "List all tools",
                    "POST /api/v1/tools/{name}": "Execute a tool",
                },
                "pathways": {
                    "GET /api/v1/pathways": "List all pathways",
                    "POST /api/v1/pathways": "Create a pathway",
                    "GET /api/v1/pathways/{id}": "Get pathway details",
                    "POST /api/v1/pathways/{id}/run": "Run a pathway",
                    "GET /api/v1/pathways/{id}/export": "Export as JSON",
                    "POST /api/v1/pathways/import": "Import from JSON",
                    "GET /api/v1/pathways/{id}/graph": "Get pathway graph data",
                },
                "agents": {
                    "GET /api/v1/agents": "List all agents",
                    "POST /api/v1/agents": "Create a new agent",
                    "GET /api/v1/agents/{id}": "Get agent details",
                    "POST /api/v1/agents/{id}/turn": "Execute an agent turn",
                    "DELETE /api/v1/agents/{id}": "Delete an agent",
                    "GET /api/v1/agents/{id}/skills": "List agent skills",
                },
                "realtime": {
                    "GET /api/v1/ws": "WebSocket (events/rpc)",
                    "POST /api/v1/webhooks/{topic}": "Publish webhook event",
                },
                "docs": {
                    "GET /api/v1/help": "Comprehensive API help",
                    "GET /api/v1/node-types": "List node types for pathway creation",
                    "GET /api/v1/openapi.json": "OpenAPI 3.0 specification",
                    "GET /api/v1/docs": "Interactive API docs (Swagger UI)",
                },
            },
            "tool_categories": sorted({name.split(".")[0] for name in get_tool_registry().names()}),
            "node_types": [
                "llm",
                "tool",
                "transform",
                "gate",
                "router",
                "code",
                "code_generator",
                "memory_read",
                "memory_write",
                "agent",
                "map",
                "retry",
                "timeout",
                "fallback",
            ],
            "docs": "https://github.com/albusstudio/albusOS",
        },
    )


async def handle_health(request: web.Request) -> web.Response:
    """GET /api/v1/health - Enhanced health check with dependency status.

    Delegates to the same ``_check_dependencies`` used by ``/readyz``,
    then adds a human-friendly overall status and timestamp.
    """
    request_id = get_request_id(request)
    checks, _ = _check_dependencies(request)

    # Derive overall status from the shared checks
    core_deps_ok = all(checks.get(d) == "ok" for d in ("runtime", "pathway_vm"))
    llm_ok = checks.get("llm") == "ok"

    if not core_deps_ok:
        overall_status = "down"
    elif not llm_ok:
        overall_status = "degraded"
    else:
        overall_status = "ok"

    resp = ReadyResponse(
        status=overall_status,
        checks=checks,
        request_id=request_id,
    )
    return json_response(request, resp.model_dump(mode="json"))


async def handle_config_validate(request: web.Request) -> web.Response:
    """GET /api/v1/config/validate - Validate configuration."""
    request_id = get_request_id(request)
    errors: list[str] = []
    warnings: list[str] = []
    deploy_errors: list[str] = []
    deploy_source: str = "unknown"
    config: AlbusConfig | None = None
    try:
        config = AlbusConfig.from_env(validate_required=False)
        config.validate()
    except ValueError as e:
        errors.append(str(e))
    except Exception as e:
        errors.append(f"Configuration error: {e!s}")

    # Validate deployment config (albus.yaml)
    try:
        deploy_config = DeploymentConfig.load()
        deploy_source = deploy_config.source
        deploy_errors = deploy_config.validate()
        if deploy_source == "none":
            warnings.append("No deployment config file found (no MCP servers will be deployed)")
    except Exception as e:
        deploy_errors = [f"Deployment configuration error: {e!s}"]

    # Check for warnings
    if config is not None and not config.llm.openai_api_key and not config.llm.anthropic_api_key:
        warnings.append("No primary LLM provider configured (OpenAI/Anthropic)")

    if config is not None and not config.persistence.database_url:
        warnings.append("Using file-based persistence (not suitable for production)")

    resp = ConfigValidateResponse(
        valid=len(errors) == 0 and len(deploy_errors) == 0,
        errors=errors,
        deployment={
            "source": deploy_source,
            "errors": deploy_errors,
        },
        warnings=warnings,
        config=config.to_dict() if config is not None else {},
        request_id=request_id,
    )
    return json_response(request, resp.model_dump(mode="json"))


async def handle_config_models(request: web.Request) -> web.Response:
    """GET /api/v1/config/models - Get current model routing configuration."""
    request_id = get_request_id(request)

    try:
        runtime_config = get_runtime_model_config()

        # Get list of available models from Ollama (if available)
        available_local_models: list[str] = []
        try:
            async with aiohttp.ClientSession() as session, session.get(
                "http://localhost:11434/api/tags", timeout=aiohttp.ClientTimeout(total=2)
            ) as resp:
                if resp.status == 200:
                    data = await resp.json()
                    available_local_models = [m["name"] for m in data.get("models", [])]
        except Exception:
            logger.debug("Ollama not running or not accessible")

        return json_response(
            request,
            {
                "success": True,
                "config": runtime_config,
                "available_local_models": available_local_models,
                "request_id": request_id,
            },
        )
    except Exception as e:
        return json_response(
            request,
            {
                "success": False,
                "error": str(e),
                "request_id": request_id,
            },
            status=500,
        )


async def handle_config_models_update(request: web.Request) -> web.Response:
    """PATCH /api/v1/config/models - Update model routing configuration at runtime.

    Body:
        {
            "routing": {
                "tool_calling": "gpt-4o",
                "code": "claude-3-5-sonnet-20241022"
            }
        }

    Note: Changes are runtime-only. To persist, update albus.yaml.
    """
    request_id = get_request_id(request)

    try:
        data = await request.json()
    except Exception:
        return json_response(
            request,
            {"success": False, "error": "Invalid JSON", "request_id": request_id},
            status=400,
        )

    try:
        routing = data.get("routing")

        # Apply changes
        set_runtime_model_config(routing=routing)

        return json_response(
            request,
            {
                "success": True,
                "message": "Model routing updated (runtime only, restart to reset)",
                "config": get_runtime_model_config(),
                "request_id": request_id,
            },
        )
    except Exception as e:
        return json_response(
            request,
            {
                "success": False,
                "error": str(e),
                "request_id": request_id,
            },
            status=500,
        )


async def handle_help(request: web.Request) -> web.Response:
    """GET /api/v1/help - Comprehensive API help."""
    # Count tools by category
    registry = get_tool_registry()
    categories: dict[str, int] = {}
    for name in registry.names():
        cat = name.split(".")[0]
        categories[cat] = categories.get(cat, 0) + 1

    return json_response(
        request,
        {
            "name": "AlbusOS",
            "version": "1.0",
            "api_version": "v1",
            "description": "A runtime for AI agents. Build pathways, wire tools, run agents.",
            "base_url": "/api/v1",
            "quick_start": {
                "1_health": "curl http://localhost:8080/api/v1/health",
                "2_list_tools": "curl http://localhost:8080/api/v1/tools",
                "3_create_pathway": 'curl -X POST http://localhost:8080/api/v1/pathways -H "Content-Type: application/json" -d \'{"name": "Add", "nodes": [{"id": "x", "type": "transform", "config": {"expr": "a + b"}}]}\'',
                "4_run_pathway": 'curl -X POST http://localhost:8080/api/v1/pathways/{id}/run -d \'{"inputs": {"a": 1, "b": 2}}\'',
            },
            "endpoints": {
                "GET /api/v1": "API overview",
                "GET /api/v1/health": "Health check with dependency status",
                "GET /api/v1/config/validate": "Validate configuration",
                "GET /api/v1/help": "This help page",
                "GET /api/v1/openapi.json": "OpenAPI 3.0 specification",
                "GET /api/v1/docs": "Interactive API docs (Swagger UI)",
                "GET /api/v1/skills": "List all skills (?builtin=true, ?tag=X)",
                "GET /api/v1/skills/{id}": "Get skill details with instructions",
                "GET /api/v1/tools": "List tools (?category=X, ?format=grouped)",
                "POST /api/v1/tools/{name}": "Execute tool",
                "GET /api/v1/node-types": "List node types for pathway creation",
                "GET /api/v1/agents": "List all agents",
                "GET /api/v1/agents/{id}": "Get agent details (includes skills)",
                "GET /api/v1/agents/{id}/skills": "List all skills for an agent",
                "POST /api/v1/agents": "Create new agent",
                "POST /api/v1/agents/{id}/turn": "Execute agent turn",
                "DELETE /api/v1/agents/{id}": "Delete agent",
                "GET /api/v1/pathways": "List all pathways",
                "POST /api/v1/pathways": "Create a pathway",
                "GET /api/v1/pathways/{id}": "Get pathway details",
                "POST /api/v1/pathways/{id}/run": "Run pathway",
                "GET /api/v1/pathways/{id}/export": "Export pathway as JSON",
                "POST /api/v1/pathways/import": "Import pathway from JSON",
                "GET /api/v1/pathways/{id}/graph": "Get pathway graph data",
                "GET /api/v1/ws": "WebSocket (events/rpc)",
                "POST /api/v1/webhooks/{topic}": "Publish webhook event",
            },
            "tools": {
                "total": len(registry),
                "categories": categories,
            },
        },
    )


async def handle_node_types(request: web.Request) -> web.Response:
    """GET /api/v1/node-types - List all node types for pathway creation.

    Query params:
        category: Filter by category (compute, control, memory, composition, streaming, agent)
        type: Get single node type detail
        format: "full" for JSON schemas, "simple" for basic info (default: simple)
    """
    # Check for single type request
    single_type = request.query.get("type")
    if single_type:
        spec = NodeTypeRegistry.get(single_type)
        if spec is None:
            return json_response(
                request,
                {
                    "error": f"Unknown node type: {single_type}",
                    "available_types": [s.type for s in NodeTypeRegistry.list_all()],
                },
                status=404,
            )
        return json_response(request, spec.to_dict())

    # Filter by category if requested
    category = request.query.get("category")
    specs = NodeTypeRegistry.by_category(category) if category else NodeTypeRegistry.list_all()

    # Format: full or simple
    format_type = request.query.get("format", "simple")

    if format_type == "full":
        # Full format with JSON schemas
        node_types = [spec.to_dict() for spec in specs]
    else:
        # Simple format for quick reference
        node_types = [
            {
                "type": spec.type,
                "name": spec.name,
                "description": spec.description,
                "category": spec.category,
                "example": spec.examples[0].config if spec.examples else None,
            }
            for spec in specs
        ]

    return json_response(
        request,
        {
            "node_types": node_types,
            "count": len(node_types),
            "categories": NodeTypeRegistry.categories(),
            "connection_format": {
                "from_node": "Source node ID",
                "to_node": "Target node ID",
                "from_output": "Output port name (default: 'output')",
                "to_input": "Input port name (default: 'input')",
            },
            "example_pathway": {
                "name": "Calculator",
                "nodes": [
                    {"id": "double", "type": "transform", "config": {"expr": "x * 2"}},
                    {
                        "id": "add_ten",
                        "type": "transform",
                        "config": {"expr": "double + 10"},
                    },
                ],
                "connections": [
                    {"from_node": "double", "to_node": "add_ten"},
                ],
            },
        },
    )


__all__ = [
    "handle_config_validate",
    "handle_health",
    "handle_healthz",
    "handle_help",
    "handle_node_types",
    "handle_readyz",
    "handle_root",
]
