"""NeurovaultEngine - main API for the cognitive engine."""

from __future__ import annotations

import logging
import json
import os
import shutil
import time
import asyncio
from pathlib import Path
from typing import Any, Optional

from neurovault.bridge import Checkpoint, ChronosBridge, Memory
from neurovault.cognitive.consolidation_strategy import ConsolidationStrategy
from neurovault.cognitive.decay import next_review_time_ms
from neurovault.cognitive.embedding_engine import EmbeddingEngine
from neurovault.cognitive.embedding_router import EmbeddingRouter
from neurovault.cognitive.emotional import EmotionalAnalyzer
from neurovault.cognitive.entity_extractor import EntityExtractor
from neurovault.cognitive.ontology import OntologyEngine
from neurovault.cognitive.prediction import ContextMonitor, PredictionEngine
from neurovault.config import NeurovaultConfig
from neurovault.content_filter import ContentFilter
from neurovault.events import EventBus, NeurovaultEvents
from neurovault.models import (
    AuditReport,
    ConsolidationReport,
    EntityGraph,
    MemoryTier,
    MergeResult,
    Relationship,
    VaultStatus,
)
from neurovault.migrations.runner import MigrationRunner
from neurovault.plugins.loader import PluginLoader
from neurovault.search.multi_signal import MultiSignalSearchEngine, RecallResult
from neurovault.search.hnsw import HNSWIndex
from neurovault.network.bundle import KnowledgeBundle
from neurovault.network.federation_client import FederationClient
from neurovault.network.identity import DeviceIdentity
from neurovault.network.sync_engine import SyncEngine
from neurovault.security.audit import AuditTrail
from neurovault.security.rbac import Permission, UserManager
from neurovault.sensors.calendar_integration import CalendarIntegration
from neurovault.storage import NeurovaultStorage

logger = logging.getLogger(__name__)


