#!/usr/bin/env python3
"""
Growth Model Simulator
----------------------
Projects MRR growth across different growth models (PLG, sales-led, community-led,
hybrid) and shows the impact of channel mix changes on growth trajectory.

Usage:
    python growth_model_simulator.py

Inputs (edit INPUTS section):
    - Starting MRR and churn rate
    - Current channel mix (% of new MRR from each source)
    - Conversion rates per model
    - Growth rate assumptions per channel

Outputs:
    - 12-month MRR projection by growth model
    - Channel mix impact analysis (what happens if you shift mix)
    - Break-even months for each model
    - Side-by-side comparison table
"""

from __future__ import annotations
import math
from dataclasses import dataclass, field
from typing import Dict, List, Optional, Tuple


# ---------------------------------------------------------------------------
# Data models
# ---------------------------------------------------------------------------

@dataclass
class ChannelSource:
    name: str
    pct_of_new_mrr: float      # Current share of new MRR (0.0–1.0)
    monthly_growth_rate: float  # How fast this channel grows month-over-month
    cac: float                  # CAC in dollars
    payback_months: float       # Months to recover CAC


@dataclass
class GrowthModel:
    name: str
    description: str
    channel_mix: Dict[str, float]  # channel name → % of new MRR
    new_mrr_monthly_base: float    # Starting new MRR/month from this model
    monthly_acceleration: float    # Acceleration factor (compounding)
    avg_ltv_cac: float             # Expected LTV:CAC at scale
    months_to_steady_state: int    # Months before model hits its natural growth rate
    notes: List[str] = field(default_factory=list)


@dataclass
class MonthSnapshot:
    month: int
    mrr: float
    new_mrr: float
    churned_mrr: float
    expansion_mrr: float
    net_new_mrr: float
    cumulative_cac_spend: float


@dataclass
class ModelProjection:
    model: GrowthModel
    snapshots: List[MonthSnapshot]
    break_even_month: Optional[int]  # Month when cumulative revenue > cumulative CAC


# ---------------------------------------------------------------------------
# INPUTS — edit these
# ---------------------------------------------------------------------------

STARTING_MRR = 85_000         # Current MRR ($)
MONTHLY_CHURN_RATE = 0.012    # Monthly churn rate (1.2% = ~14% annual)
EXPANSION_RATE = 0.008        # Monthly expansion MRR as % of existing MRR
GROSS_MARGIN = 0.75
SIMULATION_MONTHS = 18

# Channel sources (used to model mix shift scenarios)
CHANNELS: List[ChannelSource] = [
    ChannelSource("Organic/SEO",      pct_of_new_mrr=0.28, monthly_growth_rate=0.04, cac=1_800,  payback_months=9),
    ChannelSource("PLG Self-Serve",   pct_of_new_mrr=0.15, monthly_growth_rate=0.08, cac=900,   payback_months=5),
    ChannelSource("Outbound SDR",     pct_of_new_mrr=0.25, monthly_growth_rate=0.02, cac=5_100,  payback_months=21),
    ChannelSource("Paid Search",      pct_of_new_mrr=0.15, monthly_growth_rate=0.01, cac=6_200,  payback_months=26),
    ChannelSource("Events/Field",     pct_of_new_mrr=0.08, monthly_growth_rate=0.01, cac=9_800,  payback_months=41),
    ChannelSource("Partner/Channel",  pct_of_new_mrr=0.09, monthly_growth_rate=0.05, cac=3_400,  payback_months=14),
]

