"""
H-MEM Level 3: DomainKnowledge dataclass and DomainKnowledgeExtractor.

Domain knowledge represents the highest level of abstraction,
containing principles, best practices, and decision frameworks
extracted from accumulated categories.
"""

from dataclasses import dataclass, field
from datetime import datetime, timezone
from typing import List, Optional, Dict, Any
import uuid

from gsd_rlm.memory.hmem.category import Category, CategoryType
from gsd_rlm.memory.hmem.trace import Trace


def _utc_now_iso() -> str:
    """Get current UTC time as ISO format string."""
    return datetime.now(timezone.utc).isoformat()


def _generate_domain_id() -> str:
    """Generate a unique domain ID."""
    return f"domain_{uuid.uuid4().hex[:12]}"


@dataclass
class DomainKnowledge:
    """
    Highest-level knowledge abstraction (H-MEM Level 3).

    Domain knowledge aggregates patterns and strategies from
    categories into principles, best practices, and decision
    frameworks that can guide future agent behavior.
    """

    # Required identification fields
    domain_id: str
    domain_name: str  # e.g., "Python Development", "API Design"

    # Extracted knowledge
    principles: List[str] = field(default_factory=list)
    best_practices: List[str] = field(default_factory=list)
    anti_patterns: List[str] = field(default_factory=list)
    decision_frameworks: List[str] = field(default_factory=list)

    # Category linkage
    category_ids: List[str] = field(default_factory=list)

    # Quality metrics
    confidence_score: float = 0.0
    evidence_count: int = 0

    # Metadata
    created_at: str = field(default_factory=_utc_now_iso)
    updated_at: str = field(default_factory=_utc_now_iso)

    def to_dict(self) -> Dict[str, Any]:
        """
        Convert domain knowledge to dictionary for serialization.

        Returns:
            Dictionary representation suitable for JSON storage.
        """
        return {
            "domain_id": self.domain_id,
            "domain_name": self.domain_name,
            "principles": self.principles,
            "best_practices": self.best_practices,
            "anti_patterns": self.anti_patterns,
            "decision_frameworks": self.decision_frameworks,
            "category_ids": self.category_ids,
            "confidence_score": self.confidence_score,
            "evidence_count": self.evidence_count,
            "created_at": self.created_at,
            "updated_at": self.updated_at,
        }

    @classmethod
    def from_dict(cls, data: Dict[str, Any]) -> "DomainKnowledge":
        """
        Create DomainKnowledge from dictionary.

        Args:
            data: Dictionary with domain knowledge fields.

        Returns:
            DomainKnowledge instance.
        """
        now = _utc_now_iso()
        return cls(
            domain_id=data.get("domain_id", _generate_domain_id()),
            domain_name=data.get("domain_name", "General Knowledge"),
            principles=data.get("principles", []),
            best_practices=data.get("best_practices", []),
            anti_patterns=data.get("anti_patterns", []),
            decision_frameworks=data.get("decision_frameworks", []),
            category_ids=data.get("category_ids", []),
            confidence_score=data.get("confidence_score", 0.0),
            evidence_count=data.get("evidence_count", 0),
            created_at=data.get("created_at", now),
            updated_at=data.get("updated_at", now),
        )

    def __post_init__(self):
        """Validate and normalize fields after initialization."""
        # Ensure lists are always lists
        if self.principles is None:
            self.principles = []
        if self.best_practices is None:
            self.best_practices = []
        if self.anti_patterns is None:
            self.anti_patterns = []
        if self.decision_frameworks is None:
            self.decision_frameworks = []
        if self.category_ids is None:
            self.category_ids = []

        # Ensure timestamps have values
        now = _utc_now_iso()
        if not self.created_at:
            self.created_at = now
        if not self.updated_at:
            self.updated_at = now

    def touch(self) -> None:
        """Update the updated_at timestamp."""
        self.updated_at = _utc_now_iso()

    @property
    def is_high_confidence(self) -> bool:
        """Check if domain knowledge has high confidence (>0.7)."""
        return self.confidence_score >= 0.7

    @property
    def has_sufficient_evidence(self) -> bool:
        """Check if there's sufficient evidence (>=5 sources)."""
        return self.evidence_count >= 5


