"""
The 30 core metrics (spec §11 / docs/FINANCIAL_FORMULAS.md). One function per metric, each
taking a FinancialSnapshot and returning a MetricResult. No function ever returns 0 for a
missing input, and every function respects the Industry Applicability Matrix before computing.

`current` on the snapshot is assumed to already represent the desired reporting basis (TTM for
most flow metrics, spot for balance-sheet/market metrics); `history` is prior periods,
most-recent-first, assumed evenly spaced (annual) for the CAGR/lag-based functions — see
`FinancialSnapshot` docstring in app/engines/types.py.
"""
from __future__ import annotations

from typing import Optional

from app.engines.industry_applicability import get_applicability
from app.engines.metrics.formula_utils import (
    cagr, in_range, safe_div, series_at_lag, trend_slope, yoy,
)
from app.engines.types import (
    Applicability, DataQualityStatus, FinancialSnapshot, LineItems, MetricResult,
)

METHODOLOGY_VERSION = "v1"


def _result(
    key: str,
    value: Optional[float],
    snapshot: FinancialSnapshot,
    inputs: dict,
    status: DataQualityStatus = DataQualityStatus.CALCULATED,
    note: Optional[str] = None,
) -> MetricResult:
    applicability = get_applicability(key, snapshot.sector_id)
    if applicability == Applicability.NOT_MEANINGFUL:
        return MetricResult(
            key=key, value=None, status=DataQualityStatus.MISSING,
            applicability=applicability, formula_version=METHODOLOGY_VERSION,
            inputs_used=inputs, as_of=snapshot.calculation_date,
            note=note or "Not meaningful for this industry",
        )
    if value is None:
        return MetricResult(
            key=key, value=None, status=DataQualityStatus.MISSING,
            applicability=Applicability.INSUFFICIENT_DATA if applicability == Applicability.APPLICABLE else applicability,
            formula_version=METHODOLOGY_VERSION, inputs_used=inputs,
            as_of=snapshot.calculation_date, note=note,
        )
    return MetricResult(
        key=key, value=value, status=status, applicability=applicability,
        formula_version=METHODOLOGY_VERSION, inputs_used=inputs,
        as_of=snapshot.calculation_date, note=note,
    )


def _history_values(snapshot: FinancialSnapshot, field: str) -> list[Optional[float]]:
    """[current, history[0], history[1], ...] i.e. index n = n periods ago."""
    return [getattr(snapshot.current, field)] + [getattr(h, field) for h in snapshot.history]


def _growth_bundle(snapshot: FinancialSnapshot, field: str, key_prefix: str) -> dict[str, MetricResult]:
    vals = _history_values(snapshot, field)
    out = {}
    out[f"{key_prefix}_yoy"] = _result(
        f"{key_prefix}_yoy", yoy(vals[0], series_at_lag(vals, 1)), snapshot,
        {"current": vals[0], "prior": series_at_lag(vals, 1)},
    )
    for n, suffix in ((3, "cagr_3y"), (5, "cagr_5y"), (10, "cagr_10y")):
        start = series_at_lag(vals, n)
        out[f"{key_prefix}_{suffix}"] = _result(
            f"{key_prefix}_{suffix}", cagr(vals[0], start, n), snapshot,
            {"current": vals[0], f"t_minus_{n}": start},
        )
    out[f"{key_prefix}_trend"] = _result(
        f"{key_prefix}_trend", trend_slope(list(reversed(vals))), snapshot, {"series_len": len(vals)},
    )
    return out


# ---------------------------------------------------------------------------
# 1. Revenue Growth
# ---------------------------------------------------------------------------
def revenue_growth(snapshot: FinancialSnapshot) -> dict[str, MetricResult]:
    return _growth_bundle(snapshot, "revenue", "revenue_growth")


# ---------------------------------------------------------------------------
# 2. Gross Margin
# ---------------------------------------------------------------------------
def gross_margin(snapshot: FinancialSnapshot) -> MetricResult:
    c = snapshot.current
    gp = c.gross_profit if c.gross_profit is not None else (
        None if c.revenue is None or c.cogs is None else c.revenue - c.cogs
    )
    return _result("gross_margin", safe_div(gp, c.revenue), snapshot, {"gross_profit": gp, "revenue": c.revenue})


