"""Combined strategy selector with Thompson Sampling.

Provides a high-level interface for selecting fix strategies
based on error type and learned preferences.
"""

from __future__ import annotations

import logging
from typing import Any, Dict, FrozenSet, List, Optional, Set, Tuple

from autodebug.strategies.bandit_selector import (
    ContextualThompsonSelector,
    StrategyStats,
    ThompsonSamplingSelector,
)
from autodebug.strategies.handlers import get_handler, HandlerContext, HandlerResult
from autodebug.strategies.strategy_registry import (
    STRATEGIES,
    FixStrategy,
    get_applicable_strategies,
    get_strategy,
)

logger = logging.getLogger(__name__)

# Default persistence path
DEFAULT_PERSISTENCE_PATH = "~/.hikaflow/strategy_stats.json"

# =============================================================================
# E6/F-09: Strategy conflict detection
# =============================================================================
# Pairs of strategy IDs that are mutually exclusive.  When both are selected
# by Thompson Sampling, only the higher-ranked one is kept.

STRATEGY_CONFLICTS: List[FrozenSet[str]] = [
    frozenset({"fix_actual_value", "fix_expected_value"}),
    frozenset({"increase_timeout", "add_async_handling"}),
    frozenset({"use_get_default", "validate_keys_early"}),
    frozenset({"add_type_check", "fix_type_conversion"}),
    frozenset({"fix_conditional", "remove_dead_code"}),
]

# Pre-build a lookup: strategy_id -> set of conflicting strategy IDs
_CONFLICT_MAP: Dict[str, Set[str]] = {}
for pair in STRATEGY_CONFLICTS:
    for sid in pair:
        _CONFLICT_MAP.setdefault(sid, set()).update(pair - {sid})


def _remove_conflicts(strategies: List[FixStrategy]) -> List[FixStrategy]:
    """Filter out strategies that conflict with higher-ranked ones.

    Strategies earlier in the list are considered higher-ranked.  When a
    conflict is detected, the later (lower-ranked) strategy is removed.

    Args:
        strategies: Ordered list of strategies (best first)

    Returns:
        Filtered list with conflicts removed
    """
    accepted: List[FixStrategy] = []
    accepted_ids: Set[str] = set()

    for strategy in strategies:
        conflicting = _CONFLICT_MAP.get(strategy.id, set())
        if conflicting & accepted_ids:
            # This strategy conflicts with one already accepted
            logger.debug(
                f"Skipping strategy '{strategy.id}' -- conflicts with "
                f"already-selected: {conflicting & accepted_ids}"
            )
            continue
        accepted.append(strategy)
        accepted_ids.add(strategy.id)

    return accepted


