"""
Memory Models - Data structures for the memory layer.

This module defines the core data structures for the two-tier memory architecture:
- MemoryTier: Classification of memory items by retention policy
- MemoryItemType: Types of items stored in memory
- MemoryItem: A single memory entry
- MemoryPatch: A proposed change to memory
- PatchOperation: Types of patch operations
- PatchRiskLevel: Risk classification for patches

Usage:
    from agent_orchestrator.memory.models import MemoryItem, MemoryTier

    item = MemoryItem(
        id="mem-001",
        tier=MemoryTier.WORKING_KNOWLEDGE,
        item_type=MemoryItemType.RUNBOOK,
        title="Agent Recovery",
        content="...",
        tags=["recovery", "agents"],
    )
"""

import json
import uuid
from dataclasses import dataclass, field
from datetime import datetime
from enum import Enum
from typing import Any, Optional


class MemoryTier(Enum):
    """
    Memory tier classification based on retention and access policy.

    Tier 0: Immutable audit data (task runs, logs, approvals)
    Tier 1: Authoritative data (ADRs, policies, project_state.json)
    Tier 2: Working knowledge (runbooks, prompts, patterns)
    Tier 3: Ephemeral cache (raw tool outputs, scratch notes)
    """

    IMMUTABLE_AUDIT = 0  # Never deleted, admin-only write
    AUTHORITATIVE = 1  # Changes require approval
    WORKING_KNOWLEDGE = 2  # Librarian can modify
    EPHEMERAL_CACHE = 3  # Auto-expire after summarization


class MemoryItemType(Enum):
    """Types of items that can be stored in memory."""

    # Tier 1: Authoritative
    ADR = "adr"  # Architecture Decision Record
    POLICY = "policy"  # Risk policies, budget policies
    PROJECT_STATE = "project_state"  # Current project state
    CONSTRAINT = "constraint"  # Active constraints

    # Tier 2: Working Knowledge
    RUNBOOK = "runbook"  # Operational procedures
    PATTERN = "pattern"  # Code patterns, best practices
    ISSUE_FIX = "issue_fix"  # Known error → resolution pairs
    PROMPT = "prompt"  # Agent prompt templates
    TASK_SUMMARY = "task_summary"  # Summarized task outcomes

    # Tier 3: Ephemeral
    SCRATCH_NOTE = "scratch_note"  # Per-task working notes
    TOOL_OUTPUT = "tool_output"  # Raw tool/command outputs
    LOG_EXCERPT = "log_excerpt"  # Relevant log snippets

    # Tier 0: Audit (stored in DB, not as MemoryItem)
    # task_runs, approvals, health_samples - handled by persistence layer


class PatchOperation(Enum):
    """Types of memory patch operations."""

    ADD = "add"  # Add new memory item
    UPDATE = "update"  # Update existing item content
    TAG = "tag"  # Add/modify tags only
    SUPERSEDE = "supersede"  # Mark item as superseded by another
    ARCHIVE = "archive"  # Move to archive (soft delete)
    DELETE = "delete"  # Hard delete (requires HIGH approval)


class PatchRiskLevel(Enum):
    """Risk classification for memory patches."""

    LOW = "low"  # Tag fixes, formatting, metadata → auto-apply
    MEDIUM = "medium"  # Content merges, new runbooks → apply with review
    HIGH = "high"  # ADR changes, policy updates, deletions → require approval