# Growth models to simulate
GROWTH_MODELS: List[GrowthModel] = [
    GrowthModel(
        name="Current Mix",
        description="Baseline — maintain current channel allocation",
        channel_mix={"Organic/SEO": 0.28, "PLG Self-Serve": 0.15, "Outbound SDR": 0.25,
                     "Paid Search": 0.15, "Events/Field": 0.08, "Partner/Channel": 0.09},
        new_mrr_monthly_base=12_000,
        monthly_acceleration=0.025,
        avg_ltv_cac=3.2,
        months_to_steady_state=3,
        notes=["Baseline. No changes to channel mix."],
    ),
    GrowthModel(
        name="PLG-First",
        description="Shift budget toward PLG self-serve and organic; reduce paid and outbound",
        channel_mix={"Organic/SEO": 0.35, "PLG Self-Serve": 0.35, "Outbound SDR": 0.10,
                     "Paid Search": 0.08, "Events/Field": 0.04, "Partner/Channel": 0.08},
        new_mrr_monthly_base=9_500,   # Slower start — PLG takes time to activate
        monthly_acceleration=0.048,   # But compounds faster
        avg_ltv_cac=5.8,
        months_to_steady_state=6,     # PLG loops take time to build
        notes=[
            "Lower new MRR in months 1-6 while PLG loops activate.",
            "Acceleration compounds strongly after month 6.",
            "Requires product investment in activation/onboarding.",
            "Best fit if time-to-value < 30 min and viral coefficient > 0.3.",
        ],
    ),
    GrowthModel(
        name="Sales-Led Scale",
        description="Double down on outbound SDR and field; optimize for enterprise ACV",
        channel_mix={"Organic/SEO": 0.20, "PLG Self-Serve": 0.05, "Outbound SDR": 0.40,
                     "Paid Search": 0.15, "Events/Field": 0.15, "Partner/Channel": 0.05},
        new_mrr_monthly_base=15_000,  # Higher new MRR from enterprise ACV
        monthly_acceleration=0.018,   # Linear growth — headcount-constrained
        avg_ltv_cac=2.8,
        months_to_steady_state=2,
        notes=[
            "Fastest short-term new MRR if ACV > $30K.",
            "Growth is linear — adds headcount to add pipeline.",
            "CAC and payback worsen as SDR market tightens.",
            "Requires sales capacity increase to sustain.",
        ],
    ),
    GrowthModel(
        name="Community-Led",
        description="Invest in community and content; reduce paid; long-term brand play",
        channel_mix={"Organic/SEO": 0.45, "PLG Self-Serve": 0.15, "Outbound SDR": 0.15,
                     "Paid Search": 0.05, "Events/Field": 0.10, "Partner/Channel": 0.10},
        new_mrr_monthly_base=7_000,   # Slowest start
        monthly_acceleration=0.038,
        avg_ltv_cac=4.5,
        months_to_steady_state=9,     # Community takes longest to activate
        notes=[
            "Lowest new MRR in months 1-9.",
            "Community trust drives lower CAC and higher retention at scale.",
            "Best for categories where buyers seek peer validation.",
            "Requires dedicated community manager from day one.",
        ],
    ),
    GrowthModel(
        name="Hybrid PLS",
        description="PLG self-serve for SMB + sales-assisted for enterprise (Product-Led Sales)",
        channel_mix={"Organic/SEO": 0.30, "PLG Self-Serve": 0.28, "Outbound SDR": 0.22,
                     "Paid Search": 0.08, "Events/Field": 0.06, "Partner/Channel": 0.06},
        new_mrr_monthly_base=11_000,
        monthly_acceleration=0.035,
        avg_ltv_cac=4.1,
        months_to_steady_state=4,
        notes=[
            "PLG handles SMB; sales closes enterprise with PQL signals.",
            "Requires clear PQL definition and SDR/PLG handoff process.",
            "Best if you have a product with both bottom-up and top-down adoption.",
        ],
    ),
]


# ---------------------------------------------------------------------------
# Simulation engine
# ---------------------------------------------------------------------------

def simulate_model(model: GrowthModel, months: int) -> ModelProjection:
    snapshots: List[MonthSnapshot] = []
    mrr = STARTING_MRR
    cumulative_cac = 0.0
    cumulative_revenue = 0.0
    break_even_month = None

    for m in range(1, months + 1):
        # Ramp up — new_mrr accelerates each month
        if m <= model.months_to_steady_state:
            # Ramp phase: linear ramp from 60% to 100% of base
            ramp_factor = 0.6 + 0.4 * (m / model.months_to_steady_state)
        else:
            # Steady state: compound acceleration
            months_past_ramp = m - model.months_to_steady_state
            ramp_factor = 1.0 + model.monthly_acceleration * months_past_ramp

        new_mrr = model.new_mrr_monthly_base * ramp_factor
        churned_mrr = mrr * MONTHLY_CHURN_RATE
        expansion_mrr = mrr * EXPANSION_RATE
        net_new_mrr = new_mrr - churned_mrr + expansion_mrr
        mrr = mrr + net_new_mrr

        # CAC spend approximation: new_mrr / (avg_deal_mrr) * blended_cac
        # Use weighted CAC from channel mix
        weighted_cac = _weighted_cac(model.channel_mix)
        avg_deal_mrr = 1_500  # Assumption: $1,500 average deal MRR
        deals_this_month = new_mrr / avg_deal_mrr
        cac_spend = deals_this_month * weighted_cac
        cumulative_cac += cac_spend
        cumulative_revenue += mrr * GROSS_MARGIN

        if break_even_month is None and cumulative_revenue >= cumulative_cac:
            break_even_month = m

        snapshots.append(MonthSnapshot(
            month=m,
            mrr=mrr,
            new_mrr=new_mrr,
            churned_mrr=churned_mrr,
            expansion_mrr=expansion_mrr,
            net_new_mrr=net_new_mrr,
            cumulative_cac_spend=cumulative_cac,
        ))

    return ModelProjection(
        model=model,
        snapshots=snapshots,
        break_even_month=break_even_month,
    )


