#!/usr/bin/env python3
"""
Portfolio Analyzer — Product portfolio BCG matrix classification and investment analysis.

For each product, classifies into BCG quadrant (Star, Cash Cow, Question Mark, Dog)
and generates investment recommendations (Invest / Maintain / Kill).

Usage:
    python portfolio_analyzer.py                     # Run with built-in sample data
    python portfolio_analyzer.py --input data.json   # Run with your data
    python portfolio_analyzer.py --json              # Output raw JSON

JSON input format: see sample_data() function below.
"""

import json
import sys
import argparse
from typing import Optional


# ---------------------------------------------------------------------------
# Sample data
# ---------------------------------------------------------------------------

def sample_data() -> dict:
    """
    Sample portfolio. Replace with real product data.

    Fields:
      name              Product name
      revenue_quarterly Current quarter revenue (any consistent currency)
      revenue_prev_q    Revenue last quarter (for QoQ calculation)
      market_growth_pct Annual market growth rate (percent, e.g. 12.5 for 12.5%)
      your_market_share Your estimated market share (percent, e.g. 8.0 for 8%)
      largest_competitor_share  Largest competitor's share (percent)
      eng_capacity_pct  % of total engineering capacity allocated (0-100)
      d30_retention     Optional D30 retention rate (decimal, e.g. 0.45)
      nps               Optional NPS score (-100 to 100)
      notes             Optional free text notes for the report
    """
    return {
        "company": "Acme Corp",
        "total_engineering_headcount": 45,
        "products": [
            {
                "name": "CorePlatform",
                "revenue_quarterly": 480000,
                "revenue_prev_q": 430000,
                "market_growth_pct": 22.0,
                "your_market_share": 18.0,
                "largest_competitor_share": 12.0,
                "eng_capacity_pct": 35,
                "d30_retention": 0.61,
                "nps": 52,
                "notes": "Our flagship. Leading market share in fast-growing segment.",
            },
            {
                "name": "ReportingModule",
                "revenue_quarterly": 290000,
                "revenue_prev_q": 285000,
                "market_growth_pct": 5.0,
                "your_market_share": 22.0,
                "largest_competitor_share": 18.0,
                "eng_capacity_pct": 25,
                "d30_retention": 0.58,
                "nps": 38,
                "notes": "Mature product, strong margins, slow market.",
            },
            {
                "name": "MobileApp",
                "revenue_quarterly": 95000,
                "revenue_prev_q": 78000,
                "market_growth_pct": 35.0,
                "your_market_share": 3.5,
                "largest_competitor_share": 24.0,
                "eng_capacity_pct": 28,
                "d30_retention": 0.31,
                "nps": 22,
                "notes": "High growth market. We're far behind on share. Bet or exit.",
            },
            {
                "name": "LegacyConnector",
                "revenue_quarterly": 62000,
                "revenue_prev_q": 68000,
                "market_growth_pct": -3.0,
                "your_market_share": 8.0,
                "largest_competitor_share": 35.0,
                "eng_capacity_pct": 12,
                "d30_retention": 0.42,
                "nps": 14,
                "notes": "Declining market. Customers are on long-term contracts.",
            },
        ],
    }


# ---------------------------------------------------------------------------
# BCG Classification
# ---------------------------------------------------------------------------

# Growth rate threshold: markets growing faster than this are "high growth"
GROWTH_THRESHOLD_PCT = 10.0

# Market share ratio threshold: ratio > 1.0 means you lead the market
SHARE_RATIO_THRESHOLD = 1.0


def bcg_quadrant(market_growth_pct: float, share_ratio: float) -> str:
    high_growth = market_growth_pct >= GROWTH_THRESHOLD_PCT
    leading_share = share_ratio >= SHARE_RATIO_THRESHOLD

    if high_growth and leading_share:
        return "Star"
    elif not high_growth and leading_share:
        return "Cash Cow"
    elif high_growth and not leading_share:
        return "Question Mark"
    else:
        return "Dog"


def quadrant_emoji(quadrant: str) -> str:
    return {
        "Star": "⭐",
        "Cash Cow": "🐄",
        "Question Mark": "❓",
        "Dog": "🐕",
    }.get(quadrant, "?")