# ---------------------------------------------------------------------------
# 3. Operating Margin
# ---------------------------------------------------------------------------
def operating_margin(snapshot: FinancialSnapshot) -> MetricResult:
    c = snapshot.current
    return _result(
        "operating_margin", safe_div(c.operating_income, c.revenue), snapshot,
        {"operating_income": c.operating_income, "revenue": c.revenue},
    )


def effective_tax_rate(li: LineItems) -> Optional[float]:
    rate = safe_div(li.tax_expense, li.pretax_income)
    if rate is None or not in_range(rate, 0.0, 0.50):
        return None
    return rate


def _fallback_tax_rate(snapshot: FinancialSnapshot) -> Optional[float]:
    """3-year average effective tax rate among the periods where it's in-range."""
    periods = [snapshot.current] + snapshot.history[:2]
    rates = [effective_tax_rate(p) for p in periods]
    rates = [r for r in rates if r is not None]
    if not rates:
        return None
    return sum(rates) / len(rates)


# ---------------------------------------------------------------------------
# 4. ROIC
# ---------------------------------------------------------------------------
def invested_capital(li: LineItems) -> Optional[float]:
    if li.total_debt is None or li.shareholders_equity is None:
        return None
    cash = (li.cash_and_equivalents or 0) + (li.short_term_investments or 0)
    return li.total_debt + li.shareholders_equity - cash


def nopat(li: LineItems) -> tuple[Optional[float], Optional[float]]:
    """Returns (nopat, tax_rate_used). AUDIT NOTE (StockLab overhaul): not currently called by
    `roic()` below or by anything else in this codebase — `roic()` reimplements the same NOPAT
    math inline. Kept as the documented single-source NOPAT helper for the valuation engine to
    call rather than re-deriving NOPAT a third time; `roic()` should be refactored onto this
    function rather than duplicating it (tracked, not yet done — see AUDIT_METRICS.md)."""
    if li.ebit is None:
        return None, None
    rate = effective_tax_rate(li)
    used_fallback = rate is None
    if rate is None:
        rate = 0.25  # last-resort default only if no in-range rate anywhere; flagged via note
    value = li.ebit * (1 - rate)
    return value, (None if used_fallback else rate)


def roic(snapshot: FinancialSnapshot) -> MetricResult:
    c = snapshot.current
    # AUDIT FIX (final master pass, §5 metric audit — real correctness bug).
    # This line used to read `rate = effective_tax_rate(c) or _fallback_tax_rate(snapshot)`.
    # `0.0` is FALSY in Python, so a company with a genuine, in-range 0% effective tax rate had
    # that real measurement silently discarded and replaced by the 3-year average (or the 25%
    # default) — and because `note` stayed None, the result was still reported as CALCULATED
    # rather than ESTIMATED. Measured effect on a hand-built case: EBIT 200, invested capital 750,
    # a real 0% current-year rate and 30% in the two prior years produced ROIC 0.213 instead of
    # 0.267 — a 20% understatement of the platform's single most important quality metric, with no
    # visible sign anything had been substituted.
    # This is not a rare corner: loss-makers carrying tax credits, REITs (pass-through, no
    # corporate tax), companies with large NOL carryforwards and companies inside a tax holiday
    # all legitimately report a 0% effective rate. `is None` is the correct test; a rate that is
    # out of the [0, 0.5] plausibility band is already turned into None by effective_tax_rate().
    rate = effective_tax_rate(c)
    if rate is None:
        rate = _fallback_tax_rate(snapshot)
    note = None
    if rate is None:
        rate = 0.25
        note = "No in-range effective tax rate available in current or trailing 2 periods; used 25% default"
    n = None if c.ebit is None else c.ebit * (1 - rate)
    ic = invested_capital(c)
    value = safe_div(n, ic)
    status = DataQualityStatus.CALCULATED if note is None else DataQualityStatus.ESTIMATED
    return _result(
        "roic", value, snapshot,
        {"ebit": c.ebit, "tax_rate": rate, "invested_capital": ic}, status=status, note=note,
    )


