"""Knowledge-graph query endpoints for the graph explorer.

知识图谱查询接口模块。
提供实体搜索、实体详情与邻居子图、文档关联实体、文档推荐、
政策依据链、修订/废止历史、同主题文档，以及事项知识卡等功能，
供图谱浏览器和文档详情页调用。
"""

from __future__ import annotations

from typing import Annotated, Any

from fastapi import APIRouter, Depends, HTTPException, Query
from pydantic import BaseModel, Field

from app.api.deps import (
    UserContext,
    get_current_user,
    get_neo4j_client,
    get_optional_permission_context,
    get_permission_context,
)
from app.api.schemas.document import GraphSubData
from app.core.graph_query_service import GraphQueryService
from app.core.permission import PermissionContext
from app.infrastructure.neo4j_client import Neo4jClient
from app.utils.logger import get_logger

logger = get_logger(__name__)

router = APIRouter(prefix="/graph", tags=["graph"])


# ---------------------------------------------------------------------------
# Request / Response models
# ---------------------------------------------------------------------------


class EntitySearchRequest(BaseModel):
    """Payload for ``POST /graph/search``.

    实体名称搜索请求体。
    """

    name: str = Field(..., min_length=1, max_length=100, description="实体名称搜索词")
    label: str | None = Field(
        None,
        description="限定节点标签 (如 Organization, Region, PolicyTheme)",
    )
    limit: int = Field(default=20, ge=1, le=500)


class EntitySearchResult(BaseModel):
    """A single entity node returned by search."""

    id: str
    labels: list[str]
    name: str
    properties: dict[str, Any] = Field(default_factory=dict)


class RelatedDocsRequest(BaseModel):
    """Payload for ``POST /graph/related-docs``.

    通过实体查找关联文档的请求体。
    当 entity_key 非空时，优先按标签的 key_property 精确匹配实体（避免同名实体串数据）；
    否则回退到按 entity_name 匹配。
    """

    entity_name: str = Field(..., min_length=1, max_length=200)
    entity_label: str = Field(
        ...,
        description="实体标签 (如 Organization, Region, PolicyTheme)",
    )
    entity_key: str | None = Field(
        None,
        description="标签对应 key_property 的业务主键值 (如 Person 的 person_id)；"
        "非空时优先按主键匹配",
    )
    limit: int = Field(default=20, ge=1, le=100)


class RelatedDocItem(BaseModel):
    doc_id: str
    title: str
    doc_number: str
    rel_type: str


# ---------------------------------------------------------------------------
# Endpoints
# ---------------------------------------------------------------------------


@router.post(
    "/search",
    response_model=list[EntitySearchResult],
    summary="实体名称搜索",
)
async def search_entities(
    body: EntitySearchRequest,
    user: Annotated[UserContext, Depends(get_current_user)],
    perm: Annotated[PermissionContext, Depends(get_permission_context)],
    neo4j: Annotated[Neo4jClient, Depends(get_neo4j_client)],
) -> list[EntitySearchResult]:
    """Search entity nodes by name (partial match).

    Returns matched nodes across all entity labels unless ``label`` is
    specified.
    """
    svc = GraphQueryService(neo4j)
    nodes = await svc.search_entities(
        body.name,
        label=body.label,
        limit=body.limit,
        acl_tokens=perm.acl_tokens,
    )
    return [
        EntitySearchResult(
            id=n["id"],
            labels=n["labels"],
            name=n["properties"].get("name", ""),
            properties=n["properties"],
        )
        for n in nodes
    ]


