"""Admin / system-status endpoints.

管理后台接口模块。
提供系统健康状态统计、入库日志分页查询、文档删除（含 ES + Neo4j），
以及入库全链路追踪（Trace / Event / Artifact）的查询接口。
"""

from __future__ import annotations

from typing import Annotated, Any

from fastapi import APIRouter, Body, Depends, HTTPException, Query, Request
from fastapi.responses import JSONResponse

from app.api.deps import UserContext, get_current_user
from app.config import settings
from app.infrastructure.es_client import ESClient
from app.infrastructure.neo4j_client import Neo4jClient
from app.infrastructure.redis_client import RedisClient
from app.utils.logger import get_logger

logger = get_logger(__name__)

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


@router.get("/stats")
async def system_stats(
    user: Annotated[UserContext, Depends(get_current_user)],
    request: Request,
) -> dict[str, Any]:
    """Return system health and document statistics.

    聚合 ES、Neo4j、Redis 三大组件的连通状态和文档/分片/图谱统计。
    """
    es_client: ESClient = request.app.state.es_client
    neo4j_client: Neo4jClient = request.app.state.neo4j_client
    redis_client: RedisClient = request.app.state.redis_client

    import asyncio
    import httpx

    result: dict[str, Any] = {
        "total_documents": 0,
        "total_chunks": 0,
        "graph_nodes": 0,
        "today_ingested": 0,
        "es_status": "unknown",
        "neo4j_status": "unknown",
        "redis_status": "unknown",
        "celery_status": "unknown",
        "converter_status": "unknown",
    }

    # ── Define individual health checks ────────────────────────────────────

    async def check_es() -> None:
        try:
            meta_count, chunk_count, today_resp = await asyncio.gather(
                es_client.raw.count(index=settings.es_meta_index),
                es_client.raw.count(index=settings.es_chunk_index),
                es_client.raw.count(
                    index=settings.es_meta_index,
                    body={"query": {"range": {"created_at": {"gte": "now/d", "lt": "now+1d/d"}}}},
                ),
            )
            raw_meta = meta_count if isinstance(meta_count, dict) else meta_count.body
            raw_chunk = chunk_count if isinstance(chunk_count, dict) else chunk_count.body
            raw_today = today_resp if isinstance(today_resp, dict) else today_resp.body
            result["total_documents"] = raw_meta.get("count", 0)
            result["total_chunks"] = raw_chunk.get("count", 0)
            result["today_ingested"] = raw_today.get("count", 0)
            result["es_status"] = "healthy"
        except Exception as e:
            logger.error("admin_es_error", error=str(e), exc_info=e)
            result["es_status"] = "error"

    async def check_neo4j() -> None:
        try:
            result["graph_nodes"] = await neo4j_client.get_node_count()
            result["neo4j_status"] = "healthy"
        except Exception as e:
            logger.warning("admin_neo4j_error", error=str(e))
            result["neo4j_status"] = "error"

    async def check_redis() -> None:
        try:
            result["redis_status"] = "healthy" if await redis_client.ping() else "error"
        except Exception as e:
            logger.warning("admin_redis_error", error=str(e))
            result["redis_status"] = "error"

    async def check_celery() -> None:
        try:
            from app.tasks.celery_app import celery_app
            ping_result = await asyncio.get_event_loop().run_in_executor(
                None, lambda: celery_app.control.inspect(timeout=2).ping(),
            )
            result["celery_status"] = "healthy" if ping_result else "error"
        except Exception as e:
            logger.warning("admin_celery_error", error=str(e))
            result["celery_status"] = "error"

    async def check_converter() -> None:
        try:
            async with httpx.AsyncClient(timeout=3) as client:
                resp = await client.get(f"{settings.converter_base_url}/actuator/health")
                result["converter_status"] = "healthy" if resp.status_code == 200 else "error"
        except Exception as e:
            logger.warning("admin_converter_error", error=str(e))
            result["converter_status"] = "error"

    # ── Run all checks concurrently ────────────────────────────────────────
    await asyncio.gather(
        check_es(), check_neo4j(), check_redis(),
        check_celery(), check_converter(),
    )

    return result


