#!/usr/bin/env python3
"""
Unit Economics Analyzer
========================
Per-cohort LTV, per-channel CAC, payback periods, and LTV:CAC ratios.
Never blended averages — those hide what's actually happening.

Usage:
    python unit_economics_analyzer.py
    python unit_economics_analyzer.py --csv

Stdlib only. No dependencies.
"""

import argparse
import csv
import io
import sys
from dataclasses import dataclass, field
from typing import Optional


# ---------------------------------------------------------------------------
# Data structures
# ---------------------------------------------------------------------------

@dataclass
class CohortData:
    """
    Revenue data for a group of customers acquired in the same period.
    Revenue is tracked monthly: revenue[0] = month 1, revenue[1] = month 2, etc.
    """
    label: str                      # e.g. "Q1 2024"
    acquisition_period: str         # human-readable label
    customers_acquired: int
    total_cac_spend: float          # total S&M spend to acquire this cohort
    monthly_revenue: list[float]    # revenue per month from this cohort
    gross_margin_pct: float = 0.70  # blended gross margin for this cohort


@dataclass
class ChannelData:
    """Acquisition cost and customer data for a single channel."""
    channel: str
    spend: float
    customers_acquired: int
    avg_arpa: float                 # average revenue per account (monthly)
    gross_margin_pct: float = 0.70
    avg_monthly_churn: float = 0.02 # monthly churn rate for customers from this channel


@dataclass
class UnitEconomicsResult:
    """Computed unit economics for a cohort or channel."""
    label: str
    customers: int
    cac: float
    arpa: float                 # average revenue per account per month
    gross_margin_pct: float
    monthly_churn: float
    ltv: float
    ltv_cac_ratio: float
    payback_months: float
    # Cohort-specific
    m1_revenue: Optional[float] = None
    m6_revenue: Optional[float] = None
    m12_revenue: Optional[float] = None
    m24_revenue: Optional[float] = None
    m12_ltv: Optional[float] = None   # realized LTV through month 12
    retention_m6: Optional[float] = None    # % of M1 revenue retained at M6
    retention_m12: Optional[float] = None


# ---------------------------------------------------------------------------
# Calculators
# ---------------------------------------------------------------------------

def calc_ltv(arpa: float, gross_margin_pct: float, monthly_churn: float) -> float:
    """
    LTV = (ARPA × Gross Margin) / Monthly Churn Rate
    Assumes constant churn (simplified; cohort method is more accurate).
    """
    if monthly_churn <= 0:
        return float("inf")
    return (arpa * gross_margin_pct) / monthly_churn


def calc_payback(cac: float, arpa: float, gross_margin_pct: float) -> float:
    """
    CAC Payback (months) = CAC / (ARPA × Gross Margin)
    """
    denominator = arpa * gross_margin_pct
    if denominator <= 0:
        return float("inf")
    return cac / denominator


