"""3-scenario DCF (spec §23 / docs/VALUATION.md §2).

AUDIT FIX (StockLab final engineering pass, Part B1 — docs/AUDIT_DCF_B1.md). Implements the two
architectural defects `docs/AUDIT_VALUATION.md` recorded and explicitly deferred:

1. **No growth/margin trajectory.** One flat `revenue_growth_rate` and one flat
   `operating_margin` were applied across all 10 explicit years, so "growth fades toward the
   terminal rate" and "margin ramps toward a mature level" — the two most common real shapes of a
   forecast — could not be expressed at all. Now: `revenue_growth_path` / `operating_margin_path`
   accept a per-year list, `fade_path()` builds the standard linear-fade shape, and when neither
   is supplied the scalars are used exactly as before (a flat path), so every existing caller is
   bit-for-bit unchanged.

2. **Scenario multipliers that do not scale with the base case.** Bear/Bull were derived by
   multiplying the base assumptions by fixed factors (×0.6 / ×1.3 on growth), so a 3%-grower and a
   30%-grower got the same *relative* spread — which is backwards: a mature business has a
   narrower plausible range, not a proportionally identical one. Now additive shifts in
   percentage points by default (−5pp / +4pp on growth), which are comparable in magnitude across
   base rates. The old multiplicative behaviour is retained as a selectable mode rather than
   deleted, so a deployment that wants the previous numbers can still get them.

Both changes alter Bear/Bull fair values for every security. That is deliberate and is the point
of the fix — the previous behaviour is a documented defect, not a methodology preference — but it
IS a change to shipped valuation output, recorded as such in docs/AUDIT_DCF_B1.md and CHANGELOG.md
rather than slipped in.
"""
from __future__ import annotations

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

EXPLICIT_YEARS = 10


class Scenario(str, Enum):
    BEAR = "BEAR"
    BASE = "BASE"
    BULL = "BULL"


@dataclass(frozen=True)
class DCFAssumptions:
    starting_revenue: float
    revenue_growth_rate: float      # the flat fallback, used when revenue_growth_path is None
    operating_margin: float         # the flat fallback, used when operating_margin_path is None
    tax_rate: float
    reinvestment_rate: float        # (capex + Δworking capital) as a fraction of revenue
    wacc: float
    terminal_growth: float
    diluted_shares: float
    net_debt: float
    minority_interest: float = 0.0
    preferred_equity: float = 0.0
    # Part B1: optional per-year trajectories. `None` (the default) means "hold the scalar above
    # flat for all EXPLICIT_YEARS", which is exactly the pre-B1 behaviour. A supplied path must
    # have EXPLICIT_YEARS entries; run_dcf() refuses a wrong-length path with an explicit error
    # rather than padding or truncating it, because a silently-truncated forecast is a wrong
    # valuation that looks fine.
    revenue_growth_path: Optional[list[float]] = None
    operating_margin_path: Optional[list[float]] = None

    def growth_for_year(self, year: int) -> float:
        """`year` is 1-based, matching run_dcf()'s loop."""
        if self.revenue_growth_path is None:
            return self.revenue_growth_rate
        return self.revenue_growth_path[year - 1]

    def margin_for_year(self, year: int) -> float:
        if self.operating_margin_path is None:
            return self.operating_margin
        return self.operating_margin_path[year - 1]


def fade_path(start: float, end: float, years: int = EXPLICIT_YEARS,
              fade_years: Optional[int] = None) -> list[float]:
    """Linear fade from `start` to `end`, then flat at `end` for any remaining years.

    The standard "growth fades to the terminal rate" convention. With `fade_years=None` the fade
    spans the whole explicit window. With `fade_years=5` and `years=10`, the value reaches `end`
    at year 5 and holds it through year 10 — which is how a margin-ramp story is normally
    expressed ("ramps to mature margin by year 5, then holds").

    Deliberately linear rather than exponential: a linear fade is auditable by eye from the
    printed path, and there is no empirical basis in this codebase's data for preferring a
    particular curve shape. Documented as a choice, not presented as the correct one.
    """
    if years <= 0:
        return []
    span = years if fade_years is None else max(1, min(fade_years, years))
    out = []
    for i in range(1, years + 1):
        if i >= span:
            out.append(end)
        else:
            out.append(start + (end - start) * (i / span))
    return out


