"""Poppy memory plugin for Hermes Agent.

Shells out to the ``poppy`` CLI for recall/remember/forget so a hermes session
shares ``~/.poppy/memories.db`` with Claude Code and any other client wired to
the same Poppy install.

Config via environment variables (profile-scoped via each profile's .env):
  POPPY_DIR   : override the Poppy data directory (default ~/.poppy)

Working directory: $HERMES_HOME/poppy/ (profile-scoped sentinel only; the
actual data lives at $POPPY_DIR or ~/.poppy/).
"""

from __future__ import annotations

import json
import logging
import os
import shutil
import subprocess
import threading
from pathlib import Path
from typing import Any, Dict, List, Optional

from agent.memory_provider import MemoryProvider
from tools.registry import tool_error

logger = logging.getLogger(__name__)

_RECALL_TIMEOUT = 10
_REMEMBER_TIMEOUT = 30

_MIN_QUERY_LEN = 8
_MIN_OUTPUT_LEN = 20

_poppy_path_lock = threading.Lock()
_cached_poppy_path: Optional[str] = None


def _resolve_poppy_path() -> Optional[str]:
    """Find the poppy binary on PATH or well-known install locations."""
    global _cached_poppy_path
    with _poppy_path_lock:
        if _cached_poppy_path is not None:
            return _cached_poppy_path if _cached_poppy_path != "" else None

    found = shutil.which("poppy")
    if not found:
        home = Path.home()
        candidates = [
            home / ".local" / "bin" / "poppy",
            Path("/usr/local/bin/poppy"),
            Path("/opt/homebrew/bin/poppy"),
        ]
        for c in candidates:
            if c.exists():
                found = str(c)
                break

    with _poppy_path_lock:
        _cached_poppy_path = found or ""
    return found


def _run_poppy(args: List[str], timeout: int = _RECALL_TIMEOUT) -> dict:
    """Run a poppy CLI command. Returns {success, output, error}."""
    poppy_path = _resolve_poppy_path()
    if not poppy_path:
        return {"success": False, "error": "poppy not found. Install: pipx install poppy-memory"}

    cmd = [poppy_path] + args
    env = os.environ.copy()
    try:
        result = subprocess.run(
            cmd,
            capture_output=True,
            text=True,
            timeout=timeout,
            env=env,
        )
        stdout = result.stdout.strip()
        stderr = result.stderr.strip()
        if result.returncode == 0:
            return {"success": True, "output": stdout}
        detail = stderr[-1000:] or stdout[-1000:] or "no output"
        return {"success": False, "error": f"poppy exited {result.returncode}: {detail}"}
    except subprocess.TimeoutExpired:
        return {"success": False, "error": f"poppy timed out after {timeout}s"}
    except FileNotFoundError:
        global _cached_poppy_path
        with _poppy_path_lock:
            _cached_poppy_path = None
        return {"success": False, "error": "poppy not found"}
    except Exception as e:
        return {"success": False, "error": str(e)}


# ---------------------------------------------------------------------------
# Tool schemas exposed to the agent
# ---------------------------------------------------------------------------

RECALL_SCHEMA = {
    "name": "poppy_recall",
    "description": (
        "Search Poppy's developer memory for relevant context: past decisions, "
        "preferences, lessons learned, project facts. Use BEFORE suggesting an "
        "approach in an unfamiliar area, or when the user references something "
        "from a past session."
    ),
    "parameters": {
        "type": "object",
        "properties": {
            "query": {"type": "string", "description": "What to search for."},
            "project": {"type": "string", "description": "Optional project scope."},
            "limit": {"type": "integer", "description": "Max results (default 10).", "default": 10},
        },
        "required": ["query"],
    },
}

REMEMBER_SCHEMA = {
    "name": "poppy_remember",
    "description": (
        "Store a single memory in Poppy. Use for: user preferences ('we always "
        "use X'), non-obvious decisions (architecture choices, library picks), "
        "or lessons learned (something failed and we now know why). One atomic "
        "fact per call."
    ),
    "parameters": {
        "type": "object",
        "properties": {
            "content": {"type": "string", "description": "The information to remember."},
            "memory_type": {
                "type": "string",
                "enum": ["fact", "decision", "preference", "lesson"],
                "default": "fact",
            },
            "project": {"type": "string", "description": "Optional project scope."},
        },
        "required": ["content"],
    },
}

FORGET_SCHEMA = {
    "name": "poppy_forget",
    "description": "Delete a Poppy memory by id (from a prior poppy_recall result).",
    "parameters": {
        "type": "object",
        "properties": {
            "memory_id": {"type": "string", "description": "Memory id to delete."},
        },
        "required": ["memory_id"],
    },
}

STATUS_SCHEMA = {
    "name": "poppy_status",
    "description": "Check Poppy status: memory count, active engine, install location.",
    "parameters": {"type": "object", "properties": {}, "required": []},
}


# ---------------------------------------------------------------------------
# MemoryProvider implementation
# ---------------------------------------------------------------------------