def analyze_cohort(cohort: CohortData) -> UnitEconomicsResult:
    """Compute full unit economics for a cohort."""
    n = cohort.customers_acquired
    if n == 0:
        raise ValueError(f"Cohort {cohort.label}: customers_acquired cannot be 0")

    cac = cohort.total_cac_spend / n

    # ARPA from month 1 revenue
    m1_rev = cohort.monthly_revenue[0] if cohort.monthly_revenue else 0
    arpa = m1_rev / n if n > 0 else 0

    # Observed monthly churn from cohort data
    # Use revenue decline from M1 to M12 to estimate churn
    months_available = len(cohort.monthly_revenue)
    if months_available >= 12:
        m12_rev = cohort.monthly_revenue[11]
        # Revenue retention over 12 months: (M12/M1)^(1/11) per month on average
        # Implied monthly retention rate
        if m1_rev > 0 and m12_rev > 0:
            monthly_retention = (m12_rev / m1_rev) ** (1 / 11)
            monthly_churn = 1 - monthly_retention
        else:
            monthly_churn = 0.02  # default
    elif months_available >= 6:
        m6_rev = cohort.monthly_revenue[5]
        if m1_rev > 0 and m6_rev > 0:
            monthly_retention = (m6_rev / m1_rev) ** (1 / 5)
            monthly_churn = 1 - monthly_retention
        else:
            monthly_churn = 0.02
    else:
        monthly_churn = 0.02  # default if < 6 months data

    # Clamp to reasonable range
    monthly_churn = max(0.001, min(monthly_churn, 0.30))

    ltv = calc_ltv(arpa, cohort.gross_margin_pct, monthly_churn)
    payback = calc_payback(cac, arpa, cohort.gross_margin_pct)
    ltv_cac = ltv / cac if cac > 0 else float("inf")

    # Snapshot revenues
    def rev_at(month_idx: int) -> Optional[float]:
        if months_available > month_idx:
            return cohort.monthly_revenue[month_idx]
        return None

    m6 = rev_at(5)
    m12 = rev_at(11)
    m24 = rev_at(23)

    # Realized LTV through observed months (actual gross profit)
    m12_ltv = sum(cohort.monthly_revenue[:12]) * cohort.gross_margin_pct if months_available >= 12 else None

    # Retention rates
    ret_m6 = (m6 / m1_rev) if (m6 is not None and m1_rev > 0) else None
    ret_m12 = (m12 / m1_rev) if (m12 is not None and m1_rev > 0) else None

    return UnitEconomicsResult(
        label=cohort.label,
        customers=n,
        cac=cac,
        arpa=arpa,
        gross_margin_pct=cohort.gross_margin_pct,
        monthly_churn=monthly_churn,
        ltv=ltv,
        ltv_cac_ratio=ltv_cac,
        payback_months=payback,
        m1_revenue=m1_rev,
        m6_revenue=m6,
        m12_revenue=m12,
        m24_revenue=m24,
        m12_ltv=m12_ltv,
        retention_m6=ret_m6,
        retention_m12=ret_m12,
    )


def analyze_channel(ch: ChannelData) -> UnitEconomicsResult:
    """Compute unit economics for an acquisition channel."""
    if ch.customers_acquired == 0:
        raise ValueError(f"Channel {ch.channel}: customers_acquired cannot be 0")

    cac = ch.spend / ch.customers_acquired
    ltv = calc_ltv(ch.avg_arpa, ch.gross_margin_pct, ch.avg_monthly_churn)
    payback = calc_payback(cac, ch.avg_arpa, ch.gross_margin_pct)
    ltv_cac = ltv / cac if cac > 0 else float("inf")

    return UnitEconomicsResult(
        label=ch.channel,
        customers=ch.customers_acquired,
        cac=cac,
        arpa=ch.avg_arpa,
        gross_margin_pct=ch.gross_margin_pct,
        monthly_churn=ch.avg_monthly_churn,
        ltv=ltv,
        ltv_cac_ratio=ltv_cac,
        payback_months=payback,
    )


# ---------------------------------------------------------------------------
# Blended metrics (for comparison)
# ---------------------------------------------------------------------------

def blended_cac(channels: list[ChannelData]) -> float:
    total_spend = sum(c.spend for c in channels)
    total_customers = sum(c.customers_acquired for c in channels)
    return total_spend / total_customers if total_customers > 0 else 0


def blended_ltv(channels: list[ChannelData]) -> float:
    """Weighted average LTV by customers acquired."""
    total_customers = sum(c.customers_acquired for c in channels)
    if total_customers == 0:
        return 0
    weighted = sum(
        calc_ltv(c.avg_arpa, c.gross_margin_pct, c.avg_monthly_churn) * c.customers_acquired
        for c in channels
    )
    return weighted / total_customers


# ---------------------------------------------------------------------------
# Reporting
# ---------------------------------------------------------------------------