@dataclass
class MemoryItem:
    """
    A single item stored in memory.

    Items are versioned and can be superseded by newer versions.
    They support tagging for efficient retrieval.
    """

    id: str
    tier: MemoryTier
    item_type: MemoryItemType
    title: str
    content: str
    tags: list[str] = field(default_factory=list)
    source_links: list[str] = field(default_factory=list)  # commits, tickets, logs
    created_by: str = "system"  # agent_id or "human" or "system"
    created_at: datetime = field(default_factory=datetime.now)
    updated_at: datetime = field(default_factory=datetime.now)
    superseded_by: Optional[str] = None  # ID of newer item
    archived: bool = False
    embedding_ref: Optional[str] = None  # Reference to embedding in RAG index
    metadata: dict[str, Any] = field(default_factory=dict)

    @staticmethod
    def generate_id() -> str:
        """Generate a unique memory item ID."""
        return f"mem-{uuid.uuid4().hex[:12]}"

    def to_dict(self) -> dict[str, Any]:
        """Convert to dictionary for storage/serialization."""
        return {
            "id": self.id,
            "tier": self.tier.value,
            "item_type": self.item_type.value,
            "title": self.title,
            "content": self.content,
            "tags": self.tags,
            "source_links": self.source_links,
            "created_by": self.created_by,
            "created_at": self.created_at.isoformat(),
            "updated_at": self.updated_at.isoformat(),
            "superseded_by": self.superseded_by,
            "archived": self.archived,
            "embedding_ref": self.embedding_ref,
            "metadata": self.metadata,
        }

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> "MemoryItem":
        """Create from dictionary."""
        return cls(
            id=data["id"],
            tier=MemoryTier(data["tier"]),
            item_type=MemoryItemType(data["item_type"]),
            title=data["title"],
            content=data["content"],
            tags=data.get("tags", []),
            source_links=data.get("source_links", []),
            created_by=data.get("created_by", "system"),
            created_at=datetime.fromisoformat(data["created_at"]),
            updated_at=datetime.fromisoformat(data["updated_at"]),
            superseded_by=data.get("superseded_by"),
            archived=data.get("archived", False),
            embedding_ref=data.get("embedding_ref"),
            metadata=data.get("metadata", {}),
        )

    def is_active(self) -> bool:
        """Check if item is active (not superseded or archived)."""
        return not self.archived and self.superseded_by is None

    def matches_tags(self, required_tags: list[str]) -> bool:
        """Check if item has all required tags."""
        return all(tag in self.tags for tag in required_tags)

    def to_context_string(self, max_length: int = 500) -> str:
        """Convert to a context string for agent prompts."""
        content_preview = self.content[:max_length]
        if len(self.content) > max_length:
            content_preview += "..."

        return f"[{self.item_type.value}] {self.title}\n{content_preview}"


@dataclass
class MemoryPatch:
    """
    A proposed change to memory.

    Patches are validated by the Memory Write Gate before application.
    They support different operations and risk levels.
    """

    id: str
    operation: PatchOperation
    target_id: Optional[str]  # ID of item to modify (None for ADD)
    new_item: Optional[MemoryItem]  # For ADD/UPDATE operations
    changes: dict[str, Any] = field(default_factory=dict)  # For partial updates
    risk_level: PatchRiskLevel = PatchRiskLevel.MEDIUM
    proposed_by: str = "system"  # agent_id or "human" or "system"
    proposed_at: datetime = field(default_factory=datetime.now)
    evidence_links: list[str] = field(default_factory=list)  # Supporting evidence
    reason: str = ""  # Why this change is needed
    status: str = "pending"  # pending, approved, rejected, applied
    reviewed_by: Optional[str] = None
    reviewed_at: Optional[datetime] = None
    rejection_reason: Optional[str] = None

    @staticmethod
    def generate_id() -> str:
        """Generate a unique patch ID."""
        return f"patch-{uuid.uuid4().hex[:12]}"

    def to_dict(self) -> dict[str, Any]:
        """Convert to dictionary for storage/serialization."""
        return {
            "id": self.id,
            "operation": self.operation.value,
            "target_id": self.target_id,
            "new_item": self.new_item.to_dict() if self.new_item else None,
            "changes": self.changes,
            "risk_level": self.risk_level.value,
            "proposed_by": self.proposed_by,
            "proposed_at": self.proposed_at.isoformat(),
            "evidence_links": self.evidence_links,
            "reason": self.reason,
            "status": self.status,
            "reviewed_by": self.reviewed_by,
            "reviewed_at": self.reviewed_at.isoformat() if self.reviewed_at else None,
            "rejection_reason": self.rejection_reason,
        }

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> "MemoryPatch":
        """Create from dictionary."""
        new_item = None
        if data.get("new_item"):
            new_item = MemoryItem.from_dict(data["new_item"])

        return cls(
            id=data["id"],
            operation=PatchOperation(data["operation"]),
            target_id=data.get("target_id"),
            new_item=new_item,
            changes=data.get("changes", {}),
            risk_level=PatchRiskLevel(data.get("risk_level", "medium")),
            proposed_by=data.get("proposed_by", "system"),
            proposed_at=datetime.fromisoformat(data["proposed_at"]),
            evidence_links=data.get("evidence_links", []),
            reason=data.get("reason", ""),
            status=data.get("status", "pending"),
            reviewed_by=data.get("reviewed_by"),
            reviewed_at=datetime.fromisoformat(data["reviewed_at"]) if data.get("reviewed_at") else None,
            rejection_reason=data.get("rejection_reason"),
        )


@dataclass
class PatchValidationResult:
    """Result of validating a memory patch."""

    valid: bool
    errors: list[str] = field(default_factory=list)
    warnings: list[str] = field(default_factory=list)
    computed_risk_level: Optional[PatchRiskLevel] = None

    def add_error(self, error: str) -> None:
        """Add an error message."""
        self.errors.append(error)
        self.valid = False

    def add_warning(self, warning: str) -> None:
        """Add a warning message."""
        self.warnings.append(warning)


