"""Open Agent - Self-evolving personal assistant with hierarchical memory.

This module provides the OpenAgent, a minimal, self-evolving, user-driven
personal assistant that writes itself based on user needs rather than
having heavy predefined opinions.

Features hierarchical memory:
- Core Identity: Fundamental user traits (role, values, goals)
- Long-term Memory: Learned preferences and patterns
- Short-term Memory: Recent context and session state
- Skill Memory: Dynamic skills and rules learned from interactions
"""

import logging
from typing import Optional, TYPE_CHECKING
from pathlib import Path

from ..base import BaseAgent
from ..compaction_strategy import LLMSummaryStrategy
from ..events import AgentEventEmitter
from ..providers.factory import DEFAULT_MODEL
from .toolkit import OpenToolkit
from .persona import PersonaManager
from .capabilities import CapabilityRegistry
from .prompts import build_open_system_prompt
from .memory import HierarchicalMemory
from .memory_tools import MemoryToolSet
from .skill_memory import SkillMemoryManager

if TYPE_CHECKING:
    from ..toolkits.base import BaseToolkit

logger = logging.getLogger(__name__)

# ---------------------------------------------------------------------------
# Default scaffolding templates — created on first run if missing
# ---------------------------------------------------------------------------

_DEFAULT_SOUL_MD = """\
# Soul

You are a personal assistant that lives inside the user's machine.
You have full access to the terminal, filesystem, and network.
You are direct, resourceful, and bias toward action.

- Never say "I can't" — check your tools first, try code, or find a way.
- Learn from every interaction and remember what matters.
- When the user teaches you something, store it so you never have to ask again.
- Respect the user's time: be concise, avoid filler, get things done.
"""

_DEFAULT_IDENTITY_MD = """\
# Identity

<!-- Fill in your details so the assistant knows who you are. -->

- **Name:**
- **Role:**
- **Focus areas:**
- **Preferred language / stack:**
"""

_DEFAULT_USER_MD = """\
# User Preferences

<!-- Add anything the assistant should always keep in mind. -->
<!-- Examples: "I prefer concise answers", "Always use TypeScript", etc. -->
"""

_DEFAULT_BOOTSTRAP_MD = """\
# Welcome

This is your first time running the open assistant.

## Getting started

The files in `.emdash/rules/` shape how your assistant behaves:

| File | Purpose |
|------|---------|
| `SOUL.md` | Core personality and philosophy |
| `IDENTITY.md` | Who you are (name, role, stack) |
| `USER.md` | Your preferences and context |
| `BOOTSTRAP.md` | This welcome guide (safe to delete after onboarding) |

Edit these files at any time — changes take effect on the next conversation turn.

## Quick tips

1. **Tell the assistant your name** — it will remember via its memory system.
2. **Teach preferences naturally** — e.g. "I always want tests written in pytest".
3. **Check what it knows** — ask "what do you know about me?"

Happy hacking!
"""

_SCAFFOLDING_FILES: dict[str, str] = {
    "SOUL.md": _DEFAULT_SOUL_MD,
    "IDENTITY.md": _DEFAULT_IDENTITY_MD,
    "USER.md": _DEFAULT_USER_MD,
    "BOOTSTRAP.md": _DEFAULT_BOOTSTRAP_MD,
}


