"""cortex/api/routes/brain_graph.py — Brain graph endpoints.

Canonical prefix: ``/brain/`` (v2).
Deprecated prefix: ``/brain-graph/`` — preserved with RFC 8594 deprecation
headers (Sunset: 2026-06-01).  Issue #431.
"""

from __future__ import annotations

import logging
from typing import Any, Dict, List, Optional

from fastapi import APIRouter, Depends, HTTPException, Response, status
from fastapi.responses import JSONResponse

from cortex.api.deps import PREFIX, _serialise, get_plugin
from cortex.api.models import (
    BrainBuildRequest,
    BrainBuildResponse,
    BrainGraphBuildRequest,
    BrainGraphBuildResponse,
    BrainGraphListResponse,
    BrainGraphQueryRequest,
    BrainGraphQueryResponse,
    BrainNodeDetailResponse,
    BrainNodeListResponse,
    BrainQueryNode,
    BrainQueryRequest,
    BrainQueryResponse,
    BrainStatsResponse,
)

logger = logging.getLogger(__name__)

router = APIRouter()

# ---------------------------------------------------------------------------
# Deprecation helpers (#431)
# ---------------------------------------------------------------------------
_DEPRECATION_SUNSET = "2026-06-01"


def _add_deprecation_headers(response: Response, canonical_path: str) -> None:
    """Inject RFC 8594 deprecation + sunset + link headers."""
    response.headers["Deprecation"] = "true"
    response.headers["Sunset"] = _DEPRECATION_SUNSET
    response.headers["Link"] = f'<{canonical_path}>; rel="successor-version"'