# ---------------------------------------------------------------------------
# 5. ROIC - WACC  (WACC supplied externally by the valuation engine; see engines/valuation/wacc.py)
# ---------------------------------------------------------------------------
def roic_minus_wacc(snapshot: FinancialSnapshot, wacc: Optional[float]) -> MetricResult:
    r = roic(snapshot)
    if r.value is None or wacc is None:
        return _result("roic_minus_wacc", None, snapshot, {"roic": r.value, "wacc": wacc})
    return _result(
        "roic_minus_wacc", r.value - wacc, snapshot, {"roic": r.value, "wacc": wacc},
        status=r.status,
    )


# ---------------------------------------------------------------------------
# 6. EPS Growth
# ---------------------------------------------------------------------------
def eps_growth(snapshot: FinancialSnapshot) -> dict[str, MetricResult]:
    out = _growth_bundle(snapshot, "eps_diluted", "eps_growth")
    fwd = snapshot.forward_eps_estimate
    cur = snapshot.current.eps_diluted
    out["eps_growth_forward"] = _result(
        "eps_growth_forward", yoy(fwd, cur), snapshot, {"forward_eps": fwd, "current_eps": cur},
        status=DataQualityStatus.ESTIMATED,
    )
    return out


# ---------------------------------------------------------------------------
# 7/8/9. Leverage ratios
# ---------------------------------------------------------------------------
def debt_to_ebitda(snapshot: FinancialSnapshot) -> MetricResult:
    # AUDIT FIX (StockLab overhaul Part 2): negative/zero EBITDA previously fell straight through
    # safe_div and produced a numerically "valid" but financially misleading leverage ratio (e.g.
    # positive debt over negative EBITDA renders as a negative multiple, which reads as *less*
    # leverage than a healthy company). Guarded the same way P/E, P/FCF, EV/FCF and EV/FCF already
    # guard against a non-positive denominator — see FINANCIAL_FORMULAS.md #7 (updated).
    c = snapshot.current
    if c.ebitda is not None and c.ebitda <= 0:
        return _result("debt_to_ebitda", None, snapshot, {"total_debt": c.total_debt, "ebitda": c.ebitda},
                        note="Negative or zero EBITDA — Debt/EBITDA would be misleading, showing N/M")
    return _result("debt_to_ebitda", safe_div(c.total_debt, c.ebitda), snapshot,
                    {"total_debt": c.total_debt, "ebitda": c.ebitda})


def net_debt_to_ebitda(snapshot: FinancialSnapshot) -> MetricResult:
    # AUDIT FIX: same non-positive-EBITDA guard as debt_to_ebitda above. Also fixes a mislabeling
    # bug — the prior "Net cash" note fired on value < 0 alone, which is also true when EBITDA is
    # negative and net debt is positive (negative / negative... no: positive net debt / negative
    # EBITDA = negative quotient, mislabeled as a net-cash position when the company is in fact
    # net-debt with negative EBITDA). Net cash is now judged from net_debt's own sign, not the
    # ratio's sign, which is only a proxy for it when EBITDA > 0.
    c = snapshot.current
    if c.total_debt is None or c.ebitda is None:
        return _result("net_debt_to_ebitda", None, snapshot, {})
    if c.ebitda <= 0:
        return _result("net_debt_to_ebitda", None, snapshot, {"total_debt": c.total_debt, "ebitda": c.ebitda},
                        note="Negative or zero EBITDA — Net Debt/EBITDA would be misleading, showing N/M")
    cash = (c.cash_and_equivalents or 0) + (c.short_term_investments or 0)
    net_debt = c.total_debt - cash
    value = safe_div(net_debt, c.ebitda)
    note = "Net cash position (negative net debt) — displayed as Net Cash / EBITDA" if net_debt < 0 else None
    return _result("net_debt_to_ebitda", value, snapshot,
                    {"total_debt": c.total_debt, "cash": cash, "ebitda": c.ebitda}, note=note)