@router.get(
    "/entity/{entity_id}",
    summary="获取实体节点详情与邻居",
)
async def get_entity(
    entity_id: str,
    user: Annotated[UserContext, Depends(get_current_user)],
    perm: Annotated[PermissionContext, Depends(get_permission_context)],
    neo4j: Annotated[Neo4jClient, Depends(get_neo4j_client)],
    depth: int = Query(default=2, ge=1, le=3, description="图遍历深度"),
    max_nodes: int = Query(default=200, ge=1, le=1000, description="最大返回节点数"),
) -> dict[str, Any]:
    """Return a single entity node and its neighbourhood subgraph.

    Parameters
    ----------
    entity_id:
        Neo4j element ID of the entity.
    depth:
        BFS depth for neighbourhood traversal (1–3).
    max_nodes:
        Maximum number of nodes to return in the neighbourhood subgraph.
    """
    svc = GraphQueryService(neo4j)
    node = await svc.get_entity(entity_id, acl_tokens=perm.acl_tokens)
    if node is None:
        raise HTTPException(status_code=404, detail=f"Entity '{entity_id}' not found")

    # Also return the neighbourhood subgraph for the frontend canvas
    neighborhood = await svc.get_entity_neighborhood(
        entity_id,
        depth=depth,
        max_nodes=max_nodes,
        acl_tokens=perm.acl_tokens,
    )

    # 预计算布局坐标
    nodes = neighborhood.get("nodes", [])
    edges = neighborhood.get("edges", [])
    if nodes:
        GraphQueryService._compute_layout_positions(nodes, edges)

    return {
        "entity": node,
        "graph": neighborhood,
    }


@router.post(
    "/related-docs",
    response_model=list[RelatedDocItem],
    summary="通过实体查找相关文档",
)
async def get_related_docs(
    body: RelatedDocsRequest,
    user: Annotated[UserContext, Depends(get_current_user)],
    perm: Annotated[PermissionContext, Depends(get_permission_context)],
    neo4j: Annotated[Neo4jClient, Depends(get_neo4j_client)],
) -> list[RelatedDocItem]:
    """Return all documents connected to a specific entity node.

    Useful for the "关联文档" panel in the graph explorer.
    When ``entity_key`` is provided, uses key_property-based matching
    for precise entity resolution (avoids same-name contamination).
    """
    svc = GraphQueryService(neo4j)
    if body.entity_key:
        docs = await svc.get_docs_by_entity_key(
            body.entity_key,
            body.entity_label,
            limit=body.limit,
            acl_tokens=perm.acl_tokens,
        )
    else:
        docs = await svc.get_docs_by_entity(
            body.entity_name,
            body.entity_label,
            limit=body.limit,
            acl_tokens=perm.acl_tokens,
        )
    return [
        RelatedDocItem(
            doc_id=d["doc_id"],
            title=d["title"],
            doc_number=d["doc_number"],
            rel_type=d["rel_type"],
        )
        for d in docs
    ]


@router.get(
    "/document/{doc_id}/entities",
    summary="获取文档关联实体",
)
async def get_document_entities(
    doc_id: str,
    user: Annotated[UserContext, Depends(get_current_user)],
    perm: Annotated[PermissionContext, Depends(get_permission_context)],
    neo4j: Annotated[Neo4jClient, Depends(get_neo4j_client)],
) -> dict[str, Any]:
    """Return all entity nodes directly connected to a document.

    Results are grouped by label (Organization, Person, Region, etc.).
    """
    svc = GraphQueryService(neo4j)
    grouped = await svc.get_document_entities(doc_id, acl_tokens=perm.acl_tokens)
    return {"doc_id": doc_id, "entities": grouped}


@router.get(
    "/overview",
    summary="图谱概览 (最多连接的文档或主题)",
)
async def graph_overview(
    user: Annotated[UserContext, Depends(get_current_user)],
    perm: Annotated[PermissionContext, Depends(get_permission_context)],
    neo4j: Annotated[Neo4jClient, Depends(get_neo4j_client)],
    entity_name: str | None = Query(None, description="实体名称过滤"),
    label: str | None = Query(None, description="实体标签过滤"),
    limit: int = Query(default=200, ge=1, le=2000),
    cursor: str | None = Query(None, description="分页游标（偏移量）"),
) -> dict[str, Any]:
    """Return a graph overview for the explorer home page.

    When ``entity_name`` is provided, shows documents connected to that entity.
    Otherwise, shows the most-connected documents.

    Supports cursor-based pagination for progressive loading of large graphs.
    """
    svc = GraphQueryService(neo4j)
    result = await svc.get_docs_overview(
        label=label,
        entity_name=entity_name,
        limit=limit,
        cursor=cursor,
        acl_tokens=perm.acl_tokens,
    )
    return result


