"""Knowledge Base Management API routes"""

from __future__ import annotations

import hmac
import logging
import os
import tempfile
import uuid
import zipfile
from dataclasses import dataclass, field
from datetime import datetime, timezone
from pathlib import Path
from typing import Any

from fastapi import APIRouter, Depends, HTTPException, Request, Security, UploadFile, File
from fastapi.security import APIKeyHeader, APIKeyQuery
from pydantic import BaseModel

from agentkit.rag_platform.document_processor import DocumentProcessor
from agentkit.rag_platform.preview import generate_preview
from agentkit.rag_platform.sanitize import (
    MAX_FILE_SIZE,
    check_zip_bomb,
    validate_file_size,
    validate_file_type,
)
from agentkit.server.admin.context import DepartmentContext, get_department_context
from agentkit.server.admin.filtering import filter_kb_sources_by_department
from agentkit.server.auth.dependencies import require_permission
from agentkit.server.auth.models import DEFAULT_AUTH_DB_PATH
from agentkit.server.auth.permissions import Permission

logger = logging.getLogger(__name__)

router = APIRouter(tags=["kb-management"])

MAX_UPLOAD_SIZE = 50 * 1024 * 1024  # 50MB

# ZIP-based 文档格式（本质是 ZIP，需要 zip bomb 检测）
_ZIP_BASED_TYPES = frozenset({"docx", "xlsx", "pptx"})


# ---------------------------------------------------------------------------
# API Key Authentication
# ---------------------------------------------------------------------------

_api_key_header = APIKeyHeader(name="X-API-Key", auto_error=False)
_api_key_query = APIKeyQuery(name="api_key", auto_error=False)


async def _verify_api_key(
    request: Request,
    api_key_header: str | None = Security(_api_key_header),
    api_key_query: str | None = Security(_api_key_query),
) -> None:
    """Verify API key for REST endpoints. Raises HTTPException if invalid."""
    configured_api_key: str | None = None
    if hasattr(request.app.state, "server_config") and request.app.state.server_config:
        configured_api_key = request.app.state.server_config.api_key
    if configured_api_key is None and hasattr(request.app.state, "api_key"):
        configured_api_key = request.app.state.api_key

    # If no API key is configured, allow all requests (backwards compat)
    if configured_api_key is None:
        return

    provided = api_key_header or api_key_query
    if not hmac.compare_digest((provided or "").encode(), configured_api_key.encode()):
        raise HTTPException(
            status_code=401,
            detail="Invalid or missing API key. Provide via X-API-Key header or api_key query parameter.",
        )


# ---------------------------------------------------------------------------
# In-memory Knowledge Source Store
# ---------------------------------------------------------------------------


@dataclass
class KnowledgeSource:
    id: str
    name: str
    type: str  # "local" | "feishu" | "confluence" | "http"
    config: dict[str, Any] = field(default_factory=dict)
    status: str = "active"
    document_count: int = 0
    last_synced: str | None = None
    department_id: str | None = None


@dataclass
class UploadedDocument:
    document_id: str
    filename: str
    source_id: str
    chunks: int
    status: str
    created_at: str = field(default_factory=lambda: datetime.now(timezone.utc).isoformat())
    department_id: str | None = None


class KnowledgeSourceStore:
    def __init__(self, max_sources: int = 500):
        self._sources: dict[str, KnowledgeSource] = {}
        self._documents: list[UploadedDocument] = []
        self._max = max_sources

    def add_source(self, name: str, source_type: str, config: dict[str, Any]) -> KnowledgeSource:
        source_id = str(uuid.uuid4())
        source = KnowledgeSource(
            id=source_id,
            name=name,
            type=source_type,
            config=config,
        )
        self._sources[source_id] = source
        return source

    def get_source(self, source_id: str) -> KnowledgeSource | None:
        return self._sources.get(source_id)

    def remove_source(self, source_id: str) -> bool:
        if source_id in self._sources:
            del self._sources[source_id]
            return True
        return False

    def list_sources(self) -> list[KnowledgeSource]:
        return list(self._sources.values())

    def add_document(self, doc: UploadedDocument) -> None:
        self._documents.append(doc)
        # Update source document count
        source = self._sources.get(doc.source_id)
        if source:
            source.document_count += 1
            source.last_synced = doc.created_at

    def list_documents(self, source_id: str | None = None) -> list[UploadedDocument]:
        if source_id:
            return [d for d in self._documents if d.source_id == source_id]
        return list(self._documents)

    def delete_document(self, document_id: str) -> bool:
        for i, doc in enumerate(self._documents):
            if doc.document_id == document_id:
                self._documents.pop(i)
                # Update source document count
                source = self._sources.get(doc.source_id)
                if source:
                    source.document_count = max(0, source.document_count - 1)
                return True
        return False

    def update_source(self, source_id: str, data: dict[str, Any]) -> KnowledgeSource | None:
        source = self._sources.get(source_id)
        if source is None:
            return None
        if "name" in data:
            source.name = data["name"]
        if "config" in data:
            source.config = data["config"]
        if "status" in data:
            source.status = data["status"]
        return source


