#!/usr/bin/env python3
"""
okr_tracker.py — OKR Cascade and Alignment Tracker

Tracks OKR progress from company → department → team level.
Calculates scores, flags at-risk key results, and generates alignment reports.

Scoring: Google's 0.0–1.0 scale (target: 0.6–0.7; hitting 1.0 means goal was too easy)

Usage:
    python okr_tracker.py                    # Runs with sample data
    python okr_tracker.py --input okrs.json  # Custom OKR data
    python okr_tracker.py --input okrs.json --output report.txt
    python okr_tracker.py --format json      # Machine-readable output
"""

import json
import sys
import argparse
from datetime import datetime, date
from typing import Any


# ---------------------------------------------------------------------------
# Scoring Engine
# ---------------------------------------------------------------------------

# OKR health thresholds (Google-style 0.0–1.0 scale)
SCORE_THRESHOLDS = {
    "on_track": 0.70,       # Above this: healthy
    "at_risk": 0.40,        # Between at_risk and on_track: needs attention
    # Below at_risk: off track
}

STATUS_LABELS = {
    "on_track": "🟢 On Track",
    "at_risk": "🟡 At Risk",
    "off_track": "🔴 Off Track",
    "complete": "✅ Complete",
    "not_started": "⬜ Not Started",
}

RISK_LABELS = {
    "critical": "🔴 Critical",
    "high": "🟠 High",
    "medium": "🟡 Medium",
    "low": "🟢 Low",
}


def calculate_kr_score(kr: dict) -> float:
    """
    Calculate a Key Result's progress score (0.0–1.0).
    
    Supports multiple KR types:
    - numeric: current_value / target_value
    - percentage: current_pct / target_pct
    - milestone: milestone_score (0.0–1.0 provided directly)
    - boolean: done (1.0) / not done (0.0)
    """
    kr_type = kr.get("type", "numeric")

    if kr_type == "boolean":
        return 1.0 if kr.get("done", False) else 0.0

    elif kr_type == "milestone":
        # Milestone KRs have explicit score (0.0–1.0) or count of milestones hit
        milestones_total = kr.get("milestones_total", 1)
        milestones_hit = kr.get("milestones_hit", 0)
        explicit_score = kr.get("score")
        if explicit_score is not None:
            return max(0.0, min(1.0, float(explicit_score)))
        return milestones_hit / milestones_total if milestones_total > 0 else 0.0

    elif kr_type == "percentage":
        target = kr.get("target_pct", 100)
        current = kr.get("current_pct", 0)
        baseline = kr.get("baseline_pct", 0)
        if target == baseline:
            return 0.0
        score = (current - baseline) / (target - baseline)
        return max(0.0, min(1.0, score))

    else:  # numeric (default)
        target = kr.get("target_value", 0)
        current = kr.get("current_value", 0)
        baseline = kr.get("baseline_value", 0)
        if target == baseline:
            return 0.0
        # Handle "lower is better" metrics (e.g., churn, response time)
        if kr.get("lower_is_better", False):
            if current <= target:
                return 1.0
            improvement = baseline - current
            needed = baseline - target
            score = improvement / needed if needed != 0 else 0.0
        else:
            score = (current - baseline) / (target - baseline)
        return max(0.0, min(1.0, score))


def get_kr_status(score: float, quarter_progress: float, kr: dict) -> str:
    """
    Determine KR status based on score, time elapsed in quarter, and trend.
    
    A KR is at-risk if its score is significantly behind the time elapsed.
    E.g., if we're 70% through the quarter but KR is at 30%, it's at risk.
    """
    if kr.get("done", False):
        return "complete"

    # Not started
    if score == 0.0 and quarter_progress < 0.1:
        return "not_started"

    # Check against absolute thresholds
    if score >= SCORE_THRESHOLDS["on_track"]:
        return "on_track"

    # Adjust for time: if we're early in quarter, lower scores are acceptable
    adjusted_threshold = SCORE_THRESHOLDS["at_risk"] * (quarter_progress or 0.5)

    if score >= max(adjusted_threshold, SCORE_THRESHOLDS["at_risk"]):
        return "at_risk"

    return "off_track"