# ---------------------------------------------------------------------------
# Document governance endpoints (Phase 0)
# 文档治理端点：推荐、政策依据链、修订历史、同主题文档
# ---------------------------------------------------------------------------


class DocRefItem(BaseModel):
    """A document reference in recommendation / chain results."""

    doc_id: str
    title: str = ""
    doc_code: str = ""
    publish_date: str = ""
    shared_entity: str = ""
    theme_name: str = ""
    status: str = ""
    is_placeholder: bool = False


class ChainEdge(BaseModel):
    """An edge in policy chain / revision history."""

    from_doc_id: str
    to_doc_id: str
    rel_type: str


@router.get(
    "/document/{doc_id}/recommendations",
    summary="文档推荐（同机构/同主题/同地域）",
)
async def get_document_recommendations(
    doc_id: str,
    user: Annotated[UserContext, Depends(get_current_user)],
    perm: Annotated[PermissionContext, Depends(get_permission_context)],
    neo4j: Annotated[Neo4jClient, Depends(get_neo4j_client)],
    limit: int = Query(default=10, ge=1, le=50),
) -> dict[str, Any]:
    """Return recommended documents that share Organization, PolicyTheme,
    or Region with the given document.
    """
    svc = GraphQueryService(neo4j)
    return await svc.get_document_recommendations(
        doc_id,
        limit=limit,
        acl_tokens=perm.acl_tokens,
    )


@router.get(
    "/document/{doc_id}/policy-chain",
    summary="政策依据链",
)
async def get_policy_chain(
    doc_id: str,
    user: Annotated[UserContext, Depends(get_current_user)],
    perm: Annotated[PermissionContext, Depends(get_permission_context)],
    neo4j: Annotated[Neo4jClient, Depends(get_neo4j_client)],
    max_depth: int = Query(default=5, ge=1, le=10),
) -> dict[str, Any]:
    """Traverse BASED_ON relationships to show the full policy dependency chain.

    Returns the ancestor documents that this document was based on,
    and the edges between them.
    """
    svc = GraphQueryService(neo4j)
    return await svc.get_policy_chain(
        doc_id,
        max_depth=max_depth,
        acl_tokens=perm.acl_tokens,
    )


@router.get(
    "/document/{doc_id}/revision-history",
    summary="修订/废止历史",
)
async def get_revision_history(
    doc_id: str,
    user: Annotated[UserContext, Depends(get_current_user)],
    perm: Annotated[PermissionContext, Depends(get_permission_context)],
    neo4j: Annotated[Neo4jClient, Depends(get_neo4j_client)],
    max_depth: int = Query(default=5, ge=1, le=10),
) -> dict[str, Any]:
    """Return the amendment (AMENDS) and repeal (REPEALS) history.

    Shows all documents connected through AMENDS or REPEALS relationships,
    forming a revision timeline.
    """
    svc = GraphQueryService(neo4j)
    return await svc.get_revision_history(
        doc_id,
        max_depth=max_depth,
        acl_tokens=perm.acl_tokens,
    )


@router.get(
    "/document/{doc_id}/same-theme",
    summary="同主题文档",
)
async def get_same_theme_documents(
    doc_id: str,
    user: Annotated[UserContext, Depends(get_current_user)],
    perm: Annotated[PermissionContext, Depends(get_permission_context)],
    neo4j: Annotated[Neo4jClient, Depends(get_neo4j_client)],
    limit: int = Query(default=20, ge=1, le=100),
) -> list[dict[str, Any]]:
    """Return documents that share the same PolicyTheme(s)."""
    svc = GraphQueryService(neo4j)
    return await svc.get_same_theme_documents(
        doc_id,
        limit=limit,
        acl_tokens=perm.acl_tokens,
    )


# ---------------------------------------------------------------------------
# Matter (Phase 1) models & endpoints
# 政务事项模型与端点：事项搜索、事项知识卡、办理条件与材料查询
# ---------------------------------------------------------------------------


class MatterListItem(BaseModel):
    """Summary item in matter search results."""

    matter_id: str
    name: str
    description: str = ""
    matter_type: str = ""
    status: str = ""
    sample_doc_ids: list[str] = Field(default_factory=list)


