from __future__ import annotations

from contextlib import asynccontextmanager
import logging
import asyncio

from fastapi import FastAPI, HTTPException, Query
from fastapi.middleware.cors import CORSMiddleware

from .models import (
    EdgesResponse,
    GraphResponse,
    NeighborsRequest,
    NeighborsResponse,
    NodesResponse,
    SparqlQueryRequest,
    StatsResponse,
)
from .pipelines.layout_dag_radial import CycleError
from .pipelines.owl_imports_combiner import (
    build_combined_graph,
    output_location_to_path,
    resolve_output_location,
    serialize_graph_to_ttl,
)
from .pipelines.selection_neighbors import fetch_neighbor_ids_for_selection
from .pipelines.snapshot_service import GraphSnapshotService
from .rdf_store import RDFStore
from .sparql_engine import RdflibEngine, SparqlEngine, create_sparql_engine
from .settings import Settings


settings = Settings()
logger = logging.getLogger(__name__)


@asynccontextmanager
async def lifespan(app: FastAPI):
    rdflib_preloaded_graph = None

    if settings.combine_owl_imports_on_start:
        entry_location = settings.combine_entry_location or settings.ttl_path
        output_location = resolve_output_location(
            entry_location,
            output_location=settings.combine_output_location,
            output_name=settings.combine_output_name,
        )

        output_path = output_location_to_path(output_location)
        if output_path.exists() and not settings.combine_force:
            logger.info("Skipping combine step (output exists): %s", output_location)
        else:
            rdflib_preloaded_graph = await asyncio.to_thread(build_combined_graph, entry_location)
            logger.info("Finished combining imports; serializing to: %s", output_location)
            await asyncio.to_thread(serialize_graph_to_ttl, rdflib_preloaded_graph, output_location)

        if settings.graph_backend == "rdflib":
            settings.ttl_path = str(output_path)

    sparql: SparqlEngine = create_sparql_engine(settings, rdflib_graph=rdflib_preloaded_graph)
    await sparql.startup()
    app.state.sparql = sparql
    app.state.snapshot_service = GraphSnapshotService(sparql=sparql, settings=settings)

    # Only build node/edge tables when running in rdflib mode.
    if settings.graph_backend == "rdflib":
        assert isinstance(sparql, RdflibEngine)
        if sparql.graph is None:
            raise RuntimeError("rdflib graph failed to load")

        store = RDFStore(
            ttl_path=settings.ttl_path,
            include_bnodes=settings.include_bnodes,
            max_triples=settings.max_triples,
        )
        store.load(sparql.graph)
        app.state.store = store

    yield

    await sparql.shutdown()


app = FastAPI(title="visualizador_instanciados backend", lifespan=lifespan)

cors_origins = settings.cors_origin_list()
app.add_middleware(
    CORSMiddleware,
    allow_origins=cors_origins,
    allow_credentials=False,
    allow_methods=["*"],
    allow_headers=["*"],
)


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


@app.get("/api/stats", response_model=StatsResponse)
async def stats() -> StatsResponse:
    # Stats reflect exactly what we send to the frontend (/api/graph), not global graph size.
    svc: GraphSnapshotService = app.state.snapshot_service
    try:
        snap = await svc.get(node_limit=50_000, edge_limit=100_000)
    except CycleError as e:
        raise HTTPException(status_code=422, detail=str(e)) from None
    meta = snap.meta
    return StatsResponse(
        backend=meta.backend if meta else app.state.sparql.name,
        ttl_path=meta.ttl_path if meta and meta.ttl_path else settings.ttl_path,
        sparql_endpoint=meta.sparql_endpoint if meta else None,
        parsed_triples=len(snap.edges),
        nodes=len(snap.nodes),
        edges=len(snap.edges),
    )


@app.post("/api/sparql")
async def sparql_query(req: SparqlQueryRequest) -> dict:
    sparql: SparqlEngine = app.state.sparql
    data = await sparql.query_json(req.query)
    return data


@app.post("/api/neighbors", response_model=NeighborsResponse)
async def neighbors(req: NeighborsRequest) -> NeighborsResponse:
    svc: GraphSnapshotService = app.state.snapshot_service
    snap = await svc.get(node_limit=req.node_limit, edge_limit=req.edge_limit)
    sparql: SparqlEngine = app.state.sparql
    neighbor_ids = await fetch_neighbor_ids_for_selection(
        sparql,
        snapshot=snap,
        selected_ids=req.selected_ids,
        include_bnodes=settings.include_bnodes,
    )
    return NeighborsResponse(selected_ids=req.selected_ids, neighbor_ids=neighbor_ids)


@app.get("/api/nodes", response_model=NodesResponse)
def nodes(
    limit: int = Query(default=10_000, ge=1, le=200_000),
    offset: int = Query(default=0, ge=0),
) -> NodesResponse:
    if settings.graph_backend != "rdflib":
        raise HTTPException(status_code=501, detail="GET /api/nodes is only supported in GRAPH_BACKEND=rdflib mode")
    store: RDFStore = app.state.store
    return NodesResponse(total=store.node_count, nodes=store.node_slice(offset=offset, limit=limit))


@app.get("/api/edges", response_model=EdgesResponse)
def edges(
    limit: int = Query(default=50_000, ge=1, le=500_000),
    offset: int = Query(default=0, ge=0),
) -> EdgesResponse:
    if settings.graph_backend != "rdflib":
        raise HTTPException(status_code=501, detail="GET /api/edges is only supported in GRAPH_BACKEND=rdflib mode")
    store: RDFStore = app.state.store
    return EdgesResponse(total=store.edge_count, edges=store.edge_slice(offset=offset, limit=limit))


@app.get("/api/graph", response_model=GraphResponse)
async def graph(
    node_limit: int = Query(default=50_000, ge=1, le=200_000),
    edge_limit: int = Query(default=100_000, ge=1, le=500_000),
) -> GraphResponse:
    svc: GraphSnapshotService = app.state.snapshot_service
    try:
        return await svc.get(node_limit=node_limit, edge_limit=edge_limit)
    except CycleError as e:
        raise HTTPException(status_code=422, detail=str(e)) from None