def fmt(value: float, prefix: str = "$", decimals: int = 0) -> str:
    if value == float("inf"):
        return "∞"
    if abs(value) >= 1_000_000:
        return f"{prefix}{value/1_000_000:.2f}M"
    if abs(value) >= 1_000:
        return f"{prefix}{value/1_000:.1f}K"
    return f"{prefix}{value:.{decimals}f}"


def pct(value: Optional[float]) -> str:
    if value is None:
        return "n/a"
    return f"{value*100:.1f}%"


def rating(ltv_cac: float, payback: float) -> str:
    if ltv_cac == float("inf"):
        return "∞"
    if ltv_cac >= 5 and payback <= 12:
        return "🟢 Excellent"
    if ltv_cac >= 3 and payback <= 18:
        return "🟡 Good"
    if ltv_cac >= 2 and payback <= 24:
        return "🟠 Marginal"
    return "🔴 Poor"


def print_cohort_analysis(results: list[UnitEconomicsResult]) -> None:
    print("\n" + "="*80)
    print("  COHORT ANALYSIS")
    print("="*80)
    print(f"  {'Cohort':<12} {'Cust':>5} {'CAC':>8} {'ARPA/mo':>9} {'Churn/mo':>10} "
          f"{'LTV':>10} {'LTV:CAC':>8} {'Payback':>9} {'Ret@M12':>8}")
    print("  " + "-"*88)
    for r in results:
        payback_str = f"{r.payback_months:.1f}mo" if r.payback_months != float("inf") else "∞"
        ltv_str = fmt(r.ltv) if r.ltv != float("inf") else "∞"
        ltv_cac_str = f"{r.ltv_cac_ratio:.1f}x" if r.ltv_cac_ratio != float("inf") else "∞"
        print(
            f"  {r.label:<12} {r.customers:>5} {fmt(r.cac):>8} {fmt(r.arpa):>9} "
            f"{pct(r.monthly_churn):>10} {ltv_str:>10} {ltv_cac_str:>8} "
            f"{payback_str:>9} {pct(r.retention_m12):>8}"
        )

    # Trend analysis
    print("\n  Cohort Trend (is the business getting better or worse?):")
    if len(results) >= 3:
        ltv_cac_values = [r.ltv_cac_ratio for r in results if r.ltv_cac_ratio != float("inf")]
        cac_values = [r.cac for r in results]
        churn_values = [r.monthly_churn for r in results]

        if len(ltv_cac_values) >= 2:
            ltv_cac_trend = "↑ Improving" if ltv_cac_values[-1] > ltv_cac_values[0] else "↓ Deteriorating"
        else:
            ltv_cac_trend = "n/a"

        cac_trend = "↓ Decreasing (good)" if cac_values[-1] < cac_values[0] else "↑ Increasing"
        churn_trend = "↓ Improving" if churn_values[-1] < churn_values[0] else "↑ Worsening"

        print(f"    LTV:CAC:    {ltv_cac_trend}")
        print(f"    CAC:        {cac_trend}")
        print(f"    Churn rate: {churn_trend}")