# Module-level singleton
_source_store = KnowledgeSourceStore()


def get_source_store() -> KnowledgeSourceStore:
    """Return the process-wide :class:`KnowledgeSourceStore` singleton.

    Exposed as a function (rather than importing ``_source_store``
    directly) so tests can swap it out via monkeypatch, and so the
    :class:`KbService` wrapper can access the store without reaching
    into a private module attribute.
    """
    return _source_store


def set_source_store(store: KnowledgeSourceStore) -> None:
    """Inject a custom :class:`KnowledgeSourceStore` (used by tests)."""
    global _source_store
    _source_store = store


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


class AddSourceRequest(BaseModel):
    name: str
    type: str  # "local" | "feishu" | "confluence" | "http"
    config: dict[str, Any] = {}


class SearchRequest(BaseModel):
    query: str
    sources: list[str] | None = None
    source_ids: list[str] | None = None
    top_k: int = 5
    strategy: str = "vector"  # "vector" | "hybrid"


class SearchResult(BaseModel):
    content: str
    source: str
    score: float
    metadata: dict[str, Any] = {}


class UpdateSourceRequest(BaseModel):
    name: str | None = None
    config: dict[str, Any] | None = None
    status: str | None = None


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


@router.get("/kb-management/sources")
async def list_sources(
    req: Request,
    _auth: None = Depends(_verify_api_key),
    dept_ctx: DepartmentContext = Depends(get_department_context),
):
    """List all knowledge sources.

    Department filtering (U4): non-admin users see only:
    - KB sources bound to one of their departments, OR
    - KB sources with no department binding (global sources).

    Admin users (``role == "admin"``) bypass filtering and see all
    sources. Unauthenticated callers (API-key clients) see only global
    sources.
    """
    sources = _source_store.list_sources()

    if not dept_ctx.should_filter:
        return _serialize_sources(sources)

    db_path = _resolve_kb_db_path(req)
    all_ids = [s.id for s in sources]
    try:
        visible_ids = await filter_kb_sources_by_department(
            db_path, dept_ctx.department_ids, all_ids
        )
    except Exception:  # noqa: BLE001 — never block listing on DB errors
        logger.exception("Department KB filtering failed — returning empty list")
        return {"sources": []}
    visible_set = set(visible_ids)
    filtered = [s for s in sources if s.id in visible_set]
    return _serialize_sources(filtered)


def _resolve_kb_db_path(req: Request) -> Path:
    """Resolve the auth DB path from ``app.state`` or the default."""
    path = getattr(req.app.state, "auth_db_path", None)
    return Path(path) if path else DEFAULT_AUTH_DB_PATH


def _serialize_sources(sources: list[KnowledgeSource]) -> dict[str, Any]:
    """Serialize KB sources to the GET /kb-management/sources response shape."""
    return {
        "sources": [
            {
                "id": s.id,
                "name": s.name,
                "type": s.type,
                "status": s.status,
                "document_count": s.document_count,
                "last_synced": s.last_synced,
            }
            for s in sources
        ]
    }


@router.post("/kb-management/sources", status_code=201)
async def add_source(
    request: AddSourceRequest,
    req: Request,
    _auth: None = Depends(_verify_api_key),
    _user=Depends(require_permission(Permission.KB_WRITE)),
):
    """Add a knowledge source."""
    valid_types = {"local", "feishu", "confluence", "http"}
    if request.type not in valid_types:
        raise HTTPException(
            status_code=422,
            detail=f"Invalid source type '{request.type}'. Must be one of: {valid_types}",
        )

    source = _source_store.add_source(
        name=request.name,
        source_type=request.type,
        config=request.config,
    )
    return {
        "id": source.id,
        "name": source.name,
        "type": source.type,
        "status": source.status,
        "document_count": source.document_count,
        "last_synced": source.last_synced,
    }


