"""Vault 资料库 HTTP API。"""

from __future__ import annotations

from pathlib import Path
from typing import TYPE_CHECKING, Any
from urllib.parse import urlparse

import yaml

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

from niannian.core.config import resolve_config_path
from niannian.http.core_auth import require_baseline_owner, require_core_access
from niannian.vault.enhancements import enhancement_status
from niannian.vault.extract_service import ExtractService
from niannian.vault.import_service import DuplicateImportError, VaultImportService
from niannian.vault.search import VaultSearchService
from niannian.vault.scoring import VaultScoringService
from niannian.vault.store import VaultStore
from niannian.vault.taxonomy import TaxonomyService

if TYPE_CHECKING:
    from niannian.client import NianNian

_PKG_ROOT = Path(__file__).resolve().parent.parent.parent.parent


class VaultImportBody(BaseModel):
    md_body: str = Field(..., min_length=1)
    namespace: str = "user"
    hints: dict[str, Any] | None = None


class ExtractBody(BaseModel):
    namespace: str = "user"
    input_type: str = "url"
    payload: dict[str, Any] = Field(default_factory=dict)
    hints: dict[str, Any] | None = None


class TaxonomyConfirmBody(BaseModel):
    action: str = Field(..., description="adopt | assign")
    category: str
    subproject: str
    namespace: str = "user"
    vault_ids: list[str] | None = None
    proposed_subproject: str | None = None
    add_routing_rule: dict[str, Any] | None = None


class VaultCrawlBody(BaseModel):
    url: str = Field(..., min_length=8)
    hints: dict[str, Any] | None = None


class VaultImportBatchBody(BaseModel):
    namespace: str = "user"
    items: list[dict[str, Any]] = Field(..., min_length=1)


def _deny_video_hosts() -> list[str]:
    cfg_path = resolve_config_path()
    data: dict[str, Any] = {}
    if cfg_path.is_file():
        with cfg_path.open(encoding="utf-8") as f:
            data = yaml.safe_load(f) or {}
    vault = (data.get("vault") or {}) if isinstance(data, dict) else {}
    hosts = vault.get("deny_video_hosts")
    if isinstance(hosts, list) and hosts:
        return [str(h) for h in hosts]
    return ["douyin.com", "iesdouyin.com", "youtube.com", "youtu.be", "bilibili.com"]


def _duplicate_import_http(exc: DuplicateImportError) -> HTTPException:
    return HTTPException(
        status_code=409,
        detail={
            "code": "duplicate_import",
            "message": "同 source_url 或 vault_id 24h 内已导入",
            "vault_id": exc.vault_id,
            "source_url": exc.source_url,
        },
    )


def _is_video_url(url: str) -> bool:
    try:
        host = (urlparse(url).hostname or "").lower()
    except Exception:
        return False
    if not host:
        return False
    for deny in _deny_video_hosts():
        d = deny.lower()
        if host == d or host.endswith("." + d):
            return True
    return False