def _weighted_cac(channel_mix: Dict[str, float]) -> float:
    channel_cac = {ch.name: ch.cac for ch in CHANNELS}
    total = sum(
        channel_mix.get(name, 0) * cac
        for name, cac in channel_cac.items()
    )
    weight_sum = sum(channel_mix.values())
    return total / weight_sum if weight_sum > 0 else 5_000


# ---------------------------------------------------------------------------
# Reporting
# ---------------------------------------------------------------------------

def fmt_mrr(n: float) -> str:
    if n >= 1_000_000:
        return f"${n/1_000_000:.3f}M"
    return f"${n/1_000:.1f}K"


def fmt_currency(n: float) -> str:
    if n >= 1_000_000:
        return f"${n/1_000_000:.2f}M"
    if n >= 1_000:
        return f"${n/1_000:.1f}K"
    return f"${n:.0f}"


def print_header(title: str) -> None:
    width = 78
    print("\n" + "=" * width)
    print(f"  {title}")
    print("=" * width)


def print_channel_overview() -> None:
    print_header("Current Channel Mix")
    print(f"  Starting MRR: {fmt_mrr(STARTING_MRR)}  |  Monthly churn: {MONTHLY_CHURN_RATE:.1%}  |  Expansion: {EXPANSION_RATE:.1%}/mo")
    print()
    print(f"  {'Channel':<22} {'% MRR':>7} {'CAC':>8} {'Payback':>9} {'Growth/mo':>10}")
    print("  " + "-" * 60)
    for ch in sorted(CHANNELS, key=lambda c: c.pct_of_new_mrr, reverse=True):
        print(
            f"  {ch.name:<22} {ch.pct_of_new_mrr:>6.0%} "
            f"{fmt_currency(ch.cac):>8} {ch.payback_months:>7.0f}mo "
            f"{ch.monthly_growth_rate:>9.1%}"
        )


def print_model_detail(proj: ModelProjection) -> None:
    model = proj.model
    print_header(f"Model: {model.name}")
    print(f"  {model.description}")
    if model.notes:
        print()
        for note in model.notes:
            print(f"  • {note}")
    print()

    # Print monthly snapshot (every 3 months + final)
    milestones = set(range(3, SIMULATION_MONTHS + 1, 3)) | {SIMULATION_MONTHS}
    print(f"  {'Month':<7} {'MRR':>10} {'New MRR':>9} {'Churned':>9} {'Expand':>8} {'Net New':>9}")
    print("  " + "-" * 56)
    for snap in proj.snapshots:
        if snap.month in milestones:
            print(
                f"  {snap.month:<7} {fmt_mrr(snap.mrr):>10} "
                f"{fmt_mrr(snap.new_mrr):>9} {fmt_mrr(snap.churned_mrr):>9} "
                f"{fmt_mrr(snap.expansion_mrr):>8} {fmt_mrr(snap.net_new_mrr):>9}"
            )

    final = proj.snapshots[-1]
    growth_x = final.mrr / STARTING_MRR
    arr_final = final.mrr * 12
    weighted_cac = _weighted_cac(model.channel_mix)
    be = f"Month {proj.break_even_month}" if proj.break_even_month else f"> {SIMULATION_MONTHS}mo"

    print()
    print(f"  Final MRR ({SIMULATION_MONTHS}mo):    {fmt_mrr(final.mrr)}")
    print(f"  Final ARR:             {fmt_currency(arr_final)}")
    print(f"  Growth multiple:       {growth_x:.1f}x from starting MRR")
    print(f"  Weighted blended CAC:  {fmt_currency(weighted_cac)}")
    print(f"  Expected LTV:CAC:      {model.avg_ltv_cac:.1f}x")
    print(f"  Months to steady state:{model.months_to_steady_state}")
    print(f"  CAC break-even:        {be}")


