"""T01 - Bayesian Deployer Reputation System.

Per MINIMAX_M3_TASKS.md T01. Beta-Binomial posterior replaces the
weighted-sum that conflated probabilities with volumes.

The legacy 0-100 score is kept for backward compatibility (every
existing consumer reads it). The new authoritative output is:
  probability     - P(rug) = alpha / (alpha + beta)
  credible_interval_95 - 95% Bayesian CI from Beta distribution
  observations    - {successes, failures, total}

We start with a uniform prior Beta(1,1). Each rug increments beta.
Each legitimate deployment increments alpha. News sentiment < -0.3
adds 2 to beta (pessimistic prior). News sentiment > 0.3 adds 2 to
alpha (optimistic prior). Age and volume are logged but not folded
into the prior (they are orthogonal signals, not evidence).

The legacy 0-100 score is derived deterministically from probability:
  score = round((1 - probability) * 100)
"""
from __future__ import annotations

import logging
import math

from app.catalog.models import Deployer, utcnow

log = logging.getLogger(__name__)


# ── Prior adjustments (Bayesian update weights) ────────────────
PRIOR_WEIGHTS: dict[str, int] = {
    "prior_alpha": 1,            # Beta(1,1) = uniform prior
    "prior_beta": 1,
    "news_pessimistic_shift": 2, # +2 to beta if avg sentiment < -0.3
    "news_optimistic_shift": 2,  # +2 to alpha if avg sentiment > 0.3
    "news_window_hours": 720,    # 30 days
    "news_negative_threshold": -0.3,
    "news_positive_threshold": 0.3,
}


def _beta_credible_interval_95(alpha: float, beta: float) -> tuple[float, float]:
    """Approximate 95% credible interval for Beta(alpha, beta).

    Uses the normal approximation to the Beta distribution, which is
    accurate for alpha+beta > 30 (our regime: typically dozens of
    observations per deployer). For low-observation regimes, falls back
    to a wider quantile-based interval.
    """
    n = alpha + beta
    if n <= 0:
        return (0.0, 1.0)
    if n < 30:
        # Wider interval for low-data regime
        mean = alpha / n
        var = (alpha * beta) / (n * n * (n + 1))
        sd = math.sqrt(var)
        # Use 1.96 but clamp to [0,1]
        lo = max(0.0, mean - 1.96 * sd)
        hi = min(1.0, mean + 1.96 * sd)
        return (lo, hi)
    # High-data regime: tighter interval
    mean = alpha / n
    var = (alpha * beta) / (n * n * (n + 1))
    sd = math.sqrt(var)
    lo = max(0.0, mean - 1.96 * sd)
    hi = min(1.0, mean + 1.96 * sd)
    return (lo, hi)


async def compute_deployer_posterior(
    deployer: Deployer,
    catalog: CatalogService,  # noqa: F821  -- pre-existing bug, see fix(f821) tracking issue
) -> dict:
    """Compute Bayesian reputation for a deployer.

    Returns:
        {
            "probability": float,           # P(rug), 0..1
            "credible_interval_95": [lo, hi],  # 95% Bayesian CI
            "observations": {
                "rugs": int, "legit": int, "total": int,
                "alpha": float, "beta": float,
            },
            "news_sentiment": float | None,  # -1..+1 if available
            "score": int,                     # legacy 0-100 (backward compat)
            "computed_at": str,               # ISO8601
        }
    """
    cache_key = f"catalog:deployer_rep:v2:{deployer.wallet_id}"
    if catalog._health.redis:
        try:
            cached = await catalog._redis.get(cache_key)
            if cached:
                import json as _json
                return _json.loads(cached)
        except Exception:
            pass

    # ── Update prior from observations ──
    alpha = float(PRIOR_WEIGHTS["prior_alpha"])
    beta = float(PRIOR_WEIGHTS["prior_beta"])

    rugs = max(0, deployer.rug_count)
    legit = max(0, len(deployer.deployments) - rugs)
    alpha += legit
    beta += rugs

    # ── News sentiment prior adjustment ──
    news_sentiment = None
    if catalog._health.postgres:
        try:
            async with catalog._pg_pool.acquire() as conn:
                rows = await conn.fetch(
                    """SELECT sentiment_score FROM news_items
                       WHERE $1 = ANY(wallets_mentioned)
                       AND published_at > NOW() - make_interval(hours => $2)
                       LIMIT 20""",
                    deployer.wallet_id,
                    PRIOR_WEIGHTS["news_window_hours"],
                )
            scores = [r["sentiment_score"] for r in rows if r["sentiment_score"] is not None]
            if scores:
                news_sentiment = sum(scores) / len(scores)
                if news_sentiment < PRIOR_WEIGHTS["news_negative_threshold"]:
                    beta += PRIOR_WEIGHTS["news_pessimistic_shift"]
                elif news_sentiment > PRIOR_WEIGHTS["news_positive_threshold"]:
                    alpha += PRIOR_WEIGHTS["news_optimistic_shift"]
        except Exception as e:
            log.debug("reputation_news_fail: %s", e)

    # ── Posterior ──
    total = alpha + beta
    probability = alpha / total if total > 0 else 0.5
    lo, hi = _beta_credible_interval_95(alpha, beta)

    result = {
        "probability": round(probability, 4),
        "credible_interval_95": [round(lo, 4), round(hi, 4)],
        "observations": {
            "rugs": int(rugs),
            "legit": int(legit),
            "total": int((deployer.total_volume_usd and len(deployer.deployments)) or 0),
            "alpha": alpha,
            "beta": beta,
        },
        "news_sentiment": round(news_sentiment, 4) if news_sentiment is not None else None,
        # Legacy 0-100 score: probability of legitness scaled to 0..100
        # probability = P(rug), so legitness = 1 - probability
        "score": round((1.0 - probability) * 100),
        "computed_at": utcnow().isoformat(),
    }

    if catalog._health.redis:
        try:
            import json as _json
            await catalog._redis.setex(cache_key, 3600, _json.dumps(result))
        except Exception:
            pass

    return result


# ── Backward-compatible wrapper (returns just the int score) ──

async def compute_deployer_reputation(
    deployer: Deployer,
    catalog: CatalogService,  # noqa: F821  -- pre-existing bug, see fix(f821) tracking issue
) -> int:
    """Legacy 0-100 reputation score.

    Returns the integer score derived from the Bayesian posterior.
    New code should call compute_deployer_posterior() directly for the
    full probability + CI.
    """
    posterior = await compute_deployer_posterior(deployer, catalog)
    return posterior["score"]