def print_channel_analysis(results: list[UnitEconomicsResult], channels: list[ChannelData]) -> None:
    print("\n" + "="*80)
    print("  CHANNEL ANALYSIS (Per-Channel vs Blended)")
    print("="*80)
    print(f"  {'Channel':<22} {'Spend':>9} {'Cust':>5} {'CAC':>8} {'LTV':>10} {'LTV:CAC':>8} {'Payback':>9} {'Rating'}")
    print("  " + "-"*90)
    for r, ch in zip(results, channels):
        payback_str = f"{r.payback_months:.1f}mo" if r.payback_months != float("inf") else "∞"
        ltv_str = fmt(r.ltv) if r.ltv != float("inf") else "∞"
        ltv_cac_str = f"{r.ltv_cac_ratio:.1f}x" if r.ltv_cac_ratio != float("inf") else "∞"
        print(
            f"  {r.label:<22} {fmt(ch.spend):>9} {r.customers:>5} {fmt(r.cac):>8} "
            f"{ltv_str:>10} {ltv_cac_str:>8} {payback_str:>9}  {rating(r.ltv_cac_ratio, r.payback_months)}"
        )

    # Blended comparison
    b_cac = blended_cac(channels)
    b_ltv = blended_ltv(channels)
    b_ltv_cac = b_ltv / b_cac if b_cac > 0 else 0
    total_spend = sum(c.spend for c in channels)
    total_customers = sum(c.customers_acquired for c in channels)
    avg_payback = sum(
        calc_payback(b_cac, c.avg_arpa, c.gross_margin_pct) * c.customers_acquired
        for c in channels
    ) / total_customers

    print("  " + "-"*90)
    print(
        f"  {'BLENDED (dangerous)':<22} {fmt(total_spend):>9} {total_customers:>5} "
        f"{fmt(b_cac):>8} {fmt(b_ltv):>10} {b_ltv_cac:.1f}x{'':<7} "
        f"{avg_payback:.1f}mo{'':<4}  {rating(b_ltv_cac, avg_payback)}"
    )
    print("\n  ⚠️  Blended numbers hide channel-level problems. Manage channels individually.")

    # Budget reallocation
    print("\n  Recommended Budget Reallocation:")
    sorted_results = sorted(zip(results, channels), key=lambda x: x[0].ltv_cac_ratio, reverse=True)
    for r, ch in sorted_results:
        if r.ltv_cac_ratio >= 3:
            action = "✅ Scale"
        elif r.ltv_cac_ratio >= 2:
            action = "🔄 Optimize"
        else:
            action = "❌ Cut / pause"
        print(f"    {ch.channel:<22} LTV:CAC = {r.ltv_cac_ratio:.1f}x  → {action}")


def export_csv_results(cohort_results: list[UnitEconomicsResult], channel_results: list[UnitEconomicsResult]) -> str:
    buf = io.StringIO()
    writer = csv.writer(buf)
    writer.writerow(["Type", "Label", "Customers", "CAC", "ARPA_Monthly", "Gross_Margin_Pct",
                     "Monthly_Churn", "LTV", "LTV_CAC_Ratio", "Payback_Months",
                     "Retention_M6", "Retention_M12"])
    for r in cohort_results:
        writer.writerow(["cohort", r.label, r.customers, round(r.cac, 2), round(r.arpa, 2),
                         r.gross_margin_pct, round(r.monthly_churn, 4),
                         round(r.ltv, 2) if r.ltv != float("inf") else "inf",
                         round(r.ltv_cac_ratio, 2) if r.ltv_cac_ratio != float("inf") else "inf",
                         round(r.payback_months, 2) if r.payback_months != float("inf") else "inf",
                         round(r.retention_m6, 3) if r.retention_m6 else "",
                         round(r.retention_m12, 3) if r.retention_m12 else ""])
    for r in channel_results:
        writer.writerow(["channel", r.label, r.customers, round(r.cac, 2), round(r.arpa, 2),
                         r.gross_margin_pct, round(r.monthly_churn, 4),
                         round(r.ltv, 2) if r.ltv != float("inf") else "inf",
                         round(r.ltv_cac_ratio, 2) if r.ltv_cac_ratio != float("inf") else "inf",
                         round(r.payback_months, 2) if r.payback_months != float("inf") else "inf",
                         "", ""])
    return buf.getvalue()


# ---------------------------------------------------------------------------
# Sample data
# ---------------------------------------------------------------------------

