"""Coupon Mixin — multi-match coupon builder + daily bankers.

Auto-extracted mixin module — split from services/single_match_orchestrator.py.
All methods here are composed into SingleMatchOrchestrator via inheritance.
`self` attributes (self.dsn, self.enrichment, self.v25_predictor, etc.) are
initialised in the main __init__.
"""

from __future__ import annotations

import json
import re
import time
import math
import os
import pickle
from collections import defaultdict
from typing import Any, Dict, List, Optional, Set, Tuple, overload

import pandas as pd
import numpy as np

import psycopg2
from psycopg2.extras import RealDictCursor

from data.db import get_clean_dsn
from schemas.prediction import FullMatchPrediction
from schemas.match_data import MatchData
from models.v25_ensemble import V25Predictor, get_v25_predictor
try:
    from models.v27_predictor import V27Predictor, compute_divergence, compute_value_edge
except ImportError:
    class V27Predictor:  # type: ignore[no-redef]
        def __init__(self): self.models = {}
        def load_models(self): return False
        def predict_all(self, features): return {}
    def compute_divergence(*args, **kwargs):
        return {}
    def compute_value_edge(*args, **kwargs):
        return {}
from features.odds_band_analyzer import OddsBandAnalyzer
try:
    from models.basketball_v25 import (
        BasketballMatchPrediction,
        get_basketball_v25_predictor,
    )
except ImportError:
    BasketballMatchPrediction = Any  # type: ignore[misc]
    def get_basketball_v25_predictor() -> Any:
        raise ImportError("Basketball predictor is not available")
from core.engines.player_predictor import PlayerPrediction, get_player_predictor
from services.feature_enrichment import FeatureEnrichmentService
from services.betting_brain import BettingBrain
from services.v26_shadow_engine import V26ShadowEngine, get_v26_shadow_engine
from services.match_commentary import generate_match_commentary
from utils.top_leagues import load_top_league_ids
from utils.league_reliability import load_league_reliability
from config.config_loader import build_threshold_dict, get_threshold_default
from models.calibration import get_calibrator


