"""Pattern Library — in-memory storage with fuzzy matching and lifecycle.

Manages a collection of failure-recovery patterns. Supports:
- Adding/removing patterns with capacity limits
- Fuzzy signature matching with confidence thresholds
- Pattern usage tracking and confidence decay
- Category-based filtering and searching
- Bulk import/export for persistence
- Library merging for distributed setups
- Lifecycle management (decay, eviction, statistics)
"""

from typing import Any
from uuid import UUID

from xyglue.learn.models import Pattern


class PatternLibrary:
    """In-memory pattern library with fuzzy matching and lifecycle management.

    Patterns are stored by UUID. The library enforces a capacity limit,
    evicting the lowest-confidence non-seeded patterns when full.
    Seeded patterns are protected from eviction (they can still lose
    confidence but won't be removed).
    """

    def __init__(
        self,
        max_patterns: int = 500,
        min_confidence: float = 0.5,
        decay_rate: float = 0.001,
    ) -> None:
        self.max_patterns = max_patterns
        self.min_confidence = min_confidence
        self.decay_rate = decay_rate
        self._patterns: dict[UUID, Pattern] = {}

    # ── CRUD operations ────────────────────────────────────────────

    def add(self, pattern: Pattern) -> None:
        """Add a pattern, evicting the lowest if at capacity."""
        if len(self._patterns) >= self.max_patterns:
            self._evict_lowest()
        self._patterns[pattern.id] = pattern

    def add_batch(self, patterns: list[Pattern]) -> int:
        """Add multiple patterns. Returns count of patterns actually added."""
        added = 0
        for p in patterns:
            if len(self._patterns) < self.max_patterns:
                self._patterns[p.id] = p
                added += 1
            else:
                self._evict_lowest()
                if len(self._patterns) < self.max_patterns:
                    self._patterns[p.id] = p
                    added += 1
        return added

    def remove(self, pattern_id: UUID) -> bool:
        """Remove a pattern by ID. Returns True if found and removed."""
        return self._patterns.pop(pattern_id, None) is not None

    def get(self, pattern_id: UUID) -> Pattern | None:
        """Get a pattern by ID."""
        return self._patterns.get(pattern_id)

    def get_by_name(self, name: str) -> Pattern | None:
        """Find a pattern by name (first match)."""
        for p in self._patterns.values():
            if p.name == name:
                return p
        return None

    def get_all(self) -> list[Pattern]:
        """Get all patterns as a list."""
        return list(self._patterns.values())

    def clear(self) -> None:
        """Remove all patterns."""
        self._patterns.clear()

    @property
    def count(self) -> int:
        return len(self._patterns)

    # ── Matching ───────────────────────────────────────────────────

    def find_match(
        self, signature: str, min_confidence: float = 0.5
    ) -> Pattern | None:
        """Find the best matching pattern for a signature.

        Returns the highest-confidence pattern that matches the signature
        and meets the minimum confidence threshold.
        """
        best: Pattern | None = None
        best_conf = 0.0
        for pattern in self._patterns.values():
            if (
                pattern.confidence >= min_confidence
                and pattern.matches(signature)
            ):
                if pattern.confidence > best_conf:
                    best, best_conf = pattern, pattern.confidence
        return best

    def find_all_matches(
        self, signature: str, min_confidence: float = 0.3
    ) -> list[Pattern]:
        """Find all patterns matching a signature, sorted by confidence."""
        matches = [
            p for p in self._patterns.values()
            if p.confidence >= min_confidence and p.matches(signature)
        ]
        matches.sort(key=lambda p: p.confidence, reverse=True)
        return matches

    def find_ranked_matches(
        self, signature: str, min_confidence: float = 0.3, limit: int = 5
    ) -> list[tuple[Pattern, float]]:
        """Find matches ranked by combined match score and confidence.

        Returns (pattern, combined_score) tuples.
        """
        scored: list[tuple[Pattern, float]] = []
        for p in self._patterns.values():
            if p.confidence < min_confidence:
                continue
            match_score = p.match_score(signature)
            if match_score > 0:
                combined = match_score * 0.4 + p.confidence * 0.6
                scored.append((p, combined))
        scored.sort(key=lambda x: x[1], reverse=True)
        return scored[:limit]

    # ── Usage tracking ─────────────────────────────────────────────

    def record_use(self, pattern_id: UUID, success: bool) -> None:
        """Record a pattern usage and update its confidence.

        Removes non-seeded patterns that drop below minimum confidence.
        """
        pattern = self._patterns.get(pattern_id)
        if pattern:
            pattern.record_use(success)
            if pattern.confidence < self.min_confidence and not pattern.is_seeded:
                del self._patterns[pattern_id]

    # ── Category operations ────────────────────────────────────────

    def get_by_category(self, category: str) -> list[Pattern]:
        """Get all patterns in a category."""
        return [
            p for p in self._patterns.values()
            if p.category == category
        ]

    def get_categories(self) -> list[str]:
        """Get all unique categories."""
        cats = set(p.category for p in self._patterns.values() if p.category)
        return sorted(cats)

    def get_by_fix_type(self, fix_type: str) -> list[Pattern]:
        """Get all patterns with a specific fix type."""
        return [
            p for p in self._patterns.values()
            if p.fix_type == fix_type
        ]

    def get_by_tag(self, tag: str) -> list[Pattern]:
        """Get all patterns that have a specific tag."""
        return [
            p for p in self._patterns.values()
            if tag in p.tags
        ]

    # ── Lifecycle management ───────────────────────────────────────

    def decay_all(self, days: float | None = None) -> int:
        """Apply time-based decay to all patterns.

        If days is None, calculates from each pattern's last_used time.
        Returns count of patterns evicted due to low confidence.
        """
        evicted = 0
        to_remove: list[UUID] = []
        for pid, pattern in self._patterns.items():
            decay_days = days if days is not None else pattern.staleness_days
            if decay_days > 0:
                pattern.decay(decay_days, self.decay_rate)
            if pattern.should_evict:
                to_remove.append(pid)
        for pid in to_remove:
            del self._patterns[pid]
            evicted += 1
        return evicted

    def evict_stale(self, max_staleness_days: float = 90) -> int:
        """Remove non-seeded patterns that haven't been used recently.

        Returns count of evicted patterns.
        """
        to_remove: list[UUID] = []
        for pid, pattern in self._patterns.items():
            if not pattern.is_seeded and pattern.staleness_days > max_staleness_days:
                to_remove.append(pid)
        for pid in to_remove:
            del self._patterns[pid]
        return len(to_remove)

    def _evict_lowest(self) -> None:
        """Evict the lowest-confidence non-seeded pattern."""
        if not self._patterns:
            return
        lowest = min(
            self._patterns.values(),
            key=lambda p: (p.is_seeded, p.confidence),
        )
        if not lowest.is_seeded:
            del self._patterns[lowest.id]

    # ── Merging ────────────────────────────────────────────────────

    def merge_from(self, other: "PatternLibrary") -> int:
        """Merge patterns from another library.

        If a pattern with the same signature and category exists,
        merges stats. Otherwise adds the new pattern. Returns count
        of new patterns added.
        """
        added = 0
        for other_pattern in other.get_all():
            existing = self._find_similar(other_pattern)
            if existing:
                existing.merge_with(other_pattern)
            else:
                self.add(other_pattern)
                added += 1
        return added

    def _find_similar(self, pattern: Pattern) -> Pattern | None:
        """Find an existing pattern similar to the given one."""
        for p in self._patterns.values():
            if p.signature == pattern.signature and p.category == pattern.category:
                return p
        return None

    # ── Import / Export ────────────────────────────────────────────

    def export_all(self) -> list[dict[str, Any]]:
        """Export all patterns as dicts for persistence."""
        return [p.to_dict() for p in self._patterns.values()]

    def import_all(self, data: list[dict[str, Any]]) -> int:
        """Import patterns from dicts. Returns count imported."""
        count = 0
        for item in data:
            try:
                pattern = Pattern.from_dict(item)
                self.add(pattern)
                count += 1
            except Exception:
                continue
        return count

    # ── Statistics ─────────────────────────────────────────────────

    def get_stats(self) -> dict[str, Any]:
        """Return library statistics."""
        patterns = list(self._patterns.values())
        if not patterns:
            return {
                "total_patterns": 0,
                "seeded": 0,
                "learned": 0,
                "avg_confidence": 0,
                "proven_count": 0,
                "effective_count": 0,
                "categories": [],
            }

        seeded = [p for p in patterns if p.is_seeded]
        learned = [p for p in patterns if not p.is_seeded]
        proven = [p for p in patterns if p.is_proven]
        effective = [p for p in patterns if p.is_effective]
        total_uses = sum(p.uses for p in patterns)
        total_successes = sum(p.successes for p in patterns)

        return {
            "total_patterns": len(patterns),
            "seeded": len(seeded),
            "learned": len(learned),
            "proven_count": len(proven),
            "effective_count": len(effective),
            "avg_confidence": round(
                sum(p.confidence for p in patterns) / len(patterns), 3
            ),
            "total_uses": total_uses,
            "total_successes": total_successes,
            "overall_success_rate": round(
                total_successes / total_uses if total_uses > 0 else 0, 3
            ),
            "categories": sorted(set(p.category for p in patterns if p.category)),
            "fix_types": sorted(set(p.fix_type for p in patterns if p.fix_type)),
        }

    def get_top_patterns(self, limit: int = 10) -> list[Pattern]:
        """Get top patterns by confidence."""
        patterns = sorted(
            self._patterns.values(),
            key=lambda p: p.confidence,
            reverse=True,
        )
        return patterns[:limit]

    def get_least_effective(self, limit: int = 10) -> list[Pattern]:
        """Get least effective non-seeded patterns (eviction candidates)."""
        patterns = [p for p in self._patterns.values() if not p.is_seeded]
        patterns.sort(key=lambda p: (p.confidence, p.success_rate))
        return patterns[:limit]
