"""
Value Detection Engine
======================
The Smart Way to Beat the Bookmakers

This engine doesn't just predict winners - it finds VALUE.
The key insight: We don't need to predict the winner, we need to find
where the bookmaker made a mistake in their odds.

Core Philosophy:
- High Margin = High Uncertainty = Potential Value
- Model Probability > Implied Probability = Value Bet
- The goal is NOT to predict correctly, but to find +EV bets

Author: AI Engine V21
"""

import math
from dataclasses import dataclass
from typing import Dict, List, Optional, Tuple
from collections import defaultdict


@dataclass
class ValueBet:
    """Represents a value bet opportunity"""
    outcome: str  # "1", "X", "2"
    model_probability: float  # Our model's probability (0-1)
    implied_probability: float  # Bookmaker's implied probability (0-1)
    odds: float  # Bookmaker's odds
    edge: float  # model_prob - implied_prob (as percentage)
    expected_value: float  # EV = (prob * odds) - 1
    kelly_fraction: float  # Optimal bet size
    confidence: str  # "HIGH", "MEDIUM", "LOW"
    reasons: List[str]  # Why this is value
    
    def to_dict(self) -> dict:
        return {
            "outcome": self.outcome,
            "model_prob": round(self.model_probability * 100, 1),
            "implied_prob": round(self.implied_probability * 100, 1),
            "odds": self.odds,
            "edge": round(self.edge * 100, 1),
            "ev": round(self.expected_value * 100, 1),
            "kelly": round(self.kelly_fraction * 100, 1),
            "confidence": self.confidence,
            "reasons": self.reasons
        }


@dataclass
class MarginAnalysis:
    """Analysis of bookmaker margin"""
    raw_margin: float  # Sum of raw implied probabilities - 1
    true_margin: float  # Adjusted for favorite-longshot bias
    favorite_outcome: str
    favorite_odds: float
    uncertainty_level: str  # "LOW", "MEDIUM", "HIGH", "EXTREME"
    
    def to_dict(self) -> dict:
        return {
            "raw_margin": round(self.raw_margin * 100, 1),
            "true_margin": round(self.true_margin * 100, 1),
            "favorite": self.favorite_outcome,
            "favorite_odds": self.favorite_odds,
            "uncertainty": self.uncertainty_level
        }