@router.delete("/kb-management/sources/{source_id}")
async def remove_source(
    source_id: str,
    req: Request,
    _auth: None = Depends(_verify_api_key),
    _user=Depends(require_permission(Permission.KB_WRITE)),
):
    """Remove a knowledge source."""
    if not _source_store.remove_source(source_id):
        raise HTTPException(status_code=404, detail=f"Source '{source_id}' not found")
    return {"status": "removed"}


@router.post("/kb-management/sources/{source_id}/sync")
async def sync_source(
    source_id: str,
    _auth: None = Depends(_verify_api_key),
    _user=Depends(require_permission(Permission.KB_WRITE)),
):
    """Trigger source sync."""
    source = _source_store.get_source(source_id)
    if not source:
        raise HTTPException(status_code=404, detail="Source not found")
    # For now, return sync status (actual sync logic depends on source type)
    return {"status": "syncing", "source_id": source_id}


@router.put("/kb-management/sources/{source_id}")
async def update_source(
    source_id: str,
    data: UpdateSourceRequest,
    _auth: None = Depends(_verify_api_key),
    _user=Depends(require_permission(Permission.KB_WRITE)),
):
    """Update source config."""
    update_data = {k: v for k, v in data.model_dump().items() if v is not None}
    source = _source_store.update_source(source_id, update_data)
    if not source:
        raise HTTPException(status_code=404, detail="Source not found")
    return {
        "id": source.id,
        "name": source.name,
        "type": source.type,
        "status": source.status,
        "document_count": source.document_count,
        "last_synced": source.last_synced,
    }


@router.get("/kb-management/documents")
async def list_documents(
    req: Request,
    source_id: str | None = None,
    _auth: None = Depends(_verify_api_key),
    dept_ctx: DepartmentContext = Depends(get_department_context),
):
    """List all documents, optionally filtered by source_id.

    Department filtering (U4): non-admin users see only documents
    belonging to:
    - KB sources bound to one of their departments, OR
    - KB sources with no department binding (global sources).

    Admin users (``role == "admin"``) bypass filtering and see all
    documents. Unauthenticated callers (API-key clients) see only
    documents from global sources.

    If ``source_id`` is provided AND the caller is not allowed to see
    that source, an empty list is returned (rather than 404 — the
    source may exist, the caller just can't see it).
    """
    documents = _source_store.list_documents(source_id)

    if not dept_ctx.should_filter:
        return _serialize_documents(documents)

    # Compute visible source ids across ALL known sources, then filter
    # documents whose source_id is in the visible set.
    all_sources = _source_store.list_sources()
    all_source_ids = [s.id for s in all_sources]
    db_path = _resolve_kb_db_path(req)
    try:
        visible_ids = await filter_kb_sources_by_department(
            db_path, dept_ctx.department_ids, all_source_ids
        )
    except Exception:  # noqa: BLE001 — never block listing on DB errors
        logger.exception("Department KB filtering failed — returning empty list")
        return {"documents": []}
    visible_set = set(visible_ids)
    filtered = [d for d in documents if d.source_id in visible_set]
    return _serialize_documents(filtered)


def _serialize_documents(documents: list[UploadedDocument]) -> dict[str, Any]:
    """Serialize documents to the GET /kb-management/documents response shape."""
    return {
        "documents": [
            {
                "document_id": d.document_id,
                "filename": d.filename,
                "source_id": d.source_id,
                "chunks": d.chunks,
                "status": d.status,
                "created_at": d.created_at,
            }
            for d in documents
        ]
    }


@router.delete("/kb-management/documents/{document_id}")
async def delete_document(
    document_id: str,
    _auth: None = Depends(_verify_api_key),
    _user=Depends(require_permission(Permission.KB_WRITE)),
):
    """Delete a document by ID."""
    success = _source_store.delete_document(document_id)
    if not success:
        raise HTTPException(status_code=404, detail="Document not found")
    return {"status": "deleted"}


