"""FastAPI application factory for the unified ossctx UI."""

from __future__ import annotations

import contextlib
import logging
from pathlib import Path

from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from pydantic import BaseModel
from starlette.routing import Mount

from common.server import create_app
from common.version import __version__
from ui.views import build_router, configure_databases

from codectx.cli import build_app as build_code_app
from codectx.config import CodeSettings
from codectx.db.repository import CodeRepository
from docsctx.cli import build_app as build_docs_app
from docsctx.config import DocsSettings
from docsctx.db.repository import DocsRepository
from otelctx.cli import build_app as build_otel_app
from otelctx.config import OtelSettings
from otelctx.db.repository import OtelRepository
from otelctx.api.routes import ingest_payload

logger = logging.getLogger(__name__)

# Collect MCP servers for lifespan management
_mcp_servers: list = []


def _templates_dir() -> Path:
    return Path(__file__).resolve().parent / "templates"


def _static_dir() -> Path:
    return Path(__file__).resolve().parent / "static"


class ContextQuery(BaseModel):
    query: str
    budget: int = 8000
    subsystems: list[str] | None = None


def build_app(
    code_db: str | Path = ".codecontext.db",
    docs_db: str | Path = ".docscontext.db",
    otel_db: str | Path = ".otelcontext.db",
) -> FastAPI:
    """Create the unified UI application."""
    configure_databases(code_db=Path(code_db), docs_db=Path(docs_db), otel_db=Path(otel_db))

    @contextlib.asynccontextmanager
    async def lifespan(app_instance: FastAPI):
        """Manage MCP session manager lifespans."""
        async with contextlib.AsyncExitStack() as stack:
            for mcp_server in _mcp_servers:
                try:
                    await stack.enter_async_context(mcp_server.session_manager.run())
                except Exception as exc:
                    logger.warning("Failed to start MCP session manager: %s", exc)
            yield

    app = create_app("ossctx-ui", __version__)
    app.router.lifespan_context = lifespan
    app.state.templates = Jinja2Templates(directory=str(_templates_dir()))
    app.mount("/static", StaticFiles(directory=str(_static_dir())), name="static")

    app.include_router(build_router())

    app.mount("/api/code", build_code_app(CodeSettings(db_path=Path(code_db))))
    app.mount("/api/docs", build_docs_app(DocsSettings(db_path=Path(docs_db))))

    otel_settings = OtelSettings(db_path=Path(otel_db))
    otel_app = build_otel_app(otel_settings)
    app.mount("/api/otel", otel_app)

    # Mount MCP servers using streamable HTTP transport (SSE is deprecated)
    from codectx.cli import build_mcp_server as build_code_mcp
    from docsctx.mcp import build_mcp_server as build_docs_mcp
    from otelctx.mcp import build_mcp_server as build_otel_mcp

    code_mcp = build_code_mcp(CodeSettings(db_path=Path(code_db)), host="0.0.0.0", port=8081)
    code_mcp.settings.streamable_http_path = "/"
    app.mount("/mcp/code", code_mcp.streamable_http_app())
    _mcp_servers.append(code_mcp)

    otel_mcp = build_otel_mcp(otel_settings, host="0.0.0.0", port=8082)
    otel_mcp.settings.streamable_http_path = "/"
    app.mount("/mcp/otel", otel_mcp.streamable_http_app())
    _mcp_servers.append(otel_mcp)

    docs_settings = DocsSettings(db_path=Path(docs_db))
    docs_repo = DocsRepository(Path(docs_db))
    docs_mcp = build_docs_mcp(docs_repo, docs_settings, host="0.0.0.0", port=8083)
    docs_mcp.settings.streamable_http_path = "/"
    app.mount("/mcp/docs", docs_mcp.streamable_http_app())
    _mcp_servers.append(docs_mcp)

    # -- Unified context & agent MCP server -----------------------------------
    _mount_unified_mcp(app, code_db, docs_db, otel_db)

    # -- Unified context assembly REST endpoint -------------------------------
    code_repo = CodeRepository(Path(code_db))
    code_repo.create_schema()
    otel_repo = OtelRepository(Path(otel_db))
    otel_repo.create_schema()

    assembler = _create_assembler(code_repo, docs_repo, otel_repo)
    app.state.assembler = assembler

    @app.post("/api/context/assemble")
    async def assemble_context(body: ContextQuery) -> dict:
        """Assemble cross-system context for an LLM query."""
        ctx = assembler.assemble(
            body.query, context_budget=body.budget, subsystems=body.subsystems,
        )
        return ctx.to_dict()

    @app.get("/api/context/route")
    async def route_query(query: str) -> dict:
        """Determine which subsystems are relevant to a query."""
        from common.context import route_query as _route
        return {"query": query, "subsystems": _route(query)}

    # Expose standard OTLP HTTP receiver endpoints at the root for compatibility
    @app.post("/v1/traces")
    async def ingest_otlp_traces(payload: dict[str, object]) -> dict[str, int]:
        return ingest_payload(otel_repo, payload)

    @app.post("/v1/logs")
    async def ingest_otlp_logs(payload: dict[str, object]) -> dict[str, int]:
        return ingest_payload(otel_repo, payload)

    @app.post("/v1/metrics")
    async def ingest_otlp_metrics(payload: dict[str, object]) -> dict[str, int]:
        return ingest_payload(otel_repo, payload)

    return app