@dataclass(frozen=True)
class DCFResult:
    scenario: Scenario
    enterprise_value: Optional[float]
    equity_value: Optional[float]
    fair_value_per_share: Optional[float]
    fcff_path: list[float] = field(default_factory=list)
    pv_of_fcff: list[float] = field(default_factory=list)
    terminal_value: Optional[float] = None
    pv_of_terminal_value: Optional[float] = None
    assumptions: Optional[DCFAssumptions] = None
    error: Optional[str] = None

    @property
    def terminal_value_pct_of_enterprise_value(self) -> Optional[float]:
        """AUDIT (StockLab overhaul, Part 11): the spec explicitly asks that Terminal Value's
        contribution to Fair Value be shown, not buried — a DCF where the terminal value is 85%+
        of enterprise value is telling you the explicit 10-year forecast barely matters and the
        valuation is really a bet on the terminal growth/margin assumption. Exposed as a plain
        property rather than a separately-computed API field so it can never drift out of sync
        with the underlying enterprise_value/pv_of_terminal_value."""
        if self.enterprise_value is None or not self.enterprise_value or self.pv_of_terminal_value is None:
            return None
        return self.pv_of_terminal_value / self.enterprise_value


def run_dcf(scenario: Scenario, a: DCFAssumptions) -> DCFResult:
    if a.wacc is None or a.wacc <= a.terminal_growth:
        return DCFResult(scenario, None, None, None, assumptions=a,
                          error="WACC must exceed terminal growth for a finite terminal value")
    if a.diluted_shares is None or a.diluted_shares <= 0:
        return DCFResult(scenario, None, None, None, assumptions=a, error="Missing diluted shares outstanding")
    for path_name in ("revenue_growth_path", "operating_margin_path"):
        path = getattr(a, path_name)
        if path is not None and len(path) != EXPLICIT_YEARS:
            return DCFResult(scenario, None, None, None, assumptions=a,
                              error=f"{path_name} has {len(path)} entries, expected {EXPLICIT_YEARS}")

    revenue = a.starting_revenue
    fcff_path, pv_path = [], []
    for year in range(1, EXPLICIT_YEARS + 1):
        revenue = revenue * (1 + a.growth_for_year(year))
        ebit = revenue * a.margin_for_year(year)
        nopat = ebit * (1 - a.tax_rate)
        reinvestment = revenue * a.reinvestment_rate
        fcff = nopat - reinvestment
        pv = fcff / ((1 + a.wacc) ** year)
        fcff_path.append(fcff)
        pv_path.append(pv)

    terminal_value = fcff_path[-1] * (1 + a.terminal_growth) / (a.wacc - a.terminal_growth)
    pv_terminal = terminal_value / ((1 + a.wacc) ** EXPLICIT_YEARS)

    enterprise_value = sum(pv_path) + pv_terminal
    equity_value = enterprise_value - a.net_debt - a.minority_interest - a.preferred_equity
    fair_value_per_share = equity_value / a.diluted_shares

    return DCFResult(
        scenario=scenario, enterprise_value=enterprise_value, equity_value=equity_value,
        fair_value_per_share=fair_value_per_share, fcff_path=fcff_path, pv_of_fcff=pv_path,
        terminal_value=terminal_value, pv_of_terminal_value=pv_terminal, assumptions=a,
    )


# --- Scenario derivation (Part B1) -------------------------------------------------------------
#
# PREVIOUS BEHAVIOUR, kept selectable rather than deleted: Bear/Bull were derived by multiplying
# the Base case by fixed factors. `docs/AUDIT_VALUATION.md` recorded why that is wrong: the same
# x0.6 growth multiplier applied to a 3% grower (-> 1.8%, a 1.2pp haircut) and a 30% grower
# (-> 18%, a 12pp haircut) produces identical RELATIVE spreads for a mature utility and a
# hypergrowth company, when a mature business should have the NARROWER plausible range.
BEAR_ADJUSTMENTS = {"revenue_growth_rate": 0.6, "operating_margin": 0.9, "terminal_growth": 0.7}
BULL_ADJUSTMENTS = {"revenue_growth_rate": 1.3, "operating_margin": 1.1, "terminal_growth": 1.15}

# CURRENT DEFAULT (Part B1): additive shifts in percentage points. A -5pp growth shift means the
# same thing whether the base case is 3% or 30% -- it is a statement about how much slower the
# business could plausibly grow, not a proportion of however fast it happens to grow now. The
# magnitudes below are a stated judgment call, chosen to be recognisably conventional
# (a ~5pp growth band, a ~2pp margin band, a ~1pp terminal band), NOT derived from this
# codebase's data. See docs/AUDIT_DCF_B1.md for why the empirically-derived alternative
# (scenario widths from the security's own historical growth/margin volatility) is NOT
# IMPLEMENTED.
BEAR_SHIFTS_PP = {"revenue_growth_rate": -0.05, "operating_margin": -0.02, "terminal_growth": -0.01}
BULL_SHIFTS_PP = {"revenue_growth_rate": 0.04, "operating_margin": 0.02, "terminal_growth": 0.005}