def debt_to_equity(snapshot: FinancialSnapshot) -> MetricResult:
    c = snapshot.current
    if c.shareholders_equity is not None and c.shareholders_equity <= 0:
        return _result("debt_to_equity", None, snapshot, {"equity": c.shareholders_equity},
                        note="Negative or zero shareholders' equity — N/M")
    return _result("debt_to_equity", safe_div(c.total_debt, c.shareholders_equity), snapshot,
                    {"total_debt": c.total_debt, "equity": c.shareholders_equity})


# ---------------------------------------------------------------------------
# 10. Interest Coverage
# ---------------------------------------------------------------------------
def interest_coverage(snapshot: FinancialSnapshot) -> MetricResult:
    c = snapshot.current
    if (c.interest_expense or 0) == 0:
        note = "No interest expense and no debt — coverage undefined (not infinite)" if (c.total_debt or 0) == 0 else "No interest expense reported despite outstanding debt"
        return _result("interest_coverage", None, snapshot, {"ebit": c.ebit, "interest_expense": c.interest_expense}, note=note)
    return _result("interest_coverage", safe_div(c.ebit, c.interest_expense), snapshot,
                    {"ebit": c.ebit, "interest_expense": c.interest_expense})


# ---------------------------------------------------------------------------
# 11. Current Ratio
# ---------------------------------------------------------------------------
def current_ratio(snapshot: FinancialSnapshot) -> MetricResult:
    c = snapshot.current
    return _result("current_ratio", safe_div(c.current_assets, c.current_liabilities), snapshot,
                    {"current_assets": c.current_assets, "current_liabilities": c.current_liabilities})


# ---------------------------------------------------------------------------
# 12. Free Cash Flow (level + per-share; growth/margin/yield are separate metrics below)
# ---------------------------------------------------------------------------
def free_cash_flow(li: LineItems) -> Optional[float]:
    if li.operating_cash_flow is None or li.capital_expenditure is None:
        return None
    return li.operating_cash_flow - li.capital_expenditure


def fcf_level(snapshot: FinancialSnapshot) -> MetricResult:
    c = snapshot.current
    fcf = free_cash_flow(c)
    return _result("fcf", fcf, snapshot, {"ocf": c.operating_cash_flow, "capex": c.capital_expenditure})


def fcf_per_share(snapshot: FinancialSnapshot) -> MetricResult:
    c = snapshot.current
    fcf = free_cash_flow(c)
    return _result("fcf_per_share", safe_div(fcf, c.diluted_shares), snapshot,
                    {"fcf": fcf, "diluted_shares": c.diluted_shares})


# ---------------------------------------------------------------------------
# 13. FCF Growth
# ---------------------------------------------------------------------------
def fcf_growth(snapshot: FinancialSnapshot) -> dict[str, MetricResult]:
    periods = [snapshot.current] + snapshot.history
    fcf_series = [free_cash_flow(p) for p in periods]

    class _Proxy:
        pass

    # Reuse _growth_bundle logic without a synthetic LineItems: build directly.
    out = {}
    out["fcf_growth_yoy"] = _result(
        "fcf_growth_yoy", yoy(series_at_lag(fcf_series, 0), series_at_lag(fcf_series, 1)),
        snapshot, {"current": series_at_lag(fcf_series, 0), "prior": series_at_lag(fcf_series, 1)},
    )
    for n, suffix in ((3, "cagr_3y"), (5, "cagr_5y"), (10, "cagr_10y")):
        start = series_at_lag(fcf_series, n)
        out[f"fcf_growth_{suffix}"] = _result(
            f"fcf_growth_{suffix}", cagr(series_at_lag(fcf_series, 0), start, n), snapshot,
            {"current": series_at_lag(fcf_series, 0), f"t_minus_{n}": start},
        )
    out["fcf_growth_trend"] = _result(
        "fcf_growth_trend", trend_slope(list(reversed(fcf_series))), snapshot, {"series_len": len(fcf_series)},
    )
    return out