@router.post("/kb-management/documents/upload")
async def upload_document(
    req: Request,
    file: UploadFile = File(...),
    source_id: str = "",
    _auth: None = Depends(_verify_api_key),
    _user=Depends(require_permission(Permission.KB_WRITE)),
):
    """Upload a document to the knowledge base.

    U3+U7: 文件类型白名单 + 大小限制 + ZIP bomb 检测 + 内容净化 + 分段。
    返回 chunk 预览供前端确认，文档状态为 segmenting（待向量化）。
    """
    if not file.filename:
        raise HTTPException(status_code=422, detail="Filename is required")

    # U7: 文件类型白名单校验
    try:
        file_type = validate_file_type(file.filename)
    except ValueError as e:
        raise HTTPException(status_code=422, detail=str(e)) from e

    # 读取文件内容并校验大小（U7）
    content = await file.read(MAX_FILE_SIZE + 1)
    try:
        validate_file_size(len(content))
    except ValueError as e:
        raise HTTPException(status_code=413, detail=str(e)) from e

    # 保存到临时文件
    tmp_path: str | None = None
    try:
        with tempfile.NamedTemporaryFile(delete=False, suffix=f".{file_type}") as tmp:
            tmp.write(content)
            tmp_path = tmp.name

        # U7: ZIP bomb 检测（.docx/.xlsx/.pptx 本质是 ZIP）
        if file_type in _ZIP_BASED_TYPES:
            try:
                check_zip_bomb(tmp_path)
            except (ValueError, zipfile.BadZipFile) as e:
                raise HTTPException(status_code=422, detail=str(e)) from e

        # U3: 解析（含内容净化）+ 分段
        processor = DocumentProcessor()
        try:
            text = processor.parse(tmp_path, file_type)
            chunks = processor.segment(text)
        except Exception as e:
            logger.warning("Document parsing failed: %s", e)
            raise HTTPException(status_code=422, detail=f"Document parsing failed: {e}") from e

        # 确定source_id — 默认 "local"
        effective_source_id = source_id or "local"
        if effective_source_id == "local":
            local_sources = [s for s in _source_store.list_sources() if s.type == "local"]
            if not local_sources:
                _source_store.add_source("本地文档", "local", {})

        uploaded = UploadedDocument(
            document_id=str(uuid.uuid4()),
            filename=file.filename,
            source_id=effective_source_id,
            chunks=len(chunks),
            status="segmenting",
        )
        _source_store.add_document(uploaded)

        return {
            "document_id": uploaded.document_id,
            "filename": uploaded.filename,
            "source_id": uploaded.source_id,
            "chunks": uploaded.chunks,
            "status": uploaded.status,
            "created_at": uploaded.created_at,
            "total_chunks": len(chunks),
            "chunks_preview": [{"index": i, "content": c} for i, c in enumerate(chunks)],
        }
    finally:
        if tmp_path:
            try:
                os.unlink(tmp_path)
            except OSError:
                pass


@router.post("/kb-management/documents/preview")
async def preview_document(
    file: UploadFile = File(...),
    chunk_size: int = 512,
    chunk_overlap: int = 50,
    _auth: None = Depends(_verify_api_key),
    _user=Depends(require_permission(Permission.KB_WRITE)),
):
    """预览文档分段结果（不创建文档记录，不向量化）。

    U3: 上传前预览分段效果，可调整 chunk_size/chunk_overlap 参数。
    """
    if not file.filename:
        raise HTTPException(status_code=422, detail="Filename is required")

    try:
        file_type = validate_file_type(file.filename)
    except ValueError as e:
        raise HTTPException(status_code=422, detail=str(e)) from e

    content = await file.read(MAX_FILE_SIZE + 1)
    try:
        validate_file_size(len(content))
    except ValueError as e:
        raise HTTPException(status_code=413, detail=str(e)) from e

    tmp_path: str | None = None
    try:
        with tempfile.NamedTemporaryFile(delete=False, suffix=f".{file_type}") as tmp:
            tmp.write(content)
            tmp_path = tmp.name

        if file_type in _ZIP_BASED_TYPES:
            try:
                check_zip_bomb(tmp_path)
            except (ValueError, zipfile.BadZipFile) as e:
                raise HTTPException(status_code=422, detail=str(e)) from e

        try:
            result = generate_preview(
                tmp_path,
                file_type,
                chunk_size=chunk_size,
                chunk_overlap=chunk_overlap,
            )
        except Exception as e:
            logger.warning("Document preview failed: %s", e)
            raise HTTPException(status_code=422, detail=f"Document preview failed: {e}") from e

        return result.model_dump()
    finally:
        if tmp_path:
            try:
                os.unlink(tmp_path)
            except OSError:
                pass


