from __future__ import annotations

import json
import os
import time
import urllib.error
import urllib.parse
import urllib.request
from abc import ABC, abstractmethod
from pathlib import Path
from typing import Any

from niannian import NianNian
from niannian.config import load_config
from niannian.mcp.audit import write_audit
from niannian.memory.types import RecallPack


class NianNianService(ABC):
    """MCP / HTTP 统一业务层 — 本地 NianNian 或远程 NIANNIAN_API。"""

    def __init__(self, *, project: str = "global", config: dict[str, Any] | None = None) -> None:
        self.project = project
        self._config = config or load_config()

    @property
    @abstractmethod
    def home(self) -> Path | None:
        ...

    def _audit(self, tool: str, args_summary: str, ok: bool, started: float, error: str | None = None) -> None:
        write_audit(
            home=self.home,
            config=self._config,
            direction="out",
            tool=tool,
            args_summary=args_summary,
            ok=ok,
            duration_ms=int((time.monotonic() - started) * 1000),
            error=error,
        )

    @abstractmethod
    def ingest(self, text: str, *, tags: list[str] | None = None, source: str = "mcp", project: str | None = None) -> dict[str, Any]:
        ...

    @abstractmethod
    def assist(self, task: str, *, context: str = "", project: str | None = None) -> dict[str, Any]:
        ...

    @abstractmethod
    def query(self, question: str, *, project: str | None = None) -> dict[str, Any]:
        ...

    @abstractmethod
    def dream(self, *, project: str | None = None) -> dict[str, Any]:
        ...

    @abstractmethod
    def record(self, fragment: dict[str, Any] | None = None, **kwargs: Any) -> dict[str, Any]:
        ...

    @abstractmethod
    def recall(self, task: str, *, scope: dict[str, Any] | None = None, policy: str | None = None, project: str | None = None) -> dict[str, Any]:
        ...

    @abstractmethod
    def reflect(self, *, scope: dict[str, Any] | None = None, project: str | None = None) -> dict[str, Any]:
        ...

    @abstractmethod
    def health(self) -> dict[str, Any]:
        ...

    @abstractmethod
    def sync_status(self) -> dict[str, Any]:
        ...

    @abstractmethod
    def export_skill(self, name: str | None = None, project: str | None = None, tags: str | None = None) -> dict[str, Any]:
        ...

    @abstractmethod
    def vault_import(
        self,
        md_body: str,
        *,
        namespace: str = "user",
        hints: dict[str, Any] | None = None,
    ) -> dict[str, Any]:
        ...

    @abstractmethod
    def extract_status(
        self,
        *,
        vault_id: str | None = None,
        atom_id: str | None = None,
    ) -> dict[str, Any]:
        ...