# ---------------------------------------------------------------------------
# 14/15. FCF Margin, FCF Yield
# ---------------------------------------------------------------------------
def fcf_margin(snapshot: FinancialSnapshot) -> MetricResult:
    c = snapshot.current
    fcf = free_cash_flow(c)
    return _result("fcf_margin", safe_div(fcf, c.revenue), snapshot, {"fcf": fcf, "revenue": c.revenue})


def fcf_yield(snapshot: FinancialSnapshot) -> MetricResult:
    c = snapshot.current
    fcf = free_cash_flow(c)
    return _result("fcf_yield", safe_div(fcf, c.market_cap), snapshot, {"fcf": fcf, "market_cap": c.market_cap})


# ---------------------------------------------------------------------------
# 16. Dividend / Buyback / Shareholder Yield
# ---------------------------------------------------------------------------
def dividend_yield(snapshot: FinancialSnapshot) -> MetricResult:
    c = snapshot.current
    return _result("dividend_yield", safe_div(c.dividends_paid, c.market_cap), snapshot,
                    {"dividends_paid": c.dividends_paid, "market_cap": c.market_cap})


def buyback_yield(snapshot: FinancialSnapshot) -> MetricResult:
    c = snapshot.current
    if c.buybacks is None or c.market_cap in (None, 0):
        return _result("buyback_yield", None, snapshot, {})
    net = c.buybacks - (c.stock_issuance or 0)
    return _result("buyback_yield", safe_div(net, c.market_cap), snapshot,
                    {"buybacks": c.buybacks, "issuance": c.stock_issuance, "market_cap": c.market_cap})


def shareholder_yield(snapshot: FinancialSnapshot) -> MetricResult:
    """Dividend yield + net buyback yield.

    AUDIT FIX (final master pass, §0 "NO SILENT FALLBACKS"): this used to compute
    `(dy.value or 0) + (by.value or 0)` and return it as an ordinary CALCULATED metric. That
    treats an UNKNOWN component as a ZERO one. A company whose dividend data simply was not
    ingested got a shareholder yield equal to its buyback yield alone, presented with the same
    status and the same apparent reliability as a company for which both components were really
    known — and the number is understated by exactly the part that is missing.

    The sum is still returned (a partial yield is more useful than none, and refusing outright
    would lose real information about the component that IS known), but when a component is
    missing the result is now marked ESTIMATED and carries a note naming which one. Consumers that
    care — `compute_data_quality_score()` and the Confidence Score both read metric statuses —
    can then see it. `inputs_used` records both components either way, so the gap is auditable.

    A genuine zero is unaffected: a company that pays no dividend reports `dividends_paid = 0`,
    which yields `dividend_yield = 0.0`, not `None`. This branch is only reached when the datum is
    absent.
    """
    dy = dividend_yield(snapshot)
    by = buyback_yield(snapshot)
    if dy.value is None and by.value is None:
        return _result("shareholder_yield", None, snapshot,
                        {"dividend_yield": None, "buyback_yield": None},
                        note="Neither dividend nor buyback yield is available")

    missing = [name for name, r in (("dividend_yield", dy), ("buyback_yield", by)) if r.value is None]
    inputs = {"dividend_yield": dy.value, "buyback_yield": by.value, "components_missing": missing}
    if missing:
        return _result(
            "shareholder_yield", (dy.value or 0) + (by.value or 0), snapshot, inputs,
            status=DataQualityStatus.ESTIMATED,
            note=(f"Partial: {', '.join(missing)} unavailable and treated as 0 for the sum. "
                  f"The true shareholder yield is at least this value, not equal to it."),
        )
    return _result("shareholder_yield", dy.value + by.value, snapshot, inputs)


# ---------------------------------------------------------------------------
# 17. Share Count Growth
# ---------------------------------------------------------------------------
def share_count_growth(snapshot: FinancialSnapshot) -> dict[str, MetricResult]:
    return _growth_bundle(snapshot, "diluted_shares", "share_count_growth")


# ---------------------------------------------------------------------------
# 18/19. P/E, Forward P/E
# ---------------------------------------------------------------------------
def pe_ratio(snapshot: FinancialSnapshot) -> MetricResult:
    c = snapshot.current
    if c.eps_diluted is not None and c.eps_diluted <= 0:
        return _result("pe", None, snapshot, {"eps": c.eps_diluted}, note="Negative or zero EPS — N/M")
    return _result("pe", safe_div(c.price, c.eps_diluted), snapshot, {"price": c.price, "eps": c.eps_diluted})