class ConditionItem(BaseModel):
    condition_id: str | None = None
    name: str = ""
    description: str = ""
    condition_type: str = ""
    structured_value: str = ""
    operator: str = ""


class MaterialItem(BaseModel):
    material_id: str | None = None
    name: str = ""
    normalized_name: str = ""
    description: str = ""
    material_type: str = ""
    is_required: bool | None = None
    format_requirement: str = ""


class TimeLimitItem(BaseModel):
    time_limit_id: str | None = None
    name: str = ""
    duration: str = ""
    unit: str = ""
    context: str = ""
    time_type: str = ""


class TargetGroupItem(BaseModel):
    target_group_id: str | None = None
    name: str = ""
    normalized_name: str = ""
    group_type: str = ""
    description: str = ""


class OrganizationRef(BaseModel):
    """Lightweight Organization reference in matter context."""

    name: str = ""
    short_name: str = ""
    org_type: str = ""


class RegionRef(BaseModel):
    """Lightweight Region reference in matter context."""

    name: str = ""
    full_name: str = ""


class GoverningDocItem(BaseModel):
    doc_id: str
    title: str = ""
    doc_code: str = ""
    publish_date: str = ""
    status: str = ""


class MatterCardResponse(BaseModel):
    """Full matter knowledge card.

    事项知识卡完整响应，汇聚条件、材料、时限、目标群体、
    承办机构、依据文档和适用地域等信息。
    """

    matter_id: str
    name: str
    description: str = ""
    matter_type: str = ""
    status: str = ""
    conditions: list[ConditionItem] = Field(default_factory=list)
    materials: list[MaterialItem] = Field(default_factory=list)
    time_limits: list[TimeLimitItem] = Field(default_factory=list)
    target_groups: list[TargetGroupItem] = Field(default_factory=list)
    handled_by: list[OrganizationRef] = Field(default_factory=list)
    governing_docs: list[GoverningDocItem] = Field(default_factory=list)
    regions: list[RegionRef] = Field(default_factory=list)


class MatterRequirementsResponse(BaseModel):
    """Focused view of matter conditions, materials, and time limits."""

    matter_id: str
    name: str
    conditions: list[ConditionItem] = Field(default_factory=list)
    materials: list[MaterialItem] = Field(default_factory=list)
    time_limits: list[TimeLimitItem] = Field(default_factory=list)


@router.get(
    "/matters",
    response_model=list[MatterListItem],
    summary="事项搜索",
)
async def search_matters(
    perm: Annotated[PermissionContext | None, Depends(get_optional_permission_context)],
    neo4j: Annotated[Neo4jClient, Depends(get_neo4j_client)],
    query: str = Query("", max_length=100, description="事项名称搜索词"),
    limit: int = Query(default=20, ge=1, le=100),
) -> list[MatterListItem]:
    """Search Matter nodes by name (partial match).

    Returns a list of matching matters with summary info.
    """
    if not query.strip():
        return []
    svc = GraphQueryService(neo4j)
    results = await svc.search_matters(
        query.strip(),
        limit=limit,
        acl_tokens=perm.acl_tokens if perm is not None else [],
    )
    return [MatterListItem(**r) for r in results]


@router.get(
    "/matters/{matter_id}",
    response_model=MatterCardResponse,
    summary="事项知识卡",
)
async def get_matter_card(
    matter_id: str,
    perm: Annotated[PermissionContext | None, Depends(get_optional_permission_context)],
    neo4j: Annotated[Neo4jClient, Depends(get_neo4j_client)],
) -> MatterCardResponse:
    """Return the full knowledge card for a matter.

    Includes conditions, materials, time limits, target groups,
    handling organizations, governing documents, and applicable regions.
    """
    svc = GraphQueryService(neo4j)
    card = await svc.get_matter_card(
        matter_id,
        acl_tokens=perm.acl_tokens if perm is not None else [],
    )
    if card is None:
        raise HTTPException(status_code=404, detail=f"Matter '{matter_id}' not found")
    return MatterCardResponse(
        matter_id=card["matter_id"],
        name=card["name"],
        description=card.get("description", ""),
        matter_type=card.get("matter_type", ""),
        status=card.get("status", ""),
        conditions=[ConditionItem(**c) for c in card.get("conditions", [])],
        materials=[MaterialItem(**m) for m in card.get("materials", [])],
        time_limits=[TimeLimitItem(**t) for t in card.get("time_limits", [])],
        target_groups=[TargetGroupItem(**t) for t in card.get("target_groups", [])],
        handled_by=[OrganizationRef(**o) for o in card.get("handled_by", [])],
        governing_docs=[GoverningDocItem(**d) for d in card.get("governing_docs", [])],
        regions=[RegionRef(**r) for r in card.get("regions", [])],
    )


