"""
Deterministic betting judge for prediction packages.

The model layer estimates event probabilities. BettingBrain decides whether
those probabilities are trustworthy enough to risk money.
"""

from __future__ import annotations

from typing import Any, Dict, List, Optional, Tuple


class BettingBrain:
    MIN_ODDS = 1.30
    MIN_BET_SCORE = 72.0
    MIN_WATCH_SCORE = 62.0
    MIN_BAND_SAMPLE = 8
    HARD_DIVERGENCE = 0.22
    SOFT_DIVERGENCE = 0.14
    EXTREME_MODEL_PROB = 0.85
    EXTREME_GAP = 0.30

    MARKET_PRIORS = {
        "DC": 4.0,
        "OU15": 3.0,
        "OU25": 2.0,
        "BTTS": 0.0,
        "MS": -2.0,
        "OU35": -2.0,
        "HT": -6.0,
        "HTFT": -12.0,
        "CARDS": -5.0,
        "OE": -8.0,
    }

    def judge(self, package: Dict[str, Any]) -> Dict[str, Any]:
        v27_engine = package.get("v27_engine")
        if not isinstance(v27_engine, dict):
            return package

        guarded = dict(package)
        rows = self._collect_rows(guarded)
        if not rows:
            return guarded

        judged_rows: Dict[str, Dict[str, Any]] = {}
        decisions: List[Dict[str, Any]] = []
        for row in rows:
            key = self._row_key(row)
            judged = self._judge_row(dict(row), guarded)
            judged_rows[key] = judged
            decisions.append(judged["betting_brain"])

        approved = [
            row for row in judged_rows.values()
            if row.get("betting_brain", {}).get("action") == "BET"
        ]
        watchlist = [
            row for row in judged_rows.values()
            if row.get("betting_brain", {}).get("action") == "WATCH"
        ]
        approved.sort(key=self._candidate_sort_key, reverse=True)
        watchlist.sort(key=self._candidate_sort_key, reverse=True)

        original_main = guarded.get("main_pick") or {}
        main_pick = None
        decision = "NO_BET"
        decision_reason = "No candidate passed the betting brain evidence gates."

        if approved:
            main_pick = dict(approved[0])
            main_pick["is_guaranteed"] = bool(main_pick.get("betting_brain", {}).get("score", 0.0) >= 82.0)
            main_pick["pick_reason"] = "betting_brain_approved"
            decision = "BET"
            decision_reason = main_pick.get("betting_brain", {}).get("summary", "Evidence is aligned.")
        elif watchlist:
            main_pick = dict(watchlist[0])
            self._force_no_bet(main_pick, "betting_brain_watchlist")
            decision = "WATCHLIST"
            decision_reason = main_pick.get("betting_brain", {}).get("summary", "Interesting but not clean enough.")
        elif original_main:
            main_pick = dict(judged_rows.get(self._row_key(original_main), original_main))
            self._force_no_bet(main_pick, "betting_brain_no_safe_pick")

        main_key = self._row_key(main_pick) if main_pick else ""
        supporting = [
            dict(row)
            for row in judged_rows.values()
            if self._row_key(row) != main_key
        ]
        supporting.sort(key=self._candidate_sort_key, reverse=True)

        bet_summary = [
            self._summary_item(row)
            for row in sorted(judged_rows.values(), key=self._candidate_sort_key, reverse=True)
        ]

        guarded["main_pick"] = main_pick
        guarded["value_pick"] = self._pick_value_candidate(judged_rows, main_key)
        guarded["supporting_picks"] = supporting[:6]
        guarded["bet_summary"] = bet_summary

        playable = decision == "BET" and bool(main_pick and main_pick.get("playable"))
        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
        advice["reason"] = "betting_brain_approved" if playable else "betting_brain_no_bet"
        advice["decision"] = decision
        advice["confidence_band"] = self._decision_band(main_pick)
        guarded["bet_advice"] = advice

        rejected = [d for d in decisions if d.get("action") == "REJECT"]
        guarded["betting_brain"] = {
            "version": "judge-v1",
            "decision": decision,
            "reason": decision_reason,
            "main_pick_key": main_key or None,
            "approved_count": len(approved),
            "watchlist_count": len(watchlist),
            "rejected_count": len(rejected),
            "top_candidates": self._top_decisions(decisions),
            "rules": {
                "min_bet_score": self.MIN_BET_SCORE,
                "min_watch_score": self.MIN_WATCH_SCORE,
                "min_band_sample": self.MIN_BAND_SAMPLE,
                "hard_divergence": self.HARD_DIVERGENCE,
                "soft_divergence": self.SOFT_DIVERGENCE,
                "extreme_model_probability": self.EXTREME_MODEL_PROB,
                "extreme_model_market_gap": self.EXTREME_GAP,
            },
        }
        guarded["upper_brain"] = guarded["betting_brain"]
        guarded.setdefault("analysis_details", {})
        guarded["analysis_details"]["betting_brain_applied"] = True
        guarded["analysis_details"]["betting_brain_decision"] = decision
        return guarded

    def _judge_row(self, row: Dict[str, Any], package: Dict[str, Any]) -> Dict[str, Any]:
        market = str(row.get("market") or "")
        pick = str(row.get("pick") or "")
        model_prob = self._market_probability(row, package)
        odds = self._safe_float(row.get("odds"), 0.0) or 0.0
        implied = (1.0 / odds) if odds > 1.0 else 0.0
        model_gap = (model_prob - implied) if model_prob is not None and implied > 0 else None
        calibrated_conf = self._safe_float(row.get("calibrated_confidence", row.get("confidence")), 0.0) or 0.0
        play_score = self._safe_float(row.get("play_score"), 0.0) or 0.0
        ev_edge = self._safe_float(row.get("ev_edge", row.get("edge")), 0.0) or 0.0
        v27_prob = self._v27_probability(market, pick, package.get("v27_engine") or {})
        divergence = abs(model_prob - v27_prob) if model_prob is not None and v27_prob is not None else None
        triple_key = self._triple_key(market, pick)
        triple = self._triple_value(package, triple_key)
        band_sample = int(self._safe_float((triple or {}).get("band_sample"), 0.0) or 0.0)
        triple_is_value = bool((triple or {}).get("is_value"))
        consensus = str((package.get("v27_engine") or {}).get("consensus") or "").upper()

        positives: List[str] = []
        issues: List[str] = []
        vetoes: List[str] = []
        score = 0.0

        if row.get("playable"):
            score += 18.0
            positives.append("base_model_playable")
        else:
            score -= 18.0
            issues.append("base_model_not_playable")

        score += max(0.0, min(20.0, calibrated_conf * 0.22))
        score += max(-8.0, min(16.0, ev_edge * 45.0))
        score += max(0.0, min(14.0, play_score * 0.12))
        score += self.MARKET_PRIORS.get(market, -3.0)

        data_quality = package.get("data_quality") or {}
        quality_score = self._safe_float(data_quality.get("score"), 0.6) or 0.6
        score += max(-8.0, min(6.0, (quality_score - 0.55) * 16.0))
        risk = str((package.get("risk") or {}).get("level") or "MEDIUM").upper()
        score += {"LOW": 5.0, "MEDIUM": 0.0, "HIGH": -12.0, "EXTREME": -22.0}.get(risk, -4.0)

        if odds < self.MIN_ODDS:
            vetoes.append("odds_below_minimum")
        if calibrated_conf < 38.0:
            vetoes.append("calibrated_confidence_too_low")
        if play_score < 50.0:
            vetoes.append("play_score_too_low")

        if divergence is not None:
            if divergence >= self.HARD_DIVERGENCE:
                score -= 42.0
                vetoes.append("v25_v27_hard_disagreement")
            elif divergence >= self.SOFT_DIVERGENCE:
                score -= 18.0
                issues.append("v25_v27_soft_disagreement")
            else:
                score += 11.0
                positives.append("v25_v27_aligned")

        if isinstance(triple, dict):
            if triple_is_value:
                score += 18.0
                positives.append("triple_value_confirmed")
            elif market in {"DC", "MS", "OU25", "BTTS"}:
                score -= 18.0
                issues.append("triple_value_not_confirmed")

            if band_sample >= 25:
                score += 8.0
                positives.append("strong_historical_sample")
            elif band_sample >= self.MIN_BAND_SAMPLE:
                score += 3.0
                positives.append("usable_historical_sample")
            else:
                score -= 16.0
                issues.append("historical_sample_too_low")
                if market == "DC":
                    vetoes.append("dc_without_historical_sample")
        elif market in {"MS", "DC", "OU25"}:
            score -= 10.0
            issues.append("missing_triple_value_evidence")

        if consensus == "DISAGREE" and market in {"MS", "DC"}:
            score -= 12.0
            issues.append("engine_consensus_disagree")

        if (
            model_prob is not None
            and model_gap is not None
            and model_prob >= self.EXTREME_MODEL_PROB
            and model_gap >= self.EXTREME_GAP
            and not triple_is_value
        ):
            score -= 24.0
            vetoes.append("extreme_probability_without_evidence")

        if market in {"HT", "HTFT", "OE"} and score < 86.0:
            vetoes.append("volatile_market_requires_exceptional_evidence")

        score = max(0.0, min(100.0, score))
        action = "BET"
        if vetoes:
            action = "REJECT"
        elif score < self.MIN_WATCH_SCORE:
            action = "REJECT"
        elif score < self.MIN_BET_SCORE:
            action = "WATCH"

        row["betting_brain"] = {
            "action": action,
            "score": round(score, 1),
            "summary": self._summary(action, market, pick, positives, issues, vetoes),
            "positives": positives[:5],
            "issues": issues[:6],
            "vetoes": vetoes[:6],
            "model_prob": round(model_prob, 4) if model_prob is not None else None,
            "implied_prob": round(implied, 4),
            "model_market_gap": round(model_gap, 4) if model_gap is not None else None,
            "v27_prob": round(v27_prob, 4) if v27_prob is not None else None,
            "divergence": round(divergence, 4) if divergence is not None else None,
            "triple_key": triple_key,
            "triple_value": triple,
        }

        if action != "BET":
            self._force_no_bet(row, f"betting_brain_{action.lower()}")
        else:
            row["is_guaranteed"] = bool(score >= 82.0)
            row["pick_reason"] = "betting_brain_approved"
            row["stake_units"] = self._brain_stake(row, score)
            row["bet_grade"] = "A" if score >= 82.0 else "B"
            row["playable"] = True

        self._append_reason(row, f"betting_brain_{action.lower()}_{round(score)}")
        return row

    def _collect_rows(self, package: Dict[str, Any]) -> List[Dict[str, Any]]:
        rows: Dict[str, Dict[str, Any]] = {}
        for source in ("main_pick", "value_pick"):
            item = package.get(source)
            if isinstance(item, dict) and item.get("market"):
                rows[self._row_key(item)] = dict(item)

        for source in ("supporting_picks", "bet_summary"):
            for item in package.get(source) or []:
                if isinstance(item, dict) and item.get("market"):
                    key = self._row_key(item)
                    rows[key] = self._merge_row(rows.get(key), item)
        return list(rows.values())

    @staticmethod
    def _merge_row(existing: Optional[Dict[str, Any]], incoming: Dict[str, Any]) -> Dict[str, Any]:
        if existing is None:
            return dict(incoming)
        merged = dict(incoming)
        merged.update({k: v for k, v in existing.items() if v is not None})
        for key in ("decision_reasons", "reasons"):
            reasons = list(existing.get(key) or []) + list(incoming.get(key) or [])
            if reasons:
                merged[key] = list(dict.fromkeys(reasons))
        return merged

    def _pick_value_candidate(self, rows: Dict[str, Dict[str, Any]], main_key: str) -> Optional[Dict[str, Any]]:
        candidates = [
            row for key, row in rows.items()
            if key != main_key
            and row.get("betting_brain", {}).get("action") in {"BET", "WATCH"}
            and (self._safe_float(row.get("odds"), 0.0) or 0.0) >= 1.60
        ]
        candidates.sort(key=self._candidate_sort_key, reverse=True)
        return dict(candidates[0]) if candidates else None

    def _summary_item(self, row: Dict[str, Any]) -> Dict[str, Any]:
        reasons = list(row.get("decision_reasons") or row.get("reasons") or [])
        return {
            "market": row.get("market"),
            "pick": row.get("pick"),
            "raw_confidence": row.get("raw_confidence", row.get("confidence")),
            "calibrated_confidence": row.get("calibrated_confidence", row.get("confidence")),
            "bet_grade": row.get("bet_grade", "PASS"),
            "playable": bool(row.get("playable")),
            "stake_units": float(row.get("stake_units", 0.0) or 0.0),
            "play_score": row.get("play_score", 0.0),
            "ev_edge": row.get("ev_edge", row.get("edge", 0.0)),
            "implied_prob": row.get("implied_prob", 0.0),
            "odds_reliability": row.get("odds_reliability", 0.35),
            "odds": row.get("odds", 0.0),
            "reasons": reasons[:6],
            "betting_brain": row.get("betting_brain"),
        }

    @staticmethod
    def _candidate_sort_key(row: Dict[str, Any]) -> Tuple[float, float, float]:
        brain = row.get("betting_brain") or {}
        action_boost = {"BET": 2.0, "WATCH": 1.0, "REJECT": 0.0}.get(str(brain.get("action")), 0.0)
        return (
            action_boost,
            float(brain.get("score", 0.0) or 0.0),
            float(row.get("play_score", 0.0) or 0.0),
        )

    @staticmethod
    def _row_key(row: Optional[Dict[str, Any]]) -> str:
        if not isinstance(row, dict):
            return ""
        return f"{row.get('market')}:{row.get('pick')}"

    def _force_no_bet(self, row: Dict[str, Any], reason: str) -> None:
        row["playable"] = False
        row["stake_units"] = 0.0
        row["bet_grade"] = "PASS"
        row["is_guaranteed"] = False
        row["pick_reason"] = reason
        if row.get("signal_tier") == "CORE":
            row["signal_tier"] = "PASS"
        self._append_reason(row, reason)

    @staticmethod
    def _append_reason(row: Dict[str, Any], reason: str) -> None:
        key = "decision_reasons" if "decision_reasons" in row else "reasons"
        reasons = list(row.get(key) or [])
        if reason not in reasons:
            reasons.append(reason)
        row[key] = reasons[:6]

    def _brain_stake(self, row: Dict[str, Any], score: float) -> float:
        existing = self._safe_float(row.get("stake_units"), 0.0) or 0.0
        odds = self._safe_float(row.get("odds"), 0.0) or 0.0
        if odds <= 1.0:
            return 0.0
        cap = 2.0 if score >= 82.0 else 1.2
        if score < 78.0:
            cap = 0.8
        return round(max(0.25, min(existing if existing > 0 else cap, cap)), 1)

    @staticmethod
    def _decision_band(main_pick: Optional[Dict[str, Any]]) -> str:
        if not main_pick:
            return "LOW"
        score = float((main_pick.get("betting_brain") or {}).get("score", 0.0) or 0.0)
        if score >= 82.0:
            return "HIGH"
        if score >= 72.0:
            return "MEDIUM"
        return "LOW"

    @staticmethod
    def _top_decisions(decisions: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
        ordered = sorted(decisions, key=lambda d: float(d.get("score", 0.0) or 0.0), reverse=True)
        return [
            {
                "action": item.get("action"),
                "score": item.get("score"),
                "summary": item.get("summary"),
                "vetoes": item.get("vetoes", []),
                "issues": item.get("issues", []),
            }
            for item in ordered[:5]
        ]

    @staticmethod
    def _summary(action: str, market: str, pick: str, positives: List[str], issues: List[str], vetoes: List[str]) -> str:
        if action == "BET":
            return f"{market} {pick} approved: evidence is aligned enough for a controlled stake."
        if action == "WATCH":
            return f"{market} {pick} is interesting but not clean enough for stake."
        if vetoes:
            return f"{market} {pick} rejected: {', '.join(vetoes[:3])}."
        if issues:
            return f"{market} {pick} rejected: {', '.join(issues[:3])}."
        return f"{market} {pick} rejected by evidence score."

    def _market_probability(self, row: Dict[str, Any], package: Dict[str, Any]) -> Optional[float]:
        direct = self._safe_float(row.get("probability"))
        if direct is not None:
            return direct
        board = package.get("market_board") or {}
        payload = board.get(str(row.get("market") or "")) if isinstance(board, dict) else None
        probs = payload.get("probs") if isinstance(payload, dict) else None
        if not isinstance(probs, dict):
            return None
        key = self._prob_key(str(row.get("market") or ""), str(row.get("pick") or ""))
        return self._safe_float(probs.get(key)) if key else None

    def _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":
            return self._safe_float(ms.get({"1": "home", "X": "draw", "2": "away"}.get(pick, "")))
        if market == "DC":
            home = self._safe_float(ms.get("home"), 0.0) or 0.0
            draw = self._safe_float(ms.get("draw"), 0.0) or 0.0
            away = self._safe_float(ms.get("away"), 0.0) or 0.0
            return {"1X": home + draw, "X2": draw + away, "12": home + away}.get(pick)
        if market == "OU25":
            key = self._prob_key(market, pick)
            return self._safe_float(ou25.get(key)) if key else None
        return None

    def _triple_value(self, package: Dict[str, Any], key: Optional[str]) -> Optional[Dict[str, Any]]:
        if not key:
            return None
        value = ((package.get("v27_engine") or {}).get("triple_value") or {}).get(key)
        return value if isinstance(value, dict) else None

    def _triple_key(self, market: str, pick: str) -> Optional[str]:
        prob_key = self._prob_key(market, pick)
        if market == "MS":
            return {"1": "home", "2": "away"}.get(pick)
        if market == "DC" and pick.upper() in {"1X", "X2", "12"}:
            return f"dc_{pick.lower()}"
        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

    @staticmethod
    def _prob_key(market: str, pick: str) -> Optional[str]:
        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 norm or "ust" in norm or "üst" in norm:
                return "over"
            if "under" in norm or "alt" in norm:
                return "under"
        if market == "BTTS":
            if "yes" in norm or "var" in norm:
                return "yes"
            if "no" in norm or "yok" in norm:
                return "no"
        if market == "OE":
            if "odd" in norm or "tek" in norm:
                return "odd"
            if "even" in norm or "cift" in norm or "çift" in norm:
                return "even"
        if market == "HTFT" and "/" in pick:
            return pick
        return None

    @staticmethod
    def _safe_float(value: Any, default: Optional[float] = None) -> Optional[float]:
        try:
            return float(value)
        except (TypeError, ValueError):
            return default
