"""Memory Inbox processor — reads external context from memory-inbox and distributes.

Flow:
  1. Coordinator (Cursor) calls Giga MCP to pull plans/neurons and writes JSON
     to state/memory-inbox/{mind}/plans/ and state/memory-inbox/{mind}/neurons/.
  2. InboxProcessor reads those JSONs, merges into a raw context document at
     docs/shared/{mind}/context.md.
  3. The distributor agent (Charles by default) reads the raw context and writes
     tailored briefs to docs/{agent_id}/briefs/{mind}-context.md for each
     subscriber defined in config/memory-subscriptions.yaml.

This module handles steps 2 and 3. Step 1 is handled by the GigaAdapter
(bridges/giga_dev_bridge/) and the coordinator.
"""

from __future__ import annotations

import json
import logging
from datetime import UTC, datetime
from pathlib import Path
from typing import TYPE_CHECKING, Any

import yaml

if TYPE_CHECKING:
    from vastian.orchestrator import Orchestrator

logger = logging.getLogger(__name__)


def _project_root() -> Path:
    """Resolve project root from this module's location."""
    return Path(__file__).resolve().parents[3]


def load_subscriptions(config_path: Path | None = None) -> dict[str, dict]:
    """Load memory-subscriptions.yaml and return the subscriptions dict.

    Returns:
        Dict mapping mind names to subscription config, e.g.:
        {"work": {"recipients": [...], "distributor": "charles", ...}}
    """
    if config_path is None:
        from vastian.storage.data_root import get_config_dir

        config_path = get_config_dir(_project_root()) / "memory-subscriptions.yaml"
    if not config_path.exists():
        logger.warning("Memory subscriptions config not found: %s", config_path)
        return {}
    try:
        raw = yaml.safe_load(config_path.read_text(encoding="utf-8"))
        return raw.get("subscriptions", {}) if raw else {}
    except Exception as e:
        logger.error("Failed to load memory subscriptions: %s", e)
        return {}