def create_vault_router(nn: NianNian) -> APIRouter:
    router = APIRouter(tags=["vault"])
    store = VaultStore(nn._home, manifest=getattr(nn._store, "manifest", None), config=nn._config)
    taxonomy = TaxonomyService(store, home=nn._home)
    importer = VaultImportService(store, ingest_fn=nn.ingest, taxonomy=taxonomy)
    extractor = ExtractService(
        importer,
        taxonomy,
        llm=getattr(getattr(nn, "_router", None), "llm", None),
        config=nn._config,
    )
    search_svc = VaultSearchService(store, config=nn._config)

    @router.get("/v1/vault/enhancements")
    def vault_enhancements(_role: str = Depends(require_core_access)):
        idx = store.load_index()
        from niannian.vault.enhancements import vault_entry_count

        count = vault_entry_count(idx)
        return enhancement_status(nn._config, entry_count=count)

    @router.post("/v1/vault/import")
    def vault_import(body: VaultImportBody, _role: str = Depends(require_baseline_owner)):
        try:
            result = importer.import_md(body.md_body, namespace=body.namespace, hints=body.hints)
        except DuplicateImportError as exc:
            raise _duplicate_import_http(exc) from exc
        except ValueError as exc:
            raise HTTPException(status_code=400, detail=str(exc)) from exc
        return result

    @router.post("/v1/vault/import/batch")
    def vault_import_batch(body: VaultImportBatchBody, _role: str = Depends(require_baseline_owner)):
        return importer.import_batch(body.items, default_namespace=body.namespace)

    @router.get("/v1/vault/search")
    def vault_search(
        q: str = Query(""),
        namespace: str = Query("user"),
        limit: int = Query(20, ge=1, le=100),
        cultural_batch_id: str | None = Query(None),
        _role: str = Depends(require_core_access),
    ):
        items = search_svc.search(
            q,
            namespace=namespace,
            limit=limit,
            cultural_batch_id=cultural_batch_id,
        )
        return {"items": items, "count": len(items)}

    @router.get("/v1/vault/status")
    def vault_status(
        vault_id: str | None = Query(None),
        atom_id: str | None = Query(None),
        _role: str = Depends(require_core_access),
    ):
        if not vault_id and not atom_id:
            raise HTTPException(status_code=400, detail="vault_id or atom_id required")
        try:
            return search_svc.status(vault_id=vault_id, atom_id=atom_id)
        except FileNotFoundError:
            raise HTTPException(status_code=404, detail="vault_entry_not_found") from None

    @router.get("/v1/vault/tree")
    def vault_tree(namespace: str = Query("user"), _role: str = Depends(require_core_access)):
        return store.list_tree(namespace)

    @router.get("/v1/vault/pages/{namespace}/{path:path}")
    def vault_page(namespace: str, path: str, _role: str = Depends(require_core_access)):
        rel = path if path.endswith(".md") else f"{path}.md"
        if not rel.startswith(f"{namespace}/"):
            rel = f"{namespace}/{rel}"
        try:
            doc = store.read_document(rel)
        except FileNotFoundError:
            raise HTTPException(status_code=404, detail="page_not_found") from None
        return doc

    @router.get("/v1/vault/pending")
    def vault_pending(namespace: str = Query("user"), _role: str = Depends(require_core_access)):
        return {"items": taxonomy.list_pending(namespace)}

    @router.get("/v1/vault/taxonomy")
    def vault_taxonomy(namespace: str = Query("user"), _role: str = Depends(require_core_access)):
        return store.load_taxonomy(namespace)

    @router.post("/v1/vault/taxonomy/confirm")
    def vault_taxonomy_confirm(body: TaxonomyConfirmBody, _role: str = Depends(require_baseline_owner)):
        try:
            return taxonomy.confirm(
                namespace=body.namespace,
                action=body.action,
                category=body.category,
                subproject=body.subproject,
                vault_ids=body.vault_ids,
                proposed_subproject=body.proposed_subproject,
                add_routing_rule=body.add_routing_rule,
            )
        except (ValueError, FileNotFoundError) as exc:
            raise HTTPException(status_code=400, detail=str(exc)) from exc

    @router.post("/v1/extract")
    def extract(body: ExtractBody, _role: str = Depends(require_baseline_owner)):
        url = ""
        if isinstance(body.payload, dict):
            url = str(body.payload.get("url") or "")
        if body.input_type in ("video",) or (url and _is_video_url(url)):
            template = _PKG_ROOT / "config" / "vault" / "md_template.md"
            tpl = template.read_text(encoding="utf-8") if template.is_file() else ""
            raise HTTPException(
                status_code=422,
                detail={
                    "code": "extract_deferred_local",
                    "message": "视频类链接请在本地处理后 import MD",
                    "mcp_handoff": {
                        "tool": "niannian_vault_import",
                        "doc": "docs/EXTERNAL_INTEGRATION.md#vault-local",
                        "extract_spec": "niannian.extract/1",
                        "suggested_flow": [
                            "本地 Codex/Cursor 转写+整理为 MD",
                            "调用 MCP niannian_vault_import 或 POST /v1/vault/import",
                        ],
                        "template": "config/vault/md_template.md",
                    },
                    "source_url": url,
                    "template_preview": tpl[:400],
                },
            )
        if body.input_type in ("image",):
            pass  # handled by ExtractService
        try:
            return extractor.extract(
                namespace=body.namespace,
                input_type=body.input_type,
                payload=body.payload,
                hints=body.hints,
            )
        except DuplicateImportError as exc:
            raise _duplicate_import_http(exc) from exc
        except ValueError as exc:
            code = str(exc)
            if code == "host_not_allowed":
                raise HTTPException(
                    status_code=403,
                    detail={"code": "host_not_allowed", "message": "URL 不在 text_hosts 允许列表"},
                ) from exc
            if code == "ocr_unavailable":
                raise HTTPException(
                    status_code=503,
                    detail={
                        "code": "ocr_unavailable",
                        "message": "OCR 不可用，请本地整理 MD 后 import",
                        "mcp_handoff": {
                            "tool": "niannian_vault_import",
                            "template": "config/vault/md_template.md",
                        },
                    },
                ) from exc
            raise HTTPException(status_code=400, detail=str(exc)) from exc

    @router.post("/v1/vault/crawl")
    def vault_crawl(body: VaultCrawlBody, _role: str = Depends(require_baseline_owner)):
        if _is_video_url(body.url):
            raise HTTPException(
                status_code=422,
                detail={
                    "code": "extract_deferred_local",
                    "message": "视频类链接请在本地处理后 import MD",
                },
            )
        try:
            result = extractor.extract(
                namespace="niannian",
                input_type="url",
                payload={"url": body.url},
                hints=body.hints,
            )
        except DuplicateImportError as exc:
            raise _duplicate_import_http(exc) from exc
        except ValueError as exc:
            code = str(exc)
            if code == "host_not_allowed":
                raise HTTPException(
                    status_code=403,
                    detail={"code": "host_not_allowed", "message": "URL 不在 text_hosts 允许列表"},
                ) from exc
            raise HTTPException(status_code=400, detail=str(exc)) from exc
        from niannian.soul.reflect_loop import ReflectLoop

        loop = ReflectLoop(nn._subject_store, config=nn._config)
        scoring = VaultScoringService(
            store,
            scorer=loop._scorer,
            config=nn._config,
            memory_store=getattr(nn, "_store", None),
        )
        if scoring.niannian_scoring_enabled() and result.get("rel_path") and result.get("atom_id"):
            doc = store.read_document(str(result["rel_path"]))
            body_text = (doc.get("body") or "")[:2000]
            values = nn._subject_store.load_values()
            scored = scoring.score_text(body_text, values=values)
            score_row = scoring.apply_score(
                vault_id=str(result["vault_id"]),
                rel_path=str(result["rel_path"]),
                atom_id=str(result["atom_id"]),
                scored=scored,
            )
            result = {**result, **score_row}
        return result

    @router.get("/v1/vault/lint")
    def vault_lint_get(_role: str = Depends(require_baseline_owner)):
        from niannian.vault.lint_service import VaultLintService

        return VaultLintService(store).run()

    @router.post("/v1/vault/lint/run")
    def vault_lint_run(_role: str = Depends(require_baseline_owner)):
        from niannian.vault.lint_service import VaultLintService

        return VaultLintService(store).run()

    return router
