"""Multiples-based fair value (spec §24 / docs/VALUATION.md §3).

AUDIT (StockLab overhaul, Part 12): `compute_self_historical_reference_multiples` below is new
this pass. Before this audit, `app/workers/recompute.py` called this module with a **hardcoded
placeholder** `ReferenceMultiples(pe=18.0, forward_pe=16.0, ev_to_ebitda=11.0, p_fcf=20.0,
ev_to_fcf=18.0, source=ReferenceSource.INDUSTRY_MEDIAN)` for every single security regardless of
sector, size, or history — a made-up "industry median" that was never actually computed from
industry data. That is now replaced with a real median of the security's OWN historical
`metric_history` rows (`ReferenceSource.SELF_HISTORICAL_5Y_MEDIAN`) wherever enough history
exists, and an explicit `None` (NOT AVAILABLE) per field otherwise — never a fabricated number.
`ReferenceSource.INDUSTRY_MEDIAN` / `PEER_MEDIAN` remain modeled in the enum but nothing in this
codebase computes them yet — that requires a cross-security aggregate query
(`app/workers/peer_groups.py`, same not-yet-built dependency the scoring engine's peer-percentile
lookups already document) — see docs/AUDIT_VALUATION.md.
"""
from __future__ import annotations

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


class ReferenceSource(str, Enum):
    SELF_HISTORICAL_5Y_MEDIAN = "SELF_HISTORICAL_5Y_MEDIAN"
    INDUSTRY_MEDIAN = "INDUSTRY_MEDIAN"
    PEER_MEDIAN = "PEER_MEDIAN"
    #: Part B2: some fields came from the industry median, others fell back to self-historical.
    #: A single `source` label would be a lie in that case, so the mix gets its own value and
    #: `ReferenceMultiples.per_field_source` records which field came from where.
    MIXED_INDUSTRY_AND_SELF = "MIXED_INDUSTRY_AND_SELF"


@dataclass(frozen=True)
class MultipleFairValue:
    metric: str                 # e.g. "pe", "ev_to_ebitda"
    fair_value_per_share: Optional[float]
    reference_multiple: Optional[float]
    reference_source: ReferenceSource
    per_share_fundamental: Optional[float]


def _fv_from_price_multiple(per_share_fundamental: Optional[float], reference_multiple: Optional[float]) -> Optional[float]:
    if per_share_fundamental is None or reference_multiple is None:
        return None
    return per_share_fundamental * reference_multiple


def _fv_from_ev_multiple(
    per_share_denominator_fundamental: Optional[float],  # e.g. EBITDA per share, FCF per share
    reference_multiple: Optional[float],
    net_debt_per_share: Optional[float],
) -> Optional[float]:
    if per_share_denominator_fundamental is None or reference_multiple is None or net_debt_per_share is None:
        return None
    ev_per_share = per_share_denominator_fundamental * reference_multiple
    fair_value = ev_per_share - net_debt_per_share
    # AUDIT FIX (StockLab final engineering pass, Part B3 -- docs/AUDIT_FAIR_VALUE_B3.md).
    # An EV multiple can produce a NEGATIVE per-share equity value even from a perfectly positive
    # fundamental, when net debt per share exceeds the implied enterprise value per share (a real
    # outcome for a heavily indebted company). That is a meaningful economic statement -- "at this
    # multiple the enterprise does not cover its debt" -- but it is NOT a fair value per share, and
    # feeding it into the blend's average silently drags the whole valuation down or negative.
    # Downstream, compute_price_bands() would then build a "strong buy price" out of a negative
    # number. Returned as None (NOT MEANINGFUL) instead, consistent with how this codebase already
    # handles a P/E on negative EPS, rather than passed on as a number.
    return fair_value if fair_value > 0 else None


