"""Blend DCF + multiples into one Fair Value with a confidence band (spec §24 / VALUATION.md §4)."""
from __future__ import annotations

from dataclasses import dataclass, field
from enum import Enum
from statistics import median
from typing import Optional


class BusinessProfile(str, Enum):
    STABLE_FCF = "STABLE_FCF"
    CYCLICAL = "CYCLICAL"
    FINANCIALS = "FINANCIALS"
    PRE_FCF = "PRE_FCF"


# (dcf_weight, multiples_weight) — VALUATION.md §4 table. Config-overridable per strategy.
DEFAULT_PROFILE_WEIGHTS: dict[BusinessProfile, tuple[float, float]] = {
    BusinessProfile.STABLE_FCF: (0.6, 0.4),
    BusinessProfile.CYCLICAL: (0.3, 0.7),
    BusinessProfile.FINANCIALS: (0.1, 0.9),
    BusinessProfile.PRE_FCF: (0.0, 1.0),
}


def detect_business_profile(sector_code: str, fcf_history: list[Optional[float]]) -> BusinessProfile:
    if sector_code.upper() in ("FINANCIALS", "INSURANCE"):
        return BusinessProfile.FINANCIALS
    if sector_code.upper() in ("ENERGY", "MATERIALS", "AUTOMOTIVE"):
        return BusinessProfile.CYCLICAL
    recent = [v for v in fcf_history[:3] if v is not None]
    if recent and all(v <= 0 for v in recent):
        return BusinessProfile.PRE_FCF
    return BusinessProfile.STABLE_FCF


@dataclass(frozen=True)
class BlendedFairValue:
    bear_fair_value: Optional[float]
    base_fair_value: Optional[float]
    bull_fair_value: Optional[float]
    weighted_fair_value: Optional[float]
    fair_value_range: tuple[Optional[float], Optional[float]]
    confidence: float                     # 0-100
    profile: BusinessProfile
    dcf_weight: float
    multiples_weight: float
    multiples_used: list[str] = field(default_factory=list)


#: Part B3: at or above this many usable multiples, the central estimate is the MEDIAN rather than
#: the mean. Below it there is no meaningful outlier to reject and the mean is used.
MIN_MULTIPLES_FOR_MEDIAN = 3


def _central_multiple_value(values: list[Optional[float]]) -> Optional[float]:
    """The single number the multiples side contributes to the blend.

    AUDIT FIX (StockLab final engineering pass, Part B3 -- docs/AUDIT_FAIR_VALUE_B3.md). This was
    an unweighted MEAN over up to five per-share fair values (P/E, forward P/E, EV/EBITDA, P/FCF,
    EV/FCF). Those five are not independent estimates of the same quantity with similar error: a
    P/FCF fair value built on a small free-cash-flow denominator can be several multiples of the
    others, and a single such value moves the mean -- and therefore the blended Fair Value, the
    margin of safety, the price bands and ultimately the Buy/Sell recommendation -- by a large
    amount with no visible cause.

    With three or more usable multiples the median is used instead, which is the standard robust
    central estimate and cannot be moved arbitrarily far by one outlier. With one or two, the mean
    is kept: there is nothing to reject, and the median of two values IS their mean anyway.

    This changes shipped Fair Values for any security with 3+ usable multiples and a dispersed
    set. That is the point of the fix and it is recorded in CHANGELOG.md, not slipped in.
    """
    clean = [v for v in values if v is not None]
    if not clean:
        return None
    if len(clean) >= MIN_MULTIPLES_FOR_MEDIAN:
        return median(clean)
    return sum(clean) / len(clean)


def _average_ignoring_none(values: list[Optional[float]]) -> Optional[float]:
    """Kept for callers that specifically want the mean; the blend now uses
    _central_multiple_value(). Not removed, because removing a public-shaped helper other code
    may rely on is a change this pass has no way to regression-test."""
    clean = [v for v in values if v is not None]
    if not clean:
        return None
    return sum(clean) / len(clean)


def blend_fair_values(
    dcf_bear: Optional[float], dcf_base: Optional[float], dcf_bull: Optional[float],
    multiples_fair_values: dict[str, Optional[float]],
    profile: BusinessProfile,
    weights_override: Optional[tuple[float, float]] = None,
    data_completeness_pct: float = 100.0,
) -> BlendedFairValue:
    dcf_w, mult_w = weights_override or DEFAULT_PROFILE_WEIGHTS[profile]
    multiples_used = [k for k, v in multiples_fair_values.items() if v is not None]
    multiples_avg = _central_multiple_value(list(multiples_fair_values.values()))

    have_dcf = dcf_base is not None
    have_mult = multiples_avg is not None
    if not have_dcf and not have_mult:
        return BlendedFairValue(None, None, None, None, (None, None), 0.0, profile, dcf_w, mult_w, multiples_used)

    # Renormalize weights if one side is missing entirely.
    if not have_dcf:
        dcf_w_eff, mult_w_eff = 0.0, 1.0
    elif not have_mult:
        dcf_w_eff, mult_w_eff = 1.0, 0.0
    else:
        dcf_w_eff, mult_w_eff = dcf_w, mult_w

    def _blend(dcf_val):
        parts = []
        if have_dcf and dcf_w_eff:
            parts.append(dcf_w_eff * dcf_val)
        if have_mult and mult_w_eff:
            parts.append(mult_w_eff * multiples_avg)
        return sum(parts) if parts else None

    weighted_base = _blend(dcf_base)
    weighted_bear = _blend(dcf_bear) if dcf_bear is not None else (multiples_avg if not have_dcf else weighted_base)
    weighted_bull = _blend(dcf_bull) if dcf_bull is not None else (multiples_avg if not have_dcf else weighted_base)

    # Confidence: data completeness, DCF/multiples agreement, business-profile complexity penalty.
    dispersion_penalty = 0.0
    if have_dcf and have_mult and weighted_base:
        spread = abs(dcf_base - multiples_avg) / abs(weighted_base) if weighted_base else 1.0
        dispersion_penalty = min(spread, 1.0) * 30  # up to -30 points for wide disagreement
    complexity_penalty = {
        BusinessProfile.STABLE_FCF: 0, BusinessProfile.CYCLICAL: 5,
        BusinessProfile.FINANCIALS: 15, BusinessProfile.PRE_FCF: 20,
    }[profile]
    confidence = max(0.0, min(100.0, data_completeness_pct - dispersion_penalty - complexity_penalty))

    return BlendedFairValue(
        bear_fair_value=weighted_bear, base_fair_value=weighted_base, bull_fair_value=weighted_bull,
        weighted_fair_value=weighted_base,
        fair_value_range=(weighted_bear, weighted_bull),
        confidence=confidence, profile=profile, dcf_weight=dcf_w_eff, multiples_weight=mult_w_eff,
        multiples_used=multiples_used,
    )