@router.get("/ingest-logs")
async def ingest_logs(
    user: Annotated[UserContext, Depends(get_current_user)],
    request: Request,
    page: int = Query(default=1, ge=1),
    page_size: int = Query(default=20, ge=1, le=100),
) -> dict[str, Any]:
    """Return recent document ingest records from the meta index.

    从 ES meta 索引按创建时间倒序分页返回入库记录，用于管理后台入库日志页面。
    """
    es_client: ESClient = request.app.state.es_client
    from_idx = (page - 1) * page_size

    try:
        resp = await es_client.raw.search(
            index=settings.es_meta_index,
            body={
                "query": {"match_all": {}},
                "sort": [{"created_at": {"order": "desc"}}],
                "from": from_idx,
                "size": page_size,
                "_source": [
                    "doc_id", "title", "original_filename",
                    "doc_number", "issuing_org",
                    "doc_type", "status", "chunk_count",
                    "file_path", "file_type",
                    "created_at", "error", "task_id",
                    "acl_ids",
                ],
            },
        )
        raw = resp if isinstance(resp, dict) else resp.body
        hits = raw.get("hits", {})
        total = hits.get("total", {}).get("value", 0)
        records = [
            {
                "doc_id": h["_source"].get("doc_id", h["_id"]),
                "title": h["_source"].get("title", "—"),
                "original_filename": h["_source"].get("original_filename", ""),
                "doc_number": h["_source"].get("doc_number", ""),
                "issuing_org": h["_source"].get("issuing_org", ""),
                "doc_type": h["_source"].get("doc_type", ""),
                "status": h["_source"].get("status", "completed"),
                "chunk_count": h["_source"].get("chunk_count", 0),
                "file_path": h["_source"].get("file_path", ""),
                "file_type": h["_source"].get("file_type", ""),
                "created_at": h["_source"].get("created_at", ""),
                "error": h["_source"].get("error"),
                "task_id": h["_source"].get("task_id"),
                "acl_ids": h["_source"].get("acl_ids", []),
            }
            for h in hits.get("hits", [])
        ]
        return {"total": total, "page": page, "page_size": page_size, "records": records}
    except Exception as e:
        # 安全修复：不向客户端暴露内部异常详情
        logger.exception("admin_ingest_logs_error")
        raise HTTPException(status_code=500, detail="Internal server error")


@router.delete("/document/{doc_id}", status_code=200)
async def delete_document(
    doc_id: str,
    user: Annotated[UserContext, Depends(get_current_user)],
    request: Request,
) -> dict[str, Any]:
    """Permanently delete a document from ES indices and Neo4j graph.

    Steps executed in order:
    1. Delete all chunks from ``gov_doc_chunks`` (ES delete_by_query).
    2. Delete the meta record from ``gov_doc_meta`` (ES delete by ID).
    3. Delete the Document node and all its relationships from Neo4j
       (DETACH DELETE — non-blocking: failure is logged but not raised).

    Returns a summary of what was deleted.
    """
    es_client: ESClient = request.app.state.es_client
    neo4j_client: Neo4jClient = request.app.state.neo4j_client

    # 1 & 2 — ES deletion (authoritative, must succeed)
    try:
        es_result = await es_client.delete_document(doc_id)
    except Exception as exc:
        # 安全修复：不向客户端暴露内部异常详情
        logger.exception("admin_delete_es_error", doc_id=doc_id)
        raise HTTPException(status_code=500, detail="Internal server error") from exc

    if (
        es_result["deleted_chunks"] == 0
        and not es_result["deleted_meta"]
        and es_result.get("deleted_guides", 0) == 0
    ):
        # Use JSONResponse (not HTTPException) so the global 404 handler
        # in main.py does not override our custom detail message.
        return JSONResponse(
            status_code=404,
            content={"detail": f"Document '{doc_id}' not found in search index."},
        )

    # 3 — Neo4j graph deletion (best-effort)
    neo4j_result: dict[str, int] = {"deleted_nodes": 0}
    try:
        neo4j_result = await neo4j_client.delete_document_graph(doc_id)
    except Exception as exc:
        logger.warning("admin_delete_neo4j_error", doc_id=doc_id, error=str(exc))

    logger.info(
        "admin_document_deleted",
        doc_id=doc_id,
        operator=user.user_id,
        **es_result,
        **neo4j_result,
    )
    return {
        "doc_id": doc_id,
        "deleted_chunks": es_result["deleted_chunks"],
        "deleted_meta": bool(es_result["deleted_meta"]),
        "deleted_guides": es_result.get("deleted_guides", 0),
        "deleted_graph_nodes": neo4j_result["deleted_nodes"],
    }