def _create_assembler(code_repo, docs_repo, otel_repo):
    """Create a ContextAssembler with optional vector stores and graph."""
    from common.context import ContextAssembler

    # Try to initialize vector stores and graph (best-effort)
    code_vs = None
    docs_vs = None
    graph_store = None

    try:
        from codectx.vectorstore import CodeVectorStore
        code_vs = CodeVectorStore()
    except Exception:
        pass

    try:
        from docsctx.vectorstore import DocsVectorStore
        docs_vs = DocsVectorStore()
    except Exception:
        pass

    try:
        from common.graphdb import GraphStore
        graph_store = GraphStore()
        graph_store.build_from_code_repo(code_repo)
        graph_store.build_from_docs_repo(docs_repo)
    except Exception:
        pass

    return ContextAssembler(
        code_repo=code_repo,
        docs_repo=docs_repo,
        otel_repo=otel_repo,
        code_vectorstore=code_vs,
        docs_vectorstore=docs_vs,
        graph_store=graph_store,
    )


def _mount_unified_mcp(app: FastAPI, code_db, docs_db, otel_db) -> None:
    """Mount a unified MCP server with cross-system tools."""
    try:
        from mcp.server.fastmcp import FastMCP

        mcp = FastMCP(
            name="ossctx-unified",
            instructions="Unified agent interface for code, docs, and telemetry context.",
            stateless_http=True,
            json_response=True,
        )

        code_repo = CodeRepository(Path(code_db))
        code_repo.create_schema()
        docs_repo = DocsRepository(Path(docs_db))
        otel_repo = OtelRepository(Path(otel_db))
        otel_repo.create_schema()

        assembler = _create_assembler(code_repo, docs_repo, otel_repo)

        from common.agent import AgentInterface
        agent = AgentInterface(
            assembler=assembler,
            code_repo=code_repo,
            docs_repo=docs_repo,
            otel_repo=otel_repo,
        )

        @mcp.tool(description="Smart cross-context search: assembles code + docs + telemetry context for any query.")
        def query(question: str, budget: int = 8000) -> dict:
            ctx = agent.query(question, budget=budget)
            return ctx.to_dict()

        @mcp.tool(description="Get a full overview of a component: code entities, docs, and recent telemetry.")
        def component_overview(name: str) -> dict:
            from dataclasses import asdict
            return asdict(agent.get_component_overview(name))

        @mcp.tool(description="Investigate an incident: trace -> related code -> docs -> similar past incidents.")
        def incident_context(trace_id: str) -> dict:
            from dataclasses import asdict
            return asdict(agent.get_incident_context(trace_id))

        @mcp.tool(description="Show what changed between code versions.")
        def diff_versions(old_version: str) -> dict:
            return agent.diff_versions(old_version)

        @mcp.tool(description="Determine which subsystems are relevant to a query.")
        def route_query(question: str) -> dict:
            from common.context import route_query as _route
            return {"query": question, "subsystems": _route(question)}

        # Register action tools
        from common.actions.mcp import register_action_tools
        register_action_tools(mcp)

        mcp.settings.streamable_http_path = "/"
        app.mount("/mcp/unified", mcp.streamable_http_app())
        _mcp_servers.append(mcp)
    except Exception as exc:
        logger.warning("Failed to mount unified MCP server: %s", exc)