def make_sample_cohorts() -> list[CohortData]:
    """
    Series A SaaS company, 8 quarters of cohort data.
    Shows a business improving on all dimensions over time.
    """
    return [
        CohortData(
            label="Q1 2023", acquisition_period="Jan-Mar 2023",
            customers_acquired=12, total_cac_spend=54_000,
            gross_margin_pct=0.68,
            monthly_revenue=[
                10_200, 9_600, 9_100, 8_700, 8_300, 8_000,  # M1-M6
                7_800, 7_600, 7_400, 7_200, 7_000, 6_800,   # M7-M12
                6_700, 6_600, 6_500, 6_400, 6_300, 6_200,   # M13-M18
                6_100, 6_000, 5_900, 5_800, 5_700, 5_600,   # M19-M24
            ],
        ),
        CohortData(
            label="Q2 2023", acquisition_period="Apr-Jun 2023",
            customers_acquired=15, total_cac_spend=60_000,
            gross_margin_pct=0.69,
            monthly_revenue=[
                13_500, 12_900, 12_500, 12_100, 11_800, 11_500,
                11_300, 11_100, 10_900, 10_700, 10_500, 10_300,
                10_200, 10_100, 10_000, 9_900, 9_800, 9_700,
            ],
        ),
        CohortData(
            label="Q3 2023", acquisition_period="Jul-Sep 2023",
            customers_acquired=18, total_cac_spend=63_000,
            gross_margin_pct=0.70,
            monthly_revenue=[
                16_200, 15_800, 15_400, 15_100, 14_800, 14_600,
                14_400, 14_200, 14_000, 13_900, 13_800, 13_700,
                13_600, 13_500, 13_400, 13_300,
            ],
        ),
        CohortData(
            label="Q4 2023", acquisition_period="Oct-Dec 2023",
            customers_acquired=22, total_cac_spend=70_400,
            gross_margin_pct=0.71,
            monthly_revenue=[
                20_900, 20_500, 20_200, 19_900, 19_700, 19_500,
                19_300, 19_100, 19_000, 18_900, 18_800, 18_700,
            ],
        ),
        CohortData(
            label="Q1 2024", acquisition_period="Jan-Mar 2024",
            customers_acquired=28, total_cac_spend=81_200,
            gross_margin_pct=0.72,
            monthly_revenue=[
                27_200, 26_900, 26_600, 26_400, 26_200, 26_000,
                25_800, 25_700, 25_600, 25_500,
            ],
        ),
        CohortData(
            label="Q2 2024", acquisition_period="Apr-Jun 2024",
            customers_acquired=34, total_cac_spend=91_800,
            gross_margin_pct=0.72,
            monthly_revenue=[
                33_300, 33_000, 32_800, 32_600, 32_400, 32_200,
            ],
        ),
        CohortData(
            label="Q3 2024", acquisition_period="Jul-Sep 2024",
            customers_acquired=40, total_cac_spend=100_000,
            gross_margin_pct=0.73,
            monthly_revenue=[
                39_600, 39_400, 39_200,
            ],
        ),
        CohortData(
            label="Q4 2024", acquisition_period="Oct-Dec 2024",
            customers_acquired=47, total_cac_spend=112_800,
            gross_margin_pct=0.73,
            monthly_revenue=[
                47_000,
            ],
        ),
    ]


def make_sample_channels() -> list[ChannelData]:
    """
    Q4 2024 channel breakdown. Blended looks fine; per-channel reveals problems.
    """
    return [
        ChannelData("Organic / SEO",     spend=9_500,  customers_acquired=14, avg_arpa=950,  gross_margin_pct=0.73, avg_monthly_churn=0.015),
        ChannelData("Paid Search (SEM)",  spend=48_000, customers_acquired=18, avg_arpa=980,  gross_margin_pct=0.73, avg_monthly_churn=0.020),
        ChannelData("Paid Social",        spend=32_000, customers_acquired=8,  avg_arpa=900,  gross_margin_pct=0.72, avg_monthly_churn=0.025),
        ChannelData("Content / Inbound",  spend=11_000, customers_acquired=6,  avg_arpa=1100, gross_margin_pct=0.74, avg_monthly_churn=0.012),
        ChannelData("Outbound SDR",       spend=22_000, customers_acquired=4,  avg_arpa=1200, gross_margin_pct=0.73, avg_monthly_churn=0.022),
        ChannelData("Events / Webinars",  spend=18_500, customers_acquired=3,  avg_arpa=1050, gross_margin_pct=0.72, avg_monthly_churn=0.028),
        ChannelData("Partner / Referral", spend=7_800,  customers_acquired=7,  avg_arpa=1000, gross_margin_pct=0.73, avg_monthly_churn=0.013),
    ]