class StrategySelector:
    """Select strategies using Thompson Sampling with error-type filtering.

    Combines:
    1. Error-type filtering to get applicable strategies
    2. Thompson Sampling to select best strategies based on history
    """

    def __init__(
        self,
        persistence_path: Optional[str] = None,
        use_thompson_sampling: bool = True,
    ):
        """Initialize the selector.

        Args:
            persistence_path: Path to persist strategy stats
            use_thompson_sampling: Use Thompson Sampling vs static priority
        """
        self.use_thompson_sampling = use_thompson_sampling
        self.bandit = ContextualThompsonSelector(
            persistence_path=persistence_path or DEFAULT_PERSISTENCE_PATH
        )

    def select_strategies(
        self,
        error_type: str,
        error_message: Optional[str] = None,
        available_context: Optional[List[str]] = None,
        max_strategies: int = 3,
        handler_context: Optional[HandlerContext] = None,
    ) -> List[FixStrategy]:
        """Select appropriate strategies for this error.

        Args:
            error_type: Exception type (e.g., "KeyError")
            error_message: Optional error message for context
            available_context: List of available context types
            max_strategies: Maximum strategies to return
            handler_context: Optional HandlerContext for exception-specific
                handler guidance (from autodebug.strategies.handlers)

        Returns:
            List of selected strategies, best first
        """
        # Get applicable strategies based on error type
        applicable = get_applicable_strategies(error_type)

        if not applicable:
            # Fall back to generic strategies
            applicable = [s for s in STRATEGIES.values() if "*" in s.applicable_errors]

        # Consult exception-specific handlers for reordering guidance
        handler_recommended: List[str] = []
        if handler_context is not None:
            handler = get_handler(error_type)
            if handler:
                try:
                    handler_result = handler.handle(handler_context)
                    handler_recommended = handler_result.recommended_strategies
                except Exception as e:
                    logger.debug(f"Handler for {error_type} failed: {e}")

        # If handler provided recommendations, reorder applicable strategies
        # so handler-recommended ones come first (preserving their order)
        if handler_recommended:
            recommended_set = set(handler_recommended)
            # Split into handler-recommended and the rest
            front = [s for s in applicable if s.id in recommended_set]
            rest = [s for s in applicable if s.id not in recommended_set]
            # Sort front by handler recommendation order
            front.sort(key=lambda s: (
                handler_recommended.index(s.id)
                if s.id in handler_recommended else len(handler_recommended)
            ))
            applicable = front + rest

        if not self.use_thompson_sampling:
            # Just use static priority (with handler reordering already applied)
            # E6/F-09: Remove conflicting strategies before returning
            filtered = _remove_conflicts(applicable)
            return filtered[:max_strategies]

        # Use Thompson Sampling to select best strategies
        # Request extra candidates so we still have enough after conflict removal
        if error_message:
            selected = self.bandit.select_strategies(
                error_type=error_type,
                error_message=error_message,
                available_strategies=applicable,
                num_strategies=max_strategies + len(STRATEGY_CONFLICTS),
            )
        else:
            selected = self.bandit.global_selector.select_strategies(
                error_type=error_type,
                available_strategies=applicable,
                num_strategies=max_strategies + len(STRATEGY_CONFLICTS),
            )

        # E6/F-09: Remove conflicting strategies
        selected = _remove_conflicts(selected)

        return selected[:max_strategies]

    def select_single_strategy(
        self,
        error_type: str,
        error_message: Optional[str] = None,
    ) -> Optional[FixStrategy]:
        """Select the single best strategy for this error.

        Args:
            error_type: Exception type
            error_message: Optional error message

        Returns:
            Best strategy or None
        """
        selected = self.select_strategies(
            error_type=error_type,
            error_message=error_message,
            max_strategies=1,
        )
        return selected[0] if selected else None

    def record_outcome(
        self,
        error_type: str,
        strategy_id: str,
        success: bool,
        error_message: Optional[str] = None,
        outcome: Optional[str] = None,
    ) -> None:
        """Record strategy outcome for learning.

        Args:
            error_type: Exception type
            strategy_id: ID of strategy used
            success: Whether the strategy succeeded
        """
        if error_message:
            self.bandit.update(
                error_type,
                error_message,
                strategy_id,
                success,
                outcome=outcome,
            )
        else:
            self.bandit.global_selector.update(
                error_type,
                strategy_id,
                success,
                outcome=outcome,
            )

    def get_strategy_stats(self) -> Dict[str, Dict[str, Any]]:
        """Get all strategy statistics for monitoring.

        Returns:
            Dict mapping (error_type:strategy) to stats
        """
        return self.bandit.global_selector.get_all_stats()

    def get_stats_for_error(self, error_type: str) -> Dict[str, StrategyStats]:
        """Get stats for a specific error type.

        Args:
            error_type: Exception type

        Returns:
            Dict mapping strategy ID to stats
        """
        return self.bandit.global_selector.get_stats_by_error_type(error_type)

    def get_best_strategy(self, error_type: str) -> Optional[str]:
        """Get the best strategy for an error type based on history.

        Args:
            error_type: Exception type

        Returns:
            Strategy ID with highest success rate, or None
        """
        stats = self.get_stats_for_error(error_type)
        if not stats:
            return None

        # Find strategy with highest success rate (among those with data)
        best_id = None
        best_rate = -1.0
        for strategy_id, stat in stats.items():
            if stat.total > 0 and stat.success_rate > best_rate:
                best_rate = stat.success_rate
                best_id = strategy_id

        return best_id

    def format_stats_report(self) -> str:
        """Format strategy stats as a readable report.

        Returns:
            Formatted report string
        """
        stats = self.get_strategy_stats()
        if not stats:
            return "No strategy statistics collected yet."

        lines = ["Strategy Statistics:", "=" * 50]

        # Group by error type
        by_error: Dict[str, List[tuple]] = {}
        for key, data in stats.items():
            error_type, strategy_id = key.split(":", 1)
            if error_type not in by_error:
                by_error[error_type] = []
            by_error[error_type].append((strategy_id, data))

        for error_type, strategies in sorted(by_error.items()):
            lines.append(f"\n{error_type}:")

            # Sort by success rate
            strategies.sort(key=lambda x: -x[1]["rate"])

            for strategy_id, data in strategies:
                rate = data["rate"] * 100
                total = data["successes"] + data["failures"]
                conf = data.get("confidence", 0) * 100
                lines.append(
                    f"  {strategy_id}: {rate:.1f}% success "
                    f"({data['successes']}/{total} trials, {conf:.0f}% conf)"
                )

        return "\n".join(lines)