@dataclass
class ContextPacket:
    """
    A packet of context to inject into agent prompts.

    Built from operational and knowledge memory before task execution.
    """

    project_state_summary: str
    active_constraints: list[str]
    relevant_adrs: list[MemoryItem]
    relevant_runbooks: list[MemoryItem]
    relevant_patterns: list[MemoryItem]
    recent_task_summaries: list[MemoryItem]
    known_issues: list[MemoryItem]
    working_notes: list[str]  # From working memory
    timestamp: datetime = field(default_factory=datetime.now)

    def to_prompt_string(self, max_items: int = 5) -> str:
        """Convert to a string suitable for prompt injection."""
        parts = []

        # Project state
        if self.project_state_summary:
            parts.append("## Current Project State")
            parts.append(self.project_state_summary)
            parts.append("")

        # Constraints
        if self.active_constraints:
            parts.append("## Active Constraints")
            for constraint in self.active_constraints:
                parts.append(f"- {constraint}")
            parts.append("")

        # Relevant ADRs
        if self.relevant_adrs:
            parts.append("## Relevant Decisions")
            for adr in self.relevant_adrs[:max_items]:
                parts.append(f"- {adr.title}")
            parts.append("")

        # Relevant patterns/issues
        if self.known_issues:
            parts.append("## Known Issues to Avoid")
            for issue in self.known_issues[:max_items]:
                parts.append(f"- {issue.title}")
            parts.append("")

        # Working notes
        if self.working_notes:
            parts.append("## Working Notes (this session)")
            for note in self.working_notes[-5:]:  # Last 5 notes
                parts.append(f"- {note}")
            parts.append("")

        return "\n".join(parts)


# Helper functions for creating common patch types

def create_add_runbook_patch(
    title: str,
    content: str,
    tags: list[str],
    proposed_by: str,
    evidence_links: list[str],
    reason: str,
) -> MemoryPatch:
    """Create a patch to add a new runbook."""
    item = MemoryItem(
        id=MemoryItem.generate_id(),
        tier=MemoryTier.WORKING_KNOWLEDGE,
        item_type=MemoryItemType.RUNBOOK,
        title=title,
        content=content,
        tags=tags,
        created_by=proposed_by,
        source_links=evidence_links,
    )

    return MemoryPatch(
        id=MemoryPatch.generate_id(),
        operation=PatchOperation.ADD,
        target_id=None,
        new_item=item,
        risk_level=PatchRiskLevel.MEDIUM,
        proposed_by=proposed_by,
        evidence_links=evidence_links,
        reason=reason,
    )


def create_add_issue_fix_patch(
    error_pattern: str,
    fix_description: str,
    tags: list[str],
    proposed_by: str,
    evidence_links: list[str],
) -> MemoryPatch:
    """Create a patch to add a known issue/fix pair."""
    content = f"**Error Pattern:**\n{error_pattern}\n\n**Fix:**\n{fix_description}"

    item = MemoryItem(
        id=MemoryItem.generate_id(),
        tier=MemoryTier.WORKING_KNOWLEDGE,
        item_type=MemoryItemType.ISSUE_FIX,
        title=f"Issue Fix: {error_pattern[:50]}...",
        content=content,
        tags=tags + ["issue-fix"],
        created_by=proposed_by,
        source_links=evidence_links,
    )

    return MemoryPatch(
        id=MemoryPatch.generate_id(),
        operation=PatchOperation.ADD,
        target_id=None,
        new_item=item,
        risk_level=PatchRiskLevel.LOW,
        proposed_by=proposed_by,
        evidence_links=evidence_links,
        reason="New error pattern and fix discovered",
    )


def create_tag_patch(
    target_id: str,
    tags_to_add: list[str],
    proposed_by: str,
    reason: str,
) -> MemoryPatch:
    """Create a patch to add tags to an existing item."""
    return MemoryPatch(
        id=MemoryPatch.generate_id(),
        operation=PatchOperation.TAG,
        target_id=target_id,
        new_item=None,
        changes={"add_tags": tags_to_add},
        risk_level=PatchRiskLevel.LOW,
        proposed_by=proposed_by,
        reason=reason,
    )


def create_supersede_patch(
    old_item_id: str,
    new_item: MemoryItem,
    proposed_by: str,
    reason: str,
) -> MemoryPatch:
    """Create a patch to supersede an old item with a new one."""
    return MemoryPatch(
        id=MemoryPatch.generate_id(),
        operation=PatchOperation.SUPERSEDE,
        target_id=old_item_id,
        new_item=new_item,
        risk_level=PatchRiskLevel.MEDIUM,
        proposed_by=proposed_by,
        reason=reason,
    )
