"""Pure peer comparison for fundamentals (read-time).

Peers are tracked-universe issuers sharing the **first two SIC digits**,
deduplicated by CIK (GOOG/GOOGL are one issuer, one observation). This module is
the pure statistics core: given a subject value and the peer group's values for a
metric, it returns median + polarity-aware favorable percentile + peer_count, or
None when there are fewer than the minimum valid peers (the caller then omits the
industry object entirely rather than show a misleading comparison).

Grouping (which issuers share a 2-digit SIC, CIK-dedup) is the API's job; this
module only does the math. **Absolute net_debt is size-dependent and must not get
a peer percentile** — leverage is compared via net_debt_to_ebitda.
"""

from __future__ import annotations

import math
import statistics
from dataclasses import dataclass
from typing import Any

MIN_PEERS = 5


def _finite(v: Any) -> bool:
    """True for a finite number — excludes None, bool, NaN, ±inf (plan: null/invalid)."""
    return isinstance(v, (int, float)) and not isinstance(v, bool) and math.isfinite(v)

# Metric -> is a higher value more favorable? (Peer-eligible metrics only;
# absolute net_debt is intentionally absent — size-dependent.)
HIGHER_IS_BETTER: dict[str, bool] = {
    "revenue_growth_yoy": True,
    "eps_growth_yoy": True,
    "operating_margin": True,
    "fcf_margin": True,
    "fcf_yield": True,
    "net_debt_to_ebitda": False,   # lower leverage is better
    "pe": False,                   # cheaper is better
    "share_count_change_yoy": False,  # dilution is bad
}


@dataclass
class PeerStat:
    median: float
    favorable_percentile: int  # 0-100, polarity-aware (higher = more favorable)
    peer_count: int            # valid issuers in the group


def peer_stat(
    subject: float | None,
    group_values: list[float | None],
    *,
    higher_is_better: bool,
    min_peers: int = MIN_PEERS,
) -> PeerStat | None:
    """Median + favorable percentile for ``subject`` within its group.

    ``group_values`` is every issuer's value for the metric (including the
    subject), CIK-deduplicated by the caller. Null/invalid (non-finite) values are
    excluded. Returns None when fewer than ``min_peers`` valid values exist, or
    the subject is null/invalid.

    The percentile is a **tie-aware rank against the other issuers** —
    ``(worse + 0.5·tied) / (peers − 1)`` — so a whole group of equal values maps
    to 50, not 100, and the median maps to 50.
    """
    valid = [v for v in group_values if _finite(v)]
    if not _finite(subject) or len(valid) < min_peers:
        return None
    median = statistics.median(valid)

    others = valid.copy()
    try:
        others.remove(subject)  # rank the subject against the OTHER issuers
    except ValueError:
        pass
    denom = len(others)
    if denom == 0:
        return None
    if higher_is_better:
        worse = sum(1 for v in others if v < subject)
    else:
        worse = sum(1 for v in others if v > subject)
    tied = sum(1 for v in others if v == subject)
    percentile = round((worse + 0.5 * tied) / denom * 100)
    return PeerStat(median=median, favorable_percentile=percentile, peer_count=len(valid))


def peer_stat_for(
    metric_key: str, subject: float | None, group_values: list[float | None], **kwargs
) -> PeerStat | None:
    """Convenience wrapper that looks up polarity by metric key. Returns None for
    metrics not eligible for peer comparison (e.g. absolute net_debt)."""
    if metric_key not in HIGHER_IS_BETTER:
        return None
    return peer_stat(
        subject, group_values, higher_is_better=HIGHER_IS_BETTER[metric_key], **kwargs
    )


def two_digit_sic(sic: str | None) -> str | None:
    """The 2-digit SIC prefix used for grouping, or None if unusable."""
    if not sic:
        return None
    digits = str(sic).strip()
    return digits[:2] if len(digits) >= 2 and digits[:2].isdigit() else None