@router.post("/kb-management/documents/{document_id}/vectorize")
async def vectorize_document(
    document_id: str,
    _auth: None = Depends(_verify_api_key),
    _user=Depends(require_permission(Permission.KB_WRITE)),
):
    """触发文档向量化。

    U3: 当前为占位实现 — 完整异步向量化需要 PG-backed KBStore (U8)。
    旧 KnowledgeSourceStore 不跟踪文件路径，无法重新解析已上传文档。
    """
    raise HTTPException(
        status_code=503,
        detail="Vectorization requires PG-backed KBStore (U8)",
    )


@router.post("/kb-management/search")
async def search_knowledge(
    request: SearchRequest,
    req: Request,
    _auth: None = Depends(_verify_api_key),
    _user=Depends(require_permission(Permission.KB_QUERY)),
):
    """Test search/retrieval against the knowledge base."""
    # Try to use semantic memory if available
    memory_retriever = getattr(req.app.state, "memory_retriever", None)
    if (
        memory_retriever
        and hasattr(memory_retriever, "semantic_memory")
        and memory_retriever.semantic_memory
    ):
        try:
            results = await memory_retriever.semantic_memory.retrieve(
                query=request.query,
                top_k=request.top_k,
            )
            return {
                "results": [
                    {
                        "content": r.content if hasattr(r, "content") else str(r),
                        "source": r.source_id if hasattr(r, "source_id") else "",
                        "score": r.score if hasattr(r, "score") else 0.0,
                        "metadata": r.metadata if hasattr(r, "metadata") else {},
                    }
                    for r in results
                ]
            }
        except Exception as e:
            logger.warning(f"Semantic search failed: {e}")

    # Fallback: return empty results with a hint
    return {
        "results": [],
        "message": "未配置语义检索服务，请先配置知识库连接",
    }


@router.get("/kb-management/sources/{source_id}/health")
async def check_source_health(source_id: str, req: Request, _auth: None = Depends(_verify_api_key)):
    """Check the health of a knowledge source."""
    source = _source_store.get_source(source_id)
    if source is None:
        raise HTTPException(status_code=404, detail=f"Source '{source_id}' not found")

    # For local sources, always healthy
    if source.type == "local":
        return {"source_id": source_id, "status": "healthy", "message": "本地存储正常"}

    # For external sources, try to check connectivity
    # This would use the actual KB adapters in production
    return {"source_id": source_id, "status": "unknown", "message": "健康检查未实现"}


# ---------------------------------------------------------------------------
# KB Settings endpoints (U6)
# ---------------------------------------------------------------------------

from agentkit.rag_platform.settings import (  # noqa: E402
    KBSettings,
    KBSettingsUpdate,
    get_settings_store,
)


@router.get("/kb-management/kbs/{kb_id}/settings")
async def get_kb_settings(
    kb_id: str,
    req: Request,
    _auth: None = Depends(_verify_api_key),
    _user=Depends(require_permission(Permission.KB_QUERY)),
):
    """获取 KB 设置 — owner/viewer 可读。

    U6: 返回 KB 级别设置（检索模式默认/命中处理默认/caching/rerank）。
    设置不存在时返回默认值。
    """
    store = get_settings_store()
    settings = await store.get_settings(kb_id)
    if settings is None:
        # 返回默认设置（不持久化 — 首次 PUT 时创建）
        settings = KBSettings(kb_id=kb_id)
    return settings.model_dump()


@router.put("/kb-management/kbs/{kb_id}/settings")
async def update_kb_settings(
    kb_id: str,
    update: KBSettingsUpdate,
    req: Request,
    _auth: None = Depends(_verify_api_key),
    _user=Depends(require_permission(Permission.KB_WRITE)),
):
    """更新 KB 设置 — 仅 owner（或 admin）可修改。

    U6: per-KB ACL 校验 — 首次更新时设置 owner，后续更新仅 owner/admin 可调用。
    viewer（有 KB_QUERY 但非 owner）调用返回 403。
    """
    user_id = _user.get("user_id")
    role = _user.get("role", "")

    store = get_settings_store()
    existing = await store.get_settings(kb_id)

    # ACL 校验：已有设置且 owner 已设置时，仅 owner/admin 可修改
    if existing is not None and existing.owner is not None:
        if existing.owner != user_id and role != "admin":
            raise HTTPException(
                status_code=403,
                detail="仅 KB owner 可修改设置",
            )

    # 首次创建时设置 owner
    owner = user_id if (existing is None or existing.owner is None) else None
    updated = await store.update_settings(kb_id, update, owner=owner)
    return updated.model_dump()
