"""Overall Score: configurable-weight blend of the five sub-scores (spec §17).

AUDIT (StockLab overhaul, Part 7): re-normalizing weights over only the pillars that have a
value is necessary (an INSUFFICIENT_DATA pillar can't contribute a number), but doing so
*unconditionally* has a real score-inflation failure mode: a company missing its worst pillar
(most commonly Competitive Advantage, which requires >=3 measurable proxies before it computes
at all — see persistence.py) has that pillar's weight silently redistributed to the pillars that
DO have a value, which is equivalent to assuming the missing pillar would have scored the same as
the weighted average of the present ones. For a company that is missing Competitive Advantage
data specifically *because* it's a weak, commoditized, hard-to-differentiate business (plausibly
correlated with why the proxies are thin), that assumption is optimistic, not neutral.

Fix applied here: renormalization is now capped — if the combined weight of the missing pillars
exceeds `MAX_MISSING_WEIGHT_FOR_RENORMALIZATION`, Overall Score is INSUFFICIENT_DATA (None)
rather than extrapolated from a minority of the pillars. This does not fully eliminate the
inflation risk for a single missing pillar (15% Competitive Advantage weight is still
redistributed when everything else is present) — that residual risk is exactly why Confidence
(app/engines/scoring/confidence.py) is now a mandatory, separately-displayed number rather than
an optional detail: any renormalization-affected Overall Score is reflected in a lower Confidence
score, so a consumer of the API is never shown "Score: 88" without the means to see that it rests
on 4 of 5 pillars.
"""
from __future__ import annotations

from dataclasses import dataclass
from typing import Optional

from app.engines.scoring.subscores import ScoreResult

DEFAULT_WEIGHTS = {
    "quality": 0.25,
    "financial_health": 0.20,
    "growth": 0.20,
    "competitive_advantage": 0.15,
    "valuation": 0.20,
}

# If pillars worth more than this fraction of total weight are missing, Overall Score refuses to
# renormalize over the remainder and returns INSUFFICIENT_DATA instead. 0.35 permits the single
# largest pillar (Quality, 0.25) or Competitive Advantage + a fractional second pillar to be
# missing while still scoring; it refuses when 2+ substantial pillars are absent, since at that
# point "Overall Score" would rest on a minority of the framework. Configurable, not hardcoded
# into callers — spec §57 (every threshold configurable).
MAX_MISSING_WEIGHT_FOR_RENORMALIZATION = 0.35


@dataclass
class Scorecard:
    quality: ScoreResult
    financial_health: ScoreResult
    growth: ScoreResult
    competitive_advantage: ScoreResult
    valuation: ScoreResult
    overall: Optional[float]
    weights_used: dict[str, float]
    subscores_included_in_overall: list[str]
    missing_weight: float = 0.0


def compute_overall_score(
    quality: ScoreResult,
    financial_health: ScoreResult,
    growth: ScoreResult,
    competitive_advantage: ScoreResult,
    valuation: ScoreResult,
    weights: Optional[dict[str, float]] = None,
    max_missing_weight: float = MAX_MISSING_WEIGHT_FOR_RENORMALIZATION,
) -> Scorecard:
    weights = dict(weights or DEFAULT_WEIGHTS)
    parts = {
        "quality": quality, "financial_health": financial_health, "growth": growth,
        "competitive_advantage": competitive_advantage, "valuation": valuation,
    }
    # Renormalize weights over whichever sub-scores actually have a value, so one
    # INSUFFICIENT_DATA sub-score doesn't silently drag Overall toward 0 — but only up to
    # max_missing_weight of total weight; beyond that, refuse to extrapolate (see module
    # docstring for the score-inflation reasoning this guards against).
    available = {k: v for k, v in parts.items() if v.value is not None}
    missing_weight = sum(weights[k] for k in parts if k not in available)
    if not available or missing_weight > max_missing_weight:
        return Scorecard(quality, financial_health, growth, competitive_advantage, valuation,
                          overall=None, weights_used={}, subscores_included_in_overall=[],
                          missing_weight=missing_weight)
    total_w = sum(weights[k] for k in available)
    renormalized = {k: weights[k] / total_w for k in available}
    overall = sum(renormalized[k] * available[k].value for k in available)
    return Scorecard(
        quality, financial_health, growth, competitive_advantage, valuation,
        overall=overall, weights_used=renormalized,
        subscores_included_in_overall=list(available.keys()),
        missing_weight=missing_weight,
    )