def calculate_objective_score(objective: dict, quarter_progress: float) -> dict:
    """
    Score an objective based on its key results.
    Returns scored objective with KR scores and status.
    """
    key_results = objective.get("key_results", [])
    if not key_results:
        return {**objective, "score": 0.0, "status": "not_started", "key_results_scored": []}

    scored_krs = []
    for kr in key_results:
        score = calculate_kr_score(kr)
        status = get_kr_status(score, quarter_progress, kr)

        # Calculate time-adjusted gap
        expected_score = quarter_progress * 0.85  # Expect 85% of time-proportional progress
        gap = expected_score - score

        risk_level = _assess_kr_risk(score, status, gap, quarter_progress, kr)

        scored_krs.append({
            **kr,
            "score": round(score, 3),
            "score_pct": f"{score * 100:.0f}%",
            "status": status,
            "status_label": STATUS_LABELS.get(status, status),
            "expected_score": round(expected_score, 3),
            "gap_vs_expected": round(gap, 3),
            "risk_level": risk_level,
            "risk_label": RISK_LABELS.get(risk_level, risk_level),
        })

    # Objective score = weighted average of KR scores
    # Weight is explicit in KR data or defaults to equal weight
    total_weight = sum(kr.get("weight", 1.0) for kr in key_results)
    weighted_score = sum(
        kr_scored["score"] * kr.get("weight", 1.0)
        for kr_scored, kr in zip(scored_krs, key_results)
    )
    obj_score = weighted_score / total_weight if total_weight > 0 else 0.0

    # Objective status = worst KR status (a chain is only as strong as weakest link)
    status_priority = {"off_track": 0, "at_risk": 1, "not_started": 2, "on_track": 3, "complete": 4}
    obj_status = min(scored_krs, key=lambda x: status_priority.get(x["status"], 2))["status"]

    return {
        **objective,
        "score": round(obj_score, 3),
        "score_pct": f"{obj_score * 100:.0f}%",
        "status": obj_status,
        "status_label": STATUS_LABELS.get(obj_status, obj_status),
        "key_results_scored": scored_krs,
    }


def _assess_kr_risk(
    score: float,
    status: str,
    gap: float,
    quarter_progress: float,
    kr: dict,
) -> str:
    """Assess risk level for a key result."""
    if status == "complete" or status == "on_track":
        return "low"

    weeks_remaining = kr.get("weeks_remaining", max(1, int((1 - quarter_progress) * 13)))

    # Critical: off track with <4 weeks left
    if status == "off_track" and weeks_remaining <= 4:
        return "critical"

    # High: significantly behind with limited time
    if gap > 0.3 and weeks_remaining <= 6:
        return "high"

    # High: off track regardless of time
    if status == "off_track":
        return "high"

    # Medium: at risk
    if status == "at_risk":
        return "medium"

    return "low"


# ---------------------------------------------------------------------------
# OKR Cascade and Alignment Analysis
# ---------------------------------------------------------------------------

def build_okr_tree(data: dict, quarter_progress: float) -> dict:
    """
    Build scored OKR tree: company → departments → teams.
    Returns full hierarchy with scores at every level.
    """
    company = data.get("company_okrs", {})
    departments = data.get("department_okrs", [])
    teams = data.get("team_okrs", [])

    # Score company-level OKRs
    company_scored = {
        "name": company.get("name", "Company"),
        "quarter": company.get("quarter", ""),
        "objectives": [
            calculate_objective_score(obj, quarter_progress)
            for obj in company.get("objectives", [])
        ],
    }

    # Score department-level OKRs
    depts_scored = []
    for dept in departments:
        dept_objectives = [
            calculate_objective_score(obj, quarter_progress)
            for obj in dept.get("objectives", [])
        ]
        dept_score = (
            sum(o["score"] for o in dept_objectives) / len(dept_objectives)
            if dept_objectives else 0.0
        )
        depts_scored.append({
            **dept,
            "objectives": dept_objectives,
            "overall_score": round(dept_score, 3),
            "overall_score_pct": f"{dept_score * 100:.0f}%",
        })

    # Score team-level OKRs
    teams_scored = []
    for team in teams:
        team_objectives = [
            calculate_objective_score(obj, quarter_progress)
            for obj in team.get("objectives", [])
        ]
        team_score = (
            sum(o["score"] for o in team_objectives) / len(team_objectives)
            if team_objectives else 0.0
        )
        teams_scored.append({
            **team,
            "objectives": team_objectives,
            "overall_score": round(team_score, 3),
            "overall_score_pct": f"{team_score * 100:.0f}%",
        })

    return {
        "company": company_scored,
        "departments": depts_scored,
        "teams": teams_scored,
    }