@dataclass(frozen=True)
class CompanyFundamentalsPerShare:
    eps: Optional[float]
    forward_eps: Optional[float]
    ebitda_per_share: Optional[float]
    fcf_per_share: Optional[float]
    net_debt_per_share: Optional[float]


@dataclass(frozen=True)
class ReferenceMultiples:
    pe: Optional[float]
    forward_pe: Optional[float]
    ev_to_ebitda: Optional[float]
    p_fcf: Optional[float]
    ev_to_fcf: Optional[float]
    source: ReferenceSource
    #: Part B2: `{field_name: ReferenceSource}` when the fields did not all come from one place.
    #: `None` means every populated field came from `source`.
    per_field_source: Optional[dict] = None


REFERENCE_FIELDS = ("pe", "forward_pe", "ev_to_ebitda", "p_fcf", "ev_to_fcf")

PREFERENCE_INDUSTRY_THEN_SELF = "industry_then_self"
PREFERENCE_SELF_THEN_INDUSTRY = "self_then_industry"
PREFERENCE_SELF_ONLY = "self_only"


def combine_reference_multiples(
    self_historical: ReferenceMultiples,
    industry_medians: Optional[dict] = None,
    preference: str = PREFERENCE_INDUSTRY_THEN_SELF,
) -> ReferenceMultiples:
    """Combine a security's own historical multiples with its industry's medians, field by field.

    AUDIT FIX (StockLab final engineering pass, Part B2 — docs/AUDIT_PEER_GROUPS_B2.md).
    `ReferenceSource.INDUSTRY_MEDIAN` has been modelled in this enum since the first build and
    nothing ever computed it, because it needs a cross-security aggregate query that did not
    exist. `app/workers/peer_groups.py::industry_reference_multiples()` now computes it, and this
    is where the two references meet.

    `industry_medians` is `{metric_key: median}` for THIS security's industry, or `None`/`{}` when
    the industry had too few companies reporting that multiple to publish a median — in which case
    this degrades to exactly the self-historical behaviour that shipped before B2.

    The two references answer different questions and neither is universally right: the industry
    median says "what do comparable businesses trade at", the self-historical says "what has this
    business traded at". `industry_then_self` is the default because an industry median is the
    conventional anchor for a relative valuation and because a security's own history bakes in
    whatever mis-rating it has carried; `self_then_industry` and `self_only` are available for a
    deployment that disagrees. This is a stated preference with a stated reason, not a claim that
    one is correct.
    """
    if preference == PREFERENCE_SELF_ONLY or not industry_medians:
        return self_historical

    per_field: dict[str, ReferenceSource] = {}
    values: dict[str, Optional[float]] = {}
    for field_name in REFERENCE_FIELDS:
        own = getattr(self_historical, field_name)
        industry = industry_medians.get(field_name)
        if preference == PREFERENCE_SELF_THEN_INDUSTRY:
            first, first_src, second, second_src = own, self_historical.source, industry, ReferenceSource.INDUSTRY_MEDIAN
        else:
            first, first_src, second, second_src = industry, ReferenceSource.INDUSTRY_MEDIAN, own, self_historical.source
        if first is not None:
            values[field_name] = first
            per_field[field_name] = first_src
        elif second is not None:
            values[field_name] = second
            per_field[field_name] = second_src
        else:
            values[field_name] = None

    used = set(per_field.values())
    if len(used) == 1:
        source = next(iter(used))
    elif not used:
        source = self_historical.source
    else:
        source = ReferenceSource.MIXED_INDUSTRY_AND_SELF

    return ReferenceMultiples(
        **values, source=source, per_field_source=(per_field or None),
    )