@router.get(
    "/matters/{matter_id}/requirements",
    response_model=MatterRequirementsResponse,
    summary="事项办理条件与材料",
)
async def get_matter_requirements(
    matter_id: str,
    perm: Annotated[PermissionContext | None, Depends(get_optional_permission_context)],
    neo4j: Annotated[Neo4jClient, Depends(get_neo4j_client)],
) -> MatterRequirementsResponse:
    """Return conditions, materials, and time limits for a matter."""
    svc = GraphQueryService(neo4j)
    result = await svc.get_matter_requirements(
        matter_id,
        acl_tokens=perm.acl_tokens if perm is not None else [],
    )
    if result is None:
        raise HTTPException(status_code=404, detail=f"Matter '{matter_id}' not found")
    return MatterRequirementsResponse(
        matter_id=result["matter_id"],
        name=result["name"],
        conditions=[ConditionItem(**c) for c in result.get("conditions", [])],
        materials=[MaterialItem(**m) for m in result.get("materials", [])],
        time_limits=[TimeLimitItem(**t) for t in result.get("time_limits", [])],
    )


# ---------------------------------------------------------------------------
# Entity Card endpoints (Sprint 4 — Policy / Task / Project)
# 实体知识卡端点：政策卡、任务卡、项目卡
# ---------------------------------------------------------------------------


class EntityRefItem(BaseModel):
    """Lightweight entity reference in card context."""

    name: str = ""
    policy_id: str = ""
    task_id: str = ""
    project_id: str = ""


class CardDocItem(BaseModel):
    """Document reference in card results."""

    doc_id: str
    title: str = ""
    doc_code: str = ""
    publish_date: str = ""


class PolicyCardResponse(BaseModel):
    """政策知识卡响应。"""

    name: str
    summary: str = ""
    policy_id: str = ""
    properties: dict[str, Any] = Field(default_factory=dict)
    assigned_orgs: list[str] = Field(default_factory=list)
    implementing_tasks: list[EntityRefItem] = Field(default_factory=list)
    supported_projects: list[EntityRefItem] = Field(default_factory=list)
    related_themes: list[str] = Field(default_factory=list)
    source_docs: list[CardDocItem] = Field(default_factory=list)


class TaskCardResponse(BaseModel):
    """任务知识卡响应。"""

    name: str
    task_id: str = ""
    properties: dict[str, Any] = Field(default_factory=dict)
    lead_orgs: list[str] = Field(default_factory=list)
    assist_orgs: list[str] = Field(default_factory=list)
    implementing_policies: list[EntityRefItem] = Field(default_factory=list)
    evaluating_indicators: list[str] = Field(default_factory=list)
    budgets: list[dict[str, Any]] = Field(default_factory=list)
    source_docs: list[CardDocItem] = Field(default_factory=list)


class ProjectCardResponse(BaseModel):
    """项目知识卡响应。"""

    name: str
    project_id: str = ""
    properties: dict[str, Any] = Field(default_factory=dict)
    implementing_orgs: list[str] = Field(default_factory=list)
    locations: list[str] = Field(default_factory=list)
    supporting_policies: list[EntityRefItem] = Field(default_factory=list)
    budgets: list[dict[str, Any]] = Field(default_factory=list)
    technologies: list[str] = Field(default_factory=list)
    evaluating_indicators: list[str] = Field(default_factory=list)
    source_docs: list[CardDocItem] = Field(default_factory=list)


