"""Model: strength-based Poisson for football 1X2 predictions.

lambda_home = HOME_BASE * att(home) * def(away)
lambda_away = AWAY_BASE * att(away) * def(home)
att = clamp(ppg / league_avg_ppg), def = 2 - att
Constants fit 2024-26 Bundesliga calibration; same family as Dixon-Coles (minus the
low-score correlation correction, which we can add later).
"""
import math

HOME_BASE = 1.45   # league avg home goals
AWAY_BASE = 1.15   # league avg away goals
ATT_CLAMP = (0.55, 1.45)
MAX_GOALS = 9


def poisson_pmf(lmbda, k):
    return math.exp(-lmbda) * lmbda**k / math.factorial(k)


def match_probs(lambda_home, lambda_away):
    """Returns (p_home, p_draw, p_away) over an independent Poisson grid."""
    lh = max(lambda_home, 0.05)
    la = max(lambda_away, 0.05)
    ph = pd = 0.0
    for h in range(MAX_GOALS):
        for a in range(MAX_GOALS):
            p = poisson_pmf(lh, h) * poisson_pmf(la, a)
            if h > a:
                ph += p
            elif h == a:
                pd += p
    return ph, pd, 1.0 - ph - pd


def att_coef(ppg, avg_ppg):
    lo, hi = ATT_CLAMP
    return max(lo, min(hi, ppg / avg_ppg)) if avg_ppg > 0 else 1.0


def predict(home_ppg, away_ppg, avg_ppg, home_n=None, away_n=None,
            shrink=10.0, home_base=HOME_BASE, away_base=AWAY_BASE):
    """Returns dict: lambda_home, lambda_away, ph, pd, pa, pick, pick_prob.

    Shrinkage: with home_n/away_n games played, ppg is regressed toward the
    league average (prior strength shrink=6 games) — tames early-season noise
    where 3 games can make PSG look like a mid-table side.
    """
    def shrink_ppg(ppg, n):
        if n is None or n < 1:
            return ppg
        return (ppg * n + avg_ppg * shrink) / (n + shrink)

    home_ppg = shrink_ppg(home_ppg, home_n)
    away_ppg = shrink_ppg(away_ppg, away_n)
    att_h = att_coef(home_ppg, avg_ppg)
    att_a = att_coef(away_ppg, avg_ppg)
    lh = home_base * att_h * (2.0 - att_a)
    la = away_base * att_a * (2.0 - att_h)
    ph, pd, pa = match_probs(lh, la)
    pick, prob = max((("1", ph), ("X", pd), ("2", pa)), key=lambda x: x[1])
    return {"lh": lh, "la": la, "ph": ph, "pd": pd, "pa": pa,
            "pick": pick, "pick_prob": prob}


def ev_1x2(decimal_odds, model_probs):
    """(outcome, decimal_odds, model_prob) -> EV per 1u, Kelly stake cap 5%."""
    out = {}
    for outcome in ("1", "X", "2"):
        o = decimal_odds.get(outcome)
        p = model_probs[outcome]
        if not o or o <= 1.0:
            continue
        ev = p * o - 1.0
        kelly = (ev) / (o - 1.0) if o > 1.0 else 0.0
        out[outcome] = {"ev": ev, "kelly": max(0.0, min(0.05, kelly))}
    return out


def binary_markets(lambda_home, lambda_away, maxg=12):
    """Convert Poisson expectations into binary prediction-market prices.

    Returns {home_win, away_win, draw, over_2_5, under_2_5, btts} as
    probabilities (0-1). 'Win' markets resolve NO on a draw in regulation.
    Used for yes/no markets (World.xyz, Polymarket) where price == probability.
    """
    lh, la = max(lambda_home, 0.05), max(lambda_away, 0.05)
    ph = pd = 0.0
    over25 = 0.0
    for h in range(maxg):
        for a in range(maxg):
            p = poisson_pmf(lh, h) * poisson_pmf(la, a)
            if h > a:
                ph += p
            elif h == a:
                pd += p
            if h + a >= 3:
                over25 += p
    p_btts = (1.0 - math.exp(-lh)) * (1.0 - math.exp(-la))
    return {"home_win": ph, "draw": pd, "away_win": 1.0 - ph - pd,
            "over_2_5": over25, "under_2_5": 1.0 - over25, "btts": p_btts}


def world_verdict(model_p, market_price, spread=0.02, edge_min=0.05):
    """Decision rule for a yes/no market given model prob and quoted price.

    World's dealer spread is baked into the quoted price (~2-3%), so the edge
    must clear model_p - price > edge_min + spread. Returns dict with side,
    edge, and action ('BUY YES' / 'BUY NO' / 'PASS').
    """
    if market_price is None:
        return {"action": "PASS", "reason": "no price"}
    edge_yes = model_p - market_price - spread
    edge_no = (1.0 - model_p) - (1.0 - market_price) - spread
    if edge_yes >= edge_min:
        return {"action": "BUY YES", "edge": edge_yes, "price_at_most": model_p - edge_min - spread}
    if edge_no >= edge_min:
        return {"action": "BUY NO", "edge": edge_no,
                "price_at_least": model_p + edge_min + spread}
    return {"action": "PASS", "edge_yes": edge_yes, "edge_no": edge_no}