def analyze_alignment(okr_tree: dict) -> dict:
    """
    Analyze how team and department OKRs align to company OKRs.
    Flags: orphaned OKRs (no company parent), missing coverage (company OKR with no team support).
    """
    company_objective_ids = {
        obj.get("id") for obj in okr_tree["company"].get("objectives", [])
        if obj.get("id")
    }

    # Collect all alignment references from dept and team OKRs
    alignment_map: dict[str, list[str]] = {oid: [] for oid in company_objective_ids}
    orphaned = []
    all_supporting = []

    def check_objectives(objectives: list, owner_name: str, level: str):
        for obj in objectives:
            supports = obj.get("supports_company_objective_ids", [])
            if not supports:
                # Check if it's supposed to support something
                if obj.get("supports_company_objective_id"):
                    supports = [obj["supports_company_objective_id"]]

            if not supports:
                orphaned.append({
                    "level": level,
                    "owner": owner_name,
                    "objective": obj.get("title", obj.get("name", "Unknown")),
                    "issue": "No link to company objective — may be misaligned or low priority",
                })
            else:
                for cid in supports:
                    if cid in alignment_map:
                        alignment_map[cid].append(f"{level}:{owner_name}")
                        all_supporting.append(cid)
                    else:
                        orphaned.append({
                            "level": level,
                            "owner": owner_name,
                            "objective": obj.get("title", obj.get("name", "Unknown")),
                            "issue": f"References company objective '{cid}' which doesn't exist",
                        })

    for dept in okr_tree["departments"]:
        check_objectives(dept["objectives"], dept.get("name", "Unknown Dept"), "Department")

    for team in okr_tree["teams"]:
        check_objectives(team["objectives"], team.get("name", "Unknown Team"), "Team")

    # Find company objectives with no support from below
    unsupported = []
    for obj in okr_tree["company"].get("objectives", []):
        obj_id = obj.get("id")
        if obj_id and obj_id not in all_supporting:
            unsupported.append({
                "objective_id": obj_id,
                "objective": obj.get("title", obj.get("name", "Unknown")),
                "issue": "No department or team OKR explicitly supports this company objective",
            })

    coverage_score = (
        len(set(all_supporting)) / len(company_objective_ids) * 100
        if company_objective_ids else 100
    )

    return {
        "alignment_map": alignment_map,
        "orphaned_okrs": orphaned,
        "unsupported_company_objectives": unsupported,
        "coverage_score_pct": round(coverage_score, 1),
    }


def collect_at_risk_krs(okr_tree: dict) -> list[dict]:
    """Collect all at-risk and off-track key results across the full OKR tree."""
    at_risk = []

    def scan_objectives(objectives: list, owner: str, level: str):
        for obj in objectives:
            for kr in obj.get("key_results_scored", []):
                if kr["status"] in ("at_risk", "off_track"):
                    at_risk.append({
                        "level": level,
                        "owner": owner,
                        "objective": obj.get("title", obj.get("name", "Unknown")),
                        "key_result": kr.get("title", kr.get("name", "Unknown")),
                        "score": kr["score"],
                        "score_pct": kr["score_pct"],
                        "status": kr["status"],
                        "status_label": kr["status_label"],
                        "risk_level": kr["risk_level"],
                        "risk_label": kr["risk_label"],
                        "gap_vs_expected": kr["gap_vs_expected"],
                        "notes": kr.get("notes", ""),
                    })

    scan_objectives(
        okr_tree["company"].get("objectives", []),
        okr_tree["company"].get("name", "Company"),
        "Company",
    )
    for dept in okr_tree["departments"]:
        scan_objectives(dept["objectives"], dept.get("name", ""), "Department")
    for team in okr_tree["teams"]:
        scan_objectives(team["objectives"], team.get("name", ""), "Team")

    # Sort: off_track before at_risk, then by gap
    status_order = {"off_track": 0, "at_risk": 1}
    at_risk.sort(key=lambda x: (status_order.get(x["status"], 2), -x.get("gap_vs_expected", 0)))

    return at_risk


# ---------------------------------------------------------------------------
# Report Formatter
# ---------------------------------------------------------------------------

def _score_bar(score: float, width: int = 20) -> str:
    """Render a text progress bar for a 0.0–1.0 score."""
    filled = round(score * width)
    bar = "█" * filled + "░" * (width - filled)
    return f"[{bar}] {score * 100:.0f}%"