def investment_posture(quadrant: str, qoq_growth: float, retention: Optional[float]) -> str:
    """
    Invest / Maintain / Kill recommendation with nuance.
    """
    if quadrant == "Star":
        return "Invest"
    elif quadrant == "Cash Cow":
        # If cash cow is declining fast or retention is poor, consider killing
        if qoq_growth < -0.10 or (retention is not None and retention < 0.30):
            return "Kill"
        return "Maintain"
    elif quadrant == "Question Mark":
        # Fast QoQ growth signals the bet might pay off → Invest
        # Flat or slow QoQ with weak retention → Kill
        if qoq_growth >= 0.15 and (retention is None or retention >= 0.25):
            return "Invest"
        elif qoq_growth < 0.05 or (retention is not None and retention < 0.20):
            return "Kill"
        return "Evaluate"  # Needs explicit strategic decision
    else:  # Dog
        if qoq_growth > 0.10 and (retention is None or retention >= 0.35):
            return "Evaluate"  # Surprising momentum — verify before killing
        return "Kill"


def posture_color(posture: str) -> str:
    return {
        "Invest": "✓",
        "Maintain": "◑",
        "Kill": "✗",
        "Evaluate": "⚠",
    }.get(posture, "?")


# ---------------------------------------------------------------------------
# Product analysis
# ---------------------------------------------------------------------------

def analyze_product(p: dict) -> dict:
    revenue_q = p.get("revenue_quarterly", 0)
    revenue_prev = p.get("revenue_prev_q", revenue_q)
    qoq_growth = (revenue_q - revenue_prev) / revenue_prev if revenue_prev else 0.0

    your_share = p.get("your_market_share", 0)
    competitor_share = p.get("largest_competitor_share", 1)
    share_ratio = your_share / competitor_share if competitor_share else 0.0

    market_growth = p.get("market_growth_pct", 0)
    retention = p.get("d30_retention")
    nps = p.get("nps")
    eng_pct = p.get("eng_capacity_pct", 0)

    quadrant = bcg_quadrant(market_growth, share_ratio)
    posture = investment_posture(quadrant, qoq_growth, retention)

    # Alignment score: how well does engineering investment match the recommended posture?
    # Invest products should have high eng allocation; Kill products should have low.
    alignment_score = _compute_alignment(posture, eng_pct)

    return {
        "name": p.get("name", "Unknown"),
        "revenue_quarterly": revenue_q,
        "revenue_prev_q": revenue_prev,
        "qoq_growth": qoq_growth,
        "market_growth_pct": market_growth,
        "your_market_share": your_share,
        "largest_competitor_share": competitor_share,
        "share_ratio": share_ratio,
        "eng_capacity_pct": eng_pct,
        "d30_retention": retention,
        "nps": nps,
        "quadrant": quadrant,
        "posture": posture,
        "alignment_score": alignment_score,
        "notes": p.get("notes", ""),
        "findings": _product_findings(quadrant, posture, qoq_growth, share_ratio,
                                      market_growth, retention, nps, eng_pct),
    }


def _compute_alignment(posture: str, eng_pct: float) -> float:
    """
    Returns 0.0-1.0 score. High = engineering allocation matches strategic posture.
    """
    targets = {"Invest": 0.35, "Maintain": 0.15, "Kill": 0.05, "Evaluate": 0.20}
    target = targets.get(posture, 0.20)
    deviation = abs(eng_pct / 100 - target)
    return max(0.0, 1.0 - (deviation / 0.35))