class DomainKnowledgeExtractor:
    """
    Extracts domain knowledge from categories and traces (H-MEM Level 3).

    Uses LLM for intelligent extraction when available,
    falls back to pattern aggregation otherwise.
    """

    # Domain name templates by category type
    DOMAIN_TEMPLATES = {
        CategoryType.CODE_REVIEW: "Code Quality and Review",
        CategoryType.DEBUGGING: "Problem Solving and Debugging",
        CategoryType.FEATURE_DEVELOPMENT: "Software Development",
        CategoryType.DOCUMENTATION: "Technical Communication",
        CategoryType.TESTING: "Quality Assurance",
        CategoryType.REFACTORING: "Code Maintenance",
        CategoryType.ARCHITECTURE: "System Design",
        CategoryType.COLLABORATION: "Team Collaboration",
    }

    def __init__(self, llm_provider: Optional[Any] = None):
        """
        Initialize the domain knowledge extractor.

        Args:
            llm_provider: Optional LLM provider for knowledge extraction.
                         If None, uses pattern aggregation.
        """
        self.llm_provider = llm_provider

    async def extract_domain_knowledge(
        self,
        category: Category,
        traces: List[Trace],
    ) -> DomainKnowledge:
        """
        Extract domain knowledge from a category and its traces.

        Args:
            category: Category to extract from.
            traces: Traces associated with the category.

        Returns:
            Extracted DomainKnowledge.
        """
        domain_name = self._get_domain_name(category)

        domain = DomainKnowledge(
            domain_id=_generate_domain_id(),
            domain_name=domain_name,
            category_ids=[category.category_id],
            evidence_count=len(traces),
        )

        if not traces:
            return domain

        # Try LLM extraction if available
        if self.llm_provider is not None:
            try:
                extraction = await self._extract_with_llm(category, traces)
                domain.principles = extraction.get("principles", [])
                domain.best_practices = extraction.get("best_practices", [])
                domain.anti_patterns = extraction.get("anti_patterns", [])
                domain.decision_frameworks = extraction.get("decision_frameworks", [])
            except Exception:
                # Fall back to pattern aggregation
                self._aggregate_from_traces(domain, traces, category)
        else:
            # No LLM, use pattern aggregation
            self._aggregate_from_traces(domain, traces, category)

        # Calculate confidence score
        domain.confidence_score = self._calculate_confidence(traces, category)

        return domain

    def _get_domain_name(self, category: Category) -> str:
        """
        Get domain name based on category.

        Args:
            category: Category to get domain name for.

        Returns:
            Domain name string.
        """
        return self.DOMAIN_TEMPLATES.get(
            category.category_type,
            f"General {category.name}",
        )

    def _build_extraction_prompt(
        self,
        category: Category,
        traces: List[Trace],
    ) -> str:
        """
        Build a prompt for LLM extraction.

        Args:
            category: Category being analyzed.
            traces: Traces to extract from.

        Returns:
            Formatted prompt string.
        """
        # Summarize traces
        trace_summaries = []
        for i, trace in enumerate(traces[:10], 1):  # Limit to 10
            trace_summaries.append(
                f"Trace {i}:\n"
                f"  Theme: {trace.theme}\n"
                f"  Summary: {trace.summary[:200]}...\n"
                f"  Patterns: {', '.join(trace.patterns_identified[:3])}\n"
                f"  Lessons: {', '.join(trace.lessons_learned[:2])}"
            )

        traces_text = "\n\n".join(trace_summaries)

        # Include category patterns
        category_patterns = "\n".join(category.common_patterns[:5])
        category_strategies = "\n".join(category.effective_strategies[:5])

        return f"""Analyze the following {category.category_type.value} traces and extract domain knowledge.

Category: {category.name}
Type: {category.category_type.value}

Common Patterns:
{category_patterns}

Effective Strategies:
{category_strategies}

Traces:
{traces_text}

Extract domain knowledge as JSON with:
1. "principles": List of 2-5 core principles derived from these traces
2. "best_practices": List of 3-7 best practices
3. "anti_patterns": List of 1-3 anti-patterns to avoid (if evident)
4. "decision_frameworks": List of 1-3 decision frameworks or heuristics

Response as valid JSON only."""

    def _parse_extraction_response(self, response: str) -> dict:
        """
        Parse LLM response into extraction data.

        Args:
            response: Raw LLM response string.

        Returns:
            Parsed extraction dictionary.
        """
        import json
        import re

        # Try to extract JSON from response
        try:
            return json.loads(response)
        except json.JSONDecodeError:
            pass

        # Try to find JSON in response
        json_match = re.search(r"\{[\s\S]*\}", response)
        if json_match:
            try:
                return json.loads(json_match.group())
            except json.JSONDecodeError:
                pass

        # Return defaults if parsing fails
        return {
            "principles": [],
            "best_practices": [],
            "anti_patterns": [],
            "decision_frameworks": [],
        }

    async def _extract_with_llm(
        self,
        category: Category,
        traces: List[Trace],
    ) -> dict:
        """
        Use LLM to extract domain knowledge.

        Args:
            category: Category being analyzed.
            traces: Traces to extract from.

        Returns:
            Extraction data dictionary.
        """
        prompt = self._build_extraction_prompt(category, traces)

        # Call LLM provider - handle different provider interfaces
        if hasattr(self.llm_provider, "generate"):
            response = await self.llm_provider.generate(prompt)
        elif hasattr(self.llm_provider, "complete"):
            response = await self.llm_provider.complete(prompt)
        elif hasattr(self.llm_provider, "chat"):
            response = await self.llm_provider.chat(prompt)
        elif callable(self.llm_provider):
            response = self.llm_provider(prompt)
        else:
            raise ValueError("LLM provider has no recognized method")

        return self._parse_extraction_response(response)

    def _aggregate_from_traces(
        self,
        domain: DomainKnowledge,
        traces: List[Trace],
        category: Category,
    ) -> None:
        """
        Aggregate domain knowledge from traces without LLM.

        Args:
            domain: DomainKnowledge to populate.
            traces: Traces to aggregate from.
            category: Source category.
        """
        # Aggregate patterns as principles
        all_patterns = set()
        for trace in traces:
            all_patterns.update(trace.patterns_identified)
        domain.principles = list(all_patterns)[:5]

        # Aggregate strategies as best practices
        domain.best_practices = category.effective_strategies[:7]

        # Use lessons learned as additional best practices
        for trace in traces:
            for lesson in trace.lessons_learned:
                if lesson and lesson not in domain.best_practices:
                    domain.best_practices.append(lesson)
                    if len(domain.best_practices) >= 10:
                        break

        # Infer anti-patterns from failures
        failed_traces = [t for t in traces if not t.overall_success]
        if failed_traces:
            domain.anti_patterns = [
                "Avoid approaches that led to failed outcomes",
                f"Learn from {len(failed_traces)} unsuccessful attempts",
            ][:3]

        # Create simple decision framework
        if category.success_rate > 0.7:
            domain.decision_frameworks = [
                f"Current approaches show {category.success_rate:.0%} success rate",
            ]

    def _calculate_confidence(
        self,
        traces: List[Trace],
        category: Category,
    ) -> float:
        """
        Calculate confidence score for extracted knowledge.

        Args:
            traces: Evidence traces.
            category: Source category.

        Returns:
            Confidence score between 0 and 1.
        """
        # Base confidence on evidence count (capped at 10)
        evidence_factor = min(1.0, len(traces) / 10)

        # Factor in category success rate
        success_factor = category.success_rate

        # Combined confidence
        confidence = evidence_factor * success_factor

        return min(1.0, max(0.0, confidence))
