"""Hierarchical memory discovery and persistence for the LangGraph agent.

Implements Gemini CLI's memory discovery pattern: walk up the directory
tree finding MEMORY.md files at global, project, and directory levels.
Memory is injected into the system prompt at session start.

Also provides a save_memory tool for the agent to explicitly save facts
with user approval, and session-end extraction for implicit learning.

Named hik_memory to avoid collision with autodebug.agent.memory (existing).
"""

from __future__ import annotations

import logging
import re
import time
from dataclasses import dataclass
from pathlib import Path
from typing import Optional

logger = logging.getLogger(__name__)

# Max memory content injected into system prompt (chars)
MAX_MEMORY_PROMPT_CHARS = 4000


@dataclass
class MemoryFile:
    """A discovered memory file."""
    path: Path
    scope: str       # "global" | "project" | "directory"
    content: str
    priority: int    # Lower = higher priority (global=0, project=1, dir=2)


def discover_memory(project_path: str) -> list[MemoryFile]:
    """Find memory files at all levels. Returns in priority order.

    Search order:
    1. Global: ~/.hikaflow/MEMORY.md
    2. Project: {project_root}/.hikaflow/MEMORY.md
    3. Current dir: ./MEMORY.md (if different from project root)
    """
    files: list[MemoryFile] = []
    root = Path(project_path).resolve()

    # 1. Global memory
    global_path = Path.home() / ".hikaflow" / "MEMORY.md"
    if global_path.exists():
        try:
            content = global_path.read_text(encoding="utf-8", errors="replace")
            if content.strip():
                files.append(MemoryFile(
                    path=global_path,
                    scope="global",
                    content=content,
                    priority=0,
                ))
        except Exception as e:
            logger.debug("Failed to read global memory: %s", e)

    # 2. Project memory
    project_memory = root / ".hikaflow" / "MEMORY.md"
    if project_memory.exists() and project_memory != global_path:
        try:
            content = project_memory.read_text(encoding="utf-8", errors="replace")
            if content.strip():
                files.append(MemoryFile(
                    path=project_memory,
                    scope="project",
                    content=content,
                    priority=1,
                ))
        except Exception as e:
            logger.debug("Failed to read project memory: %s", e)

    # 3. Directory-level memory (check for MEMORY.md in project root)
    dir_memory = root / "MEMORY.md"
    if dir_memory.exists() and dir_memory != project_memory:
        try:
            content = dir_memory.read_text(encoding="utf-8", errors="replace")
            if content.strip():
                files.append(MemoryFile(
                    path=dir_memory,
                    scope="directory",
                    content=content,
                    priority=2,
                ))
        except Exception as e:
            logger.debug("Failed to read directory memory: %s", e)

    files.sort(key=lambda f: f.priority)
    return files


def render_memory_for_prompt(memories: list[MemoryFile]) -> str:
    """Render discovered memory files for injection into system prompt.

    Respects MAX_MEMORY_PROMPT_CHARS budget.
    """
    if not memories:
        return ""

    parts = ["[Persistent Memory]"]
    total_chars = 0

    for mem in memories:
        header = f"\n## {mem.scope.title()} Memory ({mem.path.name})"
        content = mem.content.strip()

        # Truncate if needed
        remaining = MAX_MEMORY_PROMPT_CHARS - total_chars - len(header) - 10
        if remaining <= 0:
            break
        if len(content) > remaining:
            content = content[:remaining] + "\n... [truncated]"

        parts.append(header)
        parts.append(content)
        total_chars += len(header) + len(content)

    return "\n".join(parts)


def save_memory_to_file(
    fact: str,
    scope: str = "project",
    project_path: str = ".",
) -> str:
    """Append a fact to the appropriate memory file.

    Args:
        fact: The fact to save.
        scope: "global" or "project".
        project_path: Project root directory.

    Returns:
        Status message.
    """
    if scope == "global":
        mem_dir = Path.home() / ".hikaflow"
        mem_file = mem_dir / "MEMORY.md"
    else:
        mem_dir = Path(project_path) / ".hikaflow"
        mem_file = mem_dir / "MEMORY.md"

    try:
        mem_dir.mkdir(parents=True, exist_ok=True)

        # Append to file
        with open(mem_file, "a", encoding="utf-8") as f:
            f.write(f"\n- {fact}\n")

        return f"Saved to {scope} memory: {mem_file}"
    except Exception as e:
        return f"Failed to save memory: {e}"


# ---------------------------------------------------------------------------
# Session-end extraction (lightweight, no LLM call)
# ---------------------------------------------------------------------------

_LEARNING_PATTERNS = [
    re.compile(r"I (?:learned|discovered|found) that\s+(.+?)(?:\.|$)", re.IGNORECASE),
    re.compile(r"Note for future:\s+(.+?)(?:\.|$)", re.IGNORECASE),
    re.compile(r"Important:\s+(.+?)(?:\.|$)", re.IGNORECASE),
    re.compile(r"Remember:\s+(.+?)(?:\.|$)", re.IGNORECASE),
]


def extract_session_learnings(messages: list) -> list[str]:
    """Extract learnings from AI messages in a session.

    Scans for patterns like "I learned that...", "Note for future:", etc.
    Returns list of extracted facts.
    """
    learnings = []
    seen = set()

    for msg in messages:
        content = getattr(msg, "content", "")
        if not isinstance(content, str):
            continue

        # Only look at AI messages (they have the learnings)
        msg_type = type(msg).__name__
        if "AI" not in msg_type and "assistant" not in msg_type.lower():
            continue

        for pattern in _LEARNING_PATTERNS:
            for match in pattern.finditer(content):
                fact = match.group(1).strip()
                if fact and fact not in seen and len(fact) > 10:
                    seen.add(fact)
                    learnings.append(fact)

    return learnings[:10]  # Cap at 10 learnings per session