@router.delete("/documents/batch", status_code=200)
async def delete_documents_batch(
    user: Annotated[UserContext, Depends(get_current_user)],
    request: Request,
    doc_ids: list[str] = Body(..., embed=True),
) -> dict[str, Any]:
    """批量删除选中的文档（ES + Neo4j）。"""
    if not doc_ids:
        raise HTTPException(status_code=400, detail="doc_ids 不能为空")
    if len(doc_ids) > 500:
        raise HTTPException(status_code=400, detail="单次最多删除 500 条")

    es_client: ESClient = request.app.state.es_client
    neo4j_client: Neo4jClient = request.app.state.neo4j_client

    total_chunks = 0
    total_meta = 0
    total_guides = 0
    total_graph = 0
    errors: list[str] = []

    for doc_id in doc_ids:
        try:
            es_result = await es_client.delete_document(doc_id)
            total_chunks += es_result["deleted_chunks"]
            total_meta += int(bool(es_result["deleted_meta"]))
            total_guides += es_result.get("deleted_guides", 0)
        except Exception as exc:
            logger.warning("batch_delete_es_error", doc_id=doc_id, error=str(exc))
            errors.append(doc_id)
            continue

        try:
            neo4j_result = await neo4j_client.delete_document_graph(doc_id)
            total_graph += neo4j_result["deleted_nodes"]
        except Exception as exc:
            logger.warning("batch_delete_neo4j_error", doc_id=doc_id, error=str(exc))

    logger.info(
        "admin_documents_batch_deleted",
        operator=user.user_id,
        requested=len(doc_ids),
        deleted_meta=total_meta,
        errors=len(errors),
    )
    return {
        "requested": len(doc_ids),
        "deleted_meta": total_meta,
        "deleted_chunks": total_chunks,
        "deleted_guides": total_guides,
        "deleted_graph_nodes": total_graph,
        "errors": errors,
    }


@router.delete("/documents/all", status_code=200)
async def delete_all_documents(
    user: Annotated[UserContext, Depends(get_current_user)],
    request: Request,
) -> dict[str, Any]:
    """删除全部文档数据（ES 三个索引 + Neo4j 全图）。"""
    es_client: ESClient = request.app.state.es_client
    neo4j_client: Neo4jClient = request.app.state.neo4j_client

    deleted_meta = 0
    deleted_chunks = 0
    deleted_guides = 0
    deleted_graph = 0

    # ES: delete_by_query match_all for each index
    for index_name, key in [
        (settings.es_meta_index, "meta"),
        (settings.es_chunk_index, "chunks"),
        (settings.es_service_guide_index, "guides"),
    ]:
        try:
            resp = await es_client.raw.delete_by_query(
                index=index_name,
                body={"query": {"match_all": {}}},
                refresh=True,
            )
            raw = resp if isinstance(resp, dict) else resp.body
            count = raw.get("deleted", 0)
            if key == "meta":
                deleted_meta = count
            elif key == "chunks":
                deleted_chunks = count
            else:
                deleted_guides = count
        except Exception as exc:
            logger.warning(f"delete_all_{key}_error", error=str(exc))

    # Neo4j: delete everything
    try:
        neo4j_result = await neo4j_client.delete_all_graph()
        deleted_graph = neo4j_result["deleted_nodes"]
    except Exception as exc:
        logger.warning("delete_all_neo4j_error", error=str(exc))

    logger.info(
        "admin_all_documents_deleted",
        operator=user.user_id,
        deleted_meta=deleted_meta,
        deleted_chunks=deleted_chunks,
        deleted_guides=deleted_guides,
        deleted_graph_nodes=deleted_graph,
    )
    return {
        "deleted_meta": deleted_meta,
        "deleted_chunks": deleted_chunks,
        "deleted_guides": deleted_guides,
        "deleted_graph_nodes": deleted_graph,
    }


# ── Ingest Trace endpoints (Phase A) ─────────────────────────────────────
# 入库追踪端点：支持按多条件筛选 Trace、查看统计、查看事件时间线和产物