class CouponMixin:
    def _prefilter_match_ids(self, match_ids: List[str], limit: int = 15) -> List[str]:
        """
        40+ maç gelirse hepsini analiz etmek çok yavaş.
        DB'den hızlıca en kaliteli limit adet maçı seç:
        - Odds verisi olan maçlar önce
        - football_ai_features'da gerçek ELO'su olan maçlar
        - Yüksek lig güvenilirliği
        """
        if len(match_ids) <= limit:
            return match_ids

        try:
            with psycopg2.connect(self.dsn) as conn:
                with conn.cursor(cursor_factory=RealDictCursor) as cur:
                    cur.execute("""
                        SELECT
                            m.id,
                            COUNT(oc.db_id) AS odds_count,
                            COALESCE(f.home_elo, 1500) AS home_elo,
                            lr.reliability_score
                        FROM matches m
                        LEFT JOIN odd_categories oc ON oc.match_id = m.id
                        LEFT JOIN football_ai_features f ON f.match_id = m.id
                        LEFT JOIN team_elo_ratings ter_h ON ter_h.team_id = m.home_team_id
                        LEFT JOIN (
                            SELECT league_id, AVG(home_elo) AS reliability_score
                            FROM football_ai_features
                            GROUP BY league_id
                        ) lr ON lr.league_id = m.league_id
                        WHERE m.id = ANY(%s)
                        GROUP BY m.id, f.home_elo, lr.reliability_score
                        ORDER BY
                            COUNT(oc.db_id) DESC,
                            COALESCE(f.home_elo, 1500) DESC
                        LIMIT %s
                    """, (match_ids, limit))
                    rows = cur.fetchall()
                    filtered = [r["id"] for r in rows]
                    # Eğer DB'den yeterli gelmediyse kalanları ekle
                    remaining = [m for m in match_ids if m not in filtered]
                    return filtered + remaining[:max(0, limit - len(filtered))]
        except Exception as e:
            print(f"⚠️ Prefilter failed, using original list: {e}")
            return match_ids[:limit]

    def build_coupon(
        self,
        match_ids: List[str],
        strategy: str = "BALANCED",
        max_matches: Optional[int] = None,
        min_confidence: Optional[float] = None,
    ) -> Dict[str, Any]:
        strategy_name = (strategy or "BALANCED").upper()

        strategy_config = {
            "SAFE": {"max_matches": 4, "min_conf": 66.0, "prefilter": 12},
            "BALANCED": {"max_matches": 5, "min_conf": 58.0, "prefilter": 15},
            "AGGRESSIVE": {"max_matches": 8, "min_conf": 52.0, "prefilter": 20},
            "VALUE": {"max_matches": 8, "min_conf": 48.0, "prefilter": 20},
            "MIRACLE": {"max_matches": 10, "min_conf": 44.0, "prefilter": 25},
        }
        cfg = strategy_config.get(strategy_name, strategy_config["BALANCED"])
        max_allowed = max_matches if max_matches is not None else cfg["max_matches"]
        min_conf = min_confidence if min_confidence is not None else cfg["min_conf"]
        prefilter_limit = cfg["prefilter"]

        # Çok fazla maç gelirse önce hızlı prefilter uygula
        if len(match_ids) > prefilter_limit:
            print(f"🔍 Prefiltering {len(match_ids)} → {prefilter_limit} matches for {strategy_name} coupon")
            match_ids = self._prefilter_match_ids(match_ids, prefilter_limit)

        candidates: List[Dict[str, Any]] = []
        rejected: List[Dict[str, Any]] = []

        for match_id in match_ids:
            package = self.analyze_match(match_id)
            if not package:
                rejected.append({"match_id": match_id, "reason": "match_not_found"})
                continue

            risk_level = str(package.get("risk", {}).get("level", "MEDIUM")).upper()
            data_quality = str(package.get("data_quality", {}).get("label", "MEDIUM")).upper()
            match_candidates: List[Dict[str, Any]] = []
            seen_keys: Set[Tuple[str, str]] = set()
            bet_summary = package.get("bet_summary") or []

            raw_picks = []
            for candidate in [
                package.get("main_pick"),
                package.get("value_pick"),
                *(package.get("supporting_picks") or []),
            ]:
                if isinstance(candidate, dict):
                    raw_picks.append(candidate)
            for candidate in bet_summary:
                if isinstance(candidate, dict):
                    raw_picks.append(candidate)

            for candidate in raw_picks:
                market = str(candidate.get("market") or "")
                pick = str(candidate.get("pick") or "")
                if not market or not pick:
                    continue

                dedupe_key = (market, pick)
                if dedupe_key in seen_keys:
                    continue
                seen_keys.add(dedupe_key)

                calibrated_conf = float(
                    candidate.get("calibrated_confidence", candidate.get("confidence", 0.0))
                    or 0.0
                )
                odds = float(candidate.get("odds", 0.0) or 0.0)
                probability = float(candidate.get("probability", 0.0) or 0.0)
                play_score = float(candidate.get("play_score", 0.0) or 0.0)
                ev_edge = float(
                    candidate.get("ev_edge", candidate.get("edge", 0.0)) or 0.0
                )
                playable = bool(candidate.get("playable"))
                bet_grade = str(candidate.get("bet_grade", "PASS")).upper()

                if odds <= 1.01:
                    continue

                strict_candidate = (
                    playable
                    and calibrated_conf >= min_conf
                    and bet_grade != "PASS"
                )

                if strategy_name == "SAFE":
                    strict_pass = strict_candidate
                    if odds > 2.35 or play_score < 60.0 or risk_level in {"HIGH", "EXTREME"}:
                        strict_pass = False
                    if data_quality == "LOW" or ev_edge < 0.01 or bet_grade == "PASS":
                        strict_pass = False
                    strict_score = (
                        calibrated_conf * 1.10
                        + play_score * 0.90
                        + (ev_edge * 180.0)
                        - abs(odds - 1.55) * 12.0
                    )
                    soft_pass = (
                        calibrated_conf >= max(min_conf - 10.0, 56.0)
                        and odds <= 2.70
                        and play_score >= 50.0
                        and risk_level != "EXTREME"
                        and data_quality != "LOW"
                        and ev_edge >= -0.01
                    )
                    soft_score = (
                        calibrated_conf
                        + play_score * 0.85
                        + (ev_edge * 140.0)
                        - abs(odds - 1.65) * 9.0
                    )
                elif strategy_name == "BALANCED":
                    strict_pass = strict_candidate
                    if odds > 3.40 or play_score < 52.0 or risk_level == "EXTREME":
                        strict_pass = False
                    if ev_edge < 0.0 or bet_grade == "PASS":
                        strict_pass = False
                    strict_score = (
                        calibrated_conf
                        + play_score
                        + (ev_edge * 220.0)
                        + min(odds, 3.0) * 3.0
                    )
                    soft_pass = (
                        calibrated_conf >= max(min_conf - 10.0, 48.0)
                        and odds <= 4.20
                        and play_score >= 44.0
                        and risk_level != "EXTREME"
                        and ev_edge >= -0.015
                    )
                    soft_score = (
                        calibrated_conf * 0.95
                        + play_score * 0.90
                        + (ev_edge * 180.0)
                        + min(odds, 3.5) * 3.5
                    )
                elif strategy_name == "AGGRESSIVE":
                    strict_pass = strict_candidate
                    if odds < 1.35 or odds > 7.50 or play_score < 46.0:
                        strict_pass = False
                    if risk_level == "EXTREME" or bet_grade == "PASS":
                        strict_pass = False
                    strict_score = (
                        calibrated_conf * 0.85
                        + play_score * 0.75
                        + (ev_edge * 260.0)
                        + min(odds, 6.0) * 7.0
                    )
                    soft_pass = (
                        calibrated_conf >= max(min_conf - 10.0, 42.0)
                        and 1.25 <= odds <= 8.50
                        and play_score >= 40.0
                        and risk_level != "EXTREME"
                        and ev_edge >= -0.02
                    )
                    soft_score = (
                        calibrated_conf * 0.80
                        + play_score * 0.70
                        + (ev_edge * 210.0)
                        + min(odds, 7.0) * 7.5
                    )
                elif strategy_name == "VALUE":
                    strict_pass = strict_candidate
                    if odds < 1.55 or play_score < 48.0 or ev_edge < 0.03:
                        strict_pass = False
                    if risk_level == "EXTREME" or data_quality == "LOW" or bet_grade == "PASS":
                        strict_pass = False
                    strict_score = (
                        calibrated_conf * 0.75
                        + play_score * 0.85
                        + (ev_edge * 320.0)
                        + min(odds, 6.5) * 8.0
                    )
                    soft_pass = (
                        calibrated_conf >= max(min_conf - 10.0, 40.0)
                        and odds >= 1.35
                        and play_score >= 40.0
                        and risk_level != "EXTREME"
                        and data_quality != "LOW"
                        and ev_edge >= 0.0
                    )
                    soft_score = (
                        calibrated_conf * 0.70
                        + play_score * 0.80
                        + (ev_edge * 260.0)
                        + min(odds, 7.0) * 7.0
                    )
                else:  # MIRACLE
                    strict_pass = strict_candidate
                    if odds < 2.10 or play_score < 40.0 or ev_edge < 0.01:
                        strict_pass = False
                    if risk_level == "EXTREME" or bet_grade == "PASS":
                        strict_pass = False
                    strict_score = (
                        calibrated_conf * 0.55
                        + play_score * 0.60
                        + (ev_edge * 260.0)
                        + min(odds, 10.0) * 10.0
                    )
                    soft_pass = (
                        calibrated_conf >= max(min_conf - 10.0, 36.0)
                        and odds >= 1.60
                        and play_score >= 34.0
                        and risk_level != "EXTREME"
                        and ev_edge >= -0.02
                    )
                    soft_score = (
                        calibrated_conf * 0.50
                        + play_score * 0.55
                        + (ev_edge * 200.0)
                        + min(odds, 10.0) * 9.0
                    )

                fallback_pass = (
                    calibrated_conf >= max(min_conf - 14.0, 34.0)
                    and odds >= 1.20
                    and play_score >= 32.0
                    and risk_level != "EXTREME"
                )
                fallback_score = (
                    calibrated_conf * 0.60
                    + play_score * 0.65
                    + (ev_edge * 120.0)
                    + min(odds, 6.0) * 4.0
                )

                strategy_score = strict_score
                selection_mode = "strict"
                if strict_pass:
                    pass
                elif soft_pass:
                    strategy_score = soft_score
                    selection_mode = "soft"
                elif fallback_pass:
                    strategy_score = fallback_score
                    selection_mode = "fallback"
                else:
                    continue

                match_candidates.append(
                    {
                        "match_id": package["match_info"]["match_id"],
                        "match_name": package["match_info"]["match_name"],
                        "market": market,
                        "pick": pick,
                        "probability": probability,
                        "confidence": calibrated_conf,
                        "odds": odds,
                        "risk_level": risk_level,
                        "data_quality": data_quality,
                        "bet_grade": bet_grade,
                        "playable": playable,
                        "play_score": round(play_score, 1),
                        "ev_edge": round(ev_edge, 4),
                        "selection_mode": selection_mode,
                        "strategy_score": round(strategy_score, 3),
                    }
                )

            if not match_candidates:
                rejected.append(
                    {
                        "match_id": match_id,
                        "reason": "no_strategy_fit",
                        "threshold": min_conf,
                    }
                )
                continue

            match_candidates.sort(
                key=lambda item: (
                    float(item.get("strategy_score", 0.0)),
                    float(item.get("confidence", 0.0)),
                    float(item.get("ev_edge", 0.0)),
                ),
                reverse=True,
            )
            candidates.append(match_candidates[0])

        candidates.sort(
            key=lambda item: (
                float(item.get("strategy_score", 0.0)),
                float(item.get("confidence", 0.0)),
                float(item.get("ev_edge", 0.0)),
            ),
            reverse=True,
        )
        selected = candidates[: max(1, max_allowed)]

        total_odds = 1.0
        win_probability = 1.0
        for pick in selected:
            odd = float(pick.get("odds") or 1.0)
            prob = float(pick.get("probability") or 0.0)
            total_odds *= odd if odd > 1.0 else 1.0
            win_probability *= prob

        return {
            "strategy": strategy_name,
            "generated_at": __import__("datetime").datetime.utcnow().isoformat() + "Z",
            "match_count": len(selected),
            "bets": selected,
            "total_odds": round(total_odds, 2),
            "expected_win_rate": round(win_probability, 4),
            "rejected_matches": rejected,
        }

    def get_daily_bankers_live(self, count: int = 3) -> List[Dict[str, Any]]:
        with psycopg2.connect(self.dsn) as conn:
            with conn.cursor(cursor_factory=RealDictCursor) as cur:
                cur.execute(
                    """
                    SELECT id
                    FROM live_matches
                    WHERE mst_utc > EXTRACT(EPOCH FROM NOW()) * 1000
                      AND mst_utc < EXTRACT(EPOCH FROM NOW() + INTERVAL '24 hours') * 1000
                    ORDER BY mst_utc ASC
                    LIMIT 60
                    """,
                )
                ids = [row["id"] for row in cur.fetchall()]

        if not ids:
            return []

        coupon = self.build_coupon(
            match_ids=ids,
            strategy="SAFE",
            max_matches=max(1, count),
            min_confidence=78.0,
        )
        return coupon.get("bets", [])[: max(1, count)]

    def get_daily_bankers(self, count: int = 3) -> List[Dict[str, Any]]:
        """
        Identifies the safest, highest value bets for the next 24 hours.
        """
        now_ms = int(time.time() * 1000)
        horizon_ms = now_ms + (24 * 60 * 60 * 1000)

        with psycopg2.connect(self.dsn) as conn:
            with conn.cursor(cursor_factory=RealDictCursor) as cur:
                cur.execute("""
                    SELECT m.id, m.match_name, m.mst_utc
                    FROM matches m
                    WHERE m.mst_utc >= %s AND m.mst_utc <= %s
                      AND m.status = 'NS'
                      AND EXISTS (SELECT 1 FROM odd_categories oc WHERE oc.match_id = m.id)
                    ORDER BY m.mst_utc ASC
                    LIMIT 50
                """, (now_ms, horizon_ms))
                matches = cur.fetchall()

        potential_bankers = []
        print(f"🔍 Scanning {len(matches)} upcoming matches for Bankers...")

        for match in matches:
            try:
                data = self._load_match_data(match['id'])
                if data is None: continue

                result = self.analyze_match(match['id'])

                if result and 'main_pick' in result:
                    pick = result['main_pick']
                    conf = pick.get('calibrated_confidence', pick.get('confidence', 0))
                    odds = pick.get('odds', 0)
                    market = pick.get('market', '')
                    pick_name = pick.get('pick', '')
                    
                    # Banker Criteria: High Confidence (>75%) AND Decent Odds (>1.30)
                    if conf >= 75.0 and odds >= 1.30:
                         score = conf * (odds - 1.0)
                         potential_bankers.append({
                             "match_id": match['id'],
                             "match_name": match['match_name'] or f"{data.home_team_name} vs {data.away_team_name}",
                             "league": data.league_name,
                             "pick": f"{market} - {pick_name}",
                             "confidence": conf,
                             "odds": odds,
                             "value_score": score
                         })
            except Exception:
                pass

        potential_bankers.sort(key=lambda x: x['value_score'], reverse=True)
        return potential_bankers[:count]