def compute_multiples_fair_values(
    fundamentals: CompanyFundamentalsPerShare, refs: ReferenceMultiples,
) -> list[MultipleFairValue]:
    # AUDIT FIX (Part B3): after Part B2 a ReferenceMultiples can be a MIX of industry-median and
    # self-historical fields, so labelling every MultipleFairValue with the aggregate `refs.source`
    # would tell the reader "MIXED" for a field that came unambiguously from one place. Use the
    # per-field source when B2 recorded one.
    def _src(field_name: str) -> ReferenceSource:
        if refs.per_field_source:
            return refs.per_field_source.get(field_name, refs.source)
        return refs.source

    results = [
        MultipleFairValue("pe", _fv_from_price_multiple(fundamentals.eps, refs.pe), refs.pe, _src("pe"), fundamentals.eps),
        MultipleFairValue("forward_pe", _fv_from_price_multiple(fundamentals.forward_eps, refs.forward_pe), refs.forward_pe, _src("forward_pe"), fundamentals.forward_eps),
        MultipleFairValue(
            "ev_to_ebitda",
            _fv_from_ev_multiple(fundamentals.ebitda_per_share, refs.ev_to_ebitda, fundamentals.net_debt_per_share),
            refs.ev_to_ebitda, _src("ev_to_ebitda"), fundamentals.ebitda_per_share,
        ),
        MultipleFairValue("p_fcf", _fv_from_price_multiple(fundamentals.fcf_per_share, refs.p_fcf), refs.p_fcf, _src("p_fcf"), fundamentals.fcf_per_share),
        MultipleFairValue(
            "ev_to_fcf",
            _fv_from_ev_multiple(fundamentals.fcf_per_share, refs.ev_to_fcf, fundamentals.net_debt_per_share),
            refs.ev_to_fcf, _src("ev_to_fcf"), fundamentals.fcf_per_share,
        ),
    ]
    # Negative-fundamental cases (EPS<=0, FCF<=0) naturally yield None upstream in the metrics
    # engine's N/M handling; a multiple applied to a negative fundamental is excluded here too.
    return [
        r if (r.per_share_fundamental is None or r.per_share_fundamental > 0) else
        MultipleFairValue(r.metric, None, r.reference_multiple, r.reference_source, r.per_share_fundamental)
        for r in results
    ]


MIN_HISTORICAL_PERIODS_FOR_REFERENCE_MULTIPLE = 3


def compute_self_historical_reference_multiples(
    historical_values: dict[str, list[float]],
    min_periods: int = MIN_HISTORICAL_PERIODS_FOR_REFERENCE_MULTIPLE,
) -> ReferenceMultiples:
    """
    `historical_values` maps metric key ("pe", "forward_pe", "ev_to_ebitda", "p_fcf", "ev_to_fcf")
    to that security's own historical values for the metric (from `metric_history`, most recent
    calculation excluded by the caller — this function only takes medians, it doesn't know which
    period is "current"). Only metric-status VERIFIED/CALCULATED values should be passed in by the
    caller — N/M and MISSING periods are already excluded before they reach `metric_history` as a
    stored value (they're stored as `value=None`), so callers should filter `None`s out before
    calling this.

    Returns `None` per field (not a fabricated median) whenever fewer than `min_periods` real
    historical values exist for that metric — this is the fix for the hardcoded-placeholder
    finding in docs/AUDIT_VALUATION.md. A field with only 1-2 historical points is not excluded
    out of excess caution but because a "median" of 1-2 noisy points is not meaningfully different
    from just picking one, and presenting it as "Historical" would overstate its statistical
    grounding.
    """
    def _median_or_none(key: str) -> Optional[float]:
        values = [v for v in historical_values.get(key, []) if v is not None]
        if len(values) < min_periods:
            return None
        return float(median(values))

    return ReferenceMultiples(
        pe=_median_or_none("pe"),
        forward_pe=_median_or_none("forward_pe"),
        ev_to_ebitda=_median_or_none("ev_to_ebitda"),
        p_fcf=_median_or_none("p_fcf"),
        ev_to_fcf=_median_or_none("ev_to_fcf"),
        source=ReferenceSource.SELF_HISTORICAL_5Y_MEDIAN,
    )