@router.get(
    "/policy/{entity_key:path}",
    response_model=PolicyCardResponse,
    summary="政策知识卡",
)
async def get_policy_card(
    entity_key: str,
    perm: Annotated[PermissionContext | None, Depends(get_optional_permission_context)],
    neo4j: Annotated[Neo4jClient, Depends(get_neo4j_client)],
) -> PolicyCardResponse:
    """Return the full knowledge card for a Policy entity."""
    svc = GraphQueryService(neo4j)
    card = await svc.get_policy_card(
        entity_key,
        acl_tokens=perm.acl_tokens if perm is not None else [],
    )
    if card is None:
        raise HTTPException(status_code=404, detail=f"Policy '{entity_key}' not found")
    return PolicyCardResponse(**card)


@router.get(
    "/task/{entity_key:path}",
    response_model=TaskCardResponse,
    summary="任务知识卡",
)
async def get_task_card(
    entity_key: str,
    perm: Annotated[PermissionContext | None, Depends(get_optional_permission_context)],
    neo4j: Annotated[Neo4jClient, Depends(get_neo4j_client)],
) -> TaskCardResponse:
    """Return the full knowledge card for a Task entity."""
    svc = GraphQueryService(neo4j)
    card = await svc.get_task_card(
        entity_key,
        acl_tokens=perm.acl_tokens if perm is not None else [],
    )
    if card is None:
        raise HTTPException(status_code=404, detail=f"Task '{entity_key}' not found")
    return TaskCardResponse(**card)


@router.get(
    "/project/{entity_key:path}",
    response_model=ProjectCardResponse,
    summary="项目知识卡",
)
async def get_project_card(
    entity_key: str,
    perm: Annotated[PermissionContext | None, Depends(get_optional_permission_context)],
    neo4j: Annotated[Neo4jClient, Depends(get_neo4j_client)],
) -> ProjectCardResponse:
    """Return the full knowledge card for a Project entity."""
    svc = GraphQueryService(neo4j)
    card = await svc.get_project_card(
        entity_key,
        acl_tokens=perm.acl_tokens if perm is not None else [],
    )
    if card is None:
        raise HTTPException(status_code=404, detail=f"Project '{entity_key}' not found")
    return ProjectCardResponse(**card)


# ---------------------------------------------------------------------------
# V1.5 新增五类核心层知识卡 Response Models
# ---------------------------------------------------------------------------

class CardRelatedDocItem(BaseModel):
    """Related document reference (2-hop heuristic, may be empty)."""

    doc_id: str
    title: str = ""
    doc_code: str = ""


class SystemCardResponse(BaseModel):
    """系统知识卡响应。"""

    name: str
    properties: dict[str, Any] = Field(default_factory=dict)
    operated_by: list[str] = Field(default_factory=list)
    managed_data: list[str] = Field(default_factory=list)
    technologies: list[str] = Field(default_factory=list)
    related_docs: list[CardRelatedDocItem] = Field(default_factory=list)


class DataResourceCardResponse(BaseModel):
    """数据资源知识卡响应。"""

    name: str
    properties: dict[str, Any] = Field(default_factory=dict)
    managed_by_systems: list[str] = Field(default_factory=list)
    conforms_to: list[str] = Field(default_factory=list)
    related_docs: list[CardRelatedDocItem] = Field(default_factory=list)


class BudgetCardResponse(BaseModel):
    """预算知识卡响应。"""

    name: str
    properties: dict[str, Any] = Field(default_factory=dict)
    funded_tasks: list[EntityRefItem] = Field(default_factory=list)
    funded_projects: list[EntityRefItem] = Field(default_factory=list)
    related_docs: list[CardRelatedDocItem] = Field(default_factory=list)


class IndicatorCardResponse(BaseModel):
    """指标知识卡响应。"""

    name: str
    properties: dict[str, Any] = Field(default_factory=dict)
    evaluated_tasks: list[EntityRefItem] = Field(default_factory=list)
    evaluated_projects: list[EntityRefItem] = Field(default_factory=list)
    related_docs: list[CardRelatedDocItem] = Field(default_factory=list)


class IndustryCardResponse(BaseModel):
    """产业知识卡响应。"""

    name: str
    properties: dict[str, Any] = Field(default_factory=dict)
    supported_by_policies: list[EntityRefItem] = Field(default_factory=list)
    located_in: list[str] = Field(default_factory=list)
    managed_by: list[str] = Field(default_factory=list)
    related_docs: list[CardRelatedDocItem] = Field(default_factory=list)


