"""Conductor bypass logic for eb1-delta-preview optimization.

Determines when to skip expensive conductor orchestration based on routing signals.
Implements three bypass criteria for non-OOD queries:
1. Clear decision - GNN is confident and decisive
2. Indistinguishable models - Decomposition won't help pick between similar options
3. Low expected value - Predicted performance range too small to justify cost

Design rationale:
- Conservative by default - only bypass when we're confident it's safe
- Focus on cost reduction without sacrificing quality
- Target 30-40% cost savings on queries that don't need decomposition
- OOD detection is handled by TieredRouter before this logic runs

Note: This bypass logic should only be invoked for non-OOD queries.
TieredRouter owns tier classification and routes OOD queries directly to top-tier models.
"""

from dataclasses import dataclass
from typing import Literal

from embergraphrouter.inference.tiered_router import DifficultySignals


@dataclass(frozen=True)
class ConductorThresholds:
    """Thresholds for conductor bypass decisions (non-OOD queries only).

    Note: OOD detection is handled by TieredRouter, not here.
    These thresholds only apply to queries that are already confirmed as in-distribution.

    Attributes:
        clear_decision_max_prob: Minimum confidence for "clear decision" bypass.
        clear_decision_logit_gap: Minimum gap between top-2 models for clear decision.
        indistinguishable_logit_gap: Max gap for "indistinguishable models" bypass.
        indistinguishable_max_perf_range: Max predicted perf range for indistinguishable.
        min_conductor_value: Minimum predicted perf improvement to justify conductor.
    """

    # Clear decision: GNN is confident and decisive
    clear_decision_max_prob: float = 0.6
    clear_decision_logit_gap: float = 0.2

    # Indistinguishable: Models are too similar for decomposition to help
    indistinguishable_logit_gap: float = 0.05
    indistinguishable_max_perf_range: float = 0.1

    # Low value: Conductor overhead not justified
    min_conductor_value: float = 0.15


# Preset configurations for different risk/cost tradeoffs
CONSERVATIVE_THRESHOLDS = ConductorThresholds(
    clear_decision_max_prob=0.70,  # Very high confidence required
    clear_decision_logit_gap=0.25,  # Large gap required
    indistinguishable_logit_gap=0.03,  # Very tight similarity
    indistinguishable_max_perf_range=0.08,
    min_conductor_value=0.20,  # Higher value threshold
)

BALANCED_THRESHOLDS = ConductorThresholds()  # Default values

AGGRESSIVE_THRESHOLDS = ConductorThresholds(
    clear_decision_max_prob=0.50,  # Lower confidence acceptable
    clear_decision_logit_gap=0.15,  # Smaller gap acceptable
    indistinguishable_logit_gap=0.08,  # Looser similarity
    indistinguishable_max_perf_range=0.15,
    min_conductor_value=0.10,  # Lower value threshold
)


BypassReason = Literal[
    "BYPASS_CLEAR_DECISION",
    "BYPASS_INDISTINGUISHABLE",
    "BYPASS_LOW_VALUE",
    "RUN_CONDUCTOR",
]

FallbackAction = Literal["use_gnn_pick", "use_top_tier"]


def should_bypass_conductor(
    signals: DifficultySignals,
    thresholds: ConductorThresholds,
    predicted_perf_range: float,
) -> tuple[bool, BypassReason, FallbackAction | None]:
    """Decide whether to bypass conductor based on routing signals.

    IMPORTANT: This function assumes the query is NOT OOD. OOD queries should be
    handled by TieredRouter before calling this function.

    Evaluates three bypass criteria in priority order (for non-OOD queries):
    1. Clear decision - GNN confident and decisive → use GNN pick
    2. Indistinguishable models - Too similar to benefit from decomposition → use GNN pick
    3. Low expected value - Improvement too small to justify cost → use GNN pick

    Args:
        signals: Routing signals from GNN (confidence, entropy, logit stats).
        thresholds: Configuration for bypass decision boundaries.
        predicted_perf_range: Expected performance difference between best/worst models.

    Returns:
        (should_bypass, reason, fallback_action)
        - should_bypass: True if conductor should be skipped
        - reason: String describing why bypass decision was made
        - fallback_action: "use_gnn_pick" or "use_top_tier" if bypassing, else None
    """
    # Check 1: Clear decision - GNN is confident and top model stands out
    if (
        signals.max_prob > thresholds.clear_decision_max_prob
        and signals.logit_margin > thresholds.clear_decision_logit_gap
    ):
        return (True, "BYPASS_CLEAR_DECISION", "use_gnn_pick")

    # Check 2: Models indistinguishable - decomposition won't help differentiate
    if (
        signals.logit_margin < thresholds.indistinguishable_logit_gap
        and predicted_perf_range < thresholds.indistinguishable_max_perf_range
    ):
        return (True, "BYPASS_INDISTINGUISHABLE", "use_gnn_pick")

    # Check 3: Low expected benefit - conductor cost not justified
    if predicted_perf_range < thresholds.min_conductor_value:
        return (True, "BYPASS_LOW_VALUE", "use_gnn_pick")

    # No bypass criteria met - run conductor
    return (False, "RUN_CONDUCTOR", None)