class OpenAgent(BaseAgent):
    """Self-evolving personal assistant with hierarchical memory.

    Key characteristics:
    - Minimal bootstrap prompt (<100 tokens base)
    - Hierarchical memory (core → long-term → short-term)
    - Learns user preferences over time
    - Discovers and adds capabilities dynamically
    - User-driven evolution (not opinionated)
    - Self-modifying through learning tools
    """

    # Agent identity
    name = "Open Assistant"
    agent_type = "open"
    description = "A minimal, self-evolving personal assistant that learns from you"

    def __init__(
        self,
        model: str = DEFAULT_MODEL,
        emitter: Optional[AgentEventEmitter] = None,
        max_iterations: int = 100,
        verbose: bool = False,
        enable_thinking: Optional[bool] = None,
        session_id: Optional[str] = None,
        repo_root: Optional[Path] = None,
        persona_manager: Optional[PersonaManager] = None,
        capability_registry: Optional[CapabilityRegistry] = None,
        hierarchical_memory: Optional[HierarchicalMemory] = None,
        skill_memory_manager: Optional[SkillMemoryManager] = None,
    ):
        """Initialize the Open Agent.

        Args:
            model: LLM model to use
            emitter: Event emitter for streaming output
            max_iterations: Maximum tool iterations per run
            verbose: Enable verbose logging
            enable_thinking: Enable extended thinking
            session_id: Session ID for isolation
            repo_root: Repository root path
            persona_manager: Optional pre-configured PersonaManager
            capability_registry: Optional pre-configured CapabilityRegistry
            hierarchical_memory: Optional pre-configured HierarchicalMemory
            skill_memory_manager: Optional pre-configured SkillMemoryManager
        """
        # Store fields before calling super() since _get_toolkit() needs them
        self._session_id = session_id
        self._persona_manager = persona_manager
        self._capability_registry = capability_registry
        self._hierarchical_memory = hierarchical_memory
        self._skill_memory_manager = skill_memory_manager
        self._repo_root = repo_root or Path.cwd()
        self._memory_tools: Optional[MemoryToolSet] = None

        # Call parent init (which calls _get_toolkit())
        super().__init__(
            model=model,
            emitter=emitter,
            max_iterations=max_iterations,
            verbose=verbose,
            enable_thinking=enable_thinking,
            session_id=session_id,
        )

        # Wire pre-compaction memory flush into the compaction strategy.
        # When context approaches the compaction threshold, the strategy
        # calls our hook to persist important facts before summarisation
        # destroys message detail (OpenClaw-style memory flush).
        if isinstance(self._compaction_strategy, LLMSummaryStrategy):
            self._compaction_strategy.set_pre_compact_hook(
                self._flush_memory_before_compaction
            )

    def _get_toolkit(self) -> "BaseToolkit":
        """Return the Open Toolkit with learning and memory capabilities."""
        # Initialize managers if not provided
        if self._persona_manager is None:
            self._persona_manager = PersonaManager(
                user_id=self._session_id or "default",
                emdash_dir=self._repo_root / ".emdash",
            )

        if self._capability_registry is None:
            self._capability_registry = CapabilityRegistry(
                emdash_dir=self._repo_root / ".emdash",
            )

        if self._hierarchical_memory is None:
            self._hierarchical_memory = HierarchicalMemory(
                session_id=self._session_id or "default",
                emdash_dir=self._repo_root / ".emdash",
            )

        if self._skill_memory_manager is None:
            self._skill_memory_manager = SkillMemoryManager(
                storage_dir=self._repo_root / ".emdash" / "memory" / "skills",
            )

        # Create memory tools wrapper
        self._memory_tools = MemoryToolSet(self._hierarchical_memory)

        # Ensure scaffolding files exist for first-run users
        self._ensure_default_scaffolding()

        # Create toolkit with all capabilities
        return OpenToolkit(
            repo_root=self._repo_root,
            persona_manager=self._persona_manager,
            capability_registry=self._capability_registry,
            hierarchical_memory=self._hierarchical_memory,
            skill_memory_manager=self._skill_memory_manager,
            memory_tools=self._memory_tools,
        )

    def _ensure_default_scaffolding(self) -> None:
        """Create default SOUL/IDENTITY/USER/BOOTSTRAP files if they don't exist.

        Non-destructive: existing files are never overwritten.
        Also migrates files found at the repo root into .emdash/rules/.
        """
        rules_dir = self._repo_root / ".emdash" / "rules"
        try:
            rules_dir.mkdir(parents=True, exist_ok=True)
        except OSError:
            logger.debug("Could not create rules directory: %s", rules_dir)
            return

        for filename, content in _SCAFFOLDING_FILES.items():
            dest = rules_dir / filename
            if dest.exists():
                continue

            # Migrate from repo root if the file exists there
            root_file = self._repo_root / filename
            if root_file.exists():
                try:
                    dest.write_text(root_file.read_text())
                    root_file.unlink()
                    logger.info("Migrated %s from repo root to %s", filename, dest)
                    continue
                except OSError:
                    logger.debug("Could not migrate %s", root_file)

            # Otherwise create the default template
            try:
                dest.write_text(content)
                logger.info("Created default scaffolding file: %s", dest)
            except OSError:
                logger.debug("Could not write scaffolding file: %s", dest)

    def _build_system_prompt(self) -> str:
        """Build a dynamic system prompt with all memory layers."""
        # Ensure toolkit is created
        if not hasattr(self, 'toolkit') or self.toolkit is None:
            return OPEN_BOOTSTRAP_PROMPT

        prompt = build_open_system_prompt(
            persona_manager=self._persona_manager,
            capability_registry=self._capability_registry,
            hierarchical_memory=self._hierarchical_memory,
            skill_memory_manager=self._skill_memory_manager,
            toolkit=self.toolkit,
        )

        # Load persona files: project-level first, user-level fallback
        # BOOTSTRAP.md is loaded only if it exists (first-run onboarding)
        for filename in ["SOUL.md", "USER.md", "IDENTITY.md", "BOOTSTRAP.md"]:
            content = ""
            for base in [self._repo_root / ".emdash" / "rules", Path.home() / ".emdash" / "rules"]:
                path = base / filename
                if path.exists():
                    try:
                        content = path.read_text().strip()
                        if content:
                            logger.info("Loaded rules file: %s", path)
                            break
                    except Exception:
                        logger.debug("Failed to read rules file: %s", path)
            if content:
                prompt = content + "\n\n" + prompt
            else:
                logger.debug("Rules file not found or empty: %s", filename)

        return prompt

    def _get_available_subagents(self) -> dict[str, str]:
        """Return available sub-agent types."""
        return {
            "explore": "Read-only exploration of codebase",
            "plan": "Deep analysis and architecture planning",
            "research": "Web research and information gathering",
            "coder": "Code implementation (when file changes needed)",
        }

    def _pre_run(self, query: str, **kwargs) -> None:
        """Hook called before running the agent.
        
        Adds memory-enhanced context:
        - Capability detection
        - Applicable rule/skill activation
        - Persona interaction tracking
        - Short-term memory logging
        """
        # Check for pending channel messages
        try:
            from ...channels.stored import StoredChannel
            stored = StoredChannel(self._repo_root / ".emdash")
            pending = stored.get_and_clear()
            if pending and self._hierarchical_memory:
                self._hierarchical_memory.short_term.add(
                    content=f"Pending notifications: {pending}",
                    context="channels",
                    importance=0.8,
                )
        except Exception:
            pass  # Channel system not available

        # Record this interaction in short-term memory
        if self._hierarchical_memory:
            self._hierarchical_memory.short_term.add(
                content=f"User query: {query[:200]}",  # Truncate for short-term
                context="session",
                importance=0.5,
            )

        # Track onboarding state in short-term memory
        if self._hierarchical_memory and self._persona_manager:
            core_count = len(self._hierarchical_memory.core.entries)
            interaction_count = self._persona_manager.interaction_count
            if core_count < 5 and interaction_count <= 10:
                self._hierarchical_memory.short_term.add(
                    content=f"Onboarding in progress. Core identity entries: {core_count}/5.",
                    context="onboarding",
                    importance=0.6,
                )

        # Record persona interaction
        if self._persona_manager:
            self._persona_manager.record_interaction()

        # Check for capability matches
        if self._capability_registry:
            matching_caps = self._capability_registry.find_matching(query)
            if matching_caps:
                self._capability_registry.record_usage(matching_caps[0].name)

        # Find applicable rules and skills
        active_rules = []
        active_skills = []

        if self._skill_memory_manager:
            active_rules = self._skill_memory_manager.find_applicable_rules(query)
            active_skills = self._skill_memory_manager.find_applicable_skills(query)

        # Add active rules/skills to short-term memory for this session
        if active_rules and self._hierarchical_memory:
            self._hierarchical_memory.short_term.add(
                content=f"Active rules: {[r.action for r in active_rules]}",
                context="rules",
                importance=0.7,
            )

        if active_skills and self._hierarchical_memory:
            self._hierarchical_memory.short_term.add(
                content=f"Active skills: {[s.name for s in active_skills]}",
                context="skills",
                importance=0.7,
            )

        # Rebuild system prompt with current context
        self.system_prompt = self._build_system_prompt()

    def run(self, query: str, **kwargs):
        """Run the agent with memory-enhanced context.
        
        Overrides parent to add pre-run memory setup.
        """
        # Set up memory context before running
        self._pre_run(query, **kwargs)
        
        # Run parent implementation synchronously
        return super().run(query, **kwargs)

    def _on_run_complete(self, response: str) -> None:
        """Hook called when a run completes successfully.
        
        Adds completion to short-term memory.
        """
        super()._on_run_complete(response)
        
        # After response, add to short-term memory
        if self._hierarchical_memory:
            self._hierarchical_memory.short_term.add(
                content="Completed processing query",
                context="session",
                importance=0.3,
            )

    # ------------------------------------------------------------------
    # Pre-compaction memory flush
    # ------------------------------------------------------------------

    def _flush_memory_before_compaction(
        self,
        messages: list[dict],
        emitter: Optional[AgentEventEmitter] = None,
    ) -> None:
        """Persist important facts before compaction destroys message detail.

        Called by :class:`LLMSummaryStrategy` at the soft threshold (before
        the hard compaction threshold).  Uses the LLM to extract key facts
        from the conversation and writes them to:

        1. The daily markdown log (``memory/daily/YYYY-MM-DD.md``)
        2. Long-term memory (``HierarchicalMemory.long_term``)

        Inspired by OpenClaw's pre-compaction memory flush mechanism.
        """
        if not self._hierarchical_memory:
            return

        if emitter:
            emitter.emit_thinking("Saving important context before compaction...")

        # Build a lightweight summary of messages for the extraction prompt.
        # Skip the first message (original query) and recent messages (they
        # survive compaction) — focus on the "middle" that will be lost.
        KEEP_RECENT = 6
        if len(messages) <= KEEP_RECENT + 2:
            return  # Too few messages to worry about

        middle_msgs = messages[1:-KEEP_RECENT]
        if not middle_msgs:
            return

        conversation_text = self._format_messages_for_flush(middle_msgs)

        # Ask the LLM to extract durable facts
        prompt = (
            "You are a memory extraction specialist.  The conversation below "
            "is about to be compressed and most detail will be lost.\n\n"
            "Extract ONLY information worth remembering long-term:\n"
            "- User preferences, decisions, or corrections\n"
            "- Key facts about the project or codebase\n"
            "- Important file paths and what they contain\n"
            "- Decisions made and their rationale\n"
            "- Anything the user explicitly asked to remember\n\n"
            "Return a concise bulleted list (Markdown).  "
            "If nothing is worth saving, reply with exactly: NOTHING_TO_SAVE\n\n"
            "---\n\n"
            f"{conversation_text}"
        )

        try:
            from ..providers import get_provider
            from ..providers.factory import _get_default_model_name

            model_name = _get_default_model_name()
            provider = get_provider(model_name)

            response = provider.chat(
                messages=[{"role": "user", "content": prompt}],
                system="Extract durable memories. Be concise — bullet points only.",
            )

            extracted = (response.content or "").strip()

            if not extracted or "NOTHING_TO_SAVE" in extracted:
                logger.debug("Memory flush: nothing worth saving")
                return

            # 1. Write to daily markdown log
            log_path = self._hierarchical_memory.append_daily_log(
                extracted,
                heading="Pre-compaction memory flush",
            )
            logger.info("Memory flush: wrote to %s", log_path)

            # 2. Store key facts in long-term memory
            for line in extracted.splitlines():
                line = line.strip().lstrip("-•*").strip()
                if line and len(line) > 10:
                    self._hierarchical_memory.store(
                        content=line,
                        layer="long_term",
                        importance=0.7,
                        context="compaction_flush",
                        tags=["auto_flush"],
                    )

            if emitter:
                emitter.emit_thinking("Memory flush complete — important context saved.")

        except Exception as exc:
            logger.warning("Memory flush failed (non-fatal): %s", exc)

    @staticmethod
    def _format_messages_for_flush(messages: list[dict], max_chars: int = 12000) -> str:
        """Format messages into a compact text block for the flush LLM call."""
        parts: list[str] = []
        total = 0
        for msg in messages:
            role = msg.get("role", "?")
            content = msg.get("content", "")
            if isinstance(content, list):
                content = " ".join(
                    p.get("text", "") for p in content if isinstance(p, dict)
                )
            # Truncate individual messages
            if len(content) > 2000:
                content = content[:2000] + "…"
            part = f"[{role.upper()}] {content}"
            if total + len(part) > max_chars:
                parts.append("[...truncated...]")
                break
            parts.append(part)
            total += len(part)
        return "\n\n".join(parts)

    # Memory Management Interface

    def remember(
        self,
        content: str,
        layer: str = "long_term",
        importance: float = 0.5,
        context: str = "general",
        tags: Optional[list] = None,
    ) -> dict:
        """Store something in memory.

        Args:
            content: Information to remember
            layer: Which layer ("core", "long_term", "short_term")
            importance: Importance score (0.0-1.0)
            context: Category
            tags: Optional tags

        Returns:
            Result dict
        """
        if self._memory_tools:
            return self._memory_tools.remember(
                content=content,
                layer=layer,
                importance=importance,
                context=context,
                tags=tags,
            )
        return {"success": False, "error": "Memory not initialized"}

    def recall(
        self,
        query: str,
        layers: Optional[list] = None,
        context: Optional[str] = None,
        top_k: int = 5,
    ) -> dict:
        """Search memory.

        Args:
            query: Search query
            layers: Which layers to search
            context: Filter by context
            top_k: Max results

        Returns:
            Matching memories
        """
        if self._memory_tools:
            return self._memory_tools.recall(
                query=query,
                layers=layers,
                context=context,
                top_k=top_k,
            )
        return {"success": False, "error": "Memory not initialized"}

    def show_memory_stats(self) -> dict:
        """Get memory statistics."""
        if self._memory_tools:
            return self._memory_tools.get_memory_stats()
        return {"success": False, "error": "Memory not initialized"}

    def export_memory(self) -> dict:
        """Export all memory layers."""
        if self._hierarchical_memory:
            return {
                "success": True,
                "memory": self._hierarchical_memory.export_all(),
                "skills_rules": self._skill_memory_manager.export_all() if self._skill_memory_manager else {},
            }
        return {"success": False, "error": "Memory not initialized"}

    # Persona Interface (backward compatible)

    def get_persona_summary(self) -> str:
        """Get a summary of the learned persona."""
        if self._persona_manager:
            return self._persona_manager.get_preference_summary()
        return "No persona data available."

    def export_persona(self, file_path: Optional[str] = None) -> dict:
        """Export the current persona."""
        if not self._persona_manager:
            return {}

        data = self._persona_manager.export_persona()

        if file_path:
            import json
            with open(file_path, "w") as f:
                json.dump(data, f, indent=2)

        return data

    def import_persona(self, data: dict) -> None:
        """Import persona data."""
        if self._persona_manager:
            self._persona_manager.import_persona(data)
            self.system_prompt = self._build_system_prompt()

    def reset_persona(self) -> None:
        """Reset the persona to initial state."""
        if self._persona_manager:
            self._persona_manager.reset()
        if self._capability_registry:
            self._capability_registry.reset()
        if self._hierarchical_memory:
            self._hierarchical_memory.reset()
        if self._skill_memory_manager:
            # Clear skills and rules
            for skill_id in list(self._skill_memory_manager.skills.keys()):
                del self._skill_memory_manager.skills[skill_id]
            self._skill_memory_manager._save_skills()
            for rule_id in list(self._skill_memory_manager.rules.keys()):
                del self._skill_memory_manager.rules[rule_id]
            self._skill_memory_manager._save_rules()

        self.system_prompt = self._build_system_prompt()

    # Capabilities Interface

    def list_capabilities(self) -> list:
        """List all learned capabilities."""
        caps = []
        if self._capability_registry:
            caps.extend([
                {
                    "name": c.name,
                    "description": c.description,
                    "usage_count": c.usage_count,
                    "enabled": c.enabled,
                    "type": "capability",
                }
                for c in self._capability_registry.list_all()
            ])
        if self._skill_memory_manager:
            caps.extend([
                {
                    "name": s.name,
                    "description": s.description,
                    "usage_count": s.use_count,
                    "enabled": s.enabled,
                    "type": "skill",
                }
                for s in self._skill_memory_manager.skills.values()
            ])
        return caps

    def list_rules(self) -> list:
        """List all active rules."""
        if self._skill_memory_manager:
            return [
                {
                    "id": r.id,
                    "trigger": r.trigger,
                    "action": r.action,
                    "scope": r.scope,
                    "priority": r.priority,
                    "enabled": r.enabled,
                    "usage_count": r.usage_count,
                }
                for r in self._skill_memory_manager.rules.values()
            ]
        return []

    def suggest_capability(self, message: str) -> Optional[dict]:
        """Suggest creating a new capability based on message."""
        if self._capability_registry:
            history = []
            return self._capability_registry.suggest_new(message, history)
        return None


# Re-export the bootstrap prompt
from .prompts import OPEN_BOOTSTRAP_PROMPT