class NeurovaultEngine:
    """Main NEUROVAULT API."""

    def __init__(
        self,
        bridge: ChronosBridge,
        nv_storage: NeurovaultStorage,
        config: NeurovaultConfig | None = None,
    ):
        self._bridge = bridge
        self._nv = nv_storage
        self._config = config or NeurovaultConfig.load()

        self._emotional = EmotionalAnalyzer()
        self._extractor = EntityExtractor()
        self._filter = ContentFilter(self._config)
        self._context = ContextMonitor()

        embedding_engine = getattr(self._bridge.repo, "_embedding", None)
        if embedding_engine is None:
            embedding_cfg = self._config.embedding or {}
            try:
                router_cfg = {
                    "target_dimension": int(embedding_cfg.get("target_dimension", 384)),
                    "preferred_backend": str(embedding_cfg.get("backend", "auto")),
                    "st_model": str(embedding_cfg.get("model", "all-MiniLM-L6-v2")),
                    "ollama_model": str(embedding_cfg.get("ollama_model", "nomic-embed-text")),
                    "ollama_base_url": str(embedding_cfg.get("ollama_base_url", "http://127.0.0.1:11434")),
                    "openai_base_url": str(embedding_cfg.get("openai_base_url", "http://127.0.0.1:8080")),
                    "openai_api_key": str(embedding_cfg.get("openai_api_key", "")),
                    "openai_model": str(embedding_cfg.get("openai_model", "text-embedding-3-small")),
                }
                embedding_engine = EmbeddingRouter(config=router_cfg)
            except Exception as exc:
                logger.debug("EmbeddingRouter init failed, falling back to EmbeddingEngine: %s", exc)
                try:
                    embedding_engine = EmbeddingEngine(
                        config={
                            "embedding_backend": embedding_cfg.get("backend"),
                            "embedding_model": embedding_cfg.get("model"),
                        }
                    )
                except Exception as sub_exc:
                    logger.debug("EmbeddingEngine fallback init failed: %s", sub_exc)
                    embedding_engine = None
        self._embedding_engine = embedding_engine
        self._search = MultiSignalSearchEngine(
            self._nv,
            self._bridge,
            self._embedding_engine,
            self._extractor,
        )

        self._hnsw_index: HNSWIndex | None = None
        self._init_hnsw_index()

        self._consolidation = ConsolidationStrategy.build(
            storage=self._nv,
            chronos_bridge=self._bridge,
            extractor=self._extractor,
            config=self._config,
        )
        self._prediction = PredictionEngine(self._nv, self._search, self._context)
        self._calendar = CalendarIntegration(
            recall_fn=self._calendar_recall,
            entity_lookup_fn=self._calendar_entity_lookup,
        )
        self._events = EventBus()
        self._ontology = OntologyEngine(self._nv)
        if bool(self._config.consolidation.get("bootstrap_ontology", True)):
            try:
                self._ontology.bootstrap_taxonomy()
            except Exception as exc:
                logger.debug("Ontology bootstrap skipped: %s", exc)
        self._plugin_dir = Path.home() / ".neurovault" / "plugins"
        self._plugin_loader = PluginLoader(plugin_dir=self._plugin_dir)
        self._pipeline_stats_path = Path.home() / ".neurovault" / "pipeline_stats.json"
        self._user_manager = UserManager(self._nv)
        self._audit = AuditTrail(storage=self._nv)
        self._sync_engine = SyncEngine(self._nv, self._bridge)
        self._device_identity: DeviceIdentity | None = None
        if bool(self._config.network.get("enabled", False)):
            try:
                self._device_identity = DeviceIdentity(self._nv.db_path.parent).get_or_create()
            except Exception as exc:
                logger.debug("Device identity unavailable: %s", exc)
                self._device_identity = None

        self._daemon_start_time: Optional[float] = None
        self._last_consolidation: Optional[int] = None

    @classmethod
    def init(
        cls,
        name: str,
        description: str = "",
        no_embedding: bool = False,
        base_dir: str | None = None,
    ) -> "NeurovaultEngine":
        del description
        bridge = ChronosBridge.create(name, no_embedding, base_dir)
        db_path = cls._resolve_neurovault_db_path(bridge, name=name, base_dir=base_dir)
        nv_storage = NeurovaultStorage(db_path)
        if db_path.exists():
            try:
                MigrationRunner(str(db_path)).migrate_to("0.3.0")
            except Exception as exc:
                logger.warning("Migration check failed for %s: %s", db_path, exc)
        nv_storage.initialize_schema()
        return cls(bridge, nv_storage)

    @classmethod
    def open(cls, name: str, base_dir: str | None = None) -> "NeurovaultEngine":
        bridge = ChronosBridge.from_name(name, base_dir)
        db_path = cls._resolve_neurovault_db_path(bridge, name=name, base_dir=base_dir)
        nv_storage = NeurovaultStorage(db_path)
        if db_path.exists():
            try:
                MigrationRunner(str(db_path)).migrate_to("0.3.0")
            except Exception as exc:
                logger.warning("Migration check failed for %s: %s", db_path, exc)
        nv_storage.initialize_schema()
        return cls(bridge, nv_storage)

    # ================================================================
    # Ingestion
    # ================================================================

    def ingest(
        self,
        content: str,
        source: str = "manual",
        importance: float = 0.5,
        metadata: dict | None = None,
    ) -> Memory | None:
        filtered_content, _reason = self._filter.filter(content, source)
        if filtered_content is None:
            return None

        emotion = self._emotional.analyze(filtered_content)
        boosted_importance = min(importance * emotion.importance_modifier, 1.0)

        now_ms = int(time.time() * 1000)
        full_metadata = {
            **(metadata or {}),
            "tier": MemoryTier.WORKING.value,
            "access_count": 0,
            "last_accessed": now_ms,
            "emotional_valence": emotion.valence,
            "emotional_arousal": emotion.arousal,
            "emotional_dominance": emotion.dominance,
            "entity_processed": False,
            "cluster_processed": False,
        }

        memory = self._bridge.store_memory(
            content=filtered_content,
            source=source,
            importance=boosted_importance,
            metadata=full_metadata,
        )

        raw_entities = self._extractor.extract(filtered_content)
        for raw in raw_entities:
            entity_type = raw["type"].value if hasattr(raw["type"], "value") else raw["type"]
            entity = self._nv.upsert_entity(
                name=raw["name"],
                entity_type=entity_type,
                confidence=raw.get("confidence", 0.8),
                seen_at=memory.created_at,
            )
            self._nv.link_memory_entity(memory.id, entity.id, raw.get("confidence", 0.8))

        if len(raw_entities) >= 2:
            for i, left in enumerate(raw_entities):
                for right in raw_entities[i + 1 :]:
                    self._nv.upsert_relationship(
                        source_name=left["name"],
                        target_name=right["name"],
                        relation_type=self._infer_relation(left, right),
                        strength=0.3,
                    )

        self._bridge.update_memory_metadata(memory.id, {"entity_processed": True})
        self._context.update("last_ingest", filtered_content[:100])
        self._events.emit(
            NeurovaultEvents.MEMORY_INGESTED,
            {"id": memory.id.hex(), "source": source, "importance": memory.importance},
        )
        self._sync_memory_snapshot(memory.id)
        self._index_memory_embedding(memory.id, filtered_content)
        return memory

    def start_daemon(self, vault_name: str = "default", base_dir: str | None = None):
        """Start daemon mode in-process (blocking)."""
        from neurovault.daemon import NeurovaultDaemon

        daemon = NeurovaultDaemon(vault_name=vault_name, base_dir=base_dir)
        daemon.start()

    def stop_daemon(self) -> bool:
        """Stop daemon mode via PID signal."""
        from neurovault.daemon import stop_running_daemon

        return stop_running_daemon()

    # ================================================================
    # Retrieval & Prediction
    # ================================================================

    def recall(
        self,
        query: str,
        limit: int = 10,
        mode: str = "multi",
        min_importance: float = 0.0,
        context: dict | None = None,
        since: int | None = None,
    ) -> list[RecallResult]:
        persona_id = self._bridge.get_active_persona_id()
        results = self._search.search(
            query=query,
            persona_id=persona_id,
            limit=limit,
            mode=mode,
            min_importance=min_importance,
        )
        if since is not None:
            results = [r for r in results if int(getattr(r.memory, "created_at", 0)) >= since]

        if context:
            for key, value in context.items():
                try:
                    self._context.update(str(key), str(value))
                except Exception as exc:
                    logger.debug("Context update skipped for key %s: %s", key, exc)
                    continue

        for result in results[:5]:
            self._reinforce(result.memory.id)

        self._context.update("last_query", query)
        self._events.emit(
            NeurovaultEvents.MEMORY_RECALLED,
            {"query": query, "count": len(results), "mode": mode},
        )
        return results

    def recall_by_entity(self, entity_name: str, limit: int = 10) -> list[RecallResult]:
        memory_ids = self._nv.find_memory_ids_by_entity_name(entity_name, limit=limit * 5)
        results: list[RecallResult] = []
        for memory_id in memory_ids:
            try:
                memory = self._bridge.get_memory(memory_id)
            except Exception as exc:
                logger.debug("Entity recall skipped for memory %s: %s", memory_id.hex(), exc)
                continue
            results.append(
                RecallResult(
                    memory=memory,
                    score=0.8,
                    match_type="entity",
                    entities=self._nv.get_entities_for_memory(memory_id),
                )
            )
            if len(results) >= limit:
                break
        return results

    def recall_by_graph(self, start_entity: str, depth: int = 2, limit: int = 10) -> list[RecallResult]:
        # Depth parameter is reflected through graph mode traversal in search engine.
        del depth
        return self.recall(query=start_entity, limit=limit, mode="graph")

    def update_context(self, context_type: str, value: str) -> None:
        self._context.update(context_type, value)

    def predict(self) -> list[RecallResult]:
        persona_id = self._bridge.get_active_persona_id()
        predictions = self._prediction.predict(persona_id)
        if predictions:
            self._events.emit(
                NeurovaultEvents.PREDICTION_SURFACED,
                {"count": len(predictions)},
            )
        return predictions

    def get_predictions(self) -> list[RecallResult]:
        return self.predict()

    def check_calendar_predictions(self) -> list[dict[str, Any]]:
        notifications = self._calendar.check_and_notify()
        if notifications:
            self._events.emit(
                NeurovaultEvents.PREDICTION_CALENDAR,
                {"count": len(notifications)},
            )
        return notifications

    def _calendar_recall(self, query: str, limit: int = 10, mode: str = "multi") -> list[RecallResult]:
        return self.recall(query=query, limit=limit, mode=mode)

    def _calendar_entity_lookup(self, name: str) -> list[str]:
        try:
            matches = self._nv.find_entities_by_name(name)
        except Exception as exc:
            logger.debug("Calendar entity lookup failed for %s: %s", name, exc)
            return []
        return [entity.name for entity in matches[:3] if getattr(entity, "name", "")]

    # ================================================================
    # Consolidation
    # ================================================================

    def consolidate(self, force: bool = False, phases: list[str] | None = None) -> ConsolidationReport:
        self._events.emit(
            NeurovaultEvents.CONSOLIDATION_STARTED,
            {"force": force, "phases": phases or []},
        )
        self._sync_nv_memories()
        report = self._consolidation.run(force=force, phases=phases)
        self._last_consolidation = int(time.time() * 1000)
        self._events.emit(
            NeurovaultEvents.CONSOLIDATION_COMPLETED,
            {
                "entities_extracted": report.entities_extracted,
                "relationships_found": report.relationships_found,
                "insights_generated": report.insights_generated,
            },
        )
        self._sync_nv_memories()
        return report

    def get_insights(self, limit: int = 10) -> list:
        records = self._bridge.get_active_memories_with_metadata()
        candidates = [
            r
            for r in records
            if r.get("source") == "consolidation"
            and isinstance(r.get("metadata"), dict)
            and r["metadata"].get("type") == "insight"
        ]
        candidates.sort(key=lambda item: item.get("created_at", 0), reverse=True)

        insights = []
        for item in candidates[:limit]:
            try:
                insights.append(self._bridge.get_memory(item["id"]))
            except Exception as exc:
                logger.debug("Insight memory load skipped for %s: %s", item.get("id"), exc)
                continue
        return insights

    # ================================================================
    # Knowledge Graph
    # ================================================================

    def get_entities(
        self,
        entity_type: str | None = None,
        limit: int = 50,
        sort: str = "mention_count",
    ):
        return self._nv.list_entities(entity_type=entity_type, limit=limit, sort=sort)

    def get_relationships(self, entity_name: str, direction: str = "both") -> list[Relationship]:
        entities = self._nv.find_entities_by_name(entity_name)
        if not entities:
            return []

        relationships: list[Relationship] = []
        for entity in entities:
            if direction in ("both", "out", "outgoing"):
                relationships.extend(self._nv.get_relationships_from(entity.id))
            if direction in ("both", "in", "incoming"):
                relationships.extend(self._nv.get_relationships_to(entity.id))
        return relationships

    def get_entity_graph(self, entity_name: str, depth: int = 2) -> EntityGraph | None:
        matches = self._nv.find_entities_by_name(entity_name)
        if not matches:
            return None

        root = matches[0]
        visited = set()
        queue: list[tuple[bytes, int]] = [(root.id, 0)]
        nodes = []
        rels: list[Relationship] = []

        while queue:
            entity_id, current_depth = queue.pop(0)
            if entity_id in visited or current_depth > depth:
                continue
            visited.add(entity_id)

            try:
                entity = self._nv.get_entity(entity_id)
            except Exception as exc:
                logger.debug("Entity graph node load skipped for %s: %s", entity_id.hex(), exc)
                continue
            nodes.append(entity)

            outgoing = self._nv.get_relationships_from(entity_id)
            incoming = self._nv.get_relationships_to(entity_id)
            rels.extend(outgoing)
            rels.extend(incoming)

            for rel in outgoing:
                if rel.target_entity_id not in visited:
                    queue.append((rel.target_entity_id, current_depth + 1))
            for rel in incoming:
                if rel.source_entity_id not in visited:
                    queue.append((rel.source_entity_id, current_depth + 1))

        return EntityGraph(root_entity=root, nodes=nodes, relationships=rels, depth=depth)

    def get_relationships_for_entities(
        self,
        entity_ids: list[bytes],
        min_strength: float = 0.0,
    ) -> list[Relationship]:
        if not entity_ids:
            return []
        placeholders = ",".join("?" for _ in entity_ids)
        rows = self._nv.conn.execute(
            f"""
            SELECT *
            FROM relationships
            WHERE strength >= ?
              AND source_entity_id IN ({placeholders})
              AND target_entity_id IN ({placeholders})
            ORDER BY strength DESC
            """,
            (float(min_strength), *entity_ids, *entity_ids),
        ).fetchall()
        return [self._nv._row_to_rel(row) for row in rows]

    def get_entity_hierarchy(self, entity_id: bytes | str) -> dict[str, Any]:
        if isinstance(entity_id, str):
            entity_id = bytes.fromhex(entity_id)
        entity = self._nv.get_entity(entity_id)

        ancestors = self._ontology.get_ancestors(entity_id)
        descendants = self._ontology.get_descendants(entity_id)
        siblings = self._ontology.get_siblings(entity_id)

        return {
            "entity": {
                "id": entity.id.hex() if entity.id else "",
                "name": entity.name,
                "entity_type": entity.entity_type,
            },
            "ancestors": [
                {
                    "id": item["entity"].id.hex() if item["entity"].id else "",
                    "name": item["entity"].name,
                    "entity_type": item["entity"].entity_type,
                    "depth": int(item["depth"]),
                    "relation": str(item["relation"]),
                    "confidence": float(item["confidence"]),
                }
                for item in ancestors
            ],
            "descendants": [
                {
                    "id": item["entity"].id.hex() if item["entity"].id else "",
                    "name": item["entity"].name,
                    "entity_type": item["entity"].entity_type,
                    "depth": int(item["depth"]),
                    "relation": str(item["relation"]),
                    "confidence": float(item["confidence"]),
                }
                for item in descendants
            ],
            "siblings": [
                {
                    "id": item["entity"].id.hex() if item["entity"].id else "",
                    "name": item["entity"].name,
                    "entity_type": item["entity"].entity_type,
                    "confidence": float(item["confidence"]),
                }
                for item in siblings
            ],
        }

    def get_concept_tree(self, limit: int = 200) -> list[dict[str, Any]]:
        rows = self._nv.conn.execute(
            """
            SELECT
                ch.child_entity_id,
                ch.parent_entity_id,
                ch.relation_type,
                ch.confidence,
                ce.name AS child_name,
                ce.entity_type AS child_type,
                pe.name AS parent_name,
                pe.entity_type AS parent_type
            FROM concept_hierarchy ch
            JOIN entities ce ON ce.id = ch.child_entity_id
            JOIN entities pe ON pe.id = ch.parent_entity_id
            ORDER BY ch.confidence DESC
            LIMIT ?
            """,
            (max(1, min(limit, 5000)),),
        ).fetchall()
        return [
            {
                "child_id": row["child_entity_id"].hex(),
                "child_name": str(row["child_name"]),
                "child_type": str(row["child_type"]),
                "parent_id": row["parent_entity_id"].hex(),
                "parent_name": str(row["parent_name"]),
                "parent_type": str(row["parent_type"]),
                "relation": str(row["relation_type"]),
                "confidence": float(row["confidence"]),
            }
            for row in rows
        ]

    def sensor_status(self) -> dict[str, dict[str, Any]]:
        disabled = set(self._config.disabled_sensors)
        return {
            name: {
                "enabled": name not in disabled and bool(cfg.get("enabled", True)),
                "active": name not in disabled and bool(cfg.get("enabled", True)),
                "poll_interval": cfg.get("poll_interval") if isinstance(cfg, dict) else None,
                "last_capture": "N/A",
            }
            for name, cfg in self._config.sensors.items()
        }

    def list_plugins(self) -> list[dict[str, Any]]:
        manifests = self._plugin_loader.discover()
        states = {item["plugin_id"]: item for item in self._nv.list_plugin_state()}
        seen: set[str] = set()
        rows: list[dict[str, Any]] = []

        for manifest in manifests:
            plugin_id = str(manifest.get("_id", ""))
            if not plugin_id:
                continue
            seen.add(plugin_id)
            state = states.get(plugin_id, {})
            rows.append(
                {
                    "id": plugin_id,
                    "name": str(manifest.get("name", plugin_id)),
                    "version": str(manifest.get("version", "")),
                    "description": str(manifest.get("description", "")),
                    "enabled": bool(state.get("enabled", True)),
                    "config": state.get("config", {}),
                    "path": str(manifest.get("_path", "")),
                    "source": "local",
                }
            )

        for plugin_id, state in states.items():
            if plugin_id in seen:
                continue
            rows.append(
                {
                    "id": plugin_id,
                    "name": plugin_id,
                    "version": "",
                    "description": "",
                    "enabled": bool(state.get("enabled", True)),
                    "config": state.get("config", {}),
                    "path": "",
                    "source": "state",
                }
            )

        rows.sort(key=lambda item: item["id"])
        return rows

    def plugin_status(self) -> dict[str, dict[str, Any]]:
        return {item["id"]: item for item in self.list_plugins()}

    def set_plugin_config(self, plugin_id: str, config: dict[str, Any]) -> bool:
        current = self.plugin_status().get(plugin_id, {})
        self._nv.upsert_plugin_state(
            plugin_id=plugin_id,
            enabled=bool(current.get("enabled", True)),
            config=config,
            last_poll=int(current.get("last_poll", 0)),
            error_count=int(current.get("error_count", 0)),
        )
        return True

    def enable_plugin(self, plugin_id: str) -> bool:
        current = self.plugin_status().get(plugin_id, {})
        self._nv.upsert_plugin_state(
            plugin_id=plugin_id,
            enabled=True,
            config=current.get("config", {}),
            last_poll=int(current.get("last_poll", 0)),
            error_count=int(current.get("error_count", 0)),
        )
        return True

    def disable_plugin(self, plugin_id: str) -> bool:
        current = self.plugin_status().get(plugin_id, {})
        self._nv.upsert_plugin_state(
            plugin_id=plugin_id,
            enabled=False,
            config=current.get("config", {}),
            last_poll=int(current.get("last_poll", 0)),
            error_count=int(current.get("error_count", 0)),
        )
        return True

    def plugin_health(self, plugin_id: str) -> dict[str, Any]:
        for item in self.list_plugins():
            if item["id"] == plugin_id:
                return {
                    "id": plugin_id,
                    "status": "ok" if item.get("enabled", False) else "disabled",
                    "enabled": bool(item.get("enabled", False)),
                    "path": str(item.get("path", "")),
                    "version": str(item.get("version", "")),
                }
        return {"id": plugin_id, "status": "missing", "enabled": False}

    def install_plugin(self, source: str) -> dict[str, Any]:
        source_path = Path(source).expanduser()
        if not source_path.exists() or not source_path.is_dir():
            return {
                "ok": False,
                "error": "Only local plugin directories are supported in this runtime",
                "source": source,
            }

        manifest_path = source_path / "plugin.toml"
        if not manifest_path.exists():
            return {"ok": False, "error": "plugin.toml not found", "source": source}

        self._plugin_dir.mkdir(parents=True, exist_ok=True)
        target = self._plugin_dir / source_path.name
        if target.exists():
            return {"ok": False, "error": f"plugin already exists: {source_path.name}"}
        shutil.copytree(source_path, target)
        self._nv.upsert_plugin_state(source_path.name, enabled=True, config={})
        return {"ok": True, "plugin_id": source_path.name, "path": str(target)}

    def remove_plugin(self, plugin_id: str) -> bool:
        target = self._plugin_dir / plugin_id
        removed = False
        if target.exists():
            shutil.rmtree(target)
            removed = True
        self._nv.conn.execute("DELETE FROM plugin_state WHERE plugin_id = ?", (plugin_id,))
        self._nv.conn.commit()
        return removed

    def pipeline_stats(self) -> dict[str, Any]:
        payload = {
            "enabled": bool(self._config.pipeline.get("enabled", False)),
            "ingested": 0,
            "filtered": 0,
            "deduplicated": 0,
            "errors": 0,
            "queue_size": 0,
            "last_updated": 0,
        }
        try:
            if self._pipeline_stats_path.exists():
                raw = json.loads(self._pipeline_stats_path.read_text())
                if isinstance(raw, dict):
                    payload.update(raw)
        except Exception as exc:
            logger.debug("Unable to read pipeline stats: %s", exc)
        return payload

    def stats(self) -> dict[str, Any]:
        self._sync_nv_memories()
        status = self.status
        cluster_row = self._nv.conn.execute("SELECT COUNT(*) AS cnt FROM clusters").fetchone()
        cluster_count = int(cluster_row["cnt"] if cluster_row else 0)
        return {
            "memory_count": self._nv.get_memory_count(active_only=True),
            "entity_count": status.entity_count,
            "relationship_count": status.relationship_count,
            "concept_count": status.concept_count,
            "cluster_count": cluster_count,
            "insight_count": status.insight_count,
            "tier_distribution": self._nv.get_tier_counts(active_only=True),
            "source_distribution": self._nv.get_source_counts(active_only=True),
            "plugin_count": len(self.list_plugins()),
            "pipeline": self.pipeline_stats(),
            "hnsw": self.hnsw_status(),
            "sync": self.sync_status(),
            "daemon_running": status.daemon_running,
            "daemon_pid": status.daemon_pid,
            "db_size_bytes": status.db_size_bytes,
            "chronos_version": status.chronos_version,
        }

    def hnsw_status(self) -> dict[str, Any]:
        index = self._hnsw_index
        return {
            "enabled": index is not None,
            "size": int(index.size) if index is not None else 0,
            "dimension": int(index.dimension) if index is not None else 0,
            "ef_search": int(self._config.search.get("hnsw", {}).get("ef_search", 50)),
            "M": int(self._config.search.get("hnsw", {}).get("M", 16)),
        }

    def sync_status(self) -> dict[str, Any]:
        identity = self.device_identity()
        return {
            "device": identity,
            "network_enabled": bool(self._config.network.get("enabled", False)),
            "sync_strategy": str(self._config.network.get("sync", {}).get("strategy", "newer-wins")),
            "memory_count": self._nv.count_memories(),
        }

    def device_identity(self) -> dict[str, Any]:
        if self._device_identity is None:
            return {"available": False, "device_id": "", "public_key": ""}
        return {
            "available": True,
            "device_id": self._device_identity.device_id,
            "public_key": self._device_identity.public_key_hex,
        }

    def create_delta_bundle(self, since_checkpoint: str | None = None) -> dict[str, Any]:
        self._sync_nv_memories()
        device_id = self.device_identity().get("device_id", "") or self._bridge.get_active_persona_name()
        bundle = self._sync_engine.create_delta_bundle(since_checkpoint=since_checkpoint, device_id=str(device_id))
        if self._device_identity is not None and self._device_identity.private_key is not None:
            try:
                bundle.sign(self._device_identity.private_key)
            except Exception as exc:
                logger.debug("Bundle signing skipped: %s", exc)
        return bundle.to_dict()

    def apply_delta_bundle(self, payload: dict[str, Any]) -> dict[str, int]:
        bundle = KnowledgeBundle(**payload)
        stats = self._sync_engine.apply_bundle(bundle)
        self._sync_nv_memories()
        if self._hnsw_index is not None:
            self.rebuild_hnsw_index()
        return stats

    def sync_with_peer(self, host: str, port: int = 9473, since_checkpoint: str | None = None) -> dict[str, Any]:
        if self._device_identity is None:
            return {"error": "federation identity unavailable"}
        client = FederationClient(
            device_id=self._device_identity.device_id,
            key_pair=self._device_identity,
            sync_engine=self._sync_engine,
        )

        async def _run() -> dict[str, Any]:
            return await client.sync_with_peer(host=host, port=port, since_checkpoint=since_checkpoint)

        try:
            result = asyncio.run(_run())
        except RuntimeError:
            loop = asyncio.new_event_loop()
            try:
                result = loop.run_until_complete(_run())
            finally:
                loop.close()

        if not result.get("error"):
            self._sync_nv_memories()
            if self._hnsw_index is not None:
                self.rebuild_hnsw_index()
        return result

    def rebuild_hnsw_index(self, limit: int = 5000) -> dict[str, Any]:
        if self._hnsw_index is None:
            return {"enabled": False, "indexed": 0}

        hcfg = self._config.search.get("hnsw", {})
        dim = int(self._hnsw_index.dimension)
        self._hnsw_index = HNSWIndex(
            dim=dim,
            M=int(hcfg.get("M", 16)),
            ef_construction=int(hcfg.get("ef_construction", 200)),
            ef_search=int(hcfg.get("ef_search", 50)),
            max_elements=int(hcfg.get("max_elements", 1_000_000)),
        )
        self._search.attach_hnsw(self._hnsw_index)

        indexed = 0
        rows = self._nv.list_memory_records(active_only=True, limit=max(1, limit))
        for row in rows:
            try:
                mid = row["id"]
                text = str(row["content"])
                self._index_memory_embedding(mid, text)
                indexed += 1
            except Exception as exc:
                logger.debug("HNSW rebuild skipped memory %s: %s", row.get("id"), exc)
                continue
        return {"enabled": True, "indexed": indexed, "size": self._hnsw_index.size}

    def create_user(self, username: str, role: str = "reader", created_by: str = "system") -> dict[str, str]:
        payload = self._user_manager.create_user(username=username, role=role, created_by=created_by)
        self.log_audit(
            user=created_by,
            action="user.create",
            resource=username,
            details={"role": role},
            success=True,
        )
        return payload

    def create_device_token(self, device_name: str, permissions: set[str] | None = None) -> str:
        token = self._user_manager.create_device_token(device_name=device_name, permissions=permissions)
        self.log_audit(
            user="system",
            action="device_token.create",
            resource=device_name,
            details={"permissions": sorted(list(permissions)) if permissions else [Permission.READ]},
            success=True,
        )
        return token

    def list_users(self) -> list[dict[str, Any]]:
        return self._user_manager.list_users()

    def revoke_user(self, username: str) -> bool:
        ok = self._user_manager.revoke_user(username)
        self.log_audit(
            user="system",
            action="user.revoke",
            resource=username,
            details={},
            success=ok,
        )
        return ok

    def authenticate_token(self, token: str) -> dict[str, Any] | None:
        return self._user_manager.authenticate(token)

    def check_token_permission(self, token: str, required: str) -> bool:
        user = self.authenticate_token(token)
        if user is None:
            return False
        return self._user_manager.check_permission(user, required)

    def log_audit(
        self,
        user: str,
        action: str,
        resource: str,
        details: dict[str, Any] | None = None,
        success: bool = True,
    ) -> None:
        self._audit.log(user=user, action=action, resource=resource, details=details, success=success)

    def query_audit(
        self,
        user: str | None = None,
        action: str | None = None,
        since: int | None = None,
        limit: int = 100,
    ) -> list[dict[str, Any]]:
        return self._audit.query(user=user, action=action, since=since, limit=limit)

    # ================================================================
    # Temporal / Management
    # ================================================================

    def reinforce(self, memory_id_hex: str) -> None:
        self._reinforce(bytes.fromhex(memory_id_hex))

    def _reinforce(self, memory_id: bytes) -> None:
        try:
            memory = self._bridge.get_memory(memory_id)
        except Exception as exc:
            logger.debug("Reinforce skipped for missing memory %s: %s", memory_id.hex(), exc)
            return

        metadata = dict(getattr(memory, "metadata", {}) or {})
        access_count = int(metadata.get("access_count", 0)) + 1
        metadata["access_count"] = access_count
        metadata["last_accessed"] = int(time.time() * 1000)
        if metadata.get("tier") == MemoryTier.ARCHIVE.value:
            metadata["tier"] = MemoryTier.LONG_TERM.value

        boost = 0.05 * (1 - getattr(memory, "importance", 0.5))
        self._bridge.update_memory_metadata(memory_id, metadata)
        self._bridge.update_importance(memory_id, min(getattr(memory, "importance", 0.5) + boost, 1.0))
        self._sync_memory_snapshot(memory_id)

    def review_due(self, limit: int = 20) -> list:
        due = []
        now = int(time.time() * 1000)
        records = self._bridge.get_active_memories_with_metadata()
        for rec in records:
            metadata = rec.get("metadata", {}) or {}
            if not metadata.get("review", False):
                continue
            access_count = int(metadata.get("access_count", 0))
            last_accessed = int(metadata.get("last_accessed", rec.get("created_at", now)))
            review_at = int(metadata.get("next_review_at", next_review_time_ms(access_count, last_accessed)))
            if review_at <= now:
                try:
                    due.append(self._bridge.get_memory(rec["id"]))
                except Exception as exc:
                    logger.debug("Review queue skipped memory %s: %s", rec.get("id"), exc)
                    continue
                if len(due) >= limit:
                    break
        return due

    def run_decay(self) -> dict[str, int]:
        stats = self._bridge.run_decay()
        self._sync_nv_memories()
        if int(stats.get("updated", 0)) > 0:
            self._events.emit(NeurovaultEvents.MEMORY_DECAYED, dict(stats))
        return stats

    def list_chronos_events(self, limit: int = 100, event_type: str | None = None) -> list[dict[str, Any]]:
        return self._bridge.list_events(limit=limit, event_type=event_type)

    def list_chronos_plugins(self) -> list[dict[str, Any]]:
        return self._bridge.list_plugins()

    def activate_chronos_plugin(self, plugin_id: str, config: dict[str, Any] | None = None) -> bool:
        return self._bridge.activate_plugin(plugin_id, config=config)

    def deactivate_chronos_plugin(self, plugin_id: str) -> bool:
        return self._bridge.deactivate_plugin(plugin_id)

    def get_chronos_plugin_status(self, plugin_id: str) -> dict[str, Any]:
        return self._bridge.plugin_status(plugin_id)

    def rebuild_chronos_graph(self) -> bool:
        return self._bridge.rebuild_graph()

    def get_chronos_graph_neighbors(self, entity_name: str) -> list[dict[str, Any]]:
        rows = []
        for neighbor, relationship in self._bridge.graph_neighbors(entity_name):
            try:
                neighbor_name = str(getattr(neighbor, "name", "unknown"))
                rel_type = str(getattr(relationship, "rel_type", "related_to"))
                rel_weight = float(getattr(relationship, "weight", 1.0))
                rows.append(
                    {
                        "entity": neighbor_name,
                        "relationship": rel_type,
                        "weight": rel_weight,
                    }
                )
            except Exception as exc:
                logger.debug("Graph neighbor row skipped for %s: %s", entity_name, exc)
                continue
        return rows

    def get_chronos_graph_path(self, source: str, target: str) -> list[dict[str, Any]]:
        rows = []
        for left, rel_type, right in self._bridge.graph_path(source, target):
            try:
                if isinstance(rel_type, str):
                    relationship = rel_type
                else:
                    relationship = str(getattr(rel_type, "rel_type", "related_to"))
                rows.append(
                    {
                        "source": str(getattr(left, "name", "unknown")),
                        "relationship": relationship,
                        "target": str(getattr(right, "name", "unknown")),
                    }
                )
            except Exception as exc:
                logger.debug("Graph path row skipped for %s -> %s: %s", source, target, exc)
                continue
        return rows

    def get_active_persona(self) -> str:
        return self._bridge.get_active_persona_name()

    def list_personas(self) -> list[dict[str, Any]]:
        return self._bridge.list_personas()

    def create_persona(
        self,
        name: str,
        description: str = "",
        fork_from: str | None = None,
        make_active: bool = False,
    ) -> dict[str, Any]:
        persona = self._bridge.create_persona(name=name, description=description, fork_from=fork_from)
        if make_active:
            self._bridge.use_persona(name)
            self._sync_nv_memories()
        return persona

    def use_persona(self, name: str) -> bool:
        ok = self._bridge.use_persona(name)
        if ok:
            self._sync_nv_memories()
        return ok

    def delete_persona(self, name: str, force: bool = False) -> bool:
        ok = self._bridge.delete_persona(name, force=force)
        if ok:
            self._sync_nv_memories()
        return ok

    def merge_persona(
        self,
        source_persona: str,
        strategy: str = "semantic",
        threshold: float = 0.95,
        dry_run: bool = False,
    ) -> dict[str, Any]:
        result = self._bridge.merge_persona(
            source_persona=source_persona,
            strategy=strategy,
            threshold=threshold,
            dry_run=dry_run,
        )
        if not dry_run:
            self._sync_nv_memories()
        return result

    def sync_push(self, peer_name: str, output_path: str) -> dict[str, Any]:
        return self._bridge.sync_push(peer_name=peer_name, output_path=output_path)

    def sync_pull(self, bundle_path: str, strategy: str = "newer_wins") -> dict[str, Any]:
        result = self._bridge.sync_pull(bundle_path=bundle_path, strategy=strategy)
        if not result.get("error"):
            self._sync_nv_memories()
        return result

    def generate_key(self, name: str = "default") -> dict[str, Any]:
        return self._bridge.generate_key(name=name)

    def verify_integrity(self) -> dict[str, Any]:
        return self._bridge.verify_deep()

    def forget(self, memory_id_hex: str, hard: bool = False) -> None:
        self._bridge.repo.forget(memory_id_hex, hard=hard)
        self._sync_nv_memories()

    def checkpoint(self, message: str) -> Checkpoint:
        return self._bridge.checkpoint(message)

    def checkpoint_diff(self, ref1: str, ref2: str, limit: int = 50) -> dict[str, Any]:
        return self._bridge.checkpoint_diff(ref1=ref1, ref2=ref2, limit=limit)

    def create_milestone(self, name: str, ref: str = "HEAD", message: str = "") -> dict[str, Any]:
        return self._bridge.create_milestone(name=name, ref=ref, message=message)

    def list_milestones(self, limit: int = 100) -> list[dict[str, Any]]:
        return self._bridge.list_milestones(limit=limit)

    def revert(self, ref: str = "HEAD", hard: bool = False) -> dict[str, Any]:
        result = self._bridge.revert(ref=ref, hard=hard)
        if not result.get("error"):
            self._sync_nv_memories()
        return result

    def audit(self, since: int | None = None, format: str = "object") -> AuditReport | dict | str:
        self._sync_nv_memories()
        report = AuditReport(
            generated_at=int(time.time() * 1000),
            since=since,
            total_memories=self._nv.get_memory_count(since=since, active_only=False),
            active_memories=self._nv.get_memory_count(since=since, active_only=True),
            source_counts=self._nv.get_source_counts(since=since, active_only=True),
            tier_counts=self._nv.get_tier_counts(since=since, active_only=True),
            entity_count=self._nv.get_entity_count(),
            relationship_count=self._nv.get_relationship_count(),
        )

        if format == "object":
            return report
        as_dict = {
            "generated_at": report.generated_at,
            "since": report.since,
            "total_memories": report.total_memories,
            "active_memories": report.active_memories,
            "source_counts": report.source_counts,
            "tier_counts": report.tier_counts,
            "entity_count": report.entity_count,
            "relationship_count": report.relationship_count,
        }
        if format == "dict":
            return as_dict
        if format == "json":
            return json.dumps(as_dict, indent=2)
        raise ValueError(f"Unsupported audit format: {format}")

    def export(self, path: str, format: str = "json", since: int | None = None) -> None:
        self._bridge.export_memories(path=path, fmt=format, since=since)

    def import_memories(self, path: str) -> MergeResult:
        raw = self._bridge.import_memories(path)
        self._sync_nv_memories()
        return MergeResult(
            imported=int(raw.get("imported", 0)),
            skipped=int(raw.get("skipped", 0)),
            errors=list(raw.get("errors", [])),
        )

    @property
    def chronos(self):
        return self._bridge.repo

    @property
    def persona(self):
        return getattr(self._bridge.repo, "persona", None)

    @property
    def events(self) -> EventBus:
        return self._events

    @property
    def status(self) -> VaultStatus:
        self._sync_nv_memories()
        tier_counts = {tier.value: self._nv.get_memories_by_tier(tier.value) for tier in MemoryTier}
        db_size = self._nv.db_path.stat().st_size if self._nv.db_path.exists() else 0
        pid = self._read_pid()

        sensor_status = {
            name: {
                "enabled": name not in set(self._config.disabled_sensors),
                "poll_interval": cfg.get("poll_interval") if isinstance(cfg, dict) else None,
            }
            for name, cfg in self._config.sensors.items()
        }

        return VaultStatus(
            daemon_running=pid is not None,
            daemon_pid=pid,
            uptime_seconds=(time.time() - self._daemon_start_time) if self._daemon_start_time else 0.0,
            tier_counts=tier_counts,
            entity_count=self._nv.get_entity_count(),
            relationship_count=self._nv.get_relationship_count(),
            concept_count=self._nv.get_concept_count(),
            sensor_status=sensor_status,
            last_consolidation=self._last_consolidation,
            insight_count=self._nv.get_insight_count(),
            db_size_bytes=db_size,
            chronos_version=self._bridge.chronos_version,
        )

    def _read_pid(self) -> int | None:
        pid_file = self._pid_file_path()
        try:
            if not pid_file.exists():
                return None
            text = pid_file.read_text().strip()
            if text.startswith("{"):
                payload = json.loads(text)
                pid = int(payload["pid"])
                file_uid = payload.get("uid")
                if file_uid is not None and hasattr(os, "getuid"):
                    if int(file_uid) != int(os.getuid()):
                        return None
            else:
                pid = int(text)
            os.kill(pid, 0)
            return pid
        except Exception as exc:
            logger.debug("PID file read/check failed: %s", exc)
            return None

    @staticmethod
    def _pid_file_path() -> Path:
        return Path.home() / ".neurovault" / "neurovault.pid"

    @staticmethod
    def _infer_relation(left: dict, right: dict) -> str:
        t1 = left["type"].value if hasattr(left["type"], "value") else left["type"]
        t2 = right["type"].value if hasattr(right["type"], "value") else right["type"]
        pair = frozenset([t1, t2])
        mapping = {
            frozenset(["PERSON", "PROJECT"]): "works-on",
            frozenset(["PROJECT", "TECHNOLOGY"]): "uses",
            frozenset(["PROJECT", "DATE"]): "has-deadline",
            frozenset(["PERSON", "ORGANIZATION"]): "works-at",
            frozenset(["PERSON", "PERSON"]): "collaborates-with",
        }
        return mapping.get(pair, "related-to")

    @staticmethod
    def _resolve_base_dir(bridge: ChronosBridge, override: str | None) -> Path:
        if override:
            return Path(override)

        repo_base = bridge.repo_base_dir()
        if repo_base is not None:
            return repo_base

        default_base = getattr(type(bridge.repo), "_default_base", None)
        if callable(default_base):
            try:
                return Path(default_base())
            except Exception as exc:
                logger.debug("Bridge default base resolution failed: %s", exc)

        status = getattr(bridge.repo, "status", None)
        status_path = getattr(status, "path", None) if status is not None else None
        if status_path:
            return Path(str(status_path)).parent

        return Path.home() / ".neurovault"

    @classmethod
    def _resolve_neurovault_db_path(
        cls,
        bridge: ChronosBridge,
        name: str,
        base_dir: str | None,
    ) -> Path:
        if base_dir:
            return Path(base_dir) / name / "neurovault.db"

        repo_dir = bridge.repo_dir()
        if repo_dir is not None:
            return repo_dir / "neurovault.db"

        base = cls._resolve_base_dir(bridge, None)
        repo_name = bridge.repo_name() or name
        return base / repo_name / "neurovault.db"

    def _sync_memory_snapshot(self, memory_id: bytes) -> None:
        try:
            memory = self._bridge.get_memory(memory_id)
            persona_id = self._bridge.get_active_persona_id()
            self._nv.upsert_memory_snapshot(
                memory_id=memory_id,
                persona_id=persona_id,
                content=str(getattr(memory, "content", "")),
                source=str(getattr(memory, "source", "unknown")),
                importance=float(getattr(memory, "importance", 0.5)),
                metadata=getattr(memory, "metadata", {}) or {},
                created_at=int(getattr(memory, "created_at", int(time.time() * 1000))),
                is_active=True,
            )
            self._nv.conn.commit()
        except Exception as exc:
            logger.debug("Memory snapshot sync skipped for %s: %s", memory_id.hex(), exc)

    def _sync_nv_memories(self) -> None:
        try:
            persona_id = self._bridge.get_active_persona_id()
            records = self._bridge.get_active_memories_with_metadata()
            self._nv.sync_active_memories(persona_id, records)
        except Exception as exc:
            logger.debug("Memory mirror sync skipped: %s", exc)

    def _init_hnsw_index(self) -> None:
        hcfg = self._config.search.get("hnsw", {})
        if not bool(hcfg.get("enabled", False)):
            self._hnsw_index = None
            return

        dim = int(getattr(self._embedding_engine, "dimension", 384) or 384)
        self._hnsw_index = HNSWIndex(
            dim=dim,
            M=int(hcfg.get("M", 16)),
            ef_construction=int(hcfg.get("ef_construction", 200)),
            ef_search=int(hcfg.get("ef_search", 50)),
            max_elements=int(hcfg.get("max_elements", 1_000_000)),
        )
        self._search.attach_hnsw(self._hnsw_index)
        try:
            self.rebuild_hnsw_index(limit=1000)
        except Exception as exc:
            logger.debug("Initial HNSW rebuild skipped: %s", exc)

    def _embed_text(self, text: str) -> list[float] | None:
        model = self._embedding_engine
        if model is None:
            return None
        try:
            if hasattr(model, "embed"):
                vec = model.embed(text)
            elif hasattr(model, "encode"):
                vec = model.encode(text)
            else:
                return None
        except Exception as exc:
            logger.debug("Embedding call failed: %s", exc)
            return None

        if vec is None:
            return None
        if hasattr(vec, "tolist"):
            vec = vec.tolist()
        if not isinstance(vec, list):
            return None
        return [float(v) for v in vec]

    def _index_memory_embedding(self, memory_id: bytes, content: str) -> None:
        vector = self._embed_text(content)
        if vector is None:
            return

        try:
            self._nv.set_memory_embedding(memory_id, vector)
        except Exception as exc:
            logger.debug("Embedding persistence skipped for %s: %s", memory_id.hex(), exc)

        if self._hnsw_index is None:
            return

        try:
            self._hnsw_index.add(vector, memory_id)
        except ValueError:
            if self._hnsw_index.size == 0:
                hcfg = self._config.search.get("hnsw", {})
                self._hnsw_index = HNSWIndex(
                    dim=len(vector),
                    M=int(hcfg.get("M", 16)),
                    ef_construction=int(hcfg.get("ef_construction", 200)),
                    ef_search=int(hcfg.get("ef_search", 50)),
                    max_elements=int(hcfg.get("max_elements", 1_000_000)),
                )
                self._search.attach_hnsw(self._hnsw_index)
                self._hnsw_index.add(vector, memory_id)