class PoppyMemoryProvider(MemoryProvider):
    """Poppy local-first developer memory via the poppy CLI."""

    def __init__(self):
        self._session_id = ""

    @property
    def name(self) -> str:
        return "poppy"

    def is_available(self) -> bool:
        """Check if poppy CLI is installed. No network calls."""
        return _resolve_poppy_path() is not None

    def get_config_schema(self):
        return [
            {
                "key": "poppy_dir",
                "description": "Poppy data directory (default ~/.poppy)",
                "secret": False,
                "env_var": "POPPY_DIR",
            },
        ]

    def initialize(self, session_id: str, **kwargs) -> None:
        self._session_id = session_id

    def system_prompt_block(self) -> str:
        if not _resolve_poppy_path():
            return ""
        return (
            "# Poppy Memory\n"
            "Active. Local-first developer memory shared with Claude Code.\n"
            "Use poppy_recall BEFORE acting in unfamiliar areas, poppy_remember "
            "for non-obvious decisions/preferences/lessons, poppy_forget to "
            "delete stale entries, poppy_status to check state."
        )

    def prefetch(self, query: str, *, session_id: str = "") -> str:
        """Surface relevant memories before the model is called."""
        if not query or len(query.strip()) < _MIN_QUERY_LEN:
            return ""
        result = _run_poppy(
            ["recall", "--limit", "5", "--", query.strip()[:1000]],
            timeout=_RECALL_TIMEOUT,
        )
        if result["success"] and result.get("output"):
            output = result["output"].strip()
            if len(output) > _MIN_OUTPUT_LEN and "No memories found" not in output:
                return f"## Poppy Memory\n{output}"
        return ""

    def queue_prefetch(self, query: str, *, session_id: str = "") -> None:
        """No-op: prefetch runs synchronously at turn start."""

    def sync_turn(self, user_content: str, assistant_content: str, *, session_id: str = "") -> None:
        """No-op: Poppy stores memories through its tools and mirrored memory writes."""

    def on_memory_write(self, action: str, target: str, content: str, metadata=None) -> None:
        """Mirror hermes' built-in MEMORY.md / USER.md writes into Poppy."""
        if action not in ("add", "replace") or not content:
            return

        def _write():
            try:
                memory_type = "preference" if target == "user" else "fact"
                result = _run_poppy(
                    ["remember", "--type", memory_type, "--", content[:5000]],
                    timeout=_REMEMBER_TIMEOUT,
                )
                if not result["success"]:
                    logger.warning("Poppy memory mirror remember failed: %s", result.get("error", "unknown error"))
            except Exception as e:
                logger.warning("Poppy memory mirror remember failed: %s", e)

        t = threading.Thread(target=_write, daemon=True, name="poppy-memwrite")
        t.start()

    def get_tool_schemas(self) -> List[Dict[str, Any]]:
        return [RECALL_SCHEMA, REMEMBER_SCHEMA, FORGET_SCHEMA, STATUS_SCHEMA]

    def handle_tool_call(self, tool_name: str, args: dict, **kwargs) -> str:
        if tool_name == "poppy_recall":
            return self._tool_recall(args)
        if tool_name == "poppy_remember":
            return self._tool_remember(args)
        if tool_name == "poppy_forget":
            return self._tool_forget(args)
        if tool_name == "poppy_status":
            return self._tool_status()
        return tool_error(f"Unknown tool: {tool_name}")

    def shutdown(self) -> None:
        """No-op: the provider has no background sync worker."""

    # -- Tool implementations -----------------------------------------------

    def _tool_recall(self, args: dict) -> str:
        query = args.get("query", "")
        if not query:
            return tool_error("query is required")
        cmd = ["recall", "--json"]
        if args.get("project"):
            cmd.extend(["--project", str(args["project"])])
        limit = args.get("limit") or 10
        cmd.extend(["--limit", str(int(limit))])
        cmd.extend(["--", query.strip()[:2000]])

        result = _run_poppy(cmd, timeout=_RECALL_TIMEOUT)
        if not result["success"]:
            return tool_error(result.get("error", "Recall failed"))

        output = result.get("output", "").strip()
        if not output or "No memories found" in output:
            return json.dumps({"result": "No relevant memories found."})

        try:
            parsed = json.loads(output)
            return json.dumps({"result": parsed})
        except json.JSONDecodeError:
            return json.dumps({"result": output[:8000]})

    def _tool_remember(self, args: dict) -> str:
        content = args.get("content", "")
        if not content:
            return tool_error("content is required")
        memory_type = args.get("memory_type", "fact")
        if memory_type not in ("fact", "decision", "preference", "lesson"):
            memory_type = "fact"
        cmd = ["remember", "--type", memory_type]
        if args.get("project"):
            cmd.extend(["--project", str(args["project"])])
        cmd.extend(["--", content[:5000]])

        result = _run_poppy(cmd, timeout=_REMEMBER_TIMEOUT)
        if not result["success"]:
            return tool_error(result.get("error", "Remember failed"))
        return json.dumps({"result": result.get("output", "Memory stored.").strip()})

    def _tool_forget(self, args: dict) -> str:
        memory_id = args.get("memory_id", "")
        if not memory_id:
            return tool_error("memory_id is required")
        result = _run_poppy(["forget", "--yes", "--", memory_id], timeout=_REMEMBER_TIMEOUT)
        if not result["success"]:
            return tool_error(result.get("error", "Forget failed"))
        return json.dumps({"result": result.get("output", "Forgotten.").strip()})

    def _tool_status(self) -> str:
        result = _run_poppy(["stats"], timeout=15)
        if not result["success"]:
            return tool_error(result.get("error", "Status check failed"))
        return json.dumps({"status": result.get("output", "").strip()})


# ---------------------------------------------------------------------------
# Plugin entry point
# ---------------------------------------------------------------------------


def register(ctx) -> None:
    """Register Poppy as a memory provider plugin."""
    ctx.register_memory_provider(PoppyMemoryProvider())