class LocalNianNianService(NianNianService):
    def __init__(self, nn: NianNian) -> None:
        super().__init__(project=nn.project, config=nn._config)
        self._nn = nn

    @property
    def home(self) -> Path:
        return self._nn.home

    def ingest(self, text: str, *, tags: list[str] | None = None, source: str = "mcp", project: str | None = None) -> dict[str, Any]:
        t0 = time.monotonic()
        try:
            atom = self._nn.ingest(text, tags=tags, source=source, project=project or self.project)
            self._audit("niannian_ingest", text[:80], True, t0)
            return atom.to_dict()
        except Exception as exc:
            self._audit("niannian_ingest", text[:80], False, t0, str(exc))
            raise

    def assist(self, task: str, *, context: str = "", project: str | None = None) -> dict[str, Any]:
        t0 = time.monotonic()
        try:
            result = self._nn.assist(task, context=context, project=project or self.project)
            self._audit("niannian_assist", task[:80], True, t0)
            return result.to_dict()
        except Exception as exc:
            self._audit("niannian_assist", task[:80], False, t0, str(exc))
            raise

    def query(self, question: str, *, project: str | None = None) -> dict[str, Any]:
        t0 = time.monotonic()
        try:
            result = self._nn.query(question, project=project or self.project)
            self._audit("niannian_query", question[:80], True, t0)
            return result.to_dict()
        except Exception as exc:
            self._audit("niannian_query", question[:80], False, t0, str(exc))
            raise

    def dream(self, *, project: str | None = None) -> dict[str, Any]:
        t0 = time.monotonic()
        try:
            result = self._nn.dream(project=project or self.project)
            self._audit("niannian_dream", "", True, t0)
            return result
        except Exception as exc:
            self._audit("niannian_dream", "", False, t0, str(exc))
            raise

    def record(self, fragment: dict[str, Any] | None = None, **kwargs: Any) -> dict[str, Any]:
        t0 = time.monotonic()
        kind = (fragment or {}).get("taxonomy", {}).get("kind") or kwargs.get("kind", "note")
        try:
            if fragment is not None:
                result = self._nn.record(fragment=fragment)
            else:
                result = self._nn.record(**kwargs)
            self._audit("niannian_record", f"kind={kind}", True, t0)
            return result.to_dict()
        except Exception as exc:
            self._audit("niannian_record", f"kind={kind}", False, t0, str(exc))
            raise

    def recall(self, task: str, *, scope: dict[str, Any] | None = None, policy: str | None = None, project: str | None = None) -> dict[str, Any]:
        t0 = time.monotonic()
        pol = policy or "assist_default"
        try:
            pack = self._nn.recall(task, scope=scope, policy=pol, project=project or self.project)
            data = pack.to_dict() if isinstance(pack, RecallPack) else pack
            self._audit("niannian_recall", f"policy={pol}", True, t0)
            return data if isinstance(data, dict) else {"result": data}
        except Exception as exc:
            self._audit("niannian_recall", f"policy={pol}", False, t0, str(exc))
            raise

    def reflect(self, *, scope: dict[str, Any] | None = None, project: str | None = None) -> dict[str, Any]:
        t0 = time.monotonic()
        try:
            result = self._nn.reflect(scope=scope, project=project or self.project)
            self._audit("niannian_reflect", str(scope or {}), True, t0)
            return result.to_dict()
        except Exception as exc:
            self._audit("niannian_reflect", str(scope or {}), False, t0, str(exc))
            raise

    def health(self) -> dict[str, Any]:
        return self._nn.health()

    def sync_status(self) -> dict[str, Any]:
        return self._nn.sync_status()

    def export_skill(self, name: str | None = None, project: str | None = None, tags: str | None = None) -> dict[str, Any]:
        from niannian.skills.export import export_skills

        t0 = time.monotonic()
        try:
            result = export_skills(self._nn.home, self._nn.skills, name=name, project=project, tags=tags)
            self._audit("niannian_export_skill", name or "all", True, t0)
            return result
        except Exception as exc:
            self._audit("niannian_export_skill", name or "all", False, t0, str(exc))
            raise

    def vault_import(
        self,
        md_body: str,
        *,
        namespace: str = "user",
        hints: dict[str, Any] | None = None,
    ) -> dict[str, Any]:
        from niannian.vault.import_service import VaultImportService
        from niannian.vault.store import VaultStore
        from niannian.vault.taxonomy import TaxonomyService

        t0 = time.monotonic()
        try:
            store = VaultStore(self._nn._home, manifest=getattr(self._nn._store, "manifest", None), config=self._nn._config)
            taxonomy = TaxonomyService(store, home=self._nn._home)
            importer = VaultImportService(store, ingest_fn=self._nn.ingest, taxonomy=taxonomy)
            result = importer.import_md(md_body, namespace=namespace, hints=hints)
            self._audit("niannian_vault_import", md_body[:60], True, t0)
            return result
        except Exception as exc:
            self._audit("niannian_vault_import", md_body[:60], False, t0, str(exc))
            raise

    def extract_status(
        self,
        *,
        vault_id: str | None = None,
        atom_id: str | None = None,
    ) -> dict[str, Any]:
        from niannian.vault.search import VaultSearchService
        from niannian.vault.store import VaultStore

        t0 = time.monotonic()
        key = vault_id or atom_id or ""
        try:
            store = VaultStore(self._nn._home, manifest=getattr(self._nn._store, "manifest", None), config=self._nn._config)
            result = VaultSearchService(store).status(vault_id=vault_id, atom_id=atom_id)
            self._audit("niannian_extract_status", key[:40], True, t0)
            return result
        except Exception as exc:
            self._audit("niannian_extract_status", key[:40], False, t0, str(exc))
            raise


