"""
H-MEM Level 2: Category dataclass and CategoryManager.

Categories group traces by work type (debugging, feature development,
etc.) with aggregated patterns and success metrics.
"""

import json
import sqlite3
from dataclasses import dataclass, field
from datetime import datetime, timezone
from enum import Enum
from pathlib import Path
from typing import List, Optional, Dict, Any
import uuid

from gsd_rlm.memory.hmem.episode import EpisodeType
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_category_id() -> str:
    """Generate a unique category ID."""
    return f"cat_{uuid.uuid4().hex[:12]}"


class CategoryType(Enum):
    """
    Types of work categories for trace classification.

    Categories represent the nature of work performed in traces,
    enabling pattern recognition and strategy extraction by type.
    """

    CODE_REVIEW = "code_review"
    DEBUGGING = "debugging"
    FEATURE_DEVELOPMENT = "feature_development"
    DOCUMENTATION = "documentation"
    TESTING = "testing"
    REFACTORING = "refactoring"
    ARCHITECTURE = "architecture"
    COLLABORATION = "collaboration"


@dataclass
class Category:
    """
    A category grouping traces by work type (H-MEM Level 2).

    Categories aggregate traces of similar work types, tracking
    common patterns, challenges, and effective strategies.
    """

    # Required identification fields
    category_id: str
    category_type: CategoryType
    name: str
    description: str = ""

    # Trace linkage
    trace_ids: List[str] = field(default_factory=list)
    trace_count: int = 0

    # Aggregated insights
    common_patterns: List[str] = field(default_factory=list)
    typical_challenges: List[str] = field(default_factory=list)
    effective_strategies: List[str] = field(default_factory=list)

    # Metrics
    success_rate: float = 0.0

    # Hierarchy linkage
    domain_id: Optional[str] = None  # Links to Level 3 DomainKnowledge

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

        Returns:
            Dictionary representation suitable for JSON storage.
        """
        return {
            "category_id": self.category_id,
            "category_type": self.category_type.value,
            "name": self.name,
            "description": self.description,
            "trace_ids": self.trace_ids,
            "trace_count": self.trace_count,
            "common_patterns": self.common_patterns,
            "typical_challenges": self.typical_challenges,
            "effective_strategies": self.effective_strategies,
            "success_rate": self.success_rate,
            "domain_id": self.domain_id,
        }

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

        Args:
            data: Dictionary with category fields.

        Returns:
            Category instance.
        """
        # Handle category_type conversion from string
        category_type = data.get("category_type", CategoryType.FEATURE_DEVELOPMENT)
        if isinstance(category_type, str):
            category_type = CategoryType(category_type)

        return cls(
            category_id=data.get("category_id", _generate_category_id()),
            category_type=category_type,
            name=data.get("name", ""),
            description=data.get("description", ""),
            trace_ids=data.get("trace_ids", []),
            trace_count=data.get("trace_count", 0),
            common_patterns=data.get("common_patterns", []),
            typical_challenges=data.get("typical_challenges", []),
            effective_strategies=data.get("effective_strategies", []),
            success_rate=data.get("success_rate", 0.0),
            domain_id=data.get("domain_id"),
        )

    def __post_init__(self):
        """Validate and normalize fields after initialization."""
        # Ensure lists are always lists
        if self.trace_ids is None:
            self.trace_ids = []
        if self.common_patterns is None:
            self.common_patterns = []
        if self.typical_challenges is None:
            self.typical_challenges = []
        if self.effective_strategies is None:
            self.effective_strategies = []

        # Ensure trace_count matches list length
        if self.trace_count == 0 and self.trace_ids:
            self.trace_count = len(self.trace_ids)

    @property
    def has_domain(self) -> bool:
        """Check if category has been linked to domain knowledge."""
        return self.domain_id is not None


