"""
Instance Pattern Mixin for ModuleWrapper

Provides generic instance pattern storage, variation generation, and caching
for any module wrapper. This is module-agnostic - works with card_framework,
json, or any other wrapped module.

Instance patterns represent successful usages of module components with
specific parameter values. They can be stored, varied, and cached for
fast retrieval.

Architecture:
    ┌─────────────────────────────────────────────────────────────┐
    │              InstancePatternMixin                            │
    │                                                              │
    │  ┌──────────────┐    ┌──────────────┐    ┌──────────────┐   │
    │  │ store_pattern│ →  │ generate_    │ →  │ cache_pattern│   │
    │  │              │    │ variations   │    │              │   │
    │  └──────────────┘    └──────────────┘    └──────────────┘   │
    │         ↓                   ↓                   ↓            │
    │  ┌──────────────────────────────────────────────────────┐   │
    │  │                   Qdrant / Cache                      │   │
    │  │  • instance_pattern points (Qdrant)                   │   │
    │  │  • variation families (L1/L2 cache)                   │   │
    │  │  • component class references                         │   │
    │  └──────────────────────────────────────────────────────┘   │
    └─────────────────────────────────────────────────────────────┘

Expects from other mixins:
    - QdrantMixin: client, collection_name
    - EmbeddingMixin: embedder, embed methods
    - SymbolsMixin: build_dsl_from_paths, symbol_mapping
    - CacheMixin: cache_pattern, get_cached_entry
    - RelationshipsMixin: relationships (for variation generation)
"""

import hashlib
import logging
import time
import uuid
from dataclasses import dataclass, field
from datetime import datetime
from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Type

from adapters.module_wrapper.types import (
    CacheKey,
    ComponentPaths,
    DSLNotation,
    Payload,
    RelationshipDict,
    TimestampedMixin,
)

logger = logging.getLogger(__name__)


# =============================================================================
# DATA CLASSES
# =============================================================================


@dataclass
class InstancePattern:
    """
    Represents a stored instance pattern.

    An instance pattern captures a successful usage of module components
    with specific parameter values.
    """

    pattern_id: str
    component_paths: ComponentPaths
    instance_params: Payload
    description: str = ""
    dsl_notation: Optional[DSLNotation] = None
    feedback: Optional[str] = None  # "positive", "negative", None
    metadata: Payload = field(default_factory=dict)
    created_at: float = field(default_factory=time.time)

    def to_payload(self) -> Payload:
        """Convert to Qdrant payload format."""
        return {
            "name": f"instance_pattern_{self.pattern_id[:8]}",
            "type": "instance_pattern",
            "pattern_id": self.pattern_id,
            "component_paths": self.component_paths,
            "instance_params": self.instance_params,
            "description": self.description,
            "dsl_notation": self.dsl_notation,
            "feedback": self.feedback,
            "timestamp": datetime.fromtimestamp(self.created_at).isoformat(),
            **self.metadata,
        }


@dataclass
class PatternVariation:
    """A single variation of an instance pattern."""

    variation_id: str
    variation_type: str  # "structure", "parameter", "combined"
    component_paths: ComponentPaths
    instance_params: Payload
    parent_id: str
    dsl_notation: Optional[DSLNotation] = None
    cache_key: Optional[CacheKey] = None
    created_at: float = field(default_factory=time.time)


@dataclass
class VariationFamily:
    """Collection of variations derived from a source pattern."""

    parent_id: str
    source_pattern: InstancePattern
    variations: List[PatternVariation] = field(default_factory=list)
    cache_keys: List[CacheKey] = field(default_factory=list)

    @property
    def size(self) -> int:
        return len(self.variations)


# =============================================================================
# STRUCTURE VARIATOR (Generic)
# =============================================================================