class HttpNianNianService(NianNianService):
    """远程模式 — stdio MCP 子进程，数据走 NIANNIAN_API。"""

    def __init__(
        self,
        api_base: str,
        *,
        project: str = "global",
        config: dict[str, Any] | None = None,
        api_token: str | None = None,
    ) -> None:
        super().__init__(project=project, config=config)
        self._api = api_base.rstrip("/")
        if api_token is None:
            api_token = os.environ.get("NIANNIAN_API_TOKEN")
        self._api_token = str(api_token).strip() if api_token else None

    @property
    def home(self) -> Path | None:
        return None

    def _auth_headers(self) -> dict[str, str]:
        if not self._api_token:
            return {}
        return {"Authorization": f"Bearer {self._api_token}"}

    def _request(self, method: str, path: str, payload: dict[str, Any] | None = None) -> Any:
        url = f"{self._api}{path}"
        data = None
        headers = self._auth_headers()
        if method.upper() != "GET" and payload is not None:
            data = json.dumps(payload).encode("utf-8")
            headers["Content-Type"] = "application/json"
        req = urllib.request.Request(url, data=data, headers=headers, method=method.upper())
        try:
            with urllib.request.urlopen(req, timeout=60) as resp:
                body = resp.read().decode("utf-8")
                return json.loads(body) if body else {}
        except urllib.error.HTTPError as e:
            err = e.read().decode("utf-8", errors="replace")
            raise RuntimeError(f"HTTP {e.code}: {err[:500]}") from e

    def ingest(self, text: str, *, tags: list[str] | None = None, source: str = "mcp", project: str | None = None) -> dict[str, Any]:
        t0 = time.monotonic()
        try:
            result = self._request("POST", "/v1/ingest", {"text": text, "tags": tags or [], "source": source, "project": project or self.project})
            self._audit("niannian_ingest", text[:80], True, t0)
            return result
        except Exception as exc:
            self._audit("niannian_ingest", text[:80], False, t0, str(exc))
            raise

    def assist(self, task: str, *, context: str = "", project: str | None = None) -> dict[str, Any]:
        t0 = time.monotonic()
        try:
            result = self._request("POST", "/v1/assist", {"task": task, "context": context, "project": project or self.project})
            self._audit("niannian_assist", task[:80], True, t0)
            return result
        except Exception as exc:
            self._audit("niannian_assist", task[:80], False, t0, str(exc))
            raise

    def query(self, question: str, *, project: str | None = None) -> dict[str, Any]:
        t0 = time.monotonic()
        try:
            result = self._request("POST", "/v1/query", {"question": question, "project": project or self.project})
            self._audit("niannian_query", question[:80], True, t0)
            return result
        except Exception as exc:
            self._audit("niannian_query", question[:80], False, t0, str(exc))
            raise

    def dream(self, *, project: str | None = None) -> dict[str, Any]:
        t0 = time.monotonic()
        try:
            path = "/v1/dream"
            if project:
                path += f"?project={urllib.parse.quote(project)}"
            result = self._request("POST", path, {})
            self._audit("niannian_dream", "", True, t0)
            return result
        except Exception as exc:
            self._audit("niannian_dream", "", False, t0, str(exc))
            raise

    def record(self, fragment: dict[str, Any] | None = None, **kwargs: Any) -> dict[str, Any]:
        t0 = time.monotonic()
        kind = (fragment or {}).get("taxonomy", {}).get("kind") or kwargs.get("kind", "note")
        try:
            if fragment is None:
                from niannian.memory.fragment import build_fragment

                fragment = build_fragment(kwargs.get("summary", ""), **{k: v for k, v in kwargs.items() if k != "summary"})
            result = self._request("POST", "/v1/fragments", fragment)
            self._audit("niannian_record", f"kind={kind}", True, t0)
            return result
        except Exception as exc:
            self._audit("niannian_record", f"kind={kind}", False, t0, str(exc))
            raise

    def recall(self, task: str, *, scope: dict[str, Any] | None = None, policy: str | None = None, project: str | None = None) -> dict[str, Any]:
        t0 = time.monotonic()
        pol = policy or "assist_default"
        try:
            result = self._request(
                "POST",
                "/v1/recall",
                {
                    "project": project or self.project,
                    "task": task,
                    "scope": scope or {},
                    "policy": pol,
                },
            )
            self._audit("niannian_recall", f"policy={pol}", True, t0)
            return result
        except Exception as exc:
            self._audit("niannian_recall", f"policy={pol}", False, t0, str(exc))
            raise

    def reflect(self, *, scope: dict[str, Any] | None = None, project: str | None = None) -> dict[str, Any]:
        t0 = time.monotonic()
        try:
            result = self._request("POST", "/v1/reflect", {"scope": scope or {}, "project": project or self.project})
            self._audit("niannian_reflect", str(scope or {}), True, t0)
            return result
        except Exception as exc:
            self._audit("niannian_reflect", str(scope or {}), False, t0, str(exc))
            raise

    def health(self) -> dict[str, Any]:
        return self._request("GET", "/health", None)

    def sync_status(self) -> dict[str, Any]:
        return self._request("GET", "/v1/sync/status", None)

    def export_skill(self, name: str | None = None, project: str | None = None, tags: str | None = None) -> dict[str, Any]:
        t0 = time.monotonic()
        path = "/v1/skills/export"
        if name:
            path += f"?name={urllib.parse.quote(name)}"
        try:
            result = self._request("GET", path, None)
            self._audit("niannian_export_skill", name or "all", True, t0)
            return result
        except Exception as exc:
            self._audit("niannian_export_skill", name or "all", False, t0, str(exc))
            raise

    def vault_import(
        self,
        md_body: str,
        *,
        namespace: str = "user",
        hints: dict[str, Any] | None = None,
    ) -> dict[str, Any]:
        t0 = time.monotonic()
        try:
            result = self._request(
                "POST",
                "/v1/vault/import",
                {"md_body": md_body, "namespace": namespace, "hints": hints},
            )
            self._audit("niannian_vault_import", md_body[:60], True, t0)
            return result
        except Exception as exc:
            self._audit("niannian_vault_import", md_body[:60], False, t0, str(exc))
            raise

    def extract_status(
        self,
        *,
        vault_id: str | None = None,
        atom_id: str | None = None,
    ) -> dict[str, Any]:
        t0 = time.monotonic()
        key = vault_id or atom_id or ""
        params: list[str] = []
        if vault_id:
            params.append(f"vault_id={urllib.parse.quote(vault_id)}")
        if atom_id:
            params.append(f"atom_id={urllib.parse.quote(atom_id)}")
        try:
            result = self._request("GET", f"/v1/vault/status?{'&'.join(params)}", None)
            self._audit("niannian_extract_status", key[:40], True, t0)
            return result
        except Exception as exc:
            self._audit("niannian_extract_status", key[:40], False, t0, str(exc))
            raise


def create_service(
    *,
    home: str | Path | None = None,
    project: str | None = None,
    api_url: str | None = None,
) -> NianNianService:
    proj = project or os.environ.get("NIANNIAN_PROJECT", "global")
    api = api_url or os.environ.get("NIANNIAN_API")
    if api:
        return HttpNianNianService(api, project=proj, api_token=os.environ.get("NIANNIAN_API_TOKEN"))
    nn = NianNian(home=home or os.environ.get("NIANNIAN_HOME"), project=proj)
    return LocalNianNianService(nn)