def _product_findings(
    quadrant: str, posture: str,
    qoq_growth: float, share_ratio: float, market_growth: float,
    retention: Optional[float], nps: Optional[int], eng_pct: float
) -> list:
    findings = []

    if quadrant == "Star":
        if eng_pct < 30:
            findings.append(f"⚠ Star product getting only {eng_pct}% of eng capacity — likely underinvested. Stars need fuel.")
        else:
            findings.append(f"✓ Star product with {eng_pct}% eng allocation — appropriate investment.")
        if share_ratio < 1.5:
            findings.append(f"◑ Share ratio {share_ratio:.1f}x — leading but not dominant. Accelerate to widen the gap.")
        else:
            findings.append(f"✓ Share ratio {share_ratio:.1f}x — strong lead. Defend aggressively.")

    elif quadrant == "Cash Cow":
        if eng_pct > 25:
            findings.append(f"⚠ Cash Cow getting {eng_pct}% of eng — overinvested. Reduce to 10-15% max. Redeploy to Stars.")
        else:
            findings.append(f"✓ Cash Cow with {eng_pct}% eng — appropriate. Don't innovate, just maintain.")
        if qoq_growth < -0.05:
            findings.append(f"⚠ Revenue declining {abs(qoq_growth):.0%} QoQ — monitor for transition to Dog.")
        else:
            findings.append(f"✓ Revenue stable (QoQ: {qoq_growth:+.0%}) — milk this.")

    elif quadrant == "Question Mark":
        findings.append(f"⚠ Fast market ({market_growth:.0f}% growth) but only {share_ratio:.1f}x relative share.")
        findings.append(f"  Decision required: Invest to capture share or exit. 'Maintain' loses share every quarter.")
        if qoq_growth >= 0.15:
            findings.append(f"✓ QoQ growth {qoq_growth:+.0%} — momentum building. Investment may be justified.")
        elif qoq_growth < 0.05:
            findings.append(f"✗ QoQ growth {qoq_growth:+.0%} — stalled despite hot market. Strong exit signal.")

    elif quadrant == "Dog":
        findings.append(f"✗ Low share ({share_ratio:.1f}x) in slow/declining market ({market_growth:.0f}% growth).")
        if eng_pct > 10:
            findings.append(f"✗ Dog consuming {eng_pct}% of eng capacity. Set a sunset date. Migrate customers.")
        if qoq_growth > 0:
            findings.append(f"◑ Slight QoQ growth ({qoq_growth:+.0%}) — verify whether this is genuine or contract timing.")

    if retention is not None:
        if retention < 0.30:
            findings.append(f"✗ D30 retention {retention:.0%} — users not finding value. Weak unit economics for any posture.")
        elif retention >= 0.50:
            findings.append(f"✓ D30 retention {retention:.0%} — users find value. Supports investment or stable maintenance.")

    if nps is not None:
        if nps < 0:
            findings.append(f"✗ NPS {nps} — net detractors. Word of mouth is negative. Fix before scaling.")
        elif nps >= 40:
            findings.append(f"✓ NPS {nps} — strong promoter base. Harness for referrals.")

    return findings


# ---------------------------------------------------------------------------
# Portfolio-level analysis
# ---------------------------------------------------------------------------

def analyze_portfolio(data: dict) -> dict:
    products = [analyze_product(p) for p in data.get("products", [])]

    total_revenue = sum(p["revenue_quarterly"] for p in products)
    total_eng = sum(p["eng_capacity_pct"] for p in products)

    # Revenue by quadrant
    quadrant_revenue = {}
    quadrant_eng = {}
    for p in products:
        q = p["quadrant"]
        quadrant_revenue[q] = quadrant_revenue.get(q, 0) + p["revenue_quarterly"]
        quadrant_eng[q] = quadrant_eng.get(q, 0) + p["eng_capacity_pct"]

    # Portfolio health score
    health = _portfolio_health(products, total_revenue, total_eng)

    # Portfolio-level findings
    portfolio_findings = _portfolio_findings(products, total_revenue, quadrant_revenue, quadrant_eng)

    return {
        "company": data.get("company", "Unknown"),
        "total_engineering_headcount": data.get("total_engineering_headcount"),
        "products": products,
        "total_revenue_quarterly": total_revenue,
        "quadrant_summary": {
            q: {
                "count": sum(1 for p in products if p["quadrant"] == q),
                "revenue": quadrant_revenue.get(q, 0),
                "revenue_pct": quadrant_revenue.get(q, 0) / total_revenue if total_revenue else 0,
                "eng_pct": quadrant_eng.get(q, 0),
            }
            for q in ["Star", "Cash Cow", "Question Mark", "Dog"]
        },
        "portfolio_health_score": health,
        "portfolio_findings": portfolio_findings,
    }