# THIRD MODE (Part B1, and the shipped default): floor-plus-proportional.
#
# Measuring the two modes above against each other -- actually running them, not reasoning about
# them -- showed that NEITHER achieves what docs/AUDIT_VALUATION.md asked for. Fair-value spread
# ((BULL-BEAR)/BASE) for a 10-year DCF at 9% WACC / 2.5% terminal growth:
#
#   base growth      multiplicative      additive_pp
#        3%              54%                109%
#       10%              92%                109%
#       30%             193%                108%
#
# Multiplicative widens without limit as growth rises (a x0.6/x1.3 band on a 30% grower is a
# 12pp/9pp swing compounded over ten years). Additive is flat across the range -- which means it
# is TOO WIDE for a mature business: -5pp on a 3% grower is a deep-recession scenario presented as
# a routine downside. The stated goal was a NARROWER band for mature businesses and a wider one
# for hypergrowth.
#
# Floor-plus-proportional does that: the shift is `max(floor_pp, fraction x base_rate)`. A 3%
# grower gets the floor (a 2pp haircut -> 1% growth, a recognisable slow-down); a 30% grower gets
# the proportional term (a 9pp haircut -> 21%). The floor and fraction below are a stated judgment
# call, NOT derived from this codebase's data -- the empirically-derived version (scenario widths
# from each security's own historical growth/margin volatility) remains NOT IMPLEMENTED, for the
# reason docs/AUDIT_VALUATION.md gave and this pass did not remove: it needs historical depth this
# build does not reliably have.
BEAR_HYBRID = {"growth_floor_pp": 0.02, "growth_fraction": 0.30,
               "margin_floor_pp": 0.01, "margin_fraction": 0.10,
               "terminal_floor_pp": 0.005, "terminal_fraction": 0.20}
BULL_HYBRID = {"growth_floor_pp": 0.015, "growth_fraction": 0.20,
               "margin_floor_pp": 0.01, "margin_fraction": 0.10,
               "terminal_floor_pp": 0.0025, "terminal_fraction": 0.10}

SCENARIO_MODE_ADDITIVE = "additive_pp"
SCENARIO_MODE_MULTIPLICATIVE = "multiplicative"
SCENARIO_MODE_HYBRID = "hybrid"
DEFAULT_SCENARIO_MODE = SCENARIO_MODE_HYBRID
SCENARIO_MODES = (SCENARIO_MODE_HYBRID, SCENARIO_MODE_ADDITIVE, SCENARIO_MODE_MULTIPLICATIVE)


def _hybrid_delta(base_value: float, floor_pp: float, fraction: float, sign: int) -> float:
    """Signed shift of `max(floor_pp, fraction * |base_value|)`. `sign` is -1 for Bear, +1 Bull."""
    return sign * max(floor_pp, fraction * abs(base_value))


def _shift_paths(base: DCFAssumptions, growth_delta: float, margin_delta: float,
                 growth_factor: Optional[float], margin_factor: Optional[float]) -> dict:
    """Apply the same scenario adjustment to the per-year paths, when paths are in use.

    A scenario that shifted only the scalar while leaving a supplied path untouched would silently
    produce a Bear case identical to the Base case -- the exact class of silent-no-op bug this
    pass exists to remove.
    """
    out: dict = {}
    if base.revenue_growth_path is not None:
        out["revenue_growth_path"] = [
            (g * growth_factor) if growth_factor is not None else (g + growth_delta)
            for g in base.revenue_growth_path
        ]
    if base.operating_margin_path is not None:
        out["operating_margin_path"] = [
            max((m * margin_factor) if margin_factor is not None else (m + margin_delta), 0.0)
            for m in base.operating_margin_path
        ]
    return out