class ValueDetectionEngine:
    """
    The Smart Betting Engine
    
    This engine finds value bets by comparing model probabilities
    with bookmaker implied probabilities.
    
    Key Insights:
    1. Margin > 18% → Bookmaker is unsure, potential value on underdog
    2. Margin > 20% → Bookmaker sees high risk, BIG potential value
    3. Favorite odds 1.40-1.60 → Highest upset rate historically
    4. Away favorites have higher upset rate than home favorites
    """
    
    # Historical upset rates by favorite odds range
    UPSET_RATES = {
        (1.00, 1.25): 0.08,   # 8% upset rate
        (1.25, 1.40): 0.18,   # 18% upset rate
        (1.40, 1.60): 0.33,   # 33% upset rate - DANGER ZONE
        (1.60, 1.80): 0.28,   # 28% upset rate
        (1.80, 2.00): 0.35,   # 35% upset rate
        (2.00, 2.50): 0.42,   # 42% upset rate
        (2.50, 3.00): 0.45,   # 45% upset rate
        (3.00, 5.00): 0.55,   # 55% upset rate
    }
    
    # Margin thresholds
    MARGIN_LOW = 0.06      # 6% - bookmaker very confident
    MARGIN_MEDIUM = 0.12   # 12% - normal margin
    MARGIN_HIGH = 0.18     # 18% - bookmaker unsure
    MARGIN_EXTREME = 0.22  # 22% - bookmaker very unsure
    
    def __init__(self):
        self.historical_data = []  # For learning
        self.value_threshold = 0.03  # Minimum 3% edge to consider value
        
    def calculate_margin(self, odds_1: float, odds_x: float, odds_2: float) -> MarginAnalysis:
        """
        Calculate bookmaker margin and analyze uncertainty.
        
        Higher margin = More uncertainty = More potential value
        """
        if not all([odds_1 > 1, odds_x > 1, odds_2 > 1]):
            return MarginAnalysis(0, 0, "X", 0, "UNKNOWN")
        
        # Raw implied probabilities
        imp_1 = 1 / odds_1
        imp_x = 1 / odds_x
        imp_2 = 1 / odds_2
        
        raw_margin = imp_1 + imp_x + imp_2 - 1
        
        # Determine favorite
        if odds_1 <= odds_x and odds_1 <= odds_2:
            favorite_outcome = "1"
            favorite_odds = odds_1
        elif odds_2 <= odds_1 and odds_2 <= odds_x:
            favorite_outcome = "2"
            favorite_odds = odds_2
        else:
            favorite_outcome = "X"
            favorite_odds = odds_x
        
        # Adjust for favorite-longshot bias
        # Bookmakers typically overprice longshots
        true_margin = raw_margin * 0.85  # Simplified adjustment
        
        # Determine uncertainty level
        if raw_margin < self.MARGIN_LOW:
            uncertainty = "LOW"
        elif raw_margin < self.MARGIN_MEDIUM:
            uncertainty = "MEDIUM"
        elif raw_margin < self.MARGIN_HIGH:
            uncertainty = "HIGH"
        else:
            uncertainty = "EXTREME"
        
        return MarginAnalysis(
            raw_margin=raw_margin,
            true_margin=true_margin,
            favorite_outcome=favorite_outcome,
            favorite_odds=favorite_odds,
            uncertainty_level=uncertainty
        )
    
    def get_historical_upset_rate(self, favorite_odds: float) -> float:
        """Get historical upset rate for given favorite odds"""
        for (low, high), rate in self.UPSET_RATES.items():
            if low <= favorite_odds < high:
                return rate
        return 0.40  # Default for very high odds
    
    def calculate_edge(
        self,
        model_prob: float,
        odds: float,
        margin: float
    ) -> Tuple[float, float]:
        """
        Calculate the edge (advantage) we have over the bookmaker.
        
        Returns: (edge, expected_value)
        
        Edge = Model Probability - True Implied Probability
        EV = (Probability * Odds) - 1
        """
        if odds <= 1:
            return 0, -1
        
        # Raw implied probability
        implied = 1 / odds
        
        # Adjust for margin (proportional adjustment)
        # This gives us the "true" implied probability
        # Assuming bookmaker spreads margin proportionally
        true_implied = implied  # Simplified - could be more sophisticated
        
        edge = model_prob - true_implied
        ev = (model_prob * odds) - 1
        
        return edge, ev
    
    def calculate_kelly_fraction(
        self,
        probability: float,
        odds: float,
        half_kelly: bool = True
    ) -> float:
        """
        Calculate optimal bet size using Kelly Criterion.
        
        Kelly = (p * b - 1) / (b - 1)
        where b = odds - 1
        
        We use half Kelly for safety.
        """
        if odds <= 1:
            return 0
        
        b = odds - 1
        kelly = (probability * b - 1) / b
        
        # Don't bet if negative
        if kelly < 0:
            return 0
        
        # Use half Kelly for safety
        if half_kelly:
            kelly = kelly / 2
        
        # Cap at 10% of bankroll
        return min(kelly, 0.10)
    
    def find_value_bets(
        self,
        model_probs: Dict[str, float],
        odds: Dict[str, float],
        match_context: Optional[Dict] = None
    ) -> List[ValueBet]:
        """
        Find all value bets in a match.
        
        This is the MAIN method - it finds where we have an edge.
        
        Args:
            model_probs: {"1": 0.55, "X": 0.25, "2": 0.20}
            odds: {"1": 1.25, "X": 4.50, "2": 8.00}
            match_context: Additional context (form, h2h, etc.)
        
        Returns:
            List of ValueBet objects, sorted by edge
        """
        value_bets = []
        
        # Calculate margin
        margin_analysis = self.calculate_margin(
            odds.get("1", 0),
            odds.get("X", 0),
            odds.get("2", 0)
        )
        
        # Analyze each outcome
        for outcome in ["1", "X", "2"]:
            prob = model_probs.get(outcome, 0)
            odd = odds.get(outcome, 0)
            
            if prob <= 0 or odd <= 1:
                continue
            
            edge, ev = self.calculate_edge(prob, odd, margin_analysis.raw_margin)
            kelly = self.calculate_kelly_fraction(prob, odd)
            
            # Determine if this is a value bet
            reasons = []
            
            # 1. Basic edge
            if edge > self.value_threshold:
                reasons.append(f"Edge: +{round(edge*100, 1)}% over bookmaker")
            
            # 2. High margin bonus
            if margin_analysis.raw_margin > self.MARGIN_HIGH:
                reasons.append(f"High margin ({round(margin_analysis.raw_margin*100, 1)}%) = uncertainty")
                
                # Boost edge for underdogs in high margin matches
                if outcome != margin_analysis.favorite_outcome:
                    edge += 0.02  # 2% bonus
                    reasons.append("Underdog in high-margin match = bonus value")
            
            # 3. Favorite odds trap
            fav_odds = margin_analysis.favorite_odds
            if margin_analysis.favorite_outcome != outcome:
                upset_rate = self.get_historical_upset_rate(fav_odds)
                if upset_rate > 0.25:
                    reasons.append(f"Favorite odds {fav_odds} has {round(upset_rate*100)}% upset rate")
                    
                    # Extra bonus for 1.40-1.60 range
                    if 1.40 <= fav_odds <= 1.60:
                        edge += 0.03
                        reasons.append("DANGER ZONE: 1.40-1.60 odds = highest upset risk")
            
            # 4. Away favorite risk
            if margin_analysis.favorite_outcome == "2" and outcome == "1":
                edge += 0.015
                reasons.append("Away favorite = extra home value")
            
            # 5. EV positive
            if ev > 0:
                reasons.append(f"Positive EV: +{round(ev*100, 1)}%")
            
            # Only add if we have reasons (value detected)
            if reasons and edge > 0:
                # Determine confidence
                if edge > 0.08 or (edge > 0.05 and kelly > 0.03):
                    confidence = "HIGH"
                elif edge > 0.05:
                    confidence = "MEDIUM"
                else:
                    confidence = "LOW"
                
                value_bets.append(ValueBet(
                    outcome=outcome,
                    model_probability=prob,
                    implied_probability=1/odd,
                    odds=odd,
                    edge=edge,
                    expected_value=ev,
                    kelly_fraction=kelly,
                    confidence=confidence,
                    reasons=reasons
                ))
        
        # Sort by edge (highest first)
        value_bets.sort(key=lambda x: x.edge, reverse=True)
        
        return value_bets
    
    def predict_with_value(
        self,
        model_probs: Dict[str, float],
        odds: Dict[str, float],
        match_context: Optional[Dict] = None
    ) -> Dict:
        """
        Make a prediction based on VALUE, not just probability.
        
        This is the smart way to bet:
        - If there's clear value on one outcome → Bet it
        - If there's no value → NO BET (don't force it)
        - If margin is extreme → Look for underdog value
        
        Returns:
            {
                "best_value": ValueBet or None,
                "alternative_value": ValueBet or None,
                "margin_analysis": MarginAnalysis,
                "recommendation": str,
                "confidence": str
            }
        """
        margin_analysis = self.calculate_margin(
            odds.get("1", 0),
            odds.get("X", 0),
            odds.get("2", 0)
        )
        
        value_bets = self.find_value_bets(model_probs, odds, match_context)
        
        result = {
            "margin_analysis": margin_analysis.to_dict(),
            "value_bets": [vb.to_dict() for vb in value_bets],
            "best_value": None,
            "alternative_value": None,
            "recommendation": "NO_BET",
            "confidence": "LOW",
            "reasoning": []
        }
        
        if not value_bets:
            result["reasoning"].append("No value detected in any outcome")
            result["reasoning"].append("Bookmaker odds are efficient for this match")
            return result
        
        # Get best value bet
        best = value_bets[0]
        result["best_value"] = best.to_dict()
        
        if len(value_bets) > 1:
            result["alternative_value"] = value_bets[1].to_dict()
        
        # Determine recommendation
        if best.confidence == "HIGH" and best.edge > 0.05:
            result["recommendation"] = f"BET_{best.outcome}"
            result["confidence"] = "HIGH"
            result["reasoning"] = best.reasons
            result["reasoning"].append(f"Strong value on {best.outcome} with {round(best.edge*100, 1)}% edge")
        
        elif best.confidence == "MEDIUM" or best.edge > 0.03:
            result["recommendation"] = f"CONSIDER_{best.outcome}"
            result["confidence"] = "MEDIUM"
            result["reasoning"] = best.reasons
            result["reasoning"].append(f"Moderate value on {best.outcome}")
        
        else:
            result["recommendation"] = "NO_BET"
            result["confidence"] = "LOW"
            result["reasoning"].append("Edge too small to justify bet")
            result["reasoning"].append(f"Best edge: {round(best.edge*100, 1)}% (need >3%)")
        
        # Add margin context
        if margin_analysis.uncertainty_level == "EXTREME":
            result["reasoning"].append("⚠️ EXTREME margin - high volatility match")
        elif margin_analysis.uncertainty_level == "HIGH":
            result["reasoning"].append("⚠️ High margin - bookmaker sees risk")
        
        return result


# Singleton instance
_engine_instance = None

def get_value_detection_engine() -> ValueDetectionEngine:
    """Get the singleton instance"""
    global _engine_instance
    if _engine_instance is None:
        _engine_instance = ValueDetectionEngine()
    return _engine_instance