def forward_pe_ratio(snapshot: FinancialSnapshot) -> MetricResult:
    c = snapshot.current
    fwd = snapshot.forward_eps_estimate
    if fwd is not None and fwd <= 0:
        return _result("forward_pe", None, snapshot, {"forward_eps": fwd}, note="Negative or zero forward EPS estimate — N/M", status=DataQualityStatus.ESTIMATED)
    return _result("forward_pe", safe_div(c.price, fwd), snapshot, {"price": c.price, "forward_eps": fwd},
                    status=DataQualityStatus.ESTIMATED)


# ---------------------------------------------------------------------------
# 20. EV / EBITDA
# ---------------------------------------------------------------------------
def enterprise_value(li: LineItems) -> Optional[float]:
    if li.market_cap is None or li.total_debt is None:
        return None
    cash = (li.cash_and_equivalents or 0) + (li.short_term_investments or 0)
    return li.market_cap + li.total_debt + (li.minority_interest or 0) + (li.preferred_equity or 0) - cash


def ev_to_ebitda(snapshot: FinancialSnapshot) -> MetricResult:
    # AUDIT FIX: same non-positive-EBITDA guard as debt_to_ebitda — an EV/EBITDA computed against
    # negative EBITDA is not a valuation multiple, it's a sign artifact.
    c = snapshot.current
    ev = enterprise_value(c)
    if c.ebitda is not None and c.ebitda <= 0:
        return _result("ev_to_ebitda", None, snapshot, {"ev": ev, "ebitda": c.ebitda},
                        note="Negative or zero EBITDA — EV/EBITDA would be misleading, showing N/M")
    return _result("ev_to_ebitda", safe_div(ev, c.ebitda), snapshot, {"ev": ev, "ebitda": c.ebitda})


# ---------------------------------------------------------------------------
# 21. P / FCF
# ---------------------------------------------------------------------------
def p_to_fcf(snapshot: FinancialSnapshot) -> MetricResult:
    c = snapshot.current
    fcf = free_cash_flow(c)
    if fcf is not None and fcf <= 0:
        return _result("p_fcf", None, snapshot, {"fcf": fcf}, note="Negative or zero FCF — N/M")
    return _result("p_fcf", safe_div(c.market_cap, fcf), snapshot, {"market_cap": c.market_cap, "fcf": fcf})


# ---------------------------------------------------------------------------
# 22. PEG
# ---------------------------------------------------------------------------
def peg_ratio(snapshot: FinancialSnapshot, growth_rate_pct: Optional[float]) -> MetricResult:
    """growth_rate_pct: EPS growth expressed as a percentage number (e.g. 12.0 for 12%),
    per the horizon configured by the caller (historical or forward CAGR)."""
    pe = pe_ratio(snapshot)
    if pe.value is None or growth_rate_pct is None or growth_rate_pct <= 0:
        return _result("peg", None, snapshot, {"pe": pe.value, "growth_pct": growth_rate_pct},
                        note="PEG requires a positive P/E and positive growth rate — N/M")
    return _result("peg", pe.value / growth_rate_pct, snapshot, {"pe": pe.value, "growth_pct": growth_rate_pct})


# ---------------------------------------------------------------------------
# 23. ROE
# ---------------------------------------------------------------------------
def roe(snapshot: FinancialSnapshot) -> MetricResult:
    # AUDIT FIX: negative average equity previously passed straight through safe_div. A company
    # with negative net income AND negative average equity produced a *positive* ROE (two
    # negatives), which reads as a healthy return while the company actually has a shareholders'
    # deficit — the same failure mode debt_to_equity already guards against (FINANCIAL_FORMULAS.md
    # #9); ROE did not, until this fix. Doc updated to state the guard explicitly.
    c = snapshot.current
    prior_equity = snapshot.history[0].shareholders_equity if snapshot.history else None
    if c.shareholders_equity is None:
        avg_equity = None
    elif prior_equity is None:
        avg_equity = c.shareholders_equity
    else:
        avg_equity = (c.shareholders_equity + prior_equity) / 2
    if avg_equity is not None and avg_equity <= 0:
        return _result("roe", None, snapshot, {"net_income": c.net_income, "avg_equity": avg_equity},
                        note="Negative or zero average shareholders' equity — ROE would be misleading, showing N/M")
    return _result("roe", safe_div(c.net_income, avg_equity), snapshot,
                    {"net_income": c.net_income, "avg_equity": avg_equity})