def derive_bear_bull(
    base: DCFAssumptions, mode: str = DEFAULT_SCENARIO_MODE,
) -> tuple[DCFAssumptions, DCFAssumptions]:
    """Derive Bear and Bull assumption sets from the Base case.

    `mode` selects the adjustment family: `"additive_pp"` (default, Part B1) or
    `"multiplicative"` (the pre-B1 behaviour, retained so a deployment can reproduce older
    valuations). Neither mode ever hand-picks per-company assumptions -- both are mechanical
    transforms of the Base case, which is what keeps the three scenarios comparable across
    securities.
    """
    if mode not in SCENARIO_MODES:
        raise ValueError(f"Unknown scenario mode {mode!r}; expected one of {SCENARIO_MODES}")

    if mode == SCENARIO_MODE_HYBRID:
        bear_growth_d = _hybrid_delta(base.revenue_growth_rate, BEAR_HYBRID["growth_floor_pp"],
                                      BEAR_HYBRID["growth_fraction"], -1)
        bear_margin_d = _hybrid_delta(base.operating_margin, BEAR_HYBRID["margin_floor_pp"],
                                      BEAR_HYBRID["margin_fraction"], -1)
        bear_term_d = _hybrid_delta(base.terminal_growth, BEAR_HYBRID["terminal_floor_pp"],
                                    BEAR_HYBRID["terminal_fraction"], -1)
        bull_growth_d = _hybrid_delta(base.revenue_growth_rate, BULL_HYBRID["growth_floor_pp"],
                                      BULL_HYBRID["growth_fraction"], 1)
        bull_margin_d = _hybrid_delta(base.operating_margin, BULL_HYBRID["margin_floor_pp"],
                                      BULL_HYBRID["margin_fraction"], 1)
        bull_term_d = _hybrid_delta(base.terminal_growth, BULL_HYBRID["terminal_floor_pp"],
                                    BULL_HYBRID["terminal_fraction"], 1)
        bear_growth = base.revenue_growth_rate + bear_growth_d
        bear_margin = base.operating_margin + bear_margin_d
        bear_terminal = base.terminal_growth + bear_term_d
        bull_growth = base.revenue_growth_rate + bull_growth_d
        bull_margin = base.operating_margin + bull_margin_d
        bull_terminal = base.terminal_growth + bull_term_d
        bear_paths = _shift_paths(base, bear_growth_d, bear_margin_d, None, None)
        bull_paths = _shift_paths(base, bull_growth_d, bull_margin_d, None, None)
    elif mode == SCENARIO_MODE_MULTIPLICATIVE:
        bear_growth = base.revenue_growth_rate * BEAR_ADJUSTMENTS["revenue_growth_rate"]
        bear_margin = base.operating_margin * BEAR_ADJUSTMENTS["operating_margin"]
        bear_terminal = base.terminal_growth * BEAR_ADJUSTMENTS["terminal_growth"]
        bull_growth = base.revenue_growth_rate * BULL_ADJUSTMENTS["revenue_growth_rate"]
        bull_margin = base.operating_margin * BULL_ADJUSTMENTS["operating_margin"]
        bull_terminal = base.terminal_growth * BULL_ADJUSTMENTS["terminal_growth"]
        bear_paths = _shift_paths(base, 0.0, 0.0, BEAR_ADJUSTMENTS["revenue_growth_rate"],
                                  BEAR_ADJUSTMENTS["operating_margin"])
        bull_paths = _shift_paths(base, 0.0, 0.0, BULL_ADJUSTMENTS["revenue_growth_rate"],
                                  BULL_ADJUSTMENTS["operating_margin"])
    else:
        bear_growth = base.revenue_growth_rate + BEAR_SHIFTS_PP["revenue_growth_rate"]
        bear_margin = base.operating_margin + BEAR_SHIFTS_PP["operating_margin"]
        bear_terminal = base.terminal_growth + BEAR_SHIFTS_PP["terminal_growth"]
        bull_growth = base.revenue_growth_rate + BULL_SHIFTS_PP["revenue_growth_rate"]
        bull_margin = base.operating_margin + BULL_SHIFTS_PP["operating_margin"]
        bull_terminal = base.terminal_growth + BULL_SHIFTS_PP["terminal_growth"]
        bear_paths = _shift_paths(base, BEAR_SHIFTS_PP["revenue_growth_rate"],
                                  BEAR_SHIFTS_PP["operating_margin"], None, None)
        bull_paths = _shift_paths(base, BULL_SHIFTS_PP["revenue_growth_rate"],
                                  BULL_SHIFTS_PP["operating_margin"], None, None)

    # Guard rails, identical in both modes and unchanged from the pre-B1 code:
    #  - growth floored at -50% (a DCF is not the right tool below that)
    #  - margin floored at 0 and capped at 75% (no negative-margin terminal state, no fantasy)
    #  - terminal growth kept strictly below WACC, else the Gordon denominator goes non-positive
    bear = DCFAssumptions(**{
        **base.__dict__,
        "revenue_growth_rate": max(bear_growth, -0.5),
        "operating_margin": max(bear_margin, 0.0),
        "terminal_growth": min(bear_terminal, base.wacc - 0.005),
        **bear_paths,
    })
    bull = DCFAssumptions(**{
        **base.__dict__,
        "revenue_growth_rate": bull_growth,
        "operating_margin": min(bull_margin, 0.75),
        "terminal_growth": min(bull_terminal, base.wacc - 0.005),
        **bull_paths,
    })
    return bear, bull


def run_all_scenarios(base: DCFAssumptions,
                       mode: str = DEFAULT_SCENARIO_MODE) -> dict[Scenario, DCFResult]:
    bear, bull = derive_bear_bull(base, mode=mode)
    return {
        Scenario.BEAR: run_dcf(Scenario.BEAR, bear),
        Scenario.BASE: run_dcf(Scenario.BASE, base),
        Scenario.BULL: run_dcf(Scenario.BULL, bull),
    }