def estimate_perf_range(signals: DifficultySignals) -> float:
    """Estimate performance range between best and worst model choices.

    Uses routing signals to predict how much performance varies across models.
    Higher entropy and lower confidence generally indicate wider performance spread.

    Args:
        signals: Routing signals from GNN.

    Returns:
        Estimated performance range [0.0, 1.0].
    """
    # Simple heuristic: entropy correlates with performance variance
    # High entropy = models similar = low range
    # Low entropy + low confidence = OOD = high range
    # Low entropy + high confidence = clear winner = medium range

    if signals.entropy > 2.0:
        # Very high entropy - models extremely similar
        return 0.05

    if signals.max_prob < 0.3:
        # Low confidence suggests significant model differences
        return 0.40

    if signals.max_prob > 0.7:
        # High confidence - clear winner, but others still viable
        return 0.25

    # Medium confidence - moderate spread
    return 0.15


@dataclass
class ConductorBypassMetrics:
    """Metrics tracking bypass decision effectiveness (non-OOD queries only).

    Tracks bypass rate, cost savings, latency improvements, and quality impact
    to enable data-driven threshold tuning.

    Note: OOD queries are handled by TieredRouter and should be tracked separately.
    This tracks only the three non-OOD bypass criteria.

    Attributes:
        total_queries: Total queries processed (non-OOD only).
        bypass_clear_decision: Count bypassed due to clear GNN decision.
        bypass_indistinguishable: Count bypassed due to similar models.
        bypass_low_value: Count bypassed due to low expected value.
        conductor_run: Count where conductor was invoked.
        estimated_cost_saved_usd: Estimated cost savings from bypasses.
        actual_cost_usd: Actual cost incurred.
        avg_bypass_latency_ms: Average latency for bypassed queries.
        avg_conductor_latency_ms: Average latency for conductor queries.
        bypass_accuracy: Accuracy of bypass decisions (requires ground truth labels).
    """

    total_queries: int = 0
    bypass_clear_decision: int = 0
    bypass_indistinguishable: int = 0
    bypass_low_value: int = 0
    conductor_run: int = 0

    estimated_cost_saved_usd: float = 0.0
    actual_cost_usd: float = 0.0

    avg_bypass_latency_ms: float = 0.0
    avg_conductor_latency_ms: float = 0.0

    bypass_accuracy: float | None = None  # Requires ground truth evaluation

    @property
    def bypass_rate(self) -> float:
        """Fraction of queries that bypassed conductor."""
        if self.total_queries == 0:
            return 0.0
        return (
            self.bypass_clear_decision
            + self.bypass_indistinguishable
            + self.bypass_low_value
        ) / self.total_queries

    @property
    def cost_reduction(self) -> float:
        """Fraction of costs saved via bypass."""
        total_cost = self.actual_cost_usd + self.estimated_cost_saved_usd
        if total_cost == 0.0:
            return 0.0
        return self.estimated_cost_saved_usd / total_cost

    @property
    def latency_improvement(self) -> float:
        """Latency speedup from bypassing (ratio)."""
        if self.avg_bypass_latency_ms == 0.0:
            return 0.0
        return self.avg_conductor_latency_ms / self.avg_bypass_latency_ms

    def to_dict(self) -> dict:
        """Export metrics as dictionary for logging/monitoring."""
        return {
            "total_queries": self.total_queries,
            "bypass_rate": self.bypass_rate,
            "cost_reduction": self.cost_reduction,
            "latency_improvement": self.latency_improvement,
            "bypass_breakdown": {
                "clear_decision": self.bypass_clear_decision,
                "indistinguishable": self.bypass_indistinguishable,
                "low_value": self.bypass_low_value,
            },
            "conductor_run": self.conductor_run,
            "estimated_cost_saved_usd": self.estimated_cost_saved_usd,
            "actual_cost_usd": self.actual_cost_usd,
            "avg_bypass_latency_ms": self.avg_bypass_latency_ms,
            "avg_conductor_latency_ms": self.avg_conductor_latency_ms,
            "bypass_accuracy": self.bypass_accuracy,
        }
