"""
Experience Extractor — Phase 23.6

Reads DAG memory nodes (Phase 23.5) and extracts structured Experience
records that capture reusable patterns and lessons.

Extraction pipeline:
  1. Load summary + raw nodes from MemoryGraph
  2. Group by scene / task_id / agent_id
  3. For each group: analyze sequence → identify steps, outcome, key facts
  4. Classify experience type (workflow/lesson/pattern/decision/insight)
  5. Write Experience to ExperienceRegistry

Why different from Phase 21 Skills:
  - Skills: single-task executable (extracted from MemCells/patterns)
  - Experience: multi-step narrative from DAG sessions, includes context
                and conditions, captures the WHY not just the WHAT
"""

import logging
import time
from collections import defaultdict
from typing import Dict, List, Optional

from openmemo.experience.models import Experience, EXPERIENCE_TYPES

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


class ExperienceExtractor:
    """
    Extracts structured Experiences from DAG memory nodes.

    Args:
        memory_graph    — MemoryGraph instance (Phase 23.5)
        store           — SQLiteStore (for reading observations)
        llm_fn          — Optional LLM callable for smarter extraction
                          signature: llm_fn(prompt: str) -> str
        min_nodes       — minimum nodes in a group to extract experience
    """

    def __init__(self, memory_graph, store=None,
                 llm_fn=None, min_nodes: int = 2):
        self._graph = memory_graph
        self._store = store
        self._llm_fn = llm_fn
        self._min_nodes = min_nodes

    def extract(self, agent_id: str = "", scene: str = "",
                task_id: str = "") -> List[Experience]:
        """
        Main extraction method.

        Reads summary + raw nodes, groups them into sessions,
        and extracts one Experience per meaningful group.

        Returns:
            List of extracted Experience objects (not yet saved — caller
            must persist via ExperienceRegistry.save())
        """
        summaries = self._graph.list_summaries(
            agent_id=agent_id, scene=scene, limit=200
        )
        raw_nodes = self._graph.list_raw(
            agent_id=agent_id, scene=scene, task_id=task_id, limit=500
        )

        groups = self._group_nodes(summaries, raw_nodes, task_id=task_id)

        experiences = []
        for group_key, nodes in groups.items():
            if len(nodes) < self._min_nodes:
                continue
            exp = self._extract_from_group(group_key, nodes,
                                           agent_id=agent_id, scene=scene)
            if exp:
                experiences.append(exp)

        logger.info(
            "[experience] extracted %d experiences from %d summaries + %d raw nodes",
            len(experiences), len(summaries), len(raw_nodes)
        )
        return experiences

    def extract_from_nodes(self, nodes: list,
                           agent_id: str = "",
                           scene: str = "") -> Optional[Experience]:
        """
        Extract a single experience from an explicit list of nodes.
        Useful for on-demand extraction after compression.
        """
        if not nodes or len(nodes) < self._min_nodes:
            return None
        group_key = (scene or "general", agent_id, "")
        return self._extract_from_group(group_key, nodes,
                                        agent_id=agent_id, scene=scene)

    # ─── Grouping ──────────────────────────────────────────────────

    def _group_nodes(self, summaries: list, raw_nodes: list,
                     task_id: str = "") -> Dict[tuple, list]:
        """
        Group nodes by (scene, agent_id, task_id) for experience extraction.
        Summaries are preferred as group anchors; raw nodes fill the gaps.
        """
        groups = defaultdict(list)

        for node in summaries:
            key = (node.scene or "general", node.agent_id, node.task_id or "")
            groups[key].append(node)

        summary_node_ids = {n.id for nodes in groups.values() for n in nodes}

        for node in raw_nodes:
            if node.id in summary_node_ids:
                continue
            if task_id and node.task_id and node.task_id != task_id:
                continue
            key = (node.scene or "general", node.agent_id, node.task_id or "")
            if key in groups:
                groups[key].append(node)
            elif not groups:
                groups[key].append(node)

        return dict(groups)

    # ─── Extraction ────────────────────────────────────────────────

    def _extract_from_group(self, group_key: tuple, nodes: list,
                             agent_id: str = "",
                             scene: str = "") -> Optional[Experience]:
        """Extract a single Experience from a group of memory nodes."""
        g_scene, g_agent, g_task = group_key
        eff_scene = scene or g_scene
        eff_agent = agent_id or g_agent

        nodes_sorted = sorted(nodes, key=lambda n: getattr(n, "timestamp", 0))
        source_ids = [n.id for n in nodes_sorted]

        if self._llm_fn:
            return self._extract_with_llm(
                nodes_sorted, source_ids, eff_scene, eff_agent, g_task
            )
        return self._extract_rule_based(
            nodes_sorted, source_ids, eff_scene, eff_agent, g_task
        )

    def _extract_rule_based(self, nodes: list, source_ids: list,
                             scene: str, agent_id: str,
                             task_id: str) -> Experience:
        """Rule-based experience extraction without LLM."""
        texts = [n.content for n in nodes]

        steps = self._extract_steps(texts)
        outcome = self._detect_outcome(texts)
        key_facts = self._extract_key_facts(texts)
        exp_type = self._classify_type(texts, outcome)
        title = self._generate_title(steps, outcome, scene)
        conditions = self._extract_conditions(texts, scene)

        avg_importance = sum(
            getattr(n, "importance", 0.5) for n in nodes
        ) / len(nodes)
        confidence = min(0.9, avg_importance + 0.1 * min(len(nodes), 5) / 5)

        return Experience(
            title=title,
            type=exp_type,
            source_node_ids=source_ids,
            steps=steps,
            outcome=outcome,
            key_facts=key_facts,
            conditions=conditions,
            confidence=confidence,
            scene=scene,
            agent_id=agent_id,
            metadata={"task_id": task_id, "node_count": len(nodes)},
        )

    def _extract_with_llm(self, nodes: list, source_ids: list,
                           scene: str, agent_id: str,
                           task_id: str) -> Optional[Experience]:
        """LLM-assisted experience extraction."""
        texts = [n.content for n in nodes[:10]]
        prompt = (
            "Analyze the following memory observations and extract a structured experience.\n"
            "Return a JSON object with these fields:\n"
            "  title (str): short title of the experience\n"
            "  type (str): one of workflow/lesson/pattern/decision/insight\n"
            "  steps (list[str]): ordered steps observed\n"
            "  outcome (str): final result or outcome\n"
            "  key_facts (list[str]): key learnings (max 5)\n"
            "  conditions (dict): when this experience applies (trigger, scene)\n\n"
            "Observations:\n" + "\n".join(f"- {t}" for t in texts)
            + "\n\nReturn ONLY valid JSON."
        )
        try:
            import json
            response = self._llm_fn(prompt).strip()
            if response.startswith("```"):
                response = response.split("```")[1]
                if response.startswith("json"):
                    response = response[4:]
            data = json.loads(response)
            avg_importance = sum(
                getattr(n, "importance", 0.5) for n in nodes
            ) / len(nodes)
            confidence = min(0.9, float(avg_importance) + 0.15)
            return Experience(
                title=data.get("title", "Extracted Experience"),
                type=data.get("type", "workflow"),
                source_node_ids=source_ids,
                steps=data.get("steps", []),
                outcome=data.get("outcome", ""),
                key_facts=data.get("key_facts", []),
                conditions=data.get("conditions", {"scene": scene}),
                confidence=confidence,
                scene=scene,
                agent_id=agent_id,
                metadata={"task_id": task_id, "llm_extracted": True,
                          "node_count": len(nodes)},
            )
        except Exception as e:
            logger.warning("[experience] LLM extraction failed: %s — falling back", e)
            return self._extract_rule_based(nodes, source_ids, scene, agent_id, task_id)

    # ─── Helpers ───────────────────────────────────────────────────

    def _extract_steps(self, texts: List[str]) -> List[str]:
        """Extract action steps from node text list."""
        steps = []
        step_markers = ["tool_execution", "agent_step", "task_event",
                        "tool=", "step=", "action="]
        for text in texts:
            lower = text.lower()
            if any(m in lower for m in step_markers):
                clean = text.replace("[Tool Execution]", "").replace(
                    "[Agent Step]", "").replace("[Task Event]", "").strip()
                clean = " | ".join(
                    part.strip() for part in clean.split("|")
                    if part.strip() and not part.strip().startswith("agent=")
                )
                if clean and len(clean) > 5:
                    steps.append(clean[:200])
        return steps[:8]

    def _detect_outcome(self, texts: List[str]) -> str:
        """Detect the final outcome from memory texts."""
        success_signals = {"success", "done", "complete", "ok", "passed",
                           "deployed", "finished", "approved"}
        failure_signals = {"fail", "error", "exception", "crash", "rejected",
                           "timeout", "refused"}
        for text in reversed(texts):
            lower = text.lower()
            if any(s in lower for s in success_signals):
                return "success"
            if any(s in lower for s in failure_signals):
                return "failure"
        return "completed"

    def _extract_key_facts(self, texts: List[str]) -> List[str]:
        """Extract key facts — unique important terms from texts."""
        from collections import Counter
        stopwords = {"the", "a", "an", "is", "was", "to", "of", "and",
                     "in", "for", "with", "at", "by", "from", "tool",
                     "agent", "step", "task", "scene", "action"}
        all_words = []
        for text in texts:
            words = [w.strip("[]|=") for w in text.lower().split()
                     if len(w) > 4 and w not in stopwords]
            all_words.extend(words)
        top = [w for w, _ in Counter(all_words).most_common(10)
               if len(w) > 4][:5]
        facts = []
        if top:
            facts.append(f"Key entities involved: {', '.join(top)}")
        outcome = self._detect_outcome(texts)
        facts.append(f"Outcome: {outcome}")
        if len(texts) > 2:
            facts.append(f"Sequence of {len(texts)} memory events observed")
        return facts

    def _classify_type(self, texts: List[str], outcome: str) -> str:
        """Classify experience type from content signals."""
        all_text = " ".join(texts).lower()
        if "deploy" in all_text or "build" in all_text or "install" in all_text:
            return "workflow"
        if outcome == "failure" or "error" in all_text or "fail" in all_text:
            return "lesson"
        if len(texts) >= 4:
            return "pattern"
        if "decision" in all_text or "choose" in all_text or "select" in all_text:
            return "decision"
        return "workflow"

    def _generate_title(self, steps: List[str], outcome: str,
                        scene: str) -> str:
        """Generate a short title for the experience."""
        if steps:
            first = steps[0][:50].rstrip("|").strip()
            return f"{scene.title() or 'Agent'}: {first} → {outcome}"
        return f"{scene.title() or 'Agent'} experience ({outcome})"

    def _extract_conditions(self, texts: List[str], scene: str) -> dict:
        """Extract conditions/trigger for when to apply this experience."""
        conditions: dict = {}
        if scene:
            conditions["scene"] = scene
        all_text = " ".join(texts).lower()
        triggers = []
        for kw in ["deploy", "debug", "test", "build", "review", "setup",
                   "install", "migrate", "rollback", "monitor"]:
            if kw in all_text:
                triggers.append(kw)
        if triggers:
            conditions["trigger"] = ", ".join(triggers[:3])
        return conditions