def _portfolio_health(products: list, total_revenue: float, total_eng: float) -> float:
    """
    Portfolio health 0-1. Penalizes:
    - No Stars (no growth engine)
    - Dogs consuming > 20% of eng
    - Poor alignment scores
    - Revenue concentrated in Dogs/Question Marks
    """
    score = 1.0

    quadrants = [p["quadrant"] for p in products]
    has_star = "Star" in quadrants
    has_cash_cow = "Cash Cow" in quadrants

    if not has_star:
        score -= 0.25  # No growth engine is a serious problem
    if not has_cash_cow:
        score -= 0.10  # No cash generator means funding stars from burn

    # Dog eng allocation penalty
    dog_eng = sum(p["eng_capacity_pct"] for p in products if p["quadrant"] == "Dog")
    if dog_eng > 20:
        score -= 0.20
    elif dog_eng > 10:
        score -= 0.10

    # Revenue in dogs penalty
    if total_revenue > 0:
        dog_rev_pct = sum(p["revenue_quarterly"] for p in products if p["quadrant"] == "Dog") / total_revenue
        if dog_rev_pct > 0.30:
            score -= 0.15

    # Average alignment score
    avg_alignment = sum(p["alignment_score"] for p in products) / len(products) if products else 0
    score -= (1 - avg_alignment) * 0.20

    return max(0.0, min(1.0, score))


def _portfolio_findings(
    products: list, total_revenue: float,
    quadrant_revenue: dict, quadrant_eng: dict
) -> list:
    findings = []

    stars = [p for p in products if p["quadrant"] == "Star"]
    cows = [p for p in products if p["quadrant"] == "Cash Cow"]
    questions = [p for p in products if p["quadrant"] == "Question Mark"]
    dogs = [p for p in products if p["quadrant"] == "Dog"]

    if not stars:
        findings.append("✗ CRITICAL: No Star products. You have no growth engine. Identify a Question Mark to invest in or revisit your market positioning.")
    elif len(stars) == 1:
        findings.append(f"◑ Single Star ({stars[0]['name']}). Portfolio is fragile — one product drives all growth. Diversify.")
    else:
        findings.append(f"✓ {len(stars)} Star products — healthy growth engine.")

    if not cows:
        findings.append("⚠ No Cash Cow products. Stars are consuming capital without a self-funding mechanism. Watch burn rate.")
    else:
        cow_rev = quadrant_revenue.get("Cash Cow", 0)
        cow_pct = cow_rev / total_revenue if total_revenue else 0
        findings.append(f"✓ Cash Cow revenue: {cow_pct:.0%} of total — funds Star investment.")

    if questions:
        findings.append(f"⚠ {len(questions)} Question Mark(s): {', '.join(p['name'] for p in questions)}.")
        findings.append("  Each needs a binary decision: invest to win share, or exit. Set a 2-quarter deadline.")

    if dogs:
        dog_eng_total = sum(p["eng_capacity_pct"] for p in dogs)
        findings.append(f"✗ {len(dogs)} Dog product(s): {', '.join(p['name'] for p in dogs)} consuming {dog_eng_total}% of eng capacity.")
        findings.append(f"  That's {dog_eng_total}% of your engineers on declining products. Set sunset dates.")

    # Alignment check
    misaligned = [p for p in products if p["alignment_score"] < 0.50]
    if misaligned:
        findings.append(f"⚠ Engineering allocation misaligned on: {', '.join(p['name'] for p in misaligned)}.")
        findings.append("  Rebalance: move capacity from Dogs/Cows to Stars.")

    return findings


# ---------------------------------------------------------------------------
# Report rendering
# ---------------------------------------------------------------------------

def fmt_currency(n: float) -> str:
    if n >= 1_000_000:
        return f"${n/1_000_000:.1f}M"
    elif n >= 1_000:
        return f"${n/1_000:.0f}K"
    return f"${n:.0f}"