def print_comparison_table(projections: List[ModelProjection]) -> None:
    print_header(f"Growth Model Comparison — Month {SIMULATION_MONTHS} Outcomes")
    header = (
        f"  {'Model':<20} {'MRR (final)':>12} {'ARR (final)':>12} "
        f"{'Growth':>7} {'LTV:CAC':>8} {'Break-even':>11}"
    )
    print(header)
    print("  " + "-" * 74)
    for proj in sorted(projections, key=lambda p: p.snapshots[-1].mrr, reverse=True):
        final = proj.snapshots[-1]
        growth_x = final.mrr / STARTING_MRR
        arr_final = final.mrr * 12
        be = f"Mo {proj.break_even_month}" if proj.break_even_month else f">{SIMULATION_MONTHS}mo"
        print(
            f"  {proj.model.name:<20} {fmt_mrr(final.mrr):>12} "
            f"{fmt_currency(arr_final):>12} {growth_x:>6.1f}x "
            f"{proj.model.avg_ltv_cac:>7.1f}x {be:>11}"
        )


def print_channel_mix_impact(projections: List[ModelProjection]) -> None:
    print_header("Channel Mix Impact Analysis")
    print("  How shifting channel mix changes growth trajectory:\n")
    baseline = next((p for p in projections if p.model.name == "Current Mix"), None)
    if not baseline:
        return
    baseline_final_mrr = baseline.snapshots[-1].mrr

    for proj in projections:
        if proj.model.name == "Current Mix":
            continue
        final_mrr = proj.snapshots[-1].mrr
        delta = final_mrr - baseline_final_mrr
        delta_pct = (delta / baseline_final_mrr) * 100
        arrow = "↑" if delta > 0 else "↓"
        m6_mrr = proj.snapshots[5].mrr if len(proj.snapshots) >= 6 else 0
        m6_baseline = baseline.snapshots[5].mrr if len(baseline.snapshots) >= 6 else 0
        m6_delta = m6_mrr - m6_baseline
        m6_pct = (m6_delta / m6_baseline) * 100 if m6_baseline else 0
        m6_arrow = "↑" if m6_delta > 0 else "↓"

        print(f"  {proj.model.name}:")
        print(f"    Month 6:  {m6_arrow} {abs(m6_pct):.1f}%  vs. current  ({fmt_mrr(m6_delta)} {'more' if m6_delta > 0 else 'less'} MRR)")
        print(f"    Month {SIMULATION_MONTHS}: {arrow} {abs(delta_pct):.1f}%  vs. current  ({fmt_mrr(delta)} {'more' if delta > 0 else 'less'} MRR)")
        if proj.model.months_to_steady_state > 4:
            print(f"    ⚠ Model takes {proj.model.months_to_steady_state} months to reach steady state — short-term dip expected.")
        print()


def print_decision_guide(projections: List[ModelProjection]) -> None:
    print_header("Decision Guide")
    print("  Choose your growth model based on your constraints:\n")
    guides = [
        ("ACV < $5K and fast time-to-value",         "PLG-First"),
        ("ACV > $25K and complex buying process",     "Sales-Led Scale"),
        ("Strong practitioner community exists",      "Community-Led"),
        ("Both SMB self-serve and enterprise buyers", "Hybrid PLS"),
        ("Uncertain — keep optionality",              "Current Mix"),
    ]
    for condition, model_name in guides:
        proj = next((p for p in projections if p.model.name == model_name), None)
        if proj:
            final_mrr = proj.snapshots[-1].mrr
            print(f"  If: {condition}")
            print(f"  → Use {model_name} → {fmt_mrr(final_mrr)} MRR at month {SIMULATION_MONTHS}")
            print()

    print("  Key question before switching models:")
    print("    'Do we have 12-18 months of runway to prove the new model")
    print("     while the current model continues in parallel?'")
    print("    If no → optimize current model. Don't switch.")


# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------

def main() -> None:
    print_channel_overview()

    projections = [simulate_model(model, SIMULATION_MONTHS) for model in GROWTH_MODELS]

    for proj in projections:
        print_model_detail(proj)

    print_comparison_table(projections)
    print_channel_mix_impact(projections)
    print_decision_guide(projections)

    print("\n" + "=" * 78)
    print("  Notes:")
    print(f"    Starting MRR:   {fmt_mrr(STARTING_MRR)}")
    print(f"    Simulation:     {SIMULATION_MONTHS} months")
    print(f"    Churn:          {MONTHLY_CHURN_RATE:.1%}/mo ({MONTHLY_CHURN_RATE*12:.0%} annualized)")
    print(f"    Expansion:      {EXPANSION_RATE:.1%}/mo of existing MRR")
    print(f"    Gross margin:   {GROSS_MARGIN:.0%}")
    print("    Acceleration rates are estimates — validate against your actuals.")
    print("=" * 78 + "\n")


if __name__ == "__main__":
    main()