@router.get("/ingest-traces")
async def list_ingest_traces(
    user: Annotated[UserContext, Depends(get_current_user)],
    request: Request,
    page: int = Query(default=1, ge=1),
    page_size: int = Query(default=20, ge=1, le=100),
    doc_id: str | None = Query(default=None),
    trace_id: str | None = Query(default=None),
    task_id: str | None = Query(default=None),
    status: str | None = Query(default=None),
    current_stage: str | None = Query(default=None),
    file_type: str | None = Query(default=None),
    source_type: str | None = Query(default=None),
    has_error: bool | None = Query(default=None),
    start_time: str | None = Query(default=None),
    end_time: str | None = Query(default=None),
    keyword: str | None = Query(default=None),
) -> dict[str, Any]:
    """Query ingest traces with filters and pagination.

    多条件筛选入库追踪记录，支持按文档 ID、状态、阶段、文件类型等过滤。
    """
    es_client: ESClient = request.app.state.es_client
    try:
        return await es_client.query_ingest_traces(
            page=page,
            page_size=page_size,
            doc_id=doc_id,
            trace_id=trace_id,
            task_id=task_id,
            status=status,
            current_stage=current_stage,
            file_type=file_type,
            source_type=source_type,
            has_error=has_error,
            start_time=start_time,
            end_time=end_time,
            keyword=keyword,
        )
    except Exception as e:
        # 安全修复：不向客户端暴露内部异常详情
        logger.exception("admin_ingest_traces_error")
        raise HTTPException(status_code=500, detail="Internal server error")


@router.get("/ingest-traces/stats")
async def ingest_trace_stats(
    user: Annotated[UserContext, Depends(get_current_user)],
    request: Request,
) -> dict[str, Any]:
    """Get aggregate statistics for ingest traces.

    返回入库追踪的聚合统计：今日总数、运行中、已完成、失败数及平均耗时。
    """
    es_client: ESClient = request.app.state.es_client
    try:
        return await es_client.get_trace_stats()
    except Exception as e:
        logger.error("admin_trace_stats_error", error=str(e), exc_info=e)
        return {
            "today_total": 0,
            "running": 0,
            "completed": 0,
            "failed": 0,
            "avg_duration_ms": 0,
        }


@router.post("/ingest-traces/cleanup-stale")
async def cleanup_stale_traces(
    user: Annotated[UserContext, Depends(get_current_user)],
    request: Request,
    stale_minutes: int = Query(default=30, ge=5, le=1440),
) -> dict[str, Any]:
    """将超时仍为 running 的 trace 标记为 failed，防止 worker 崩溃导致 trace 永远卡住。

    Mark traces stuck in 'running' beyond stale_minutes as 'failed'.
    This handles cases where the worker crashed/was killed without calling finish_trace.
    """
    from datetime import datetime, timezone, timedelta

    es_client: ESClient = request.app.state.es_client
    cutoff = (datetime.now(timezone.utc) - timedelta(minutes=stale_minutes)).isoformat()
    try:
        # Find all running traces started before the cutoff
        resp = await es_client.raw.search(
            index=settings.es_trace_index,
            body={
                "query": {
                    "bool": {
                        "filter": [
                            {"term": {"status": "running"}},
                            {"range": {"started_at": {"lt": cutoff}}},
                        ]
                    }
                },
                "size": 200,
                "_source": ["trace_id", "started_at", "current_stage"],
            },
        )
        raw = resp if isinstance(resp, dict) else resp.body
        hits = raw.get("hits", {}).get("hits", [])

        now_iso = datetime.now(timezone.utc).isoformat()
        cleaned = 0
        for hit in hits:
            trace_id = hit["_id"]
            started_at = hit["_source"].get("started_at", "")
            try:
                started = datetime.fromisoformat(started_at)
                duration_ms = int(
                    (datetime.now(timezone.utc) - started).total_seconds() * 1000
                )
            except Exception:
                duration_ms = 0

            await es_client.raw.update(
                index=settings.es_trace_index,
                id=trace_id,
                body={
                    "doc": {
                        "status": "failed",
                        "error_code": "INGEST_STALE_TIMEOUT",
                        "error_message": f"Trace 超过 {stale_minutes} 分钟未完成，标记为失败 (worker 可能已崩溃)",
                        "finished_at": now_iso,
                        "duration_ms": duration_ms,
                        "updated_at": now_iso,
                    }
                },
            )
            cleaned += 1
            logger.info("stale_trace_cleaned", trace_id=trace_id, stale_minutes=stale_minutes)

        return {"cleaned": cleaned, "cutoff": cutoff, "stale_minutes": stale_minutes}
    except Exception as e:
        logger.exception("cleanup_stale_traces_error")
        raise HTTPException(status_code=500, detail="Internal server error")