def format_report(
    okr_tree: dict,
    alignment: dict,
    at_risk_krs: list[dict],
    quarter_progress: float,
    quarter_label: str,
) -> str:
    """Format full OKR tracking report as plain text."""
    lines = []
    now = datetime.now().strftime("%Y-%m-%d %H:%M")
    company_name = okr_tree["company"].get("name", "Company")

    lines.append("=" * 70)
    lines.append(f"OKR TRACKING REPORT — {company_name}")
    lines.append(f"Quarter: {quarter_label}   |   Quarter progress: {quarter_progress * 100:.0f}%")
    lines.append(f"Generated: {now}")
    lines.append("=" * 70)

    # --- Executive Summary ---
    lines.append("\n📊 EXECUTIVE SUMMARY")
    lines.append("-" * 40)

    company_objectives = okr_tree["company"].get("objectives", [])
    if company_objectives:
        company_avg = sum(o["score"] for o in company_objectives) / len(company_objectives)
        on_track = sum(1 for o in company_objectives if o["status"] == "on_track")
        at_risk = sum(1 for o in company_objectives if o["status"] == "at_risk")
        off_track = sum(1 for o in company_objectives if o["status"] == "off_track")

        lines.append(f"Company OKR Score:    {_score_bar(company_avg)}")
        lines.append(f"Objectives:           {len(company_objectives)} total — "
                     f"🟢 {on_track} on track, 🟡 {at_risk} at risk, 🔴 {off_track} off track")
        lines.append(f"At-risk KRs (all):    {len(at_risk_krs)}")
        lines.append(f"Alignment coverage:   {alignment['coverage_score_pct']}% of company objectives have team support")

        # Overall health assessment
        if company_avg >= 0.7:
            health = "🟢 HEALTHY — On track for a strong quarter"
        elif company_avg >= 0.5:
            health = "🟡 CAUTION — Some objectives need attention"
        elif company_avg >= 0.3:
            health = "🔴 AT RISK — Multiple objectives behind; intervention needed"
        else:
            health = "🚨 CRITICAL — Quarter in serious jeopardy; executive review required"
        lines.append(f"\nOverall Health: {health}")

    # --- Company OKRs ---
    lines.append("\n\n🏢 COMPANY OKRs")
    lines.append("-" * 40)

    for obj in company_objectives:
        lines.append(f"\n  Objective: {obj.get('title', obj.get('name', 'Unknown'))}")
        lines.append(f"  Owner: {obj.get('owner', 'Unassigned')}  |  Score: {_score_bar(obj['score'], 15)}  {obj['status_label']}")

        for kr in obj.get("key_results_scored", []):
            risk_marker = f"  {kr['risk_label']}" if kr["risk_level"] in ("critical", "high") else ""
            lines.append(f"\n    KR: {kr.get('title', kr.get('name', 'Unknown'))}")
            lines.append(f"        Score: {_score_bar(kr['score'], 12)}  {kr['status_label']}{risk_marker}")

            # Show actual progress
            if kr.get("type") == "numeric":
                current = kr.get("current_value", "?")
                target = kr.get("target_value", "?")
                baseline = kr.get("baseline_value", 0)
                unit = kr.get("unit", "")
                lines.append(f"        Progress: {current}{unit} / {target}{unit}  (baseline: {baseline}{unit})")
            elif kr.get("type") == "percentage":
                lines.append(f"        Progress: {kr.get('current_pct', '?')}% / {kr.get('target_pct', '?')}%")
            elif kr.get("type") == "milestone":
                hit = kr.get("milestones_hit", "?")
                total = kr.get("milestones_total", "?")
                lines.append(f"        Milestones: {hit} / {total}")

            if kr.get("notes"):
                lines.append(f"        Note: {kr['notes']}")

    # --- Department OKRs ---
    lines.append("\n\n🏬 DEPARTMENT OKRs")
    lines.append("-" * 40)

    for dept in okr_tree["departments"]:
        lines.append(f"\n  📁 {dept.get('name', 'Unknown')}  |  Score: {_score_bar(dept['overall_score'], 15)}")

        for obj in dept.get("objectives", []):
            lines.append(f"\n     Objective: {obj.get('title', obj.get('name', 'Unknown'))}")
            lines.append(f"     Owner: {obj.get('owner', 'Unassigned')}  |  {obj['status_label']}")
            supports = obj.get("supports_company_objective_ids", [])
            if supports:
                lines.append(f"     Supports: Company Objective(s) {', '.join(supports)}")

            for kr in obj.get("key_results_scored", []):
                risk_marker = f"  {kr['risk_label']}" if kr["risk_level"] in ("critical", "high") else ""
                lines.append(f"\n       KR: {kr.get('title', kr.get('name', 'Unknown'))}")
                lines.append(f"           {_score_bar(kr['score'], 10)}  {kr['status_label']}{risk_marker}")

    # --- Team OKRs ---
    if okr_tree["teams"]:
        lines.append("\n\n👥 TEAM OKRs")
        lines.append("-" * 40)

        for team in okr_tree["teams"]:
            lines.append(f"\n  📋 {team.get('name', 'Unknown')}  |  Score: {_score_bar(team['overall_score'], 15)}")

            for obj in team.get("objectives", []):
                lines.append(f"\n     Objective: {obj.get('title', obj.get('name', 'Unknown'))}")
                supports = obj.get("supports_company_objective_ids", [])
                if supports:
                    lines.append(f"     Supports: {', '.join(supports)}")

                for kr in obj.get("key_results_scored", []):
                    risk_marker = f"  {kr['risk_label']}" if kr["risk_level"] in ("critical", "high") else ""
                    lines.append(
                        f"       • {kr.get('title', kr.get('name', 'Unknown'))}: "
                        f"{kr['score_pct']} {kr['status_label']}{risk_marker}"
                    )

    # --- At-Risk KRs ---
    lines.append("\n\n⚠️  AT-RISK KEY RESULTS (Action Required)")
    lines.append("-" * 40)

    if not at_risk_krs:
        lines.append("✅ No key results currently at risk or off track.")
    else:
        critical = [kr for kr in at_risk_krs if kr["risk_level"] == "critical"]
        high = [kr for kr in at_risk_krs if kr["risk_level"] == "high"]
        medium = [kr for kr in at_risk_krs if kr["risk_level"] == "medium"]

        for group_label, group in [("🔴 CRITICAL", critical), ("🟠 HIGH", high), ("🟡 MEDIUM", medium)]:
            if not group:
                continue
            lines.append(f"\n{group_label} ({len(group)} items):")
            for kr in group:
                lines.append(f"\n  [{kr['level']}] {kr['owner']}")
                lines.append(f"  Obj: {kr['objective']}")
                lines.append(f"  KR:  {kr['key_result']}")
                lines.append(f"  Score: {kr['score_pct']}  {kr['status_label']}  (gap vs expected: {kr['gap_vs_expected'] * 100:.0f}pp)")
                if kr["notes"]:
                    lines.append(f"  Note: {kr['notes']}")

    # --- Alignment Report ---
    lines.append("\n\n🔗 ALIGNMENT REPORT")
    lines.append("-" * 40)
    lines.append(f"Alignment coverage: {alignment['coverage_score_pct']}% of company objectives have explicit support\n")

    # Show alignment map
    lines.append("Company Objective Coverage:")
    for obj in company_objectives:
        obj_id = obj.get("id", "")
        supporters = alignment["alignment_map"].get(obj_id, [])
        obj_name = obj.get("title", obj.get("name", obj_id))
        count = len(supporters)
        marker = "✅" if count > 0 else "⚠️ "
        lines.append(f"  {marker} [{obj_id}] {obj_name}")
        if supporters:
            for s in supporters:
                lines.append(f"       ↑ {s}")
        else:
            lines.append(f"       ↑ (no department or team OKR supports this)")

    if alignment["unsupported_company_objectives"]:
        lines.append(f"\n⚠️  Unsupported Company Objectives ({len(alignment['unsupported_company_objectives'])}):")
        for u in alignment["unsupported_company_objectives"]:
            lines.append(f"  • [{u['objective_id']}] {u['objective']}")
            lines.append(f"    → {u['issue']}")

    if alignment["orphaned_okrs"]:
        lines.append(f"\n⚠️  Orphaned OKRs (not linked to company objectives):")
        for o in alignment["orphaned_okrs"]:
            lines.append(f"  • [{o['level']}] {o['owner']}: {o['objective']}")
            lines.append(f"    → {o['issue']}")

    # --- Recommendations ---
    lines.append("\n\n📋 RECOMMENDED ACTIONS")
    lines.append("-" * 40)

    recs = _generate_recommendations(okr_tree, at_risk_krs, alignment, quarter_progress)
    for i, rec in enumerate(recs, 1):
        lines.append(f"\n{i}. {rec['title']}")
        lines.append(f"   {rec['detail']}")
        lines.append(f"   Owner: {rec['owner']}  |  When: {rec['when']}")

    lines.append("\n" + "=" * 70)
    lines.append("END OF REPORT")
    lines.append("=" * 70)

    return "\n".join(lines)