class CategoryManager:
    """
    Manages categories for trace classification (H-MEM Level 2).

    Provides SQLite-backed storage for categories with type-based
    classification and statistics aggregation.
    """

    # Mapping from EpisodeType to CategoryType
    EPISODE_TO_CATEGORY_MAP = {
        EpisodeType.TASK_EXECUTION: CategoryType.FEATURE_DEVELOPMENT,
        EpisodeType.PROBLEM_SOLVING: CategoryType.DEBUGGING,
        EpisodeType.ERROR_RECOVERY: CategoryType.DEBUGGING,
        EpisodeType.LEARNING: CategoryType.DOCUMENTATION,
        EpisodeType.COLLABORATION: CategoryType.COLLABORATION,
    }

    def __init__(self, db_path: str = "memory/hmem.db"):
        """
        Initialize the category manager.

        Args:
            db_path: Path to SQLite database file.
        """
        self.db_path = Path(db_path)
        self._init_db()

    def _get_connection(self) -> sqlite3.Connection:
        """
        Get a new database connection.

        Returns:
            SQLite connection with row factory enabled.
        """
        return sqlite3.connect(str(self.db_path))

    def _init_db(self) -> None:
        """Initialize database schema with categories table."""
        # Ensure parent directory exists
        self.db_path.parent.mkdir(parents=True, exist_ok=True)

        conn = self._get_connection()
        try:
            cursor = conn.cursor()

            # Create categories table
            cursor.execute("""
                CREATE TABLE IF NOT EXISTS categories (
                    category_id TEXT PRIMARY KEY,
                    category_type TEXT NOT NULL,
                    name TEXT NOT NULL,
                    description TEXT DEFAULT '',
                    trace_ids TEXT DEFAULT '[]',
                    trace_count INTEGER DEFAULT 0,
                    common_patterns TEXT DEFAULT '[]',
                    typical_challenges TEXT DEFAULT '[]',
                    effective_strategies TEXT DEFAULT '[]',
                    success_rate REAL DEFAULT 0.0,
                    domain_id TEXT
                )
            """)

            # Create indexes
            cursor.execute("""
                CREATE INDEX IF NOT EXISTS idx_categories_type
                ON categories(category_type)
            """)
            cursor.execute("""
                CREATE INDEX IF NOT EXISTS idx_categories_domain_id
                ON categories(domain_id)
            """)

            conn.commit()
        finally:
            conn.close()

    def get_category_for_episode_type(self, episode_type: EpisodeType) -> CategoryType:
        """
        Map EpisodeType to CategoryType.

        Args:
            episode_type: Episode type to map.

        Returns:
            Corresponding CategoryType.
        """
        return self.EPISODE_TO_CATEGORY_MAP.get(
            episode_type, CategoryType.FEATURE_DEVELOPMENT
        )

    def categorize_trace(
        self,
        trace: Trace,
        category_type: Optional[CategoryType] = None,
    ) -> Category:
        """
        Assign a trace to a category, creating if needed.

        Args:
            trace: Trace to categorize.
            category_type: Optional category type override.
                          If None, inferred from trace patterns.

        Returns:
            Category the trace was assigned to.
        """
        # Determine category type if not provided
        if category_type is None:
            # Default to feature development
            category_type = CategoryType.FEATURE_DEVELOPMENT

        # Get or create category for this type
        category = self._get_or_create_category(category_type)

        # Add trace to category
        if trace.trace_id not in category.trace_ids:
            category.trace_ids.append(trace.trace_id)
            category.trace_count = len(category.trace_ids)

        # Update trace's category reference
        trace.category_id = category.category_id

        # Update category with trace insights
        self._merge_trace_insights(category, trace)

        # Persist changes
        self._store_category(category)

        return category

    def get_traces_for_category(self, category_id: str) -> List[str]:
        """
        Get all trace IDs for a category.

        Args:
            category_id: Category identifier.

        Returns:
            List of trace IDs in the category.
        """
        conn = self._get_connection()
        try:
            cursor = conn.cursor()
            cursor.execute(
                "SELECT trace_ids FROM categories WHERE category_id = ?",
                (category_id,),
            )
            row = cursor.fetchone()
            if row:
                return json.loads(row[0])
            return []
        except (json.JSONDecodeError, TypeError):
            return []
        finally:
            conn.close()

    def update_category_stats(self, category: Category) -> None:
        """
        Recalculate category statistics from trace data.

        Args:
            category: Category to update.
        """
        # This would typically query traces and recalculate
        # For now, just persist current stats
        self._store_category(category)

    def get_category(self, category_id: str) -> Optional[Category]:
        """
        Retrieve a category by ID.

        Args:
            category_id: Category identifier.

        Returns:
            Category if found, None otherwise.
        """
        conn = self._get_connection()
        try:
            cursor = conn.cursor()
            cursor.execute(
                "SELECT * FROM categories WHERE category_id = ?",
                (category_id,),
            )
            row = cursor.fetchone()
            if row:
                return self._row_to_category(row)
            return None
        finally:
            conn.close()

    def get_categories_by_type(self, category_type: CategoryType) -> List[Category]:
        """
        Get all categories of a specific type.

        Args:
            category_type: Type to filter by.

        Returns:
            List of matching categories.
        """
        conn = self._get_connection()
        try:
            cursor = conn.cursor()
            cursor.execute(
                "SELECT * FROM categories WHERE category_type = ?",
                (category_type.value,),
            )
            rows = cursor.fetchall()
            return [self._row_to_category(row) for row in rows]
        finally:
            conn.close()

    def get_all_categories(self) -> List[Category]:
        """
        Get all categories.

        Returns:
            List of all categories.
        """
        conn = self._get_connection()
        try:
            cursor = conn.cursor()
            cursor.execute("SELECT * FROM categories")
            rows = cursor.fetchall()
            return [self._row_to_category(row) for row in rows]
        finally:
            conn.close()

    def _get_or_create_category(self, category_type: CategoryType) -> Category:
        """
        Get existing category for type or create new one.

        Args:
            category_type: Type of category to get/create.

        Returns:
            Category instance.
        """
        # Try to get existing category of this type
        categories = self.get_categories_by_type(category_type)

        if categories:
            # Return the first matching category
            return categories[0]

        # Create new category
        category = Category(
            category_id=_generate_category_id(),
            category_type=category_type,
            name=self._get_default_name(category_type),
            description=self._get_default_description(category_type),
        )

        self._store_category(category)
        return category

    def _get_default_name(self, category_type: CategoryType) -> str:
        """Get default name for a category type."""
        names = {
            CategoryType.CODE_REVIEW: "Code Review",
            CategoryType.DEBUGGING: "Debugging",
            CategoryType.FEATURE_DEVELOPMENT: "Feature Development",
            CategoryType.DOCUMENTATION: "Documentation",
            CategoryType.TESTING: "Testing",
            CategoryType.REFACTORING: "Refactoring",
            CategoryType.ARCHITECTURE: "Architecture",
            CategoryType.COLLABORATION: "Collaboration",
        }
        return names.get(category_type, category_type.value.replace("_", " ").title())

    def _get_default_description(self, category_type: CategoryType) -> str:
        """Get default description for a category type."""
        descriptions = {
            CategoryType.CODE_REVIEW: "Traces related to code review activities",
            CategoryType.DEBUGGING: "Traces related to debugging and problem solving",
            CategoryType.FEATURE_DEVELOPMENT: "Traces related to developing new features",
            CategoryType.DOCUMENTATION: "Traces related to documentation work",
            CategoryType.TESTING: "Traces related to testing activities",
            CategoryType.REFACTORING: "Traces related to code refactoring",
            CategoryType.ARCHITECTURE: "Traces related to architectural decisions",
            CategoryType.COLLABORATION: "Traces related to collaborative work",
        }
        return descriptions.get(
            category_type, f"Traces for {category_type.value} activities"
        )

    def _merge_trace_insights(self, category: Category, trace: Trace) -> None:
        """
        Merge insights from a trace into category.

        Args:
            category: Category to update.
            trace: Trace with insights to merge.
        """
        # Merge patterns (avoid duplicates)
        for pattern in trace.patterns_identified:
            if pattern and pattern not in category.common_patterns:
                category.common_patterns.append(pattern)

        # Merge lessons as challenges/strategies
        for lesson in trace.lessons_learned:
            if lesson:
                # Simple heuristic: lessons become strategies
                if lesson not in category.effective_strategies:
                    category.effective_strategies.append(lesson)

        # Update success rate (simple running calculation)
        if trace.overall_success:
            category.success_rate = min(1.0, category.success_rate + 0.1)
        else:
            category.success_rate = max(0.0, category.success_rate - 0.1)

    def _store_category(self, category: Category) -> None:
        """
        Store category in database (INSERT OR REPLACE).

        Args:
            category: Category to persist.
        """
        conn = self._get_connection()
        try:
            cursor = conn.cursor()

            cursor.execute(
                """
                INSERT OR REPLACE INTO categories (
                    category_id, category_type, name, description,
                    trace_ids, trace_count, common_patterns,
                    typical_challenges, effective_strategies,
                    success_rate, domain_id
                ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
                """,
                (
                    category.category_id,
                    category.category_type.value,
                    category.name,
                    category.description,
                    json.dumps(category.trace_ids),
                    category.trace_count,
                    json.dumps(category.common_patterns),
                    json.dumps(category.typical_challenges),
                    json.dumps(category.effective_strategies),
                    category.success_rate,
                    category.domain_id,
                ),
            )

            conn.commit()
        finally:
            conn.close()

    def _row_to_category(self, row: tuple) -> Category:
        """
        Convert a database row to a Category dataclass.

        Args:
            row: SQLite row tuple.

        Returns:
            Category instance.
        """
        (
            category_id,
            category_type,
            name,
            description,
            trace_ids_json,
            trace_count,
            common_patterns_json,
            typical_challenges_json,
            effective_strategies_json,
            success_rate,
            domain_id,
        ) = row

        # Deserialize JSON fields
        trace_ids = json.loads(trace_ids_json) if trace_ids_json else []
        common_patterns = (
            json.loads(common_patterns_json) if common_patterns_json else []
        )
        typical_challenges = (
            json.loads(typical_challenges_json) if typical_challenges_json else []
        )
        effective_strategies = (
            json.loads(effective_strategies_json) if effective_strategies_json else []
        )

        return Category(
            category_id=category_id,
            category_type=CategoryType(category_type),
            name=name,
            description=description or "",
            trace_ids=trace_ids,
            trace_count=trace_count or 0,
            common_patterns=common_patterns,
            typical_challenges=typical_challenges,
            effective_strategies=effective_strategies,
            success_rate=success_rate or 0.0,
            domain_id=domain_id,
        )

    def count(self) -> int:
        """
        Get total number of categories.

        Returns:
            Total category count.
        """
        conn = self._get_connection()
        try:
            cursor = conn.cursor()
            cursor.execute("SELECT COUNT(*) FROM categories")
            return cursor.fetchone()[0]
        finally:
            conn.close()