def _make_routes(verify_api_key):
    """Register brain graph routes with the given auth dependency."""

    # ==================================================================
    # DEPRECATED — /brain-graph/ prefix (sunset 2026-06-01)  #431
    # ==================================================================

    @router.post(
        f"{PREFIX}/brain-graph/build",
        response_model=BrainGraphBuildResponse,
        status_code=status.HTTP_200_OK,
        summary="Build brain graph (DEPRECATED — use /brain/build)",
        tags=["brain-graph (deprecated)"],
        deprecated=True,
    )
    async def brain_graph_build(
        body: BrainGraphBuildRequest,
        response: Response,
        _: None = Depends(verify_api_key),
        plugin: Any = Depends(get_plugin),
    ) -> BrainGraphBuildResponse:
        """Trigger a brain-graph rebuild (deprecated). Use POST /brain/build instead."""
        _add_deprecation_headers(response, f"{PREFIX}/brain/build")
        errors: List[str] = []
        nodes_created = 0
        edges_created = 0
        graph_id: Optional[str] = None

        if not body.source_path and not body.text:
            raise HTTPException(
                status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
                detail="Provide either 'source_path' or 'text'",
            )

        try:
            from cortex.brain_graph.builder import BrainGraphBuilder
            builder = BrainGraphBuilder(
                storage=plugin.storage,
                llm=plugin.llm,
                config=plugin.config,
            )
            if body.source_path:
                result = await builder.build(
                    source=body.source_path,
                    owner_id=plugin.config.owner_id,
                )
            else:
                result = await builder.build(
                    source=body.text,
                    source_type="text",
                    source_id=body.source_id or "api-text",
                    owner_id=plugin.config.owner_id,
                )
            nodes_created = len(getattr(result, "nodes", []))
            edges_created = len(getattr(result, "edges", []))
            graph_id = getattr(result, "graph_id", None) or getattr(result, "id", None)
        except NotImplementedError as exc:
            errors.append(f"Brain graph builder not yet implemented: {exc}")
        except Exception as exc:
            err = f"Brain graph build failed: {exc}"
            logger.error(err, exc_info=True)
            errors.append(err)

        return BrainGraphBuildResponse(
            nodes_created=nodes_created,
            edges_created=edges_created,
            graph_id=graph_id,
            errors=errors,
        )

    @router.post(
        f"{PREFIX}/brain-graph/query",
        response_model=BrainGraphQueryResponse,
        status_code=status.HTTP_200_OK,
        summary="Query brain graph (DEPRECATED — use /brain/query)",
        tags=["brain-graph (deprecated)"],
        deprecated=True,
    )
    async def brain_graph_query(
        body: BrainGraphQueryRequest,
        response: Response,
        _: None = Depends(verify_api_key),
        plugin: Any = Depends(get_plugin),
    ) -> BrainGraphQueryResponse:
        """Query the brain graph (deprecated). Use POST /brain/query instead."""
        _add_deprecation_headers(response, f"{PREFIX}/brain/query")
        errors: List[str] = []
        nodes: List[Dict[str, Any]] = []
        edges: List[Dict[str, Any]] = []
        context = ""
        token_count = 0

        try:
            from cortex.brain_graph.query import BrainGraphQueryEngine
            engine = BrainGraphQueryEngine(
                storage=plugin.storage,
                llm=plugin.llm,
                config=plugin.config,
            )
            result = await engine.query(
                query=body.query,
                owner_id=plugin.config.owner_id,
                max_depth=body.max_depth,
                token_budget=body.limit * 50,
            )
            nodes = [_serialise(n.__dict__) for n in getattr(result, "nodes", [])]
            context = getattr(result, "context", "")
            token_count = getattr(result, "token_count", 0)
            traversal = getattr(result, "traversal_path", [])
            if traversal and plugin.storage and hasattr(plugin.storage, "get_brain_graph_nodes"):
                for t in traversal:
                    edges.append({"source": t.get("from"), "target": t.get("to"), "relation": t.get("relation", "")})
        except NotImplementedError as exc:
            errors.append(f"Brain graph query engine not yet implemented: {exc}")
        except Exception as exc:
            err = f"Brain graph query failed: {exc}"
            logger.error(err, exc_info=True)
            errors.append(err)

        return BrainGraphQueryResponse(
            nodes=nodes,
            edges=edges,
            context=context,
            token_count=token_count,
            errors=errors,
        )

    @router.get(
        f"{PREFIX}/brain-graph",
        response_model=BrainGraphListResponse,
        summary="List brain graph nodes (DEPRECATED — use /brain/nodes)",
        tags=["brain-graph (deprecated)"],
        deprecated=True,
    )
    async def brain_graph_list(
        limit: int = 50,
        offset: int = 0,
        type: Optional[str] = None,
        _: None = Depends(verify_api_key),
        plugin: Any = Depends(get_plugin),
        response: Response = None,
    ) -> BrainGraphListResponse:
        """List brain-graph nodes (deprecated). Use GET /brain/nodes instead."""
        _add_deprecation_headers(response, f"{PREFIX}/brain/nodes")
        errors: List[str] = []
        nodes: List[Dict[str, Any]] = []
        total = 0

        if plugin.storage is None:
            errors.append("Storage not available")
            return BrainGraphListResponse(errors=errors)

        all_raw_nodes: List[Any] = []
        try:
            if hasattr(plugin.storage, "list_brain_graphs") and hasattr(plugin.storage, "get_brain_graph_nodes"):
                graphs = await plugin.storage.list_brain_graphs()
                for g in graphs:
                    g_nodes = await plugin.storage.get_brain_graph_nodes(g.id)
                    all_raw_nodes.extend(g_nodes)

                if type:
                    all_raw_nodes = [n for n in all_raw_nodes if getattr(n, "node_type", "") == type]

                total = len(all_raw_nodes)
                page = all_raw_nodes[offset: offset + limit]
                nodes = [_serialise(n.__dict__) if hasattr(n, "__dict__") else _serialise(n) for n in page]
            else:
                errors.append("Brain graph listing not supported by storage")
        except Exception as exc:
            err = f"Brain graph list failed: {exc}"
            logger.error(err, exc_info=True)
            errors.append(err)

        return BrainGraphListResponse(nodes=nodes, total=total, errors=errors)

    # ==================================================================
    # CANONICAL — /brain/ prefix (v2)
    # ==================================================================

    @router.post(
        f"{PREFIX}/brain/build",
        response_model=BrainBuildResponse,
        status_code=status.HTTP_200_OK,
        summary="Trigger brain graph build/refresh",
        tags=["brain"],
    )
    async def brain_build(
        body: BrainBuildRequest,
        _: None = Depends(verify_api_key),
        plugin: Any = Depends(get_plugin),
    ) -> BrainBuildResponse:
        """Trigger a brain graph build or refresh."""
        errors: List[str] = []
        nodes_added = 0
        nodes_updated = 0
        edges_created = 0

        if not body.watch_dirs:
            raise HTTPException(
                status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
                detail="watch_dirs must not be empty",
            )

        try:
            from cortex.brain_graph.builder import BrainGraphBuilder
            import fnmatch
            import os as _os

            builder = BrainGraphBuilder(
                storage=plugin.storage,
                llm=plugin.llm,
                config=plugin.config,
            )

            for watch_dir in body.watch_dirs:
                if not _os.path.isdir(watch_dir):
                    errors.append(f"Not a directory: {watch_dir}")
                    continue

                for root, dirs, files in _os.walk(watch_dir):
                    for fname in files:
                        fpath = _os.path.join(root, fname)
                        rel = _os.path.relpath(fpath, watch_dir)

                        if body.exclude_patterns:
                            if any(fnmatch.fnmatch(rel, p) for p in body.exclude_patterns):
                                continue
                        if body.include_patterns:
                            if not any(fnmatch.fnmatch(rel, p) for p in body.include_patterns):
                                continue

                        try:
                            result = await builder.build(
                                source=fpath,
                                owner_id=plugin.config.owner_id,
                            )
                            n = len(getattr(result, "nodes", []))
                            e = len(getattr(result, "edges", []))
                            if getattr(result, "updated", False):
                                nodes_updated += n
                            else:
                                nodes_added += n
                            edges_created += e
                        except Exception as exc:
                            errors.append(f"Build failed for {fpath}: {exc}")

        except Exception as exc:
            err = f"Brain build failed: {exc}"
            logger.error(err, exc_info=True)
            errors.append(err)

        return BrainBuildResponse(
            nodes_added=nodes_added,
            nodes_updated=nodes_updated,
            edges_created=edges_created,
            errors=errors,
        )

    @router.post(
        f"{PREFIX}/brain/query",
        response_model=BrainQueryResponse,
        status_code=status.HTTP_200_OK,
        summary="Query the brain graph",
        tags=["brain"],
    )
    async def brain_query(
        body: BrainQueryRequest,
        _: None = Depends(verify_api_key),
        plugin: Any = Depends(get_plugin),
    ) -> BrainQueryResponse:
        """Query the brain graph for nodes relevant to the given query."""
        errors: List[str] = []
        nodes: List[BrainQueryNode] = []

        if plugin.storage is None:
            errors.append("Storage not available")
            return BrainQueryResponse(nodes=nodes, errors=errors)

        try:
            from cortex.brain_graph.query import BrainGraphQueryEngine

            engine = BrainGraphQueryEngine(
                storage=plugin.storage,
                llm=plugin.llm,
                config=plugin.config,
            )
            result = await engine.query(
                query=body.query,
                owner_id=plugin.config.owner_id,
                max_depth=3,
                token_budget=body.max_results * 200,
            )

            raw_nodes = getattr(result, "nodes", [])
            if body.node_type:
                raw_nodes = [n for n in raw_nodes if getattr(n, "node_type", "") == body.node_type]

            for idx, node in enumerate(raw_nodes[: body.max_results]):
                content = getattr(node, "content", "") or ""
                label = getattr(node, "label", "") or getattr(node, "title", "") or ""
                node_id = getattr(node, "id", "") or ""
                node_type = getattr(node, "node_type", "") or "unknown"
                score = 1.0 - (idx * 0.05)

                conns: List[Dict[str, Any]] = []
                try:
                    if hasattr(plugin.storage, "get_brain_graph_edges"):
                        graph_id = getattr(node, "graph_id", "")
                        if graph_id:
                            edges = await plugin.storage.get_brain_graph_edges(graph_id)
                            for edge in edges:
                                src = getattr(edge, "source_id", "")
                                tgt = getattr(edge, "target_id", "")
                                if src == node_id or tgt == node_id:
                                    conns.append({
                                        "node_id": tgt if src == node_id else src,
                                        "label": getattr(edge, "relation_type", ""),
                                        "relation": getattr(edge, "relation_type", ""),
                                    })
                except Exception as e:
                    logger.warning("http_server: unhandled exception: %s", e)

                nodes.append(BrainQueryNode(
                    id=node_id,
                    title=label,
                    content_preview=content[:500],
                    type=node_type,
                    relevance_score=max(0.0, score),
                    connections=conns,
                ))

        except Exception as exc:
            err = f"Brain query failed: {exc}"
            logger.error(err, exc_info=True)
            errors.append(err)

        return BrainQueryResponse(nodes=nodes, errors=errors)

    @router.get(
        f"{PREFIX}/brain/nodes",
        response_model=BrainNodeListResponse,
        summary="List all brain nodes",
        tags=["brain"],
    )
    async def brain_nodes_list(
        type: Optional[str] = None,
        limit: int = 50,
        offset: int = 0,
        _: None = Depends(verify_api_key),
        plugin: Any = Depends(get_plugin),
    ) -> BrainNodeListResponse:
        """List brain graph nodes across all graphs."""
        errors: List[str] = []
        items: List[Dict[str, Any]] = []
        total = 0

        if plugin.storage is None:
            errors.append("Storage not available")
            return BrainNodeListResponse(errors=errors)

        limit = max(1, min(limit, 500))
        offset = max(0, offset)

        try:
            raw_nodes = await plugin.storage.list_brain_nodes(
                node_type=type,
                limit=limit,
                offset=offset,
                owner_id=plugin.config.owner_id,
            )
            total = await plugin.storage.count_brain_nodes(
                node_type=type,
                owner_id=plugin.config.owner_id,
            )
            for node in raw_nodes:
                d = _serialise(node.__dict__) if hasattr(node, "__dict__") else _serialise(node)
                items.append(d)
        except Exception as exc:
            err = f"Brain nodes list failed: {exc}"
            logger.error(err, exc_info=True)
            errors.append(err)

        return BrainNodeListResponse(items=items, total=total, errors=errors)

    @router.get(
        f"{PREFIX}/brain/nodes/{{node_id}}",
        response_model=BrainNodeDetailResponse,
        summary="Brain node detail",
        tags=["brain"],
    )
    async def brain_node_detail(
        node_id: str,
        _: None = Depends(verify_api_key),
        plugin: Any = Depends(get_plugin),
    ) -> BrainNodeDetailResponse:
        """Return full detail for a single brain graph node."""
        errors: List[str] = []

        if plugin.storage is None:
            errors.append("Storage not available")
            return BrainNodeDetailResponse(errors=errors)

        node = await plugin.storage.get_brain_node(node_id)
        if node is None:
            raise HTTPException(
                status_code=status.HTTP_404_NOT_FOUND,
                detail=f"Brain node '{node_id}' not found",
            )

        import json as _json
        metadata: Dict[str, Any] = {}
        try:
            metadata = _json.loads(getattr(node, "metadata_json", "") or "{}")
        except Exception as e:
            logger.warning("http_server: unhandled exception: %s", e)

        source_path: Optional[str] = metadata.get("source_path") or metadata.get("path")

        conns: List[Dict[str, Any]] = []
        try:
            edges = await plugin.storage.get_brain_graph_edges(
                getattr(node, "graph_id", "")
            )
            for edge in edges:
                src = getattr(edge, "source_id", "")
                tgt = getattr(edge, "target_id", "")
                if src == node_id or tgt == node_id:
                    conns.append({
                        "node_id": tgt if src == node_id else src,
                        "relation": getattr(edge, "relation_type", ""),
                        "weight": getattr(edge, "weight", 1.0),
                        "direction": "out" if src == node_id else "in",
                    })
        except Exception as exc:
            errors.append(f"Edge fetch failed: {exc}")

        content = getattr(node, "content", "") or ""
        return BrainNodeDetailResponse(
            id=node_id,
            title=getattr(node, "label", "") or node_id,
            content=content,
            type=getattr(node, "node_type", "") or "unknown",
            source_path=source_path,
            connections=conns,
            created_at=str(getattr(node, "created_at", "") or ""),
            updated_at=str(metadata.get("updated_at", getattr(node, "created_at", "")) or ""),
            errors=errors,
        )

    @router.delete(
        f"{PREFIX}/brain/nodes/{{node_id}}",
        summary="Delete a brain node",
        tags=["brain"],
    )
    async def brain_node_delete(
        node_id: str,
        _: None = Depends(verify_api_key),
        plugin: Any = Depends(get_plugin),
    ) -> JSONResponse:
        """Delete a brain graph node and all its connected edges."""
        if plugin.storage is None:
            raise HTTPException(
                status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
                detail="Storage not available",
            )

        node = await plugin.storage.get_brain_node(node_id)
        if node is None:
            raise HTTPException(
                status_code=status.HTTP_404_NOT_FOUND,
                detail=f"Brain node '{node_id}' not found",
            )

        try:
            await plugin.storage.delete_brain_node(node_id)
        except Exception as exc:
            raise HTTPException(
                status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
                detail="Internal server error",
            ) from exc

        return JSONResponse(content={"deleted": True, "node_id": node_id})

    @router.get(
        f"{PREFIX}/brain/stats",
        response_model=BrainStatsResponse,
        summary="Brain graph statistics",
        tags=["brain"],
    )
    async def brain_stats(
        _: None = Depends(verify_api_key),
        plugin: Any = Depends(get_plugin),
    ) -> BrainStatsResponse:
        """Return aggregate brain graph statistics."""
        errors: List[str] = []

        if plugin.storage is None:
            errors.append("Storage not available")
            return BrainStatsResponse(errors=errors)

        try:
            stats = await plugin.storage.get_brain_stats(
                owner_id=plugin.config.owner_id,
            )
            return BrainStatsResponse(
                total_nodes=stats.get("total_nodes", 0),
                total_edges=stats.get("total_edges", 0),
                node_types=stats.get("node_types", {}),
                last_build=stats.get("last_build"),
                errors=errors,
            )
        except Exception as exc:
            err = f"Brain stats failed: {exc}"
            logger.error(err, exc_info=True)
            errors.append(err)
            return BrainStatsResponse(errors=errors)