class StructureVariator:
    """
    Generates structural variations using component relationships.

    Works with any module wrapper that has a relationships dict.
    """

    def __init__(self, relationships: RelationshipDict):
        """
        Args:
            relationships: Dict mapping parent component → list of valid children
        """
        self.relationships: RelationshipDict = relationships
        self._sibling_cache: Dict[str, List[str]] = {}

    def get_valid_children(self, component_name: str) -> List[str]:
        """Get valid children for a component."""
        return self.relationships.get(component_name, [])

    def get_siblings(self, component_name: str) -> List[str]:
        """Get sibling components (components with same parent)."""
        if component_name in self._sibling_cache:
            return self._sibling_cache[component_name]

        siblings = set()
        for parent, children in self.relationships.items():
            if component_name in children:
                siblings.update(children)

        siblings.discard(component_name)
        result = list(siblings)
        self._sibling_cache[component_name] = result
        return result

    def swap_sibling(
        self,
        component_paths: ComponentPaths,
        target_index: Optional[int] = None,
    ) -> Optional[ComponentPaths]:
        """Swap a component with a valid sibling."""
        import random

        if not component_paths:
            return None

        names = [p.split(".")[-1] if "." in p else p for p in component_paths]

        if target_index is None:
            target_index = random.randint(0, len(names) - 1)

        target = names[target_index]
        siblings = self.get_siblings(target)

        if not siblings:
            return None

        replacement = random.choice(siblings)
        new_names = names.copy()
        new_names[target_index] = replacement

        return new_names

    def add_child(
        self,
        component_paths: ComponentPaths,
        parent_name: Optional[str] = None,
    ) -> Optional[ComponentPaths]:
        """Add a valid child component."""
        import random

        names = [p.split(".")[-1] if "." in p else p for p in component_paths]

        if not names:
            return None

        if parent_name is None:
            parent_name = random.choice(names)

        valid_children = self.get_valid_children(parent_name)
        if not valid_children:
            return None

        existing = set(names)
        available = [c for c in valid_children if c not in existing]

        if not available:
            return None

        new_child = random.choice(available)
        new_names = names.copy()

        try:
            parent_idx = names.index(parent_name)
            new_names.insert(parent_idx + 1, new_child)
        except ValueError:
            new_names.append(new_child)

        return new_names

    def remove_optional(
        self,
        component_paths: ComponentPaths,
        required: Optional[Set[str]] = None,
    ) -> Optional[ComponentPaths]:
        """Remove a non-required component."""
        import random

        if len(component_paths) <= 1:
            return None

        names = [p.split(".")[-1] if "." in p else p for p in component_paths]
        required = required or set()

        removable = [n for n in names if n not in required]

        if not removable:
            return None

        to_remove = random.choice(removable)
        new_names = [n for n in names if n != to_remove]

        return new_names if new_names else None

    def expand_repeated(
        self,
        component_paths: ComponentPaths,
        repeatable: Optional[List[str]] = None,
        max_expand: int = 2,
    ) -> Optional[ComponentPaths]:
        """Add more instances of a repeatable component."""
        import random

        names = [p.split(".")[-1] if "." in p else p for p in component_paths]
        repeatable = repeatable or []

        candidates = [n for n in names if n in repeatable]

        if not candidates:
            return None

        to_repeat = random.choice(candidates)
        count = random.randint(1, max_expand)

        new_names = names.copy()
        try:
            idx = names.index(to_repeat)
            for _ in range(count):
                new_names.insert(idx + 1, to_repeat)
        except ValueError:
            pass

        return new_names

    def generate_variations(
        self,
        component_paths: ComponentPaths,
        num_variations: int = 3,
        required_components: Optional[Set[str]] = None,
        repeatable_components: Optional[List[str]] = None,
    ) -> List[ComponentPaths]:
        """Generate multiple structural variations."""
        import random

        variations = []
        strategies = [
            lambda p: self.swap_sibling(p),
            lambda p: self.add_child(p),
            lambda p: self.remove_optional(p, required_components),
            lambda p: self.expand_repeated(p, repeatable_components),
        ]

        attempts = 0
        max_attempts = num_variations * 4

        while len(variations) < num_variations and attempts < max_attempts:
            attempts += 1
            strategy = random.choice(strategies)

            try:
                result = strategy(component_paths)
                if result and result not in variations and result != component_paths:
                    variations.append(result)
            except Exception as e:
                logger.debug(f"Variation strategy failed: {e}")

        return variations


# =============================================================================
# PARAMETER VARIATOR (Generic)
# =============================================================================


