"""
Memory Engine Interface — Phase 23.5

MemoryEngine is the central integration point that ties together:
  - Observation Layer (ingest raw events)
  - DAG Memory Graph (structured storage)
  - Expandable Recall (summary-first retrieval)
  - Compression Worker (background summarization)

This replaces the need to call each layer separately — agents
just call engine.ingest() and engine.recall().

Usage:
    from openmemo import OpenMemo
    from openmemo.core.engine import MemoryEngine

    memo = OpenMemo()
    engine = MemoryEngine(store=memo.store)

    # ingest an observation
    node = engine.ingest(obs)

    # recall with optional expand
    result = engine.recall("deploy backend", expand_level=2)

    # expand a specific node
    result = engine.expand("node_abc123")

    # trigger compression manually
    engine.compress()
"""

import logging
from typing import Dict, List, Optional

from openmemo.core.observation import Observation
from openmemo.core.memory_graph import MemoryGraph, MemoryNode
from openmemo.core.expandable_recall import ExpandableRecallEngine

logger = logging.getLogger("openmemo.engine")


class MemoryEngine:
    """
    Unified Memory Engine — single integration point for Phase 23.5.

    Args:
        store           — SQLiteStore (or compatible store)
        agent_id        — default agent_id for this engine instance
        compress_every  — auto-compress after N ingests (0 = disabled)
        llm_fn          — optional LLM callable for smarter summarization
                          signature: llm_fn(prompt: str) -> str
    """

    def __init__(self, store, agent_id: str = "",
                 compress_every: int = 20,
                 llm_fn=None):
        self._store = store
        self._agent_id = agent_id
        self._compress_every = compress_every
        self._llm_fn = llm_fn
        self._ingest_count = 0

        self.graph = MemoryGraph(store)
        self.recall_engine = ExpandableRecallEngine(self.graph)

    # ─── Core API ──────────────────────────────────────────────────

    def ingest(self, obs: Observation) -> MemoryNode:
        """
        Ingest an Observation into the DAG Memory Graph.

        Also persists the raw Observation to the observations table
        for audit trail. Auto-triggers compression if threshold is met.
        """
        self._store.put_observation(obs.to_dict())

        node = self.graph.ingest(obs)

        self._ingest_count += 1
        if (self._compress_every > 0 and
                self._ingest_count % self._compress_every == 0):
            self._auto_compress(agent_id=obs.agent_id,
                                scene=obs.context.get("scene", ""))

        return node

    def recall(self, query: str, scene: str = "", agent_id: str = "",
               expand_level: int = 1, top_k: int = 5) -> Dict:
        """
        Recall memories matching query.

        Args:
            query        — search string
            scene        — filter by scene (optional)
            agent_id     — filter by agent (optional)
            expand_level — 1=summary only, 2+=include raw details
            top_k        — max results

        Returns dict with keys: summary, nodes, details, expand_level,
                                total_raw, query_time_ms
        """
        agent = agent_id or self._agent_id
        return self.recall_engine.recall(
            query=query, scene=scene,
            agent_id=agent, expand_level=expand_level, top_k=top_k
        )

    def expand(self, node_id: str) -> Dict:
        """Expand a node to show its raw DAG children."""
        return self.recall_engine.expand(node_id)

    def compress(self, agent_id: str = "", scene: str = "") -> Dict:
        """
        Trigger memory compression — clusters raw nodes and creates
        summary nodes in the DAG.

        Returns compression stats.
        """
        from openmemo.workers.compression_worker import CompressionWorker
        worker = CompressionWorker(
            graph=self.graph,
            store=self._store,
            llm_fn=self._llm_fn,
        )
        return worker.run_once(agent_id=agent_id or self._agent_id,
                               scene=scene)

    def ingest_tool_use(self, tool: str, action: str, result: str,
                        task_id: str = "", scene: str = "",
                        agent_id: str = "") -> MemoryNode:
        """Convenience: ingest a tool execution directly."""
        obs = Observation.tool_execution(
            tool=tool, action=action, result=result,
            agent_id=agent_id or self._agent_id,
            task_id=task_id, scene=scene,
        )
        return self.ingest(obs)

    def ingest_step(self, step: str, reasoning: str = "",
                    task_id: str = "", scene: str = "",
                    agent_id: str = "") -> MemoryNode:
        """Convenience: ingest an agent reasoning step."""
        obs = Observation.agent_step(
            step=step, reasoning=reasoning,
            agent_id=agent_id or self._agent_id,
            task_id=task_id, scene=scene,
        )
        return self.ingest(obs)

    # ─── Progressive Recall ────────────────────────────────────────

    def recall_for_prompt(self, query: str, scene: str = "",
                          agent_id: str = "") -> str:
        """
        Returns a compact context string ready for prompt injection.
        Uses progressive recall (summary → expand top result).
        """
        result = self.recall_engine.progressive_recall(
            query=query, scene=scene,
            agent_id=agent_id or self._agent_id,
        )
        return result.get("context", "")

    # ─── Stats ─────────────────────────────────────────────────────

    def stats(self, agent_id: str = "", scene: str = "") -> Dict:
        agent = agent_id or self._agent_id
        raw_nodes = self.graph.list_raw(agent_id=agent, scene=scene, limit=1000)
        summaries = self.graph.list_summaries(agent_id=agent, scene=scene, limit=1000)
        uncompressed = self.graph.count_uncompressed_raw(
            agent_id=agent, scene=scene
        )
        obs_count = len(self._store.list_observations(
            agent_id=agent, limit=10000
        ))
        return {
            "raw_nodes": len(raw_nodes),
            "summary_nodes": len(summaries),
            "uncompressed_raw": uncompressed,
            "observations": obs_count,
            "ingest_count": self._ingest_count,
            "compress_every": self._compress_every,
            "agent_id": agent,
            "scene": scene,
        }

    # ─── Internal ──────────────────────────────────────────────────

    def _auto_compress(self, agent_id: str = "", scene: str = ""):
        """Background auto-compression trigger."""
        try:
            from openmemo.workers.compression_worker import CompressionWorker
            worker = CompressionWorker(
                graph=self.graph,
                store=self._store,
                llm_fn=self._llm_fn,
            )
            result = worker.run_once(agent_id=agent_id, scene=scene,
                                     min_raw=5)
            if result.get("summaries_created", 0) > 0:
                logger.info(
                    "[engine] auto-compress: %d summaries created from %d raws",
                    result["summaries_created"], result["raw_processed"]
                )
        except Exception as e:
            logger.warning("[engine] auto-compress failed: %s", e)