# ---------------------------------------------------------------------------
# V1.5 新增五类核心层知识卡 Endpoints
# ---------------------------------------------------------------------------

@router.get(
    "/system/{entity_key:path}",
    response_model=SystemCardResponse,
    summary="系统知识卡",
)
async def get_system_card(
    entity_key: str,
    perm: Annotated[PermissionContext | None, Depends(get_optional_permission_context)],
    neo4j: Annotated[Neo4jClient, Depends(get_neo4j_client)],
) -> SystemCardResponse:
    """Return the full knowledge card for a System entity."""
    svc = GraphQueryService(neo4j)
    card = await svc.get_system_card(
        entity_key,
        acl_tokens=perm.acl_tokens if perm is not None else [],
    )
    if card is None:
        raise HTTPException(status_code=404, detail=f"System '{entity_key}' not found")
    return SystemCardResponse(**card)


@router.get(
    "/data-resource/{entity_key:path}",
    response_model=DataResourceCardResponse,
    summary="数据资源知识卡",
)
async def get_data_resource_card(
    entity_key: str,
    perm: Annotated[PermissionContext | None, Depends(get_optional_permission_context)],
    neo4j: Annotated[Neo4jClient, Depends(get_neo4j_client)],
) -> DataResourceCardResponse:
    """Return the full knowledge card for a DataResource entity."""
    svc = GraphQueryService(neo4j)
    card = await svc.get_data_resource_card(
        entity_key,
        acl_tokens=perm.acl_tokens if perm is not None else [],
    )
    if card is None:
        raise HTTPException(status_code=404, detail=f"DataResource '{entity_key}' not found")
    return DataResourceCardResponse(**card)


@router.get(
    "/budget/{entity_key:path}",
    response_model=BudgetCardResponse,
    summary="预算知识卡",
)
async def get_budget_card(
    entity_key: str,
    perm: Annotated[PermissionContext | None, Depends(get_optional_permission_context)],
    neo4j: Annotated[Neo4jClient, Depends(get_neo4j_client)],
) -> BudgetCardResponse:
    """Return the full knowledge card for a Budget entity."""
    svc = GraphQueryService(neo4j)
    card = await svc.get_budget_card(
        entity_key,
        acl_tokens=perm.acl_tokens if perm is not None else [],
    )
    if card is None:
        raise HTTPException(status_code=404, detail=f"Budget '{entity_key}' not found")
    return BudgetCardResponse(**card)


@router.get(
    "/indicator/{entity_key:path}",
    response_model=IndicatorCardResponse,
    summary="指标知识卡",
)
async def get_indicator_card(
    entity_key: str,
    perm: Annotated[PermissionContext | None, Depends(get_optional_permission_context)],
    neo4j: Annotated[Neo4jClient, Depends(get_neo4j_client)],
) -> IndicatorCardResponse:
    """Return the full knowledge card for an Indicator entity."""
    svc = GraphQueryService(neo4j)
    card = await svc.get_indicator_card(
        entity_key,
        acl_tokens=perm.acl_tokens if perm is not None else [],
    )
    if card is None:
        raise HTTPException(status_code=404, detail=f"Indicator '{entity_key}' not found")
    return IndicatorCardResponse(**card)


@router.get(
    "/industry/{entity_key:path}",
    response_model=IndustryCardResponse,
    summary="产业知识卡",
)
async def get_industry_card(
    entity_key: str,
    perm: Annotated[PermissionContext | None, Depends(get_optional_permission_context)],
    neo4j: Annotated[Neo4jClient, Depends(get_neo4j_client)],
) -> IndustryCardResponse:
    """Return the full knowledge card for an Industry entity."""
    svc = GraphQueryService(neo4j)
    card = await svc.get_industry_card(
        entity_key,
        acl_tokens=perm.acl_tokens if perm is not None else [],
    )
    if card is None:
        raise HTTPException(status_code=404, detail=f"Industry '{entity_key}' not found")
    return IndustryCardResponse(**card)