@router.get("/ingest-traces/{trace_id}")
async def get_ingest_trace(
    trace_id: str,
    user: Annotated[UserContext, Depends(get_current_user)],
    request: Request,
) -> dict[str, Any]:
    """Get a single trace detail."""
    es_client: ESClient = request.app.state.es_client
    trace = await es_client.get_ingest_trace(trace_id)
    if not trace:
        raise HTTPException(status_code=404, detail=f"Trace '{trace_id}' not found")
    return trace


@router.get("/ingest-traces/{trace_id}/events")
async def list_ingest_events(
    trace_id: str,
    user: Annotated[UserContext, Depends(get_current_user)],
    request: Request,
    page: int = Query(default=1, ge=1),
    page_size: int = Query(default=100, ge=1, le=500),
    stage: str | None = Query(default=None),
    status: str | None = Query(default=None),
    event_type: str | None = Query(default=None),
) -> dict[str, Any]:
    """Query events for a trace, ordered by seq."""
    es_client: ESClient = request.app.state.es_client
    try:
        return await es_client.query_ingest_events(
            trace_id,
            page=page,
            page_size=page_size,
            stage=stage,
            status=status,
            event_type=event_type,
        )
    except Exception as e:
        # 安全修复：不向客户端暴露内部异常详情
        logger.exception("admin_ingest_events_error")
        raise HTTPException(status_code=500, detail="Internal server error")


@router.get("/ingest-events/{event_id}")
async def get_ingest_event(
    event_id: str,
    user: Annotated[UserContext, Depends(get_current_user)],
    request: Request,
) -> dict[str, Any]:
    """Get a single event detail with full details JSON."""
    es_client: ESClient = request.app.state.es_client
    event = await es_client.get_ingest_event(event_id)
    if not event:
        raise HTTPException(status_code=404, detail=f"Event '{event_id}' not found")
    return event


# ── Ingest Artifact endpoints (Phase B) ──────────────────────────────────
# 入库产物端点：查询某次入库过程中生成的中间产物（如解析结果、分块数据等）


@router.get("/ingest-traces/{trace_id}/artifacts")
async def list_ingest_artifacts(
    trace_id: str,
    user: Annotated[UserContext, Depends(get_current_user)],
    request: Request,
    page: int = Query(default=1, ge=1),
    page_size: int = Query(default=50, ge=1, le=200),
    artifact_type: str | None = Query(default=None),
    stage: str | None = Query(default=None),
) -> dict[str, Any]:
    """Query artifacts for a trace."""
    es_client: ESClient = request.app.state.es_client
    try:
        return await es_client.query_ingest_artifacts(
            trace_id,
            page=page,
            page_size=page_size,
            artifact_type=artifact_type,
            stage=stage,
        )
    except Exception as e:
        # 安全修复：不向客户端暴露内部异常详情
        logger.exception("admin_ingest_artifacts_error")
        raise HTTPException(status_code=500, detail="Internal server error")


@router.get("/ingest-artifacts/{artifact_id}")
async def get_ingest_artifact(
    artifact_id: str,
    user: Annotated[UserContext, Depends(get_current_user)],
    request: Request,
    mode: str = Query(default="preview"),
) -> dict[str, Any]:
    """Get a single artifact detail. mode=preview|full."""
    es_client: ESClient = request.app.state.es_client
    artifact = await es_client.get_ingest_artifact(artifact_id)
    if not artifact:
        raise HTTPException(status_code=404, detail=f"Artifact '{artifact_id}' not found")
    # In preview mode, truncate large payloads
    if mode == "preview" and artifact.get("payload_text"):
        artifact["payload_text"] = artifact["payload_text"][:2000]
    return artifact