def _generate_recommendations(
    okr_tree: dict,
    at_risk_krs: list[dict],
    alignment: dict,
    quarter_progress: float,
) -> list[dict]:
    """Generate actionable recommendations based on OKR analysis."""
    recs = []

    # Critical KRs
    critical = [kr for kr in at_risk_krs if kr["risk_level"] == "critical"]
    if critical:
        recs.append({
            "title": f"Emergency review: {len(critical)} critical key result(s) need immediate intervention",
            "detail": f"Critical KRs: {', '.join(kr['key_result'] for kr in critical[:3])}. "
                      f"With limited time remaining, these need escalation today.",
            "owner": "COO + KR owners",
            "when": "This week",
        })

    # Off-track objectives
    off_track_objs = [
        o for o in okr_tree["company"].get("objectives", [])
        if o["status"] == "off_track"
    ]
    if off_track_objs:
        recs.append({
            "title": f"Scope reset for {len(off_track_objs)} off-track company objective(s)",
            "detail": "When a company objective is off track by mid-quarter, "
                      "the options are: (1) resource surge, (2) scope reduction, or (3) accept the miss. "
                      "Choose explicitly — don't let it drift.",
            "owner": "CEO + COO",
            "when": "Within 1 week",
        })

    # Alignment gaps
    if alignment["coverage_score_pct"] < 80:
        recs.append({
            "title": "OKR alignment gap — not all company objectives have team support",
            "detail": f"Only {alignment['coverage_score_pct']}% of company objectives have explicit team/dept OKRs supporting them. "
                      "Either add supporting OKRs or acknowledge these objectives are founder-owned.",
            "owner": "COO + VPs",
            "when": "Next OKR planning cycle",
        })

    if alignment["orphaned_okrs"]:
        recs.append({
            "title": f"{len(alignment['orphaned_okrs'])} orphaned OKR(s) with no company objective linkage",
            "detail": "Team OKRs that don't connect to company objectives waste capacity. "
                      "Either link them explicitly or discontinue them.",
            "owner": "Team leads + COO",
            "when": "OKR review session",
        })

    # Late quarter: force ranking
    if quarter_progress >= 0.67:
        at_risk_count = sum(
            1 for o in okr_tree["company"].get("objectives", [])
            if o["status"] in ("at_risk", "off_track")
        )
        if at_risk_count > 0:
            recs.append({
                "title": f"Late quarter: force-rank which at-risk OKRs to save vs. accept as miss",
                "detail": f"{at_risk_count} objectives at risk with <{int((1 - quarter_progress) * 13)} weeks left. "
                           "You cannot save everything. Pick the 1–2 most important and resource them fully. "
                           "Explicitly accept the others as misses and learn from them.",
                "owner": "CEO + COO",
                "when": "Immediately",
            })

    # Measurement gaps
    unscored_krs = []
    for obj in okr_tree["company"].get("objectives", []):
        for kr in obj.get("key_results_scored", []):
            if kr["score"] == 0.0 and kr["status"] == "not_started" and quarter_progress > 0.25:
                unscored_krs.append(kr.get("title", kr.get("name", "Unknown")))

    if unscored_krs:
        recs.append({
            "title": f"{len(unscored_krs)} key result(s) show no progress past Q1",
            "detail": "KRs with zero progress after 25% of quarter has elapsed are either not started, "
                      "unmeasured, or forgotten. Require owners to update scores this week.",
            "owner": "KR owners",
            "when": "This week — before next leadership sync",
        })

    return recs