# ---------------------------------------------------------------------------
# 24. Earnings Yield
# ---------------------------------------------------------------------------
def earnings_yield(snapshot: FinancialSnapshot) -> MetricResult:
    c = snapshot.current
    return _result("earnings_yield", safe_div(c.eps_diluted, c.price), snapshot,
                    {"eps": c.eps_diluted, "price": c.price})


# ---------------------------------------------------------------------------
# 25. EV / FCF
# ---------------------------------------------------------------------------
def ev_to_fcf(snapshot: FinancialSnapshot) -> MetricResult:
    c = snapshot.current
    ev = enterprise_value(c)
    fcf = free_cash_flow(c)
    if fcf is not None and fcf <= 0:
        return _result("ev_to_fcf", None, snapshot, {"ev": ev, "fcf": fcf}, note="Negative or zero FCF — N/M")
    return _result("ev_to_fcf", safe_div(ev, fcf), snapshot, {"ev": ev, "fcf": fcf})


# ---------------------------------------------------------------------------
# 26. Net Margin
# ---------------------------------------------------------------------------
def net_margin(snapshot: FinancialSnapshot) -> MetricResult:
    c = snapshot.current
    return _result("net_margin", safe_div(c.net_income, c.revenue), snapshot,
                    {"net_income": c.net_income, "revenue": c.revenue})


# ---------------------------------------------------------------------------
# 27. Capex / Revenue
# ---------------------------------------------------------------------------
def capex_to_revenue(snapshot: FinancialSnapshot) -> MetricResult:
    c = snapshot.current
    return _result("capex_to_revenue", safe_div(c.capital_expenditure, c.revenue), snapshot,
                    {"capex": c.capital_expenditure, "revenue": c.revenue})


# ---------------------------------------------------------------------------
# 28. FCF Payout Ratio
# ---------------------------------------------------------------------------
def fcf_payout_ratio(snapshot: FinancialSnapshot) -> MetricResult:
    c = snapshot.current
    fcf = free_cash_flow(c)
    if fcf is not None and fcf <= 0:
        return _result("fcf_payout_ratio", None, snapshot, {"fcf": fcf},
                        note="Negative or zero FCF — payout ratio would be misleading, showing N/M")
    return _result("fcf_payout_ratio", safe_div(c.dividends_paid, fcf), snapshot,
                    {"dividends_paid": c.dividends_paid, "fcf": fcf})


# ---------------------------------------------------------------------------
# 29. Dividend Growth (+ cut/freeze/increase events)
# ---------------------------------------------------------------------------
def dividend_per_share(li: LineItems) -> Optional[float]:
    return safe_div(li.dividends_paid, li.diluted_shares)


