"""
Project Journal Protocol - Context handoff between agents.

This module implements the project journal protocol that ensures context
is properly handed off between agent runs. Every agent run MUST:

1. Read project_state.json before starting
2. Write back a StatusPacket at the end
3. Append to agent_journal.md with decision rationale

This solves the "Agent B overwrote Agent A's decision" problem by making
state explicit and machine-readable.
"""

import json
from datetime import datetime
from pathlib import Path
from typing import Any, Optional

from .status_packet import StatusPacket


class ProjectJournal:
    """
    Manager for project state and agent journal.

    Handles reading and writing:
    - project_state.json: Machine-readable state
    - agent_journal.md: Human-readable log

    Usage:
        journal = ProjectJournal(Path("/path/to/project"))

        # At start of agent run
        state = journal.read_state()

        # At end of agent run
        journal.write_status(status_packet)

        # When making significant decisions
        journal.record_decision(agent_id, "Use JWT", "Matches existing patterns")
    """

    def __init__(self, project_root: Path) -> None:
        """
        Initialize the project journal.

        Args:
            project_root: Root directory of the project
        """
        self.project_root = project_root
        self.state_file = project_root / "project_state.json"
        self.journal_file = project_root / "agent_journal.md"

    def read_state(self) -> dict[str, Any]:
        """
        Read the current project state.

        MUST be called at the start of every agent run.

        Returns:
            The current project state dictionary
        """
        if self.state_file.exists():
            try:
                return json.loads(self.state_file.read_text())
            except json.JSONDecodeError:
                # If corrupted, return initial state
                return self._create_initial_state()
        return self._create_initial_state()

    def write_status(self, packet: StatusPacket) -> None:
        """
        Write status packet at end of agent run.

        MUST be called at the end of every agent run.

        Updates both:
        - project_state.json (machine-readable)
        - agent_journal.md (human-readable)

        Args:
            packet: The StatusPacket from the agent run
        """
        # Update machine-readable state
        state = self.read_state()
        state["last_updated"] = datetime.now().isoformat()
        state["updated_by"] = packet.agent_id

        # Apply state changes from packet
        for key, value in packet.state_changes.items():
            if key in state:
                if isinstance(state[key], list) and isinstance(value, list):
                    # Merge lists
                    state[key].extend(value)
                elif isinstance(state[key], dict) and isinstance(value, dict):
                    # Merge dicts
                    state[key].update(value)
                else:
                    state[key] = value
            else:
                state[key] = value

        # Write updated state
        self.state_file.write_text(json.dumps(state, indent=2))

        # Append to human-readable journal
        self._append_journal(packet)

    def _append_journal(self, packet: StatusPacket) -> None:
        """
        Append an entry to the agent journal.

        Args:
            packet: The StatusPacket to log
        """
        # Create journal file with header if it doesn't exist
        if not self.journal_file.exists():
            self.journal_file.write_text("# Agent Journal\n\n")

        # Format the entry
        timestamp = packet.timestamp.strftime("%H:%M")
        date_str = packet.timestamp.strftime("%Y-%m-%d")

        # Check if we need a new date header
        content = self.journal_file.read_text()
        if f"## {date_str}" not in content:
            date_header = f"\n## {date_str}\n\n"
        else:
            date_header = ""

        # Build entry
        files_str = ", ".join(packet.artifacts.files_modified) or "None"
        tests_str = json.dumps(packet.artifacts.test_results)
        next_steps_str = ", ".join(packet.next_steps) or "None"
        blockers_str = ", ".join(packet.blockers) or "None"

        entry = f"""{date_header}### {timestamp} - {packet.agent_id}
**Task:** {packet.progress_summary}
**Status:** {packet.status}
**Files Modified:** {files_str}
**Tests:** {tests_str}
**Next Steps:** {next_steps_str}
**Blockers:** {blockers_str}

"""

        # Append to journal
        with open(self.journal_file, "a") as f:
            f.write(entry)

    def record_decision(
        self,
        agent_id: str,
        decision: str,
        rationale: str,
        reversible: bool = True,
    ) -> None:
        """
        Record a significant decision.

        Use this when an agent makes an important architectural or
        implementation decision that other agents should know about.

        Args:
            agent_id: ID of the agent making the decision
            decision: What was decided
            rationale: Why it was decided
            reversible: Whether the decision can be easily undone
        """
        state = self.read_state()

        # Ensure decisions_made list exists
        if "decisions_made" not in state:
            state["decisions_made"] = []

        # Add the decision
        decision_entry = {
            "id": f"dec-{len(state['decisions_made']) + 1:03d}",
            "timestamp": datetime.now().isoformat(),
            "agent": agent_id,
            "decision": decision,
            "rationale": rationale,
            "reversible": reversible,
        }
        state["decisions_made"].append(decision_entry)

        # Update state file
        state["last_updated"] = datetime.now().isoformat()
        state["updated_by"] = agent_id
        self.state_file.write_text(json.dumps(state, indent=2))

        # Also append to journal
        self._append_decision_to_journal(agent_id, decision, rationale)

    def _append_decision_to_journal(
        self, agent_id: str, decision: str, rationale: str
    ) -> None:
        """Append a decision entry to the journal."""
        if not self.journal_file.exists():
            self.journal_file.write_text("# Agent Journal\n\n")

        timestamp = datetime.now().strftime("%H:%M")
        entry = f"""### {timestamp} - {agent_id} (Decision)
**Decision:** {decision}
**Rationale:** {rationale}

"""
        with open(self.journal_file, "a") as f:
            f.write(entry)

    def add_objective(
        self,
        description: str,
        priority: int = 0,
        assigned_agent: Optional[str] = None,
    ) -> str:
        """
        Add a new objective to the project state.

        Args:
            description: What needs to be accomplished
            priority: Priority level (higher = more important)
            assigned_agent: Agent to assign (optional)

        Returns:
            The generated objective ID
        """
        state = self.read_state()

        if "current_objectives" not in state:
            state["current_objectives"] = []

        obj_id = f"obj-{len(state['current_objectives']) + 1:03d}"
        objective = {
            "id": obj_id,
            "description": description,
            "status": "pending",
            "priority": priority,
        }
        if assigned_agent:
            objective["assigned_agent"] = assigned_agent

        state["current_objectives"].append(objective)
        state["last_updated"] = datetime.now().isoformat()
        self.state_file.write_text(json.dumps(state, indent=2))

        return obj_id

    def update_objective_status(self, obj_id: str, status: str) -> None:
        """
        Update the status of an objective.

        Args:
            obj_id: The objective ID
            status: New status (pending, in_progress, completed, blocked)
        """
        state = self.read_state()

        for obj in state.get("current_objectives", []):
            if obj["id"] == obj_id:
                obj["status"] = status
                break

        state["last_updated"] = datetime.now().isoformat()
        self.state_file.write_text(json.dumps(state, indent=2))

    def add_risk(self, description: str, severity: str, mitigation: str) -> str:
        """
        Add a risk to the project state.

        Args:
            description: What the risk is
            severity: low, medium, high, critical
            mitigation: How to mitigate the risk

        Returns:
            The generated risk ID
        """
        state = self.read_state()

        if "open_risks" not in state:
            state["open_risks"] = []

        risk_id = f"risk-{len(state['open_risks']) + 1:03d}"
        risk = {
            "id": risk_id,
            "description": description,
            "severity": severity,
            "mitigation": mitigation,
        }
        state["open_risks"].append(risk)
        state["last_updated"] = datetime.now().isoformat()
        self.state_file.write_text(json.dumps(state, indent=2))

        return risk_id

    def add_constraint(self, constraint: str) -> None:
        """
        Add a constraint that all agents must follow.

        Args:
            constraint: The constraint description
        """
        state = self.read_state()

        if "constraints" not in state:
            state["constraints"] = []

        if constraint not in state["constraints"]:
            state["constraints"].append(constraint)
            state["last_updated"] = datetime.now().isoformat()
            self.state_file.write_text(json.dumps(state, indent=2))

    def get_recent_decisions(self, limit: int = 5) -> list[dict[str, Any]]:
        """
        Get recent decisions for context injection.

        Args:
            limit: Maximum number of decisions to return

        Returns:
            List of recent decision dictionaries
        """
        state = self.read_state()
        decisions = state.get("decisions_made", [])
        return decisions[-limit:] if decisions else []

    def get_active_objectives(self) -> list[dict[str, Any]]:
        """
        Get objectives that are not yet completed.

        Returns:
            List of active objective dictionaries
        """
        state = self.read_state()
        objectives = state.get("current_objectives", [])
        return [obj for obj in objectives if obj.get("status") != "completed"]

    def get_context_for_agent(self) -> dict[str, Any]:
        """
        Get context suitable for injecting into agent prompts.

        Returns:
            Dictionary with project_state, constraints, recent_decisions
        """
        state = self.read_state()
        return {
            "project_state": state,
            "constraints": state.get("constraints", []),
            "recent_decisions": [
                d["decision"] for d in self.get_recent_decisions()
            ],
            "active_objectives": self.get_active_objectives(),
        }

    def _create_initial_state(self) -> dict[str, Any]:
        """Create initial project state structure."""
        return {
            "version": "1.0",
            "last_updated": datetime.now().isoformat(),
            "updated_by": "system",
            "current_objectives": [],
            "active_branches": {},
            "active_worktrees": {},
            "constraints": [],
            "decisions_made": [],
            "open_risks": [],
            "definition_of_done": [
                "All tests pass",
                "No critical security issues",
                "Code reviewed",
            ],
            "blocked_items": [],
            "shared_context": {},
        }

    def reset(self) -> None:
        """Reset the project state to initial values."""
        initial_state = self._create_initial_state()
        self.state_file.write_text(json.dumps(initial_state, indent=2))

        # Reset journal
        self.journal_file.write_text("# Agent Journal\n\n")