def format_json_output(okr_tree: dict, alignment: dict, at_risk_krs: list[dict]) -> str:
    """Format analysis as machine-readable JSON."""
    return json.dumps(
        {
            "generated_at": datetime.now().isoformat(),
            "company_score": (
                sum(o["score"] for o in okr_tree["company"].get("objectives", []))
                / max(1, len(okr_tree["company"].get("objectives", [])))
            ),
            "at_risk_count": len(at_risk_krs),
            "alignment_coverage_pct": alignment["coverage_score_pct"],
            "objectives": okr_tree["company"].get("objectives", []),
            "departments": okr_tree["departments"],
            "teams": okr_tree["teams"],
            "at_risk_key_results": at_risk_krs,
            "alignment": alignment,
        },
        indent=2,
    )


# ---------------------------------------------------------------------------
# Main Entrypoint
# ---------------------------------------------------------------------------

def main():
    parser = argparse.ArgumentParser(
        description="OKR Cascade and Alignment Tracker — COO Advisor Tool",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog=__doc__,
    )
    parser.add_argument("--input", "-i", help="Path to JSON OKR data file", default=None)
    parser.add_argument("--output", "-o", help="Path to write report (default: stdout)", default=None)
    parser.add_argument(
        "--format", "-f",
        choices=["text", "json"],
        default="text",
        help="Output format: text (default) or json",
    )
    parser.add_argument(
        "--quarter-progress",
        type=float,
        default=None,
        help="Override quarter progress (0.0–1.0). Default: auto-calculated from quarter dates.",
    )
    args = parser.parse_args()

    if args.input:
        try:
            with open(args.input, "r") as f:
                data = json.load(f)
        except FileNotFoundError:
            print(f"Error: Input 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 specified — running with sample data.\n")
        data = SAMPLE_DATA

    # Determine quarter progress
    if args.quarter_progress is not None:
        quarter_progress = args.quarter_progress
    else:
        quarter_progress = _calculate_quarter_progress(data)

    quarter_label = data.get("company_okrs", {}).get("quarter", "Unknown Quarter")

    # Run analysis
    okr_tree = build_okr_tree(data, quarter_progress)
    alignment = analyze_alignment(okr_tree)
    at_risk_krs = collect_at_risk_krs(okr_tree)

    # Format output
    if args.format == "json":
        output = format_json_output(okr_tree, alignment, at_risk_krs)
    else:
        output = format_report(okr_tree, alignment, at_risk_krs, quarter_progress, quarter_label)

    if args.output:
        with open(args.output, "w") as f:
            f.write(output)
        print(f"Report written to: {args.output}")
    else:
        print(output)