# ---------------------------------------------------------------------------
# Entry point
# ---------------------------------------------------------------------------

def main() -> None:
    parser = argparse.ArgumentParser(description="Unit Economics Analyzer")
    parser.add_argument("--csv", action="store_true", help="Export results as CSV to stdout")
    args = parser.parse_args()

    cohorts = make_sample_cohorts()
    channels = make_sample_channels()

    print("\n" + "="*80)
    print("  UNIT ECONOMICS ANALYZER")
    print("  Sample Company: Series A SaaS | Q4 2024 Snapshot")
    print("  Gross Margin: ~72% | Monthly Churn: derived from cohort data")
    print("="*80)

    cohort_results = [analyze_cohort(c) for c in cohorts]
    channel_results = [analyze_channel(c) for c in channels]

    print_cohort_analysis(cohort_results)
    print_channel_analysis(channel_results, channels)

    # Health summary
    print("\n" + "="*80)
    print("  HEALTH SUMMARY")
    print("="*80)
    latest = cohort_results[-1]
    prev = cohort_results[-4] if len(cohort_results) >= 4 else cohort_results[0]

    print(f"\n  Latest Cohort ({latest.label}):")
    print(f"    CAC:          {fmt(latest.cac)}")
    ltv_str = fmt(latest.ltv) if latest.ltv != float("inf") else "∞"
    ltv_cac_str = f"{latest.ltv_cac_ratio:.1f}x" if latest.ltv_cac_ratio != float("inf") else "∞"
    payback_str = f"{latest.payback_months:.1f} months" if latest.payback_months != float("inf") else "∞"
    print(f"    LTV:          {ltv_str}")
    print(f"    LTV:CAC:      {ltv_cac_str}  (target: > 3x)")
    print(f"    CAC Payback:  {payback_str}  (target: < 18mo)")
    print(f"    Rating:       {rating(latest.ltv_cac_ratio, latest.payback_months)}")

    # Trend vs 4 quarters ago
    print(f"\n  Trend vs {prev.label}:")
    cac_delta = (latest.cac - prev.cac) / prev.cac * 100
    ltv_delta_str = "n/a"
    if latest.ltv != float("inf") and prev.ltv != float("inf"):
        ltv_delta = (latest.ltv - prev.ltv) / prev.ltv * 100
        ltv_delta_str = f"{ltv_delta:+.1f}%"
    cac_str = "↓ Better" if cac_delta < 0 else "↑ Worse"
    print(f"    CAC:    {cac_delta:+.1f}%  ({cac_str})")
    print(f"    LTV:    {ltv_delta_str}")

    print("\n  Benchmark Reference:")
    print("    LTV:CAC > 5x  → Scale aggressively")
    print("    LTV:CAC 3-5x  → Healthy; grow at current pace")
    print("    LTV:CAC 2-3x  → Marginal; optimize before scaling")
    print("    LTV:CAC < 2x  → Acquiring unprofitably; stop and fix")
    print("    Payback < 12mo → Outstanding capital efficiency")
    print("    Payback 12-18mo → Good for B2B SaaS")
    print("    Payback > 24mo → Requires long-dated capital to scale")

    if args.csv:
        print("\n\n--- CSV EXPORT ---\n")
        sys.stdout.write(export_csv_results(cohort_results, channel_results))


if __name__ == "__main__":
    main()