def render_report(result: dict) -> str:
    lines = []
    lines.append("=" * 65)
    lines.append(f"  PORTFOLIO ANALYZER — {result['company']}")
    lines.append(f"  Total Quarterly Revenue: {fmt_currency(result['total_revenue_quarterly'])}")
    if result.get("total_engineering_headcount"):
        lines.append(f"  Engineering Headcount: {result['total_engineering_headcount']}")
    lines.append("=" * 65)
    lines.append("")

    # Portfolio health
    health = result["portfolio_health_score"]
    bar_len = 40
    filled = round(health * bar_len)
    bar = "█" * filled + "░" * (bar_len - filled)
    lines.append(f"  Portfolio Health: {health:.0%}")
    lines.append(f"  [{bar}]")
    lines.append("")

    # Quadrant summary
    lines.append("  QUADRANT SUMMARY")
    lines.append("  " + "-" * 55)
    header = f"  {'Quadrant':<15} {'Count':>5} {'Revenue':>10} {'Rev%':>6} {'Eng%':>6}"
    lines.append(header)
    lines.append("  " + "-" * 55)
    total_rev = result["total_revenue_quarterly"]
    for q in ["Star", "Cash Cow", "Question Mark", "Dog"]:
        qs = result["quadrant_summary"][q]
        emoji = quadrant_emoji(q)
        label = f"{emoji} {q}"
        rev_pct = f"{qs['revenue_pct']:.0%}" if qs["count"] else "-"
        eng = f"{qs['eng_pct']}%" if qs["count"] else "-"
        rev = fmt_currency(qs["revenue"]) if qs["count"] else "-"
        lines.append(f"  {label:<15} {qs['count']:>5} {rev:>10} {rev_pct:>6} {eng:>6}")
    lines.append("")

    # Per-product breakdown
    lines.append("  PRODUCT BREAKDOWN")
    lines.append("  " + "-" * 65)
    for p in result["products"]:
        emoji = quadrant_emoji(p["quadrant"])
        pc = posture_color(p["posture"])
        lines.append(
            f"  {emoji} {p['name']} — {p['quadrant']} → {pc} {p['posture']}"
        )
        lines.append(
            f"     Revenue: {fmt_currency(p['revenue_quarterly'])}/qtr  "
            f"QoQ: {p['qoq_growth']:+.0%}  "
            f"Mkt growth: {p['market_growth_pct']:+.0f}%"
        )
        lines.append(
            f"     Share ratio: {p['share_ratio']:.1f}x  "
            f"Eng: {p['eng_capacity_pct']}%  "
            f"Alignment: {p['alignment_score']:.0%}"
        )
        if p.get("d30_retention") is not None:
            lines.append(
                f"     D30 retention: {p['d30_retention']:.0%}  "
                f"NPS: {p['nps'] if p['nps'] is not None else 'N/A'}"
            )
        if p.get("notes"):
            lines.append(f"     Note: {p['notes']}")
        for f in p.get("findings", []):
            lines.append(f"     {f}")
        lines.append("")

    # Portfolio-level findings
    lines.append("  PORTFOLIO FINDINGS")
    lines.append("  " + "-" * 65)
    for f in result.get("portfolio_findings", []):
        lines.append(f"  {f}")
    lines.append("")
    lines.append("=" * 65)

    return "\n".join(lines)


# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------

def main():
    parser = argparse.ArgumentParser(
        description="Portfolio Analyzer — BCG matrix classification and investment recommendations",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog=__doc__,
    )
    parser.add_argument(
        "--input", "-i",
        metavar="FILE",
        help="JSON file with portfolio data (default: built-in sample data)",
    )
    parser.add_argument(
        "--json",
        action="store_true",
        help="Output raw JSON result",
    )
    args = parser.parse_args()

    if args.input:
        try:
            with open(args.input) as f:
                data = json.load(f)
        except FileNotFoundError:
            print(f"Error: file not found: {args.input}", file=sys.stderr)
            sys.exit(1)
        except json.JSONDecodeError as e:
            print(f"Error: invalid JSON: {e}", file=sys.stderr)
            sys.exit(1)
    else:
        print("No input file provided — running with sample data.\n")
        data = sample_data()

    result = analyze_portfolio(data)

    if args.json:
        # Make result JSON-serializable
        def clean(obj):
            if isinstance(obj, dict):
                return {k: clean(v) for k, v in obj.items()}
            elif isinstance(obj, list):
                return [clean(v) for v in obj]
            elif isinstance(obj, float):
                return round(obj, 4)
            return obj
        print(json.dumps(clean(result), indent=2))
    else:
        print(render_report(result))


if __name__ == "__main__":
    main()