def _calculate_quarter_progress(data: dict) -> float:
    """Auto-calculate quarter progress from start/end dates in data, or default to 0.5."""
    q = data.get("company_okrs", {})
    start_str = q.get("quarter_start")
    end_str = q.get("quarter_end")

    if not start_str or not end_str:
        return 0.5  # Default to mid-quarter if not specified

    try:
        start = date.fromisoformat(start_str)
        end = date.fromisoformat(end_str)
        today = date.today()
        total_days = (end - start).days
        elapsed_days = (today - start).days
        progress = elapsed_days / total_days if total_days > 0 else 0.5
        return max(0.0, min(1.0, progress))
    except (ValueError, TypeError):
        return 0.5


# ---------------------------------------------------------------------------
# Sample Data
# ---------------------------------------------------------------------------

SAMPLE_DATA = {
    "company_okrs": {
        "name": "AcmeSaaS",
        "quarter": "Q1 2025",
        "quarter_start": "2025-01-01",
        "quarter_end": "2025-03-31",
        "objectives": [
            {
                "id": "CO1",
                "title": "Achieve breakout revenue growth",
                "owner": "CEO",
                "key_results": [
                    {
                        "id": "CO1-KR1",
                        "title": "Reach $5M net new ARR",
                        "type": "numeric",
                        "baseline_value": 0,
                        "current_value": 2800000,
                        "target_value": 5000000,
                        "unit": "",
                        "notes": "Strong January, February softer; pipeline looks better for March",
                    },
                    {
                        "id": "CO1-KR2",
                        "title": "Achieve 115% NRR",
                        "type": "percentage",
                        "baseline_pct": 108,
                        "current_pct": 110,
                        "target_pct": 115,
                        "notes": "Expansion motion improved; churn still elevated in SMB segment",
                    },
                    {
                        "id": "CO1-KR3",
                        "title": "Close 3 enterprise deals (>$150K ACV)",
                        "type": "numeric",
                        "baseline_value": 0,
                        "current_value": 1,
                        "target_value": 3,
                        "unit": " deals",
                        "notes": "1 closed, 2 in late-stage negotiation",
                    },
                ],
            },
            {
                "id": "CO2",
                "title": "Build a world-class product that customers love",
                "owner": "CPO",
                "key_results": [
                    {
                        "id": "CO2-KR1",
                        "title": "Increase feature adoption rate to 65% (% of customers using 3+ core features)",
                        "type": "percentage",
                        "baseline_pct": 48,
                        "current_pct": 52,
                        "target_pct": 65,
                        "notes": "Onboarding improvements shipped; adoption curve is moving",
                    },
                    {
                        "id": "CO2-KR2",
                        "title": "Ship the integration platform (milestone)",
                        "type": "milestone",
                        "milestones_total": 4,
                        "milestones_hit": 1,
                        "milestones": [
                            "API design complete",
                            "Internal alpha",
                            "Beta with 5 customers",
                            "GA launch",
                        ],
                        "notes": "API design shipped. Internal alpha delayed 2 weeks.",
                    },
                    {
                        "id": "CO2-KR3",
                        "title": "NPS score reaches 45",
                        "type": "numeric",
                        "baseline_value": 32,
                        "current_value": 38,
                        "target_value": 45,
                        "unit": "",
                    },
                ],
            },
            {
                "id": "CO3",
                "title": "Build an operationally excellent company",
                "owner": "COO",
                "key_results": [
                    {
                        "id": "CO3-KR1",
                        "title": "Reduce burn multiple from 1.8x to 1.3x",
                        "type": "numeric",
                        "baseline_value": 1.8,
                        "current_value": 1.65,
                        "target_value": 1.3,
                        "lower_is_better": True,
                        "unit": "x",
                    },
                    {
                        "id": "CO3-KR2",
                        "title": "Achieve <30-day customer onboarding (avg)",
                        "type": "numeric",
                        "baseline_value": 47,
                        "current_value": 38,
                        "target_value": 30,
                        "lower_is_better": True,
                        "unit": " days",
                        "notes": "Good progress; blocked by technical setup step (avg 12 days)",
                    },
                    {
                        "id": "CO3-KR3",
                        "title": "Voluntary attrition <10%",
                        "type": "numeric",
                        "baseline_value": 15,
                        "current_value": 12,
                        "target_value": 10,
                        "lower_is_better": True,
                        "unit": "%",
                        "notes": "2 unexpected departures in January; retention initiatives launched",
                    },
                ],
            },
        ],
    },
    "department_okrs": [
        {
            "name": "Sales",
            "owner": "VP Sales",
            "objectives": [
                {
                    "title": "Drive net new ARR to hit company growth target",
                    "owner": "VP Sales",
                    "supports_company_objective_ids": ["CO1"],
                    "key_results": [
                        {
                            "title": "Close $4M in new business ARR",
                            "type": "numeric",
                            "baseline_value": 0,
                            "current_value": 2200000,
                            "target_value": 4000000,
                            "unit": "",
                        },
                        {
                            "title": "Maintain pipeline coverage ratio ≥3x",
                            "type": "numeric",
                            "baseline_value": 2.5,
                            "current_value": 3.1,
                            "target_value": 3.0,
                            "unit": "x",
                        },
                        {
                            "title": "Reduce average sales cycle to 42 days",
                            "type": "numeric",
                            "baseline_value": 58,
                            "current_value": 50,
                            "target_value": 42,
                            "lower_is_better": True,
                            "unit": " days",
                        },
                    ],
                }
            ],
        },
        {
            "name": "Engineering",
            "owner": "VP Engineering",
            "objectives": [
                {
                    "title": "Deliver the integration platform on schedule",
                    "owner": "VP Engineering",
                    "supports_company_objective_ids": ["CO2"],
                    "key_results": [
                        {
                            "title": "Integration platform beta live with 5 customers",
                            "type": "milestone",
                            "milestones_total": 3,
                            "milestones_hit": 1,
                            "notes": "Alpha delayed — dependency on API gateway refactor",
                        },
                        {
                            "title": "Deploy frequency ≥10/week",
                            "type": "numeric",
                            "baseline_value": 6,
                            "current_value": 9,
                            "target_value": 10,
                            "unit": "/week",
                        },
                        {
                            "title": "P0/P1 incidents <2 per month",
                            "type": "numeric",
                            "baseline_value": 5,
                            "current_value": 2.5,
                            "target_value": 2,
                            "lower_is_better": True,
                            "unit": "/month",
                        },
                    ],
                }
            ],
        },
        {
            "name": "Customer Success",
            "owner": "VP CS",
            "objectives": [
                {
                    "title": "Drive retention and expansion to fuel NRR growth",
                    "owner": "VP CS",
                    "supports_company_objective_ids": ["CO1", "CO2"],
                    "key_results": [
                        {
                            "title": "Gross retention ≥92%",
                            "type": "percentage",
                            "baseline_pct": 88,
                            "current_pct": 89,
                            "target_pct": 92,
                            "notes": "3 at-risk accounts in red status",
                        },
                        {
                            "title": "Average onboarding time ≤30 days",
                            "type": "numeric",
                            "baseline_value": 47,
                            "current_value": 38,
                            "target_value": 30,
                            "lower_is_better": True,
                            "unit": " days",
                        },
                        {
                            "title": "Expansion ARR from existing customers: $800K",
                            "type": "numeric",
                            "baseline_value": 0,
                            "current_value": 580000,
                            "target_value": 800000,
                            "unit": "",
                        },
                    ],
                }
            ],
        },
    ],
    "team_okrs": [
        {
            "name": "Platform Engineering",
            "department": "Engineering",
            "objectives": [
                {
                    "title": "Build the integration API infrastructure",
                    "supports_company_objective_ids": ["CO2"],
                    "key_results": [
                        {
                            "title": "API gateway v2 deployed to production",
                            "type": "boolean",
                            "done": False,
                            "notes": "Targeting end of week 8",
                        },
                        {
                            "title": "Webhook system handles 10K events/sec",
                            "type": "boolean",
                            "done": False,
                        },
                        {
                            "title": "P99 API latency <200ms",
                            "type": "numeric",
                            "baseline_value": 380,
                            "current_value": 290,
                            "target_value": 200,
                            "lower_is_better": True,
                            "unit": "ms",
                        },
                    ],
                }
            ],
        },
        {
            "name": "Enterprise Sales Team",
            "department": "Sales",
            "objectives": [
                {
                    "title": "Land 3 enterprise accounts",
                    "supports_company_objective_ids": ["CO1"],
                    "key_results": [
                        {
                            "title": "3 enterprise deals closed",
                            "type": "numeric",
                            "baseline_value": 0,
                            "current_value": 1,
                            "target_value": 3,
                            "unit": " deals",
                        },
                        {
                            "title": "5 enterprise POCs initiated",
                            "type": "numeric",
                            "baseline_value": 0,
                            "current_value": 4,
                            "target_value": 5,
                            "unit": " POCs",
                        },
                    ],
                }
            ],
        },
    ],
}


if __name__ == "__main__":
    main()