class ParameterVariator:
    """
    Generates parameter variations for instance patterns.

    Works with any parameter dictionary.
    """

    def __init__(self, custom_variators: Optional[Dict[str, Callable]] = None):
        """
        Args:
            custom_variators: Dict mapping param name patterns → variation functions
        """
        self.custom_variators = custom_variators or {}

    def vary_string(self, value: str) -> str:
        """Generate a string variation."""
        import random

        variations = [
            lambda v: v.upper(),
            lambda v: v.lower(),
            lambda v: v.title(),
            lambda v: f"[{v}]",
            lambda v: f"• {v}",
            lambda v: v[: len(v) // 2] + "..." if len(v) > 10 else v,
        ]

        return random.choice(variations)(value)

    def vary_number(self, value: float, variance: float = 0.2) -> float:
        """Generate a numeric variation."""
        import random

        factor = random.uniform(1 - variance, 1 + variance)
        return value * factor

    def vary_list(self, value: list, max_items: int = 10) -> list:
        """Generate a list variation."""
        import random

        if not value:
            return value

        variations = [
            lambda v: random.sample(v, len(v)),  # Shuffle
            lambda v: v[: max(1, len(v) - 1)],  # Truncate
            lambda v: [v[0]] + v if v else v,  # Duplicate first
            lambda v: list(reversed(v)),  # Reverse
        ]

        return random.choice(variations)(value)[:max_items]

    def vary_dict(self, value: dict) -> dict:
        """Recursively vary a dictionary."""
        return self.vary_params(value)

    def vary_params(
        self,
        params: Payload,
        vary_keys: Optional[List[str]] = None,
        max_keys_to_vary: int = 3,
    ) -> Payload:
        """
        Generate a variation of parameters.

        Args:
            params: Source parameters
            vary_keys: Specific keys to vary (random selection if None)
            max_keys_to_vary: Max number of keys to vary

        Returns:
            New params dict with variations
        """
        import random

        new_params = params.copy()
        keys_to_vary = vary_keys or list(params.keys())

        num_to_vary = min(len(keys_to_vary), random.randint(1, max_keys_to_vary))
        selected_keys = random.sample(keys_to_vary, num_to_vary)

        for key in selected_keys:
            value = params.get(key)

            if value is None:
                continue

            # Check for custom variator
            for pattern, variator in self.custom_variators.items():
                if pattern in key.lower():
                    try:
                        new_params[key] = variator(value)
                        break
                    except Exception:
                        pass
            else:
                # Use default variators
                if isinstance(value, str):
                    new_params[key] = self.vary_string(value)
                elif isinstance(value, list):
                    new_params[key] = self.vary_list(value)
                elif isinstance(value, dict):
                    new_params[key] = self.vary_dict(value)
                elif isinstance(value, bool):
                    if random.random() < 0.3:
                        new_params[key] = not value
                elif isinstance(value, (int, float)):
                    new_params[key] = type(value)(self.vary_number(float(value)))

        return new_params

    def generate_variations(
        self,
        params: Payload,
        num_variations: int = 3,
    ) -> List[Payload]:
        """Generate multiple parameter variations."""
        variations = []
        seen_hashes = set()

        attempts = 0
        max_attempts = num_variations * 3

        while len(variations) < num_variations and attempts < max_attempts:
            attempts += 1

            varied = self.vary_params(params)
            param_hash = hashlib.md5(str(sorted(varied.items())).encode()).hexdigest()

            if param_hash not in seen_hashes:
                seen_hashes.add(param_hash)
                variations.append(varied)

        return variations


# =============================================================================
# INSTANCE PATTERN MIXIN
# =============================================================================


class InstancePatternMixin:
    """
    Mixin providing instance pattern management for ModuleWrapper.

    Expects the following attributes on self:
    - client: Qdrant client (from QdrantMixin)
    - collection_name: str (from QdrantMixin)
    - relationships: Dict[str, List[str]] (from RelationshipsMixin)
    - symbol_mapping: Dict[str, str] (from SymbolsMixin)
    - build_dsl_from_paths: Callable (from SymbolsMixin)
    - _get_component_cache: Callable (from CacheMixin)
    - get_component_by_path: Callable (from ModuleWrapperBase)
    """

    # --- Mixin dependency contract ---
    _MIXIN_PROVIDES = frozenset(
        {
            "store_instance_pattern",
            "generate_pattern_variations",
            "get_variation_family",
            "get_random_variation",
            "get_cached_variation",
            "configure_patterns",
            "pattern_stats",
        }
    )
    _MIXIN_REQUIRES = frozenset(
        {
            "client",
            "collection_name",
            "relationships",
            "symbol_mapping",
            "build_dsl_from_paths",
            "get_component_by_path",
            "embedder",
        }
    )
    _MIXIN_INIT_ORDER = 60

    # Configuration
    _pattern_config = {
        "max_patterns": 500,
        "default_structure_variations": 3,
        "default_param_variations": 2,
        "required_components": set(),  # Components that can't be removed
        "repeatable_components": [],  # Components that can be duplicated
    }

    def configure_patterns(
        self,
        max_patterns: Optional[int] = None,
        default_structure_variations: Optional[int] = None,
        default_param_variations: Optional[int] = None,
        required_components: Optional[Set[str]] = None,
        repeatable_components: Optional[List[str]] = None,
    ) -> None:
        """
        Configure instance pattern behavior.

        Args:
            max_patterns: Maximum patterns to keep in storage
            default_structure_variations: Default number of structure variations
            default_param_variations: Default number of parameter variations
            required_components: Components that can't be removed in variations
            repeatable_components: Components that can be duplicated in variations
        """
        if max_patterns is not None:
            self._pattern_config["max_patterns"] = max_patterns
        if default_structure_variations is not None:
            self._pattern_config["default_structure_variations"] = (
                default_structure_variations
            )
        if default_param_variations is not None:
            self._pattern_config["default_param_variations"] = default_param_variations
        if required_components is not None:
            self._pattern_config["required_components"] = required_components
        if repeatable_components is not None:
            self._pattern_config["repeatable_components"] = repeatable_components

    def _get_structure_variator(self) -> StructureVariator:
        """Get or create structure variator."""
        if not hasattr(self, "_structure_variator"):
            relationships = getattr(self, "relationships", {})
            self._structure_variator = StructureVariator(relationships)
        return self._structure_variator

    def _get_param_variator(self) -> ParameterVariator:
        """Get or create parameter variator."""
        if not hasattr(self, "_param_variator"):
            self._param_variator = ParameterVariator()
        return self._param_variator

    def _get_variation_families(self) -> Dict[str, VariationFamily]:
        """Get variation families registry."""
        if not hasattr(self, "_variation_families"):
            self._variation_families = {}
        return self._variation_families

    def _generate_pattern_id(self, pattern: InstancePattern) -> str:
        """Generate a unique pattern ID."""
        content = (
            f"{pattern.component_paths}{pattern.instance_params}{pattern.description}"
        )
        return hashlib.sha256(content.encode()).hexdigest()[:16]

    def _build_pattern_dsl(
        self, component_paths: ComponentPaths
    ) -> Optional[DSLNotation]:
        """Build DSL notation for a pattern."""
        if hasattr(self, "build_dsl_from_paths"):
            try:
                return self.build_dsl_from_paths(component_paths)
            except Exception:
                pass
        return None

    def store_instance_pattern(
        self,
        component_paths: ComponentPaths,
        instance_params: Payload,
        description: str = "",
        feedback: Optional[str] = None,
        metadata: Optional[Payload] = None,
        generate_variations: bool = True,
        num_structure_variations: Optional[int] = None,
        num_param_variations: Optional[int] = None,
        cache_pattern: bool = True,
    ) -> Optional[str]:
        """
        Store an instance pattern.

        Args:
            component_paths: List of component paths/names used
            instance_params: Parameters used to instantiate components
            description: Human-readable description
            feedback: Feedback status ("positive", "negative", None)
            metadata: Additional metadata to store
            generate_variations: Whether to generate and cache variations
            num_structure_variations: Override for structure variation count
            num_param_variations: Override for parameter variation count
            cache_pattern: Whether to cache the pattern

        Returns:
            Pattern ID if stored successfully, None on error
        """
        # Build pattern
        pattern = InstancePattern(
            pattern_id=str(uuid.uuid4()),
            component_paths=component_paths,
            instance_params=instance_params,
            description=description,
            dsl_notation=self._build_pattern_dsl(component_paths),
            feedback=feedback,
            metadata=metadata or {},
        )

        # Store to Qdrant if available
        stored = self._store_pattern_to_qdrant(pattern)

        # Cache the pattern
        if cache_pattern and hasattr(self, "cache_pattern"):
            try:
                self.cache_pattern(
                    key=pattern.pattern_id,
                    component_paths=component_paths,
                    instance_params=instance_params,
                    dsl_notation=pattern.dsl_notation,
                    structure_description=description,
                )
            except Exception as e:
                logger.warning(f"Failed to cache pattern: {e}")

        # Generate variations if requested (skip only for explicitly negative feedback)
        if generate_variations and feedback != "negative":
            self.generate_pattern_variations(
                pattern=pattern,
                num_structure_variations=num_structure_variations,
                num_param_variations=num_param_variations,
                cache_variations=True,
            )

        return pattern.pattern_id if stored else None

    def _store_pattern_to_qdrant(self, pattern: InstancePattern) -> bool:
        """Store pattern to Qdrant collection."""
        client = getattr(self, "client", None)
        collection_name = getattr(self, "collection_name", None)

        if not client or not collection_name:
            logger.debug("Qdrant not available for pattern storage")
            return True  # Still return True - pattern exists in memory

        try:
            # Get embedder
            embedder = getattr(self, "embedder", None)
            if not embedder:
                logger.warning("No embedder available for pattern storage")
                return False

            # Build embedding text
            embedding_text = self._build_pattern_embedding_text(pattern)

            # Generate embedding
            try:
                vectors_raw = list(embedder.embed([embedding_text]))[0]
                vectors = vectors_raw.tolist()
            except Exception as e:
                logger.warning(f"Failed to embed pattern: {e}")
                return False

            # Create point
            from qdrant_client.models import PointStruct

            point = PointStruct(
                id=pattern.pattern_id,
                vector=vectors,
                payload=pattern.to_payload(),
            )

            # Upsert
            client.upsert(
                collection_name=collection_name,
                points=[point],
            )

            logger.info(f"Stored instance pattern: {pattern.pattern_id[:8]}...")
            return True

        except Exception as e:
            logger.error(f"Failed to store pattern to Qdrant: {e}")
            return False

    def _build_pattern_embedding_text(self, pattern: InstancePattern) -> str:
        """Build text for pattern embedding."""
        parts = []

        # Add DSL
        if pattern.dsl_notation:
            parts.append(pattern.dsl_notation)

        # Add component names
        names = [p.split(".")[-1] if "." in p else p for p in pattern.component_paths]
        parts.append(" ".join(names))

        # Add description
        if pattern.description:
            parts.append(pattern.description[:100])

        # Add key param values
        for key, value in pattern.instance_params.items():
            if isinstance(value, str) and len(value) < 50:
                parts.append(f"{key}={value}")

        return " | ".join(parts)

    def generate_pattern_variations(
        self,
        pattern: InstancePattern,
        num_structure_variations: Optional[int] = None,
        num_param_variations: Optional[int] = None,
        cache_variations: bool = True,
    ) -> VariationFamily:
        """
        Generate variations of an instance pattern.

        Uses the relationship DAG for structural variations and
        parameter variators for value variations.

        Args:
            pattern: Source pattern
            num_structure_variations: Number of structural variations
            num_param_variations: Number of parameter variations per structure
            cache_variations: Whether to cache generated variations

        Returns:
            VariationFamily containing all variations
        """
        config = self._pattern_config
        num_struct = num_structure_variations or config["default_structure_variations"]
        num_param = num_param_variations or config["default_param_variations"]

        # Create family
        family = VariationFamily(
            parent_id=pattern.pattern_id,
            source_pattern=pattern,
        )

        # Get variators
        struct_variator = self._get_structure_variator()
        param_variator = self._get_param_variator()

        # Generate structural variations
        struct_variations = struct_variator.generate_variations(
            pattern.component_paths,
            num_struct,
            config["required_components"],
            config["repeatable_components"],
        )

        # Include original structure
        all_structures = [pattern.component_paths] + struct_variations

        # For each structure, generate parameter variations
        variation_idx = 0
        for struct_idx, struct_paths in enumerate(all_structures):
            # Generate param variations
            if struct_idx == 0:
                param_sets = [
                    pattern.instance_params
                ] + param_variator.generate_variations(
                    pattern.instance_params, num_param - 1
                )
            else:
                param_sets = param_variator.generate_variations(
                    pattern.instance_params, num_param
                )

            for param_idx, params in enumerate(param_sets):
                var_type = (
                    "original"
                    if struct_idx == 0 and param_idx == 0
                    else "structure"
                    if struct_idx > 0
                    else "parameter"
                )

                variation = PatternVariation(
                    variation_id=f"{pattern.pattern_id}:v{variation_idx}",
                    variation_type=var_type,
                    component_paths=struct_paths,
                    instance_params=params,
                    parent_id=pattern.pattern_id,
                    dsl_notation=self._build_pattern_dsl(struct_paths),
                    cache_key=f"{pattern.pattern_id}:v{variation_idx}",
                )

                family.variations.append(variation)
                family.cache_keys.append(variation.cache_key)

                # Cache the variation
                if cache_variations:
                    self._cache_variation(variation)

                variation_idx += 1

        # Store family
        self._get_variation_families()[pattern.pattern_id] = family

        logger.info(
            f"Generated {family.size} variations for {pattern.pattern_id[:8]}... "
            f"({num_struct} structures × {num_param} params)"
        )

        return family

    def _cache_variation(self, variation: PatternVariation) -> bool:
        """Cache a single variation."""
        if not hasattr(self, "cache_pattern"):
            return False

        try:
            self.cache_pattern(
                key=variation.cache_key,
                component_paths=variation.component_paths,
                instance_params=variation.instance_params,
                dsl_notation=variation.dsl_notation,
            )
            return True
        except Exception as e:
            logger.warning(f"Failed to cache variation: {e}")
            return False

    def get_variation_family(self, pattern_id: str) -> Optional[VariationFamily]:
        """Get a variation family by parent pattern ID."""
        return self._get_variation_families().get(pattern_id)

    def get_random_variation(
        self,
        pattern_id: str,
        variation_type: Optional[str] = None,
    ) -> Optional[PatternVariation]:
        """
        Get a random variation from a family.

        Args:
            pattern_id: Parent pattern ID
            variation_type: Optional filter ("structure", "parameter", "original")

        Returns:
            Random variation or None
        """
        import random

        family = self.get_variation_family(pattern_id)
        if not family:
            return None

        if variation_type:
            matching = [
                v for v in family.variations if v.variation_type == variation_type
            ]
            return random.choice(matching) if matching else None

        return random.choice(family.variations) if family.variations else None

    def get_cached_variation(
        self,
        pattern_id: str,
        variation_type: Optional[str] = None,
    ) -> Optional[Payload]:
        """
        Get a cached variation with hydrated component classes.

        Args:
            pattern_id: Parent pattern ID
            variation_type: Optional type filter

        Returns:
            Dict with component_classes or None
        """
        variation = self.get_random_variation(pattern_id, variation_type)
        if not variation:
            return None

        # Get from cache
        if hasattr(self, "get_cached_entry"):
            entry = self.get_cached_entry(
                variation.cache_key, variation.component_paths
            )
            if entry:
                return {
                    "key": entry.key,
                    "component_paths": entry.component_paths,
                    "instance_params": entry.instance_params,
                    "dsl_notation": entry.dsl_notation,
                    "component_classes": entry.component_classes,
                    "variation_type": variation.variation_type,
                    "_from_cache": True,
                }

        return None

    @property
    def pattern_stats(self) -> Payload:
        """Get instance pattern statistics."""
        families = self._get_variation_families()
        total_variations = sum(f.size for f in families.values())

        return {
            "num_families": len(families),
            "total_variations": total_variations,
            "avg_variations_per_family": (
                total_variations / len(families) if families else 0
            ),
            "config": self._pattern_config,
        }
