"""Upper Brain Mixin — V27 cross-check guards and assessments.

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 UpperBrainMixin:
    def _apply_upper_brain_guards(
        self, package: Dict[str, Any], data: Any = None
    ) -> Dict[str, Any]:
        # V35c: hand the brain the REAL bookmaker MS odds so its reference rows
        # can never display synthetic 1/p prices as if they were offered.
        ms_real_odds = None
        if data is not None:
            try:
                odds = getattr(data, "odds_data", None) or {}
                ms_real_odds = {
                    "1": self._real_market_odds(odds, "ms_h"),
                    "X": self._real_market_odds(odds, "ms_d"),
                    "2": self._real_market_odds(odds, "ms_a"),
                }
            except Exception:
                ms_real_odds = None
        return BettingBrain().judge(package, ms_real_odds=ms_real_odds)

        v27_engine = package.get("v27_engine")
        if not isinstance(v27_engine, dict) or not v27_engine.get("triple_value"):
            return package

        guarded = dict(package)
        vetoed_keys = set()
        guarded_keys = set()

        def mark_guard(item: Dict[str, Any]) -> Dict[str, Any]:
            if not isinstance(item, dict):
                return item

            out = dict(item)
            assessment = self._upper_brain_assessment(out, guarded)
            if not assessment.get("applies"):
                return out

            key = f"{out.get('market')}:{out.get('pick')}"
            guarded_keys.add(key)
            out["upper_brain"] = assessment

            reason_key = "decision_reasons" if "decision_reasons" in out else "reasons"
            reasons = list(out.get(reason_key) or [])
            for reason in assessment.get("reason_codes", []):
                if reason not in reasons:
                    reasons.append(reason)
            out[reason_key] = reasons[:6]

            if assessment.get("veto"):
                vetoed_keys.add(key)
                out["playable"] = False
                out["stake_units"] = 0.0
                out["bet_grade"] = "PASS"
                out["is_guaranteed"] = False
                out["pick_reason"] = "upper_brain_veto"
                if "signal_tier" in out:
                    out["signal_tier"] = "PASS"
            elif assessment.get("downgrade"):
                out["is_guaranteed"] = False
                if out.get("signal_tier") == "CORE":
                    out["signal_tier"] = "LEAN"
                if out.get("pick_reason") == "high_accuracy_market":
                    out["pick_reason"] = "upper_brain_downgraded"

            return out

        main_pick = mark_guard(guarded.get("main_pick") or {})
        value_pick = mark_guard(guarded.get("value_pick") or {}) if guarded.get("value_pick") else None
        supporting = [
            mark_guard(row)
            for row in list(guarded.get("supporting_picks") or [])
            if isinstance(row, dict)
        ]
        bet_summary = [
            mark_guard(row)
            for row in list(guarded.get("bet_summary") or [])
            if isinstance(row, dict)
        ]

        main_safe = bool(main_pick and main_pick.get("playable") and not main_pick.get("upper_brain", {}).get("veto"))
        if not main_safe:
            candidates = [
                row for row in supporting
                if row.get("playable")
                and not row.get("upper_brain", {}).get("veto")
                and float(row.get("odds", 0.0) or 0.0) >= 1.30
            ]
            candidates.sort(key=lambda row: float(row.get("play_score", 0.0) or 0.0), reverse=True)
            if candidates:
                main_pick = dict(candidates[0])
                main_pick["is_guaranteed"] = False
                main_pick["pick_reason"] = "upper_brain_reselected"
                reasons = list(main_pick.get("decision_reasons") or [])
                if "upper_brain_reselected_after_veto" not in reasons:
                    reasons.append("upper_brain_reselected_after_veto")
                main_pick["decision_reasons"] = reasons[:6]
            elif main_pick:
                main_pick["is_guaranteed"] = False
                main_pick["pick_reason"] = "upper_brain_no_safe_pick"

        if main_pick:
            supporting = [
                row for row in supporting
                if not (
                    row.get("market") == main_pick.get("market")
                    and row.get("pick") == main_pick.get("pick")
                )
            ][:6]

        guarded["main_pick"] = main_pick if main_pick else None
        guarded["value_pick"] = value_pick
        guarded["supporting_picks"] = supporting
        guarded["bet_summary"] = bet_summary

        playable = bool(main_pick and main_pick.get("playable") and not main_pick.get("upper_brain", {}).get("veto"))
        advice = dict(guarded.get("bet_advice") or {})
        advice["playable"] = playable
        advice["suggested_stake_units"] = float(main_pick.get("stake_units", 0.0)) if playable else 0.0
        if playable:
            advice["reason"] = "playable_pick_found"
        elif vetoed_keys:
            advice["reason"] = "upper_brain_no_safe_pick"
        else:
            advice["reason"] = "no_bet_conditions_met"
        guarded["bet_advice"] = advice

        guarded["upper_brain"] = {
            "applied": True,
            "guarded_count": len(guarded_keys),
            "vetoed_count": len(vetoed_keys),
            "vetoed": sorted(vetoed_keys)[:8],
            "rules": {
                "min_band_sample": 8,
                "max_v25_v27_divergence": 0.18,
                "dc_requires_triple_value": True,
            },
        }
        guarded.setdefault("analysis_details", {})
        guarded["analysis_details"]["upper_brain_guards_applied"] = True
        guarded["analysis_details"]["upper_brain_vetoed_count"] = len(vetoed_keys)
        return guarded

    def _upper_brain_assessment(
        self,
        item: Dict[str, Any],
        package: Dict[str, Any],
    ) -> Dict[str, Any]:
        market = str(item.get("market") or "")
        pick = str(item.get("pick") or "")
        if not market or not pick:
            return {"applies": False}

        v27_engine = package.get("v27_engine") or {}
        triple_value = v27_engine.get("triple_value") or {}
        model_prob = self._upper_brain_market_probability(item, package)
        v27_prob = self._upper_brain_v27_probability(market, pick, v27_engine)
        triple_key = self._upper_brain_triple_key(market, pick)
        triple = triple_value.get(triple_key) if triple_key else None

        veto = False
        downgrade = False
        reasons: List[str] = []
        divergence = None

        if model_prob is not None and v27_prob is not None:
            divergence = abs(float(model_prob) - float(v27_prob))
            if divergence >= 0.18:
                veto = True
                reasons.append("upper_brain_v25_v27_divergence")
            elif divergence >= 0.12:
                downgrade = True
                reasons.append("upper_brain_v25_v27_warning")

        if isinstance(triple, dict):
            band_sample = int(float(triple.get("band_sample", 0) or 0))
            is_value = bool(triple.get("is_value"))
            if market == "DC":
                if band_sample < 8:
                    veto = True
                    reasons.append("upper_brain_band_sample_too_low")
                elif not is_value:
                    veto = True
                    reasons.append("upper_brain_triple_value_rejected")
            elif market in {"MS", "OU25"} and band_sample > 0 and band_sample < 8:
                downgrade = True
                reasons.append("upper_brain_band_sample_thin")
            elif market in {"OU15", "HT_OU05"} and band_sample < 8:
                downgrade = True
                reasons.append("upper_brain_band_sample_thin")

        consensus = str(v27_engine.get("consensus") or "").upper()
        if consensus == "DISAGREE" and market in {"MS", "DC"} and not veto:
            downgrade = True
            reasons.append("upper_brain_consensus_disagree")

        applies = bool(reasons or triple is not None or v27_prob is not None)
        return {
            "applies": applies,
            "veto": veto,
            "downgrade": downgrade,
            "reason_codes": reasons,
            "model_prob": round(float(model_prob), 4) if model_prob is not None else None,
            "v27_prob": round(float(v27_prob), 4) if v27_prob is not None else None,
            "divergence": round(float(divergence), 4) if divergence is not None else None,
            "triple_key": triple_key,
            "triple_value": triple,
        }

    def _upper_brain_market_probability(
        self,
        item: Dict[str, Any],
        package: Dict[str, Any],
    ) -> Optional[float]:
        raw_prob = item.get("probability")
        if raw_prob is not None:
            try:
                return float(raw_prob)
            except (TypeError, ValueError):
                pass

        market = str(item.get("market") or "")
        pick = str(item.get("pick") or "")
        board = package.get("market_board") or {}
        payload = board.get(market) if isinstance(board, dict) else None
        probs = payload.get("probs") if isinstance(payload, dict) else None
        if not isinstance(probs, dict):
            return None

        prob_key = self._upper_brain_prob_key(market, pick)
        if prob_key is None:
            return None
        return self._safe_float(probs.get(prob_key))

    def _upper_brain_v27_probability(
        self,
        market: str,
        pick: str,
        v27_engine: Dict[str, Any],
    ) -> Optional[float]:
        predictions = v27_engine.get("predictions") or {}
        ms = predictions.get("ms") or {}
        ou25 = predictions.get("ou25") or {}

        if market == "MS":
            ms_key = {"1": "home", "X": "draw", "2": "away"}.get(pick or "")
            return self._safe_float(ms.get(ms_key), 0.0) if ms_key else 0.0
        if market == "DC":
            if pick == "1X":
                return self._safe_float(ms.get("home"), 0.0) + self._safe_float(ms.get("draw"), 0.0)
            if pick == "X2":
                return self._safe_float(ms.get("draw"), 0.0) + self._safe_float(ms.get("away"), 0.0)
            if pick == "12":
                return self._safe_float(ms.get("home"), 0.0) + self._safe_float(ms.get("away"), 0.0)
        if market == "OU25":
            prob_key = self._upper_brain_prob_key(market, pick)
            return self._safe_float(ou25.get(prob_key), 0.0) if prob_key else 0.0
        return 0.0

    @staticmethod
    def _upper_brain_prob_key(market: str, pick: str) -> Optional[str]:
        pick_norm = str(pick or "").strip().casefold()
        if market in {"MS", "HT", "HCAP"}:
            return pick if pick in {"1", "X", "2"} else None
        if market == "DC":
            return pick.upper() if pick.upper() in {"1X", "X2", "12"} else None
        if market in {"OU15", "OU25", "OU35", "HT_OU05", "HT_OU15", "CARDS"}:
            if "over" in pick_norm or "st" in pick_norm:
                return "over"
            if "under" in pick_norm or "alt" in pick_norm:
                return "under"
        if market == "BTTS":
            if "yes" in pick_norm or "var" in pick_norm:
                return "yes"
            if "no" in pick_norm or "yok" in pick_norm:
                return "no"
        if market == "OE":
            if "odd" in pick_norm or "tek" in pick_norm:
                return "odd"
            if "even" in pick_norm or "ift" in pick_norm:
                return "even"
        if market == "HTFT" and "/" in pick:
            return pick
        return None

    def _upper_brain_triple_key(self, market: str, pick: str) -> Optional[str]:
        prob_key = self._upper_brain_prob_key(market, pick)
        if market == "MS":
            return {"1": "home", "2": "away"}.get(pick)
        if market == "DC":
            return f"dc_{pick.lower()}" if pick.upper() in {"1X", "X2", "12"} else None
        if market in {"OU15", "OU25", "OU35"} and prob_key == "over":
            return f"{market.lower()}_over"
        if market == "BTTS" and prob_key == "yes":
            return "btts_yes"
        if market == "HT":
            return {"1": "ht_home", "2": "ht_away"}.get(pick)
        if market in {"HT_OU05", "HT_OU15"} and prob_key == "over":
            return f"{market.lower()}_over"
        if market == "OE" and prob_key == "odd":
            return "oe_odd"
        if market == "CARDS" and prob_key == "over":
            return "cards_over"
        if market == "HTFT" and "/" in pick:
            return f"htft_{pick.replace('/', '').lower()}"
        return None
