"""Bond risk scoring — pure computation, no I/O or side effects."""
from __future__ import annotations

from typing import Optional

from app.scoring.types import BondRiskResult


# ── Credit rating → numeric score (0–100) ────────────────────────────────────

_RATING_SCALE: dict[str, float] = {
    # Investment grade
    "AAA": 100, "Aaa": 100,
    "AA+": 95,  "Aa1": 95,
    "AA":  90,  "Aa2": 90,
    "AA-": 87,  "Aa3": 87,
    "A+":  84,  "A1":  84,
    "A":   80,  "A2":  80,
    "A-":  77,  "A3":  77,
    "BBB+": 73, "Baa1": 73,
    "BBB":  70, "Baa2": 70,
    "BBB-": 65, "Baa3": 65,
    # High yield
    "BB+": 55,  "Ba1": 55,
    "BB":  50,  "Ba2": 50,
    "BB-": 45,  "Ba3": 45,
    "B+":  40,  "B1":  40,
    "B":   35,  "B2":  35,
    "B-":  30,  "B3":  30,
    # Distressed / default
    "CCC+": 22, "Caa1": 22,
    "CCC":  18, "Caa2": 18,
    "CCC-": 14, "Caa3": 14,
    "CC":   10, "Ca":   10,
    "C":     5,
    "D":     0, "DDD": 0, "DD": 0, "WD": 0,
}

# Investment-grade cutoff in the numeric scale
_IG_THRESHOLD = 65.0

# Duration is excluded: ModifiedDuration is not accessible on the current LSEG subscription.
# Weights are redistributed proportionally from the original 40/35/25 split.
_WEIGHTS = {"rating": 0.60, "spread": 0.40}


def _best_rating_score(
    moodys: Optional[str],
    sp: Optional[str],
    fitch: Optional[str],
) -> Optional[float]:
    scores = [
        _RATING_SCALE[r.strip()]
        for r in [moodys, sp, fitch]
        if r and r.strip() in _RATING_SCALE
    ]
    return max(scores) if scores else None


def _rating_grade_label(score: float) -> str:
    if score >= _IG_THRESHOLD:
        return "Investment Grade"
    if score >= 35:
        return "High Yield"
    return "Distressed"


def _spread_score(spread_bps: Optional[float]) -> float:
    """Tighter spread → lower credit risk → higher score (0–100)."""
    if spread_bps is None:
        return 50.0
    if spread_bps <= 50:
        return 100.0
    if spread_bps <= 100:
        return 90.0
    if spread_bps <= 200:
        return 75.0
    if spread_bps <= 350:
        return 55.0
    if spread_bps <= 500:
        return 40.0
    if spread_bps <= 750:
        return 25.0
    return 10.0


def _composite_grade(score: float) -> str:
    if score >= 75:
        return "LOW RISK"
    if score >= 55:
        return "MODERATE RISK"
    if score >= 35:
        return "ELEVATED RISK"
    return "HIGH RISK"


def compute_bond_risk(
    *,
    isin: str,
    rating_moodys: Optional[str] = None,
    rating_sp: Optional[str] = None,
    rating_fitch: Optional[str] = None,
    z_spread: Optional[float] = None,
    asset_swap_spread: Optional[float] = None,
) -> BondRiskResult:
    """
    Compute a composite 0–100 risk score for a single bond.

    Weights: Rating 60 % · Spread 40 %.
    If no rating is available the full weight falls on spread.
    Spread priority: Z-spread > Asset Swap Spread.
    """
    spread_bps: Optional[float] = None
    spread_used = "none"
    if z_spread is not None:
        spread_bps, spread_used = z_spread, "z_spread"
    elif asset_swap_spread is not None:
        spread_bps, spread_used = asset_swap_spread, "asset_swap_spread"

    rat_score = _best_rating_score(rating_moodys, rating_sp, rating_fitch)
    spr_score = _spread_score(spread_bps)

    if rat_score is not None:
        composite = rat_score * _WEIGHTS["rating"] + spr_score * _WEIGHTS["spread"]
    else:
        composite = spr_score

    composite = round(min(max(composite, 0.0), 100.0), 1)

    return BondRiskResult(
        isin=isin,
        composite_score=composite,
        composite_grade=_composite_grade(composite),
        rating_score=rat_score,
        rating_grade=_rating_grade_label(rat_score) if rat_score is not None else None,
        spread_score=round(spr_score, 1),
        spread_used=spread_used,
        spread_bps=spread_bps,
        is_investment_grade=rat_score is not None and rat_score >= _IG_THRESHOLD,
    )