# =============================================================================
# Bridge between strategy_registry and bandit_selector (H-164)
# =============================================================================


def registry_strategies_to_bandit_arms(error_type: str) -> List[FixStrategy]:
    """Map strategy registry entries to bandit arms for unified selection.

    This bridges the two disconnected strategy systems:
    - strategy_registry.py: static strategy definitions with priority
    - bandit_selector.py: Thompson Sampling with learned success rates

    The bandit selector operates on FixStrategy objects (which have .id and
    .priority). This function provides the applicable registry strategies
    as bandit arms, enabling the bandit to learn which registry strategies
    work best for each error type.

    Args:
        error_type: Exception type (e.g., "KeyError", "TypeError")

    Returns:
        List of FixStrategy objects suitable as bandit arms
    """
    return get_applicable_strategies(error_type)


def select_with_bandit_bridge(
    error_type: str,
    persistence_path: Optional[str] = None,
    max_strategies: int = 3,
) -> List[FixStrategy]:
    """Select strategies using the bandit over registry strategies.

    Combines static registry filtering with Thompson Sampling learning.
    This is the unified entry point that connects both systems.

    Args:
        error_type: Exception type
        persistence_path: Optional persistence path for bandit stats
        max_strategies: Maximum strategies to return

    Returns:
        Selected strategies ordered by bandit sampling
    """
    arms = registry_strategies_to_bandit_arms(error_type)
    if not arms:
        return []

    bandit = ThompsonSamplingSelector(
        persistence_path=persistence_path or DEFAULT_PERSISTENCE_PATH,
    )
    return bandit.select_strategies(
        error_type=error_type,
        available_strategies=arms,
        num_strategies=max_strategies,
    )


# =============================================================================
# Module-level convenience functions
# E6/F-13: Use a cached singleton per persistence_path to avoid redundant
# disk I/O when called in rapid succession (e.g. batch recording).
# =============================================================================

_selector_cache: Dict[Optional[str], StrategySelector] = {}


def clear_selector_cache() -> None:
    """Clear the module-level selector cache.

    Useful for test isolation — prevents cached state from leaking
    between test cases that use different configurations.
    """
    _selector_cache.clear()


def _get_cached_selector(persistence_path: Optional[str] = None) -> StrategySelector:
    """Return a cached StrategySelector for the given persistence path.

    Avoids creating a new instance (and re-reading stats from disk) on
    every call to the convenience functions.
    """
    if persistence_path not in _selector_cache:
        _selector_cache[persistence_path] = StrategySelector(
            persistence_path=persistence_path
        )
    return _selector_cache[persistence_path]


def select_strategies(
    error_type: str,
    max_strategies: int = 3,
    persistence_path: Optional[str] = None,
    error_message: Optional[str] = None,
) -> List[FixStrategy]:
    """Convenience function to select strategies.

    Args:
        error_type: Exception type
        max_strategies: Maximum strategies to return
        persistence_path: Optional custom persistence path

    Returns:
        List of selected strategies
    """
    selector = _get_cached_selector(persistence_path)
    return selector.select_strategies(
        error_type,
        error_message=error_message,
        max_strategies=max_strategies,
    )


def record_outcome(
    error_type: str,
    strategy_id: str,
    success: bool,
    persistence_path: Optional[str] = None,
    error_message: Optional[str] = None,
    outcome: Optional[str] = None,
) -> None:
    """Convenience function to record strategy outcome.

    Args:
        error_type: Exception type
        strategy_id: Strategy ID
        success: Whether strategy succeeded
        persistence_path: Optional custom persistence path
    """
    selector = _get_cached_selector(persistence_path)
    selector.record_outcome(
        error_type,
        strategy_id,
        success,
        error_message=error_message,
        outcome=outcome,
    )


def batch_record_outcomes(
    outcomes: List[Tuple[str, str, bool]],
    persistence_path: Optional[str] = None,
) -> None:
    """Record multiple strategy outcomes efficiently (E6/F-13).

    Loads stats once, applies all updates, then saves once.

    Args:
        outcomes: List of (error_type, strategy_id, success) tuples
        persistence_path: Optional custom persistence path
    """
    selector = _get_cached_selector(persistence_path)
    for error_type, strategy_id, success in outcomes:
        selector.record_outcome(error_type, strategy_id, success)