class InboxProcessor:
    """Processes memory-inbox JSON files into raw context docs + agent briefs."""

    def __init__(
        self,
        orchestrator: Orchestrator | None = None,
        project_root: Path | None = None,
    ) -> None:
        self.orchestrator = orchestrator
        self.root = project_root or _project_root()
        self.inbox_root = self.root / "state" / "memory-inbox"
        self.docs_root = self.root / "docs"
        self.subscriptions = load_subscriptions(self.root / "config" / "memory-subscriptions.yaml")

    # ------------------------------------------------------------------
    # Step 2: Merge inbox JSON into raw context doc
    # ------------------------------------------------------------------

    def process_inbox(self, mind: str) -> dict[str, Any]:
        """Read all JSON from state/memory-inbox/{mind}/ and write raw context.

        Returns a summary dict with counts and the raw_doc path.
        """
        plans = self._read_json_dir(self.inbox_root / mind / "plans")
        neurons = self._read_json_dir(self.inbox_root / mind / "neurons")

        if not plans and not neurons:
            logger.info("No inbox items for mind '%s'", mind)
            return {"mind": mind, "plans": 0, "neurons": 0, "raw_doc": None}

        # Build raw context markdown
        lines: list[str] = []
        lines.append(f"# External Context: {mind}")
        lines.append("")
        lines.append(f"*Pulled at {datetime.now(UTC).strftime('%Y-%m-%d %H:%M UTC')}*")
        lines.append("")

        if plans:
            lines.append("## Plans")
            lines.append("")
            for p in plans:
                key = p.get("key_name", p.get("title", "untitled"))
                content = p.get("content", "")
                lines.append(f"### {key}")
                lines.append("")
                lines.append(content)
                lines.append("")

        if neurons:
            lines.append("## Knowledge / Neurons")
            lines.append("")
            for n in neurons:
                title = n.get("title", "untitled")
                body = n.get("body", "")
                lines.append(f"### {title}")
                lines.append("")
                lines.append(body)
                lines.append("")

        raw_text = "\n".join(lines)

        # Write raw context doc
        sub = self.subscriptions.get(mind, {})
        raw_doc_rel = sub.get("raw_doc", f"docs/shared/{mind}/context.md")
        raw_doc_path = self.root / raw_doc_rel
        raw_doc_path.parent.mkdir(parents=True, exist_ok=True)
        raw_doc_path.write_text(raw_text, encoding="utf-8")
        logger.info(
            "Wrote raw context for mind '%s': %d plans, %d neurons -> %s",
            mind,
            len(plans),
            len(neurons),
            raw_doc_path,
        )

        return {
            "mind": mind,
            "plans": len(plans),
            "neurons": len(neurons),
            "raw_doc": str(raw_doc_path),
            "raw_text": raw_text,
        }

    # ------------------------------------------------------------------
    # Step 3: Distribute tailored briefs to subscribers
    # ------------------------------------------------------------------

    async def distribute(self, mind: str, raw_text: str | None = None) -> list[str]:
        """Have the distributor agent write tailored briefs for each subscriber.

        If raw_text is None, reads from the raw_doc path. Returns list of
        brief paths written.

        When no orchestrator is available (offline mode), writes a simple
        copy of the raw context to each recipient's briefs/ dir.
        """
        sub = self.subscriptions.get(mind)
        if not sub:
            logger.warning("No subscription config for mind '%s'", mind)
            return []

        recipients: list[str] = sub.get("recipients", [])
        distributor: str = sub.get("distributor", "charles")

        if not recipients:
            logger.warning("No recipients for mind '%s'", mind)
            return []

        # Load raw text if not provided
        if raw_text is None:
            raw_doc_rel = sub.get("raw_doc", f"docs/shared/{mind}/context.md")
            raw_doc_path = self.root / raw_doc_rel
            if not raw_doc_path.exists():
                logger.error("Raw context doc not found: %s", raw_doc_path)
                return []
            raw_text = raw_doc_path.read_text(encoding="utf-8")

        paths_written: list[str] = []

        if self.orchestrator is not None:
            # Use the distributor agent to write tailored briefs
            paths_written = await self._distribute_via_agent(
                mind, distributor, recipients, raw_text
            )
        else:
            # Offline mode: write a simple copy to each recipient
            paths_written = self._distribute_offline(mind, recipients, raw_text)

        return paths_written

    async def _distribute_via_agent(
        self,
        mind: str,
        distributor: str,
        recipients: list[str],
        raw_text: str,
    ) -> list[str]:
        """Use the distributor agent (e.g. Charles) to write tailored briefs."""
        assert self.orchestrator is not None

        paths_written: list[str] = []
        for agent_id in recipients:
            brief_path = self.docs_root / agent_id / "briefs" / f"{mind}-context.md"
            brief_path.parent.mkdir(parents=True, exist_ok=True)

            # Ask the distributor to tailor the context for this specific agent
            prompt = (
                f"You are distributing external context from the '{mind}' mind to agent '{agent_id}'.\n\n"
                f"Below is the raw external context. Write a tailored brief for {agent_id} that:\n"
                f"- Highlights information relevant to {agent_id}'s role and responsibilities\n"
                f"- Omits information that is not relevant to them\n"
                f"- Uses clear, concise language\n"
                f"- Starts with a one-line summary of what this context covers\n\n"
                f"---\n\n{raw_text}\n\n---\n\n"
                f"Write ONLY the brief content (markdown). Do not include preamble or meta-commentary."
            )

            try:
                response = await self.orchestrator.handle_user_message_direct(
                    content=prompt,
                    agent_id=distributor,
                )
                # Write the tailored brief
                header = (
                    f"<!-- Distributed by {distributor} | mind: {mind} | "
                    f"generated: {datetime.now(UTC).strftime('%Y-%m-%d %H:%M UTC')} -->\n\n"
                )
                brief_path.write_text(header + response, encoding="utf-8")
                paths_written.append(str(brief_path))
                logger.info("Brief written for %s: %s", agent_id, brief_path)
            except Exception as e:
                logger.error("Failed to distribute to %s via %s: %s", agent_id, distributor, e)
                # Fallback: write raw context as-is
                self._write_offline_brief(mind, agent_id, raw_text)
                paths_written.append(str(brief_path))

        return paths_written

    def _distribute_offline(
        self,
        mind: str,
        recipients: list[str],
        raw_text: str,
    ) -> list[str]:
        """Offline fallback: write raw context to each recipient's briefs dir."""
        paths: list[str] = []
        for agent_id in recipients:
            path = self._write_offline_brief(mind, agent_id, raw_text)
            paths.append(str(path))
        return paths

    def _write_offline_brief(self, mind: str, agent_id: str, raw_text: str) -> Path:
        """Write a simple copy of the raw context as an agent brief."""
        brief_path = self.docs_root / agent_id / "briefs" / f"{mind}-context.md"
        brief_path.parent.mkdir(parents=True, exist_ok=True)
        header = (
            f"<!-- Raw context (offline mode) | mind: {mind} | "
            f"copied: {datetime.now(UTC).strftime('%Y-%m-%d %H:%M UTC')} -->\n\n"
        )
        brief_path.write_text(header + raw_text, encoding="utf-8")
        logger.info("Offline brief written for %s: %s", agent_id, brief_path)
        return brief_path

    # ------------------------------------------------------------------
    # Full pipeline: process + distribute
    # ------------------------------------------------------------------

    async def pull_and_distribute(self, mind: str) -> dict[str, Any]:
        """Full pipeline: process inbox JSON -> raw context -> distribute briefs.

        Returns summary dict with counts and paths.
        """
        result = self.process_inbox(mind)
        if result.get("raw_doc") is None:
            return result

        raw_text = result.pop("raw_text", None)
        briefs = await self.distribute(mind, raw_text)
        result["briefs_written"] = briefs
        result["recipients"] = self.subscriptions.get(mind, {}).get("recipients", [])
        return result

    # ------------------------------------------------------------------
    # Status / introspection
    # ------------------------------------------------------------------

    def get_status(self) -> dict[str, Any]:
        """Return status of all configured mind inboxes."""
        status: dict[str, Any] = {"minds": {}}
        for mind_name, sub in self.subscriptions.items():
            plans_dir = self.inbox_root / mind_name / "plans"
            neurons_dir = self.inbox_root / mind_name / "neurons"
            raw_doc_rel = sub.get("raw_doc", f"docs/shared/{mind_name}/context.md")
            raw_doc_path = self.root / raw_doc_rel

            plan_count = len(list(plans_dir.glob("*.json"))) if plans_dir.exists() else 0
            neuron_count = len(list(neurons_dir.glob("*.json"))) if neurons_dir.exists() else 0

            status["minds"][mind_name] = {
                "pending_plans": plan_count,
                "pending_neurons": neuron_count,
                "raw_doc_exists": raw_doc_path.exists(),
                "raw_doc": str(raw_doc_rel),
                "recipients": sub.get("recipients", []),
                "distributor": sub.get("distributor", "charles"),
                "auto_sync": sub.get("auto_sync", False),
            }
        return status

    # ------------------------------------------------------------------
    # Helpers
    # ------------------------------------------------------------------

    @staticmethod
    def _read_json_dir(directory: Path) -> list[dict]:
        """Read all .json files from a directory, sorted by name."""
        if not directory.exists():
            return []
        results: list[dict] = []
        for path in sorted(directory.glob("*.json")):
            try:
                data = json.loads(path.read_text(encoding="utf-8"))
                results.append(data)
            except (json.JSONDecodeError, OSError) as e:
                logger.warning("Skipping malformed JSON %s: %s", path.name, e)
        return results