def dividend_growth(snapshot: FinancialSnapshot) -> dict[str, MetricResult]:
    periods = [snapshot.current] + snapshot.history
    dps_series = [dividend_per_share(p) for p in periods]
    out = {}
    out["dividend_growth_yoy"] = _result(
        "dividend_growth_yoy", yoy(series_at_lag(dps_series, 0), series_at_lag(dps_series, 1)),
        snapshot, {"current": series_at_lag(dps_series, 0), "prior": series_at_lag(dps_series, 1)},
    )
    for n, suffix in ((3, "cagr_3y"), (5, "cagr_5y"), (10, "cagr_10y")):
        start = series_at_lag(dps_series, n)
        out[f"dividend_growth_{suffix}"] = _result(
            f"dividend_growth_{suffix}", cagr(series_at_lag(dps_series, 0), start, n), snapshot,
            {"current": series_at_lag(dps_series, 0), f"t_minus_{n}": start},
        )
    cur, prior = series_at_lag(dps_series, 0), series_at_lag(dps_series, 1)
    if cur is not None and prior is not None and prior > 0:
        if cur < prior:
            event = "DIVIDEND_CUT"
        elif cur == prior:
            event = "DIVIDEND_FREEZE"
        else:
            event = "DIVIDEND_INCREASE"
    else:
        event = None
    out["dividend_event"] = MetricResult(
        key="dividend_event", value=None, status=DataQualityStatus.CALCULATED,
        applicability=Applicability.APPLICABLE, formula_version=METHODOLOGY_VERSION,
        inputs_used={"current_dps": cur, "prior_dps": prior}, as_of=snapshot.calculation_date,
        note=event,
    )
    return out


# ---------------------------------------------------------------------------
# 30. 5Y Revenue CAGR (explicit named field per spec, same math as revenue_growth_cagr_5y)
# ---------------------------------------------------------------------------
def five_year_revenue_cagr(snapshot: FinancialSnapshot) -> MetricResult:
    vals = _history_values(snapshot, "revenue")
    start = series_at_lag(vals, 5)
    return _result("revenue_cagr_5y", cagr(vals[0], start, 5), snapshot,
                    {"current": vals[0], "t_minus_5": start})


# ---------------------------------------------------------------------------
# Orchestrator
# ---------------------------------------------------------------------------
def compute_all_metrics(
    snapshot: FinancialSnapshot,
    wacc: Optional[float] = None,
    peg_growth_pct: Optional[float] = None,
) -> dict[str, MetricResult]:
    """Compute all 30 metrics (+ their growth-horizon sub-fields) for one snapshot."""
    results: dict[str, MetricResult] = {}
    results.update(revenue_growth(snapshot))
    results["gross_margin"] = gross_margin(snapshot)
    results["operating_margin"] = operating_margin(snapshot)
    results["roic"] = roic(snapshot)
    results["roic_minus_wacc"] = roic_minus_wacc(snapshot, wacc)
    results.update(eps_growth(snapshot))
    results["debt_to_ebitda"] = debt_to_ebitda(snapshot)
    results["net_debt_to_ebitda"] = net_debt_to_ebitda(snapshot)
    results["debt_to_equity"] = debt_to_equity(snapshot)
    results["interest_coverage"] = interest_coverage(snapshot)
    results["current_ratio"] = current_ratio(snapshot)
    results["fcf"] = fcf_level(snapshot)
    results["fcf_per_share"] = fcf_per_share(snapshot)
    results.update(fcf_growth(snapshot))
    results["fcf_margin"] = fcf_margin(snapshot)
    results["fcf_yield"] = fcf_yield(snapshot)
    results["dividend_yield"] = dividend_yield(snapshot)
    results["buyback_yield"] = buyback_yield(snapshot)
    results["shareholder_yield"] = shareholder_yield(snapshot)
    results.update(share_count_growth(snapshot))
    results["pe"] = pe_ratio(snapshot)
    results["forward_pe"] = forward_pe_ratio(snapshot)
    results["ev_to_ebitda"] = ev_to_ebitda(snapshot)
    results["p_fcf"] = p_to_fcf(snapshot)
    growth_for_peg = peg_growth_pct
    if growth_for_peg is None:
        eg = results.get("eps_growth_cagr_3y")
        growth_for_peg = eg.value * 100 if eg and eg.value is not None else None
    results["peg"] = peg_ratio(snapshot, growth_for_peg)
    results["roe"] = roe(snapshot)
    results["earnings_yield"] = earnings_yield(snapshot)
    results["ev_to_fcf"] = ev_to_fcf(snapshot)
    results["net_margin"] = net_margin(snapshot)
    results["capex_to_revenue"] = capex_to_revenue(snapshot)
    results["fcf_payout_ratio"] = fcf_payout_ratio(snapshot)
    results.update(dividend_growth(snapshot))
    results["revenue_cagr_5y"] = five_year_revenue_cagr(snapshot)
    return results
