"""Command-line interface for Janus Labs."""

import argparse
import hashlib
import json
import os
import tempfile
from pathlib import Path
import sys

from config.detection import detect_config, detect_execution_context
from suite.comparison import (
    compare_results,
    comparison_to_dict,
    export_comparison_json,
    print_comparison_text,
)
from suite.export.github import generate_github_summary, print_github_annotations
from suite.export.html import export_html
from suite.export.json_export import export_json, load_json
from suite.registry import get_suite
from suite.runner import SuiteRunConfig, run_suite
from suite.thresholds import default_thresholds, load_thresholds

from cli.submit import cmd_submit, submit_result
from cli.output import print_benchmark_result, print_step, print_error, print_warning
from cli.clipboard import copy_to_clipboard, is_clipboard_available


__version__ = "1.1.5"

# Quickstart guide shown in --help and post-install
QUICKSTART_TEXT = """
================================================================================
  QUICKSTART -- Profile Your AI Coding Agent
================================================================================

  # Run the full suite (no API key needed)
  janus-labs run --suite refactor-storm -o result.json

  # Submit to the public leaderboard
  janus-labs submit result.json --github <handle>

  # Offline dry-run (no network)
  janus-labs run --suite refactor-storm --mock -o result.json

  # Suite shortcut (same as run --suite)
  janus-labs refactor-storm -o result.json

================================================================================
  FULL PIPELINE -- Test a specific agent end-to-end
================================================================================

  janus-labs run --suite refactor-storm --full --agent codex -o result.json
  janus-labs run --suite refactor-storm --full --agent copilot -o result.json
  janus-labs run --suite refactor-storm --full --agent claude -o result.json
  janus-labs run --suite refactor-storm --full --agent-cmd "my-agent {prompt_file}"

================================================================================
  ADVANCED -- Manual per-behavior workflow
================================================================================

  janus-labs init --suite refactor-storm --output ./janus-task
  cd janus-task/BHV-001-test-cheating
  <run your AI agent on the task>
  janus-labs score --workspace . -o result.json
  janus-labs submit result.json --github <handle>

================================================================================
  MORE COMMANDS
================================================================================

  janus-labs                     Interactive menu (guided)
  janus-labs smoke-test          Quick validation (mock data)
  janus-labs compare             Compare two results
  janus-labs diagnose            Instruction resilience check
  janus-labs profile             Generate capability profiles

  Tips:
    --interactive              Enable progress board and submit prompt
    Command not found? Use: python -m janus_labs
    Aliases: janus-labs, janus (both work)
================================================================================
"""


def build_parser() -> argparse.ArgumentParser:
    """Build the top-level CLI parser."""
    parser = argparse.ArgumentParser(
        prog="janus-labs",
        description="Janus Labs - 3DMark for AI Agents",
        epilog=QUICKSTART_TEXT,
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )
    parser.add_argument(
        "--version", "-V",
        action="version",
        version=f"%(prog)s {__version__}",
    )
    parser.add_argument(
        "--no-update-check",
        action="store_true",
        help="Skip version update check (useful for CI)",
    )
    # Make subparsers optional - launch interactive menu if no command given
    subparsers = parser.add_subparsers(dest="command", required=False)

    run_parser = subparsers.add_parser("run", help="Run a benchmark suite")
    run_parser.add_argument("--suite", required=True, help="Suite ID to run")
    run_parser.add_argument("--output", "-o", default="result.json", help="Output file")
    run_parser.add_argument(
        "--format",
        choices=["json", "html", "both"],
        default="json",
    )
    run_parser.add_argument(
        "--judge",
        action="store_true",
        help="Use local LLM-as-judge scoring (requires API key)",
    )
    run_parser.add_argument(
        "--mock",
        action="store_true",
        help="Use mock scoring (offline, deterministic fake scores)",
    )
    run_parser.add_argument(
        "--model",
        default="gpt-4o",
        help="LLM model for judge scoring (default: gpt-4o)",
    )
    run_parser.add_argument(
        "--interactive",
        action="store_true",
        help="Enable interactive mode (progress board, submit prompt)",
    )
    run_parser.add_argument(
        "--no-interactive",
        action="store_true",
        help=argparse.SUPPRESS,
    )
    run_parser.add_argument(
        "--request-delay",
        type=float,
        default=1.2,
        help="Delay between judge API calls in seconds (default: 1.2s, ~50 RPM)",
    )
    # JL-165: Full pipeline flags
    run_parser.add_argument(
        "--full",
        action="store_true",
        help="Full pipeline: init -> agent -> score -> output (JL-165)",
    )
    run_parser.add_argument(
        "--agent",
        choices=["codex", "gemini", "copilot", "claude"],
        help="Agent preset for --full mode",
    )
    run_parser.add_argument(
        "--agent-cmd",
        type=str,
        help="Custom agent command for --full mode (use {prompt_file} placeholder)",
    )
    run_parser.add_argument(
        "--timeout",
        type=int,
        default=300,
        help="Per-behavior timeout in seconds for --full mode (default: 300)",
    )
    run_parser.add_argument(
        "--workspace-dir",
        type=str,
        help="Reuse existing workspace directory (skip init) for --full mode",
    )

    compare_parser = subparsers.add_parser("compare", help="Compare two results")
    compare_parser.add_argument("baseline", nargs="?", default=None, help="Baseline result JSON")
    compare_parser.add_argument("current", nargs="?", default=None, help="Current result JSON")
    compare_parser.add_argument(
        "--auto-baseline",
        action="store_true",
        help="Use pre-computed vanilla baseline instead of file",
    )
    compare_parser.add_argument(
        "--threshold",
        type=float,
        default=5.0,
        help="Regression threshold (%%)",
    )
    compare_parser.add_argument(
        "--config",
        "-c",
        help="Threshold config YAML file (default: use suite defaults)",
    )
    compare_parser.add_argument(
        "--output",
        "-o",
        help="Output comparison result to JSON file",
    )
    compare_parser.add_argument(
        "--format",
        choices=["text", "json", "github"],
        default="text",
        help="Output format (github = GitHub Actions annotations)",
    )

    export_parser = subparsers.add_parser("export", help="Export result to format")
    export_parser.add_argument("input", help="Input JSON result")
    export_parser.add_argument("--format", choices=["html", "json"], required=True)
    export_parser.add_argument("--output", "-o", help="Output file")

    baseline_parser = subparsers.add_parser("baseline", help="Manage baselines")
    baseline_sub = baseline_parser.add_subparsers(dest="baseline_command", required=True)
    update_parser = baseline_sub.add_parser("update", help="Update baseline from current result")
    update_parser.add_argument("result", help="Current result JSON to promote to baseline")
    update_parser.add_argument("--output", "-o", default="baseline.json", help="Baseline output path")
    update_parser.add_argument("--force", "-f", action="store_true", help="Overwrite existing baseline")

    show_parser = baseline_sub.add_parser("show", help="Show baseline info")
    show_parser.add_argument("baseline", help="Baseline JSON file")
    baseline_sub.add_parser("list", help="List pre-computed vanilla baselines")

    submit_parser = subparsers.add_parser("submit", help="Submit results to leaderboard")
    submit_parser.add_argument("result_file", help="Path to result.json")
    submit_parser.add_argument(
        "--dry-run", action="store_true", help="Show payload without submitting"
    )
    submit_parser.add_argument(
        "--github", type=str, help="GitHub handle for attribution"
    )
    submit_parser.add_argument(
        "--no-judge",
        action="store_true",
        help="Skip backend LLM judging (submit score only)",
    )
    submit_parser.add_argument(
        "--no-open",
        action="store_true",
        help="Do not open browser after submit (useful for CI)",
    )

    init_parser = subparsers.add_parser("init", help="Initialize task workspace for all behaviors in suite")
    init_parser.add_argument("--suite", default="refactor-storm", help="Suite ID (default: refactor-storm)")
    init_parser.add_argument(
        "--output", "-o",
        default="./janus-task",
        help="Output directory for workspaces (behaviors in subdirs)",
    )

    score_parser = subparsers.add_parser("score", help="Score completed task")
    score_parser.add_argument(
        "--workspace", "-w",
        default=".",
        help="Path to task workspace (default: current directory)",
    )
    score_parser.add_argument(
        "--output", "-o",
        default="result.json",
        help="Output result to JSON file (default: result.json)",
    )
    score_parser.add_argument(
        "--judge",
        action="store_true",
        help="Enable LLM-as-judge scoring via DeepEval GEval (requires API key)",
    )
    score_parser.add_argument(
        "--model",
        default="gpt-4o",
        help="LLM model for judge scoring (default: gpt-4o)",
    )
    score_parser.add_argument(
        "--bundle",
        help="Path to bundle.json for judge scoring (optional, uses mock if not provided)",
    )
    score_parser.add_argument(
        "--agent",
        help="Agent identifier (auto-detected if not specified: claude-code, github-copilot, gemini-cli, codex, manual)",
    )
    score_parser.add_argument(
        "--agent-model",
        help="Model used by the agent (auto-detected if not specified: opus-4.5, gpt-5.2, gemini-3-pro)",
    )

    diagnose_parser = subparsers.add_parser(
        "diagnose",
        help="Measure instruction resilience -- compare configured vs vanilla scores",
    )
    diagnose_parser.add_argument(
        "result_file",
        nargs="?",
        default="result.json",
        help="Path to result JSON file (default: result.json)",
    )
    diagnose_parser.add_argument(
        "--workspace", "-w",
        default=".",
        help="Path to workspace with instruction files (default: current directory)",
    )
    diagnose_parser.add_argument(
        "--agent",
        help="Override auto-detected agent name for baseline matching",
    )
    diagnose_parser.add_argument(
        "--model",
        help="Override auto-detected model for baseline matching",
    )
    diagnose_parser.add_argument(
        "--json",
        action="store_true",
        dest="json_output",
        help="Output JSON instead of formatted table",
    )

    profile_parser = subparsers.add_parser("profile", help="Generate agent capability profile")
    profile_parser.add_argument(
        "--baseline", type=str,
        help="Path to baseline JSON file (generates single profile)",
    )
    profile_parser.add_argument(
        "--baselines-dir", type=str,
        help="Path to directory of baseline JSONs (generates all profiles)",
    )
    profile_parser.add_argument(
        "--output-dir", type=str, default="data/profiles",
        help="Output directory for profiles (default: data/profiles)",
    )
    profile_parser.add_argument(
        "--format",
        choices=["json", "text"],
        default="text",
        help="Output format (default: text)",
    )
    profile_parser.add_argument(
        "--json", action="store_true",
        help="Output as JSON (shorthand for --format json)",
    )
    profile_parser.add_argument(
        "--reliability", action="store_true",
        help="Enable K>=3 reliability mode: group baselines by agent/model and compute variance",
    )
    profile_parser.add_argument(
        "--leaderboard", action="store_true",
        help="Generate HTML leaderboard with radar charts (saves to output dir)",
    )

    status_parser = subparsers.add_parser("status", help="Show current workspace status")
    status_parser.add_argument(
        "--workspace", "-w",
        default=".",
        help="Path to workspace (default: current directory)",
    )

    def _add_smoke_test_args(p):
        p.add_argument("--suite", default="refactor-storm", help="Suite ID (default: refactor-storm)")
        p.add_argument("--behavior", default="BHV-001-test-cheating", help="Behavior ID (default: BHV-001-test-cheating)")
        p.add_argument("--submit", action="store_true", help="Submit results to public leaderboard")
        p.add_argument("--github", type=str, help="GitHub handle for attribution (requires --submit)")
        p.add_argument("--model", default="gpt-4o", help="LLM model for judge scoring (default: gpt-4o)")
        p.add_argument("--no-copy", dest="copy", action="store_false", help="Don't copy share URL to clipboard after submit")
        p.add_argument("--output", "-o", help="Output result to JSON file (default: temp file)")

    smoke_parser = subparsers.add_parser(
        "smoke-test",
        help="Quick validation with mock data (does NOT benchmark your agent config)",
    )
    _add_smoke_test_args(smoke_parser)

    subparsers.add_parser(
        "doctor",
        help="Check environment dependencies and agent availability",
    )

    return parser


def main():
    # Rewrite hidden aliases before argparse sees them (avoids ==SUPPRESS== leak
    # in --help on Python 3.13+). These aliases still work but never register
    # as visible subcommands.
    from suite.registry import list_suites as _ls
    if len(sys.argv) > 1:
        _cmd = sys.argv[1]
        if _cmd == "bench":
            sys.argv[1] = "smoke-test"
        elif _cmd in _ls():
            sys.argv = [sys.argv[0], "run", "--suite", _cmd] + sys.argv[2:]

    parser = build_parser()

    args = parser.parse_args()

    # JL-230: Version update check (skip if --no-update-check, env var, or CI)
    if not getattr(args, 'no_update_check', False) and not os.environ.get('JANUS_NO_UPDATE_CHECK'):
        from cli.version_check import check_for_updates
        check_for_updates(__version__)

    # JL-210.3: Launch interactive menu if no command given
    if args.command is None:
        from cli.interactive import main_menu
        return main_menu()

    if args.command == "run":
        return cmd_run(args)
    if args.command == "compare":
        return cmd_compare(args)
    if args.command == "export":
        return cmd_export(args)
    if args.command == "baseline":
        return cmd_baseline(args)
    if args.command == "submit":
        return cmd_submit(args)
    if args.command == "init":
        return cmd_init(args)
    if args.command == "score":
        return cmd_score(args)
    if args.command == "smoke-test":
        return cmd_smoke_test(args)
    if args.command == "bench":
        # Deprecation warning for bench command
        print("=" * 60)
        print("  DEPRECATION WARNING: 'janus bench' renamed to 'janus smoke-test'")
        print("=" * 60)
        print()
        print("  This command uses MOCK data and does NOT benchmark your agent.")
        print()
        print("  To benchmark your actual agent, use:")
        print("    janus-labs run --suite refactor-storm -o result.json")
        print()
        return cmd_smoke_test(args)
    if args.command == "status":
        return cmd_status(args)
    if args.command == "diagnose":
        return cmd_diagnose(args)
    if args.command == "doctor":
        from cli.doctor import cmd_doctor

        return cmd_doctor(args)
    if args.command == "profile":
        return cmd_profile(args)

    # JL-210.6: Handle suite aliases
    # Check if command is a known suite ID
    from suite.registry import list_suites, get_suite
    known_suites = list_suites()
    if args.command in known_suites:
        # Convert alias to run command args
        args.suite = args.command
        return cmd_run(args)

    # Unknown command - show helpful error with suggestions
    print(f"Unknown command: {args.command}", file=sys.stderr)
    print(file=sys.stderr)
    print("Available commands:", file=sys.stderr)
    print(
        "  run, init, score, compare, diagnose, doctor, profile, submit, smoke-test, status",
        file=sys.stderr,
    )
    print(file=sys.stderr)
    print("Suite shortcuts (aliases for 'run --suite <name>'):", file=sys.stderr)
    for sid in known_suites:
        print(f"  {sid}", file=sys.stderr)
    print(file=sys.stderr)
    print("Run 'janus-labs --help' for more information.", file=sys.stderr)
    return 1


def _mock_execute_fn(rollout_index: int, behavior_id: str) -> dict:
    """Hash-based stub for testing without LLM. Produces deterministic fake scores."""
    value = abs(hash(f"{behavior_id}:{rollout_index}")) % 100
    score = 0.6 + (value / 250.0)
    return {"score": min(score, 0.99)}


# Alias for backwards compatibility
_default_execute_fn = _mock_execute_fn


def _create_backend_judge_execute_fn(suite, config_metadata, interactive: bool = True, request_delay: float = 1.2):
    """
    Create an execute function that uses backend-hosted LLM judging
    with rate limiting resilience (JL-210.2).

    This calls the Janus Labs /api/judge endpoint with circuit breaker
    and exponential backoff, eliminating the need for users to have
    their own API key.

    Args:
        suite: BenchmarkSuite containing behavior specs
        config_metadata: Configuration metadata for fingerprinting
        interactive: Whether to prompt user on rate limit (default: True)
        request_delay: Seconds between API calls to avoid rate limits (default: 1.2s)

    Returns:
        Execute function compatible with run_suite()
    """
    from gauge.resilience import ResilientJudgeClient

    # Backend endpoint
    BACKEND_URL = os.environ.get(
        "JANUS_BACKEND_URL",
        "https://fulfilling-courtesy-production-9c2c.up.railway.app"
    )

    # Create resilient client with circuit breaker and request throttling
    client = ResilientJudgeClient(
        base_url=BACKEND_URL,
        interactive=interactive,
        request_spacing=request_delay,
    )

    # Build behavior lookup for O(1) access
    behavior_map = {b.behavior_id: b for b in suite.behaviors}

    def execute_fn(rollout_index: int, behavior_id: str) -> dict:
        behavior = behavior_map.get(behavior_id)
        if behavior is None:
            print(f"Warning: Unknown behavior {behavior_id}", file=sys.stderr)
            return {"score": 0.0, "status": "SKIPPED", "error": "Unknown behavior"}

        # Check if we should use mock (circuit open + user chose mock)
        if client.should_use_mock:
            # Use deterministic mock scoring
            value = abs(hash(f"{behavior_id}:{rollout_index}")) % 100
            score = 0.6 + (value / 250.0)
            return {
                "score": min(score, 0.99),
                "status": "OK",
                "judge_method": "mock_fallback"
            }

        # Create mock bundle for backend judging
        mock_diff = f"""
# Mock solution for {behavior.name}
# Rollout {rollout_index}

def solution():
    '''Implementation of {behavior_id}'''
    pass
"""
        payload = {
            "behavior_id": behavior_id,
            "config_fingerprint": config_metadata.config_hash[:12] if config_metadata.config_hash else "default12345",
            "rollouts": [{
                "prompt": f"Task: {behavior.name}\n{behavior.description}",
                "response": f"Agent completed task {behavior_id}",
                "repo_diff": mock_diff,
                "test_output": "All tests passed (mock)",
            }],
            "behavior_name": behavior.name,
            "behavior_description": behavior.description,
        }

        result = client.judge(payload)

        if result.success:
            return {
                "score": result.score,
                "status": "OK",
                "judge_method": "backend"
            }
        else:
            return {
                "score": None,
                "status": result.status,
                "error": result.error,
                "judge_method": "backend_failed"
            }

    return execute_fn


def _create_geval_execute_fn(suite, model: str):
    """
    Create an execute function that uses GEval LLM-as-judge scoring.

    This replaces the hash stub with real LLM evaluation, providing
    differentiated scores based on qualitative assessment.

    Args:
        suite: BenchmarkSuite containing behavior specs
        model: LLM model for judging (e.g., gpt-4o, claude-3-5-sonnet)

    Returns:
        Execute function compatible with run_suite()
    """
    from gauge.judge import score_with_judge, create_mock_bundle

    # Build behavior lookup for O(1) access
    behavior_map = {b.behavior_id: b for b in suite.behaviors}

    def execute_fn(rollout_index: int, behavior_id: str) -> dict:
        behavior = behavior_map.get(behavior_id)
        if behavior is None:
            print(f"Warning: Unknown behavior {behavior_id}", file=sys.stderr)
            return {"score": 0.0}

        # Create mock bundle for GEval evaluation
        # In real usage, this would contain actual agent execution artifacts
        mock_diff = f"""
# Mock solution for {behavior.name}
# Rollout {rollout_index}

def solution():
    '''Implementation of {behavior_id}'''
    # Code would be captured from actual agent execution
    pass
"""
        bundle = create_mock_bundle(
            code_diff=mock_diff,
            test_output="All tests passed (mock)",
            exit_code="success",
        )

        # Use outcome score of 0.7 as baseline (reasonable mock performance)
        # Real implementation would use actual outcome-based scoring
        baseline_outcome = 0.7

        try:
            result = score_with_judge(
                behavior=behavior,
                bundle=bundle,
                outcome_score=baseline_outcome,
                model=model,
            )
            return {"score": result.combined_score}
        except ValueError as e:
            print(f"Judge error: {e}", file=sys.stderr)
            return {"score": 0.0}

    return execute_fn


def cmd_init(args) -> int:
    """Initialize task workspaces for all behaviors in suite."""
    from scaffold.workspace import init_workspace
    import time as _time

    suite = get_suite(args.suite)
    if suite is None:
        print(f"Unknown suite: {args.suite}", file=sys.stderr)
        print("Try: Use 'refactor-storm' (default) or check available suites.", file=sys.stderr)
        return 1

    base_dir = Path(args.output)
    if base_dir.exists() and any(base_dir.iterdir()):
        print(f"Directory not empty: {base_dir}", file=sys.stderr)
        print(f"Try: janus-labs init --output ./janus-task-2", file=sys.stderr)
        print(f"  Or: rm -rf {base_dir} && janus-labs init", file=sys.stderr)
        return 1

    # Create base directory
    base_dir.mkdir(parents=True, exist_ok=True)

    print(f"Initializing suite: {suite.suite_id} v{suite.version}")
    print(f"Output directory: {base_dir}")
    print("This creates one workspace per behavior and may take a few minutes.")
    print()

    # Initialize workspace for each behavior in subdirectories
    initialized = []
    total = len(suite.behaviors)
    start = _time.perf_counter()
    for index, behavior in enumerate(suite.behaviors, 1):
        behavior_start = _time.perf_counter()
        print(f"[{index}/{total}] Initializing {behavior.behavior_id} - {behavior.name}...", flush=True)
        behavior_dir = base_dir / behavior.behavior_id
        metadata = init_workspace(behavior_dir, suite, behavior)
        initialized.append(behavior)
        elapsed = _time.perf_counter() - behavior_start
        print(
            f"      ready in {elapsed:.1f}s -> {metadata.workspace_path}",
            flush=True,
        )

    # Summary output
    total_elapsed = _time.perf_counter() - start
    print(f"Initialized {len(initialized)} behavior workspaces in: {base_dir}")
    print(f"  Suite: {suite.suite_id} v{suite.version}")
    print(f"  Total time: {total_elapsed:.1f}s")
    print()
    for b in initialized:
        print(f"  {b.behavior_id}/")
        print(f"    {b.name}")
    print()
    print("Next steps:")
    print(f"  1. cd {base_dir}/<behavior-id>")
    print("  2. Open in VS Code and run your AI agent on each task")
    print("  3. Run: janus-labs score  (in each behavior directory)")

    return 0


def cmd_score(args) -> int:
    """Score a completed task workspace."""
    from scaffold.workspace import load_task_metadata
    from scaffold.scorer import score_outcome

    workspace = Path(args.workspace).resolve()

    metadata = load_task_metadata(workspace)
    if metadata is None:
        print("Not a Janus Labs task workspace.", file=sys.stderr)
        print("No .janus-task.json found.", file=sys.stderr)
        print("Try: cd into a janus-task directory, or use --workspace <path>", file=sys.stderr)
        return 1

    import time as _time
    _score_start = _time.perf_counter()

    print(f"Scoring task: {metadata.behavior_id}")
    print(f"  Suite: {metadata.suite_id}")
    print(f"  Behavior: {metadata.behavior_name}")
    print()

    # Outcome-based scoring (always runs)
    result = score_outcome(
        workspace_dir=workspace,
        behavior_id=metadata.behavior_id,
        threshold=metadata.threshold,
        rubric=metadata.rubric,
    )

    # Track if no changes were made
    no_changes = not result.git_diff["files_changed"]

    # Display prominent score banner (JL-102)
    from cli.output import print_score_banner, print_detail, print_next_step

    print_score_banner(
        score=result.raw_score,
        threshold=metadata.threshold,
        passed=result.passed_threshold,
        behavior_name=metadata.behavior_name,
    )
    print_detail(
        f"Outcome score: {result.raw_score:.1f}/10 ({result.normalized_score * 100:.1f}/100 normalized)"
    )

    # Details breakdown
    tests = result.test_results
    print_detail(f"Tests: {tests['passed']} passed, {tests['failed']} failed")

    files_changed = result.git_diff["files_changed"]
    if files_changed:
        file_list = ", ".join(files_changed[:3])
        if len(files_changed) > 3:
            file_list += f" (+{len(files_changed) - 3} more)"
        print_detail(f"Files changed: {len(files_changed)} ({file_list})")
    else:
        print_detail("Files changed: 0 (no committed changes)")

    # Scoring notes (filter to most relevant)
    for note in result.scoring_notes:
        if note not in ["No changes made"]:  # Skip redundant notes
            print_detail(note)

    # Next step suggestion
    if no_changes:
        print_next_step("git add . && git commit -m 'Implement solution'")

    # Judge scoring (optional, requires --judge flag)
    judge_result = None
    if args.judge:
        print()
        print("=" * 60)
        print("LLM-as-Judge Scoring (GEval)")
        print("=" * 60)

        try:
            from gauge.judge import score_with_judge, load_bundle_from_file, create_mock_bundle
            from forge.behavior import BehaviorSpec

            # Reconstruct BehaviorSpec from metadata
            behavior = BehaviorSpec(
                behavior_id=metadata.behavior_id,
                name=metadata.behavior_name,
                description=metadata.behavior_description,
                rubric=metadata.rubric,
                threshold=metadata.threshold,
                disconfirmers=metadata.disconfirmers,
                taxonomy_code=metadata.taxonomy_code,
            )

            # Load bundle: explicit file > captured bundle > mock bundle
            if args.bundle:
                print(f"Loading bundle from: {args.bundle}")
                bundle = load_bundle_from_file(args.bundle)
            elif result.bundle:
                # E8-S2: Use real captured bundle from workspace
                print("Using captured workspace bundle")
                bundle = result.bundle
            else:
                print("Using mock bundle (no bundle captured)")
                bundle = create_mock_bundle(
                    code_diff=result.git_diff.get("patch", ""),
                    test_output=result.test_results.get("output", ""),
                    exit_code="success" if result.passed_threshold else "error",
                )

            print(f"Model: {args.model}")
            print()

            judge_result = score_with_judge(
                behavior=behavior,
                bundle=bundle,
                outcome_score=result.normalized_score,
                model=args.model,
            )

            print(f"GEval Score: {judge_result.geval_score_10:.1f}/10")
            print(f"Reason: {judge_result.reason}")
            print()
            print(f"Combined Score: {judge_result.combined_score_10:.1f}/10")
            print("  (40% outcome + 60% qualitative)")

        except ValueError as e:
            print(f"Judge scoring failed: {e}", file=sys.stderr)
            return 1
        except Exception as e:
            print(f"Judge scoring error: {e}", file=sys.stderr)
            # Continue without judge score

    # Output results
    if args.output:
        # Detect execution context (agent, model, interface)
        exec_ctx = detect_execution_context(
            workspace_path=workspace,
            agent_override=getattr(args, "agent", None),
            model_override=getattr(args, "agent_model", None),
        )

        # JL-130: Capture config metadata (scaffolding awareness)
        config_meta = detect_config(workspace)

        output_data = {
            "behavior_id": result.behavior_id,
            "score": result.raw_score,  # Required by submit command
            "outcome_score": result.raw_score,
            "normalized_score": result.normalized_score,
            "passed": result.passed_threshold,
            "threshold": metadata.threshold,
            "notes": result.scoring_notes,
            "git_diff": result.git_diff,
            "test_results": result.test_results,
            "workspace_hash": result.workspace_hash,  # Anti-cheat validation
            "duration_ms": max(1, int((_time.perf_counter() - _score_start) * 1000)),
            # Execution context for leaderboard segmentation
            "agent": exec_ctx.agent,
            "model": exec_ctx.model,
            "execution_context": {
                "agent": exec_ctx.agent,
                "model": exec_ctx.model,
                "interface": exec_ctx.interface,
                "ide": exec_ctx.ide,
                "detected": exec_ctx.detected,
            },
            # JL-130: Config metadata for scaffolding awareness
            "config_metadata": {
                "config_source": config_meta.config_source,
                "config_hash": config_meta.config_hash,
                "config_files": config_meta.config_files,
                "captured_at": config_meta.captured_at,
            },
        }

        if judge_result:
            output_data["judge"] = {
                "geval_score": judge_result.geval_score_10,
                "combined_score": judge_result.combined_score_10,
                "reason": judge_result.reason,
                "model": judge_result.model,
            }

        Path(args.output).write_text(json.dumps(output_data, indent=2))
        print_detail(f"Results saved to: {args.output}")

        # E8-S2: Save bundle for future GEval evaluation
        if result.bundle:
            bundle_path = Path(args.output).with_suffix(".bundle.json")
            bundle_path.write_text(json.dumps(result.bundle, indent=2))
            print_detail(f"Bundle saved to: {bundle_path}")

        # Next step: submit to leaderboard
        if result.passed_threshold:
            print_next_step(f"janus-labs submit {args.output} --github <handle>")

    return 0 if result.passed_threshold else 1


def cmd_status(args) -> int:
    """Show current workspace status."""
    import subprocess

    workspace = Path(args.workspace)
    task_file = workspace / ".janus-task.json"

    if not task_file.exists():
        print("Not in a Janus workspace.", file=sys.stderr)
        print("Try: janus-labs init --suite refactor-storm --output ./janus-task", file=sys.stderr)
        return 1

    task_data = json.loads(task_file.read_text())

    print("Janus Labs Workspace Status")
    print("=" * 40)
    print(f"  Behavior: {task_data.get('behavior_id', 'unknown')}")
    print(f"  Suite:    {task_data.get('suite_id', 'unknown')}")
    created = task_data.get("initialized_at") or task_data.get("created_at", "unknown")
    print(f"  Created:  {created}")

    committed = None
    try:
        result = subprocess.run(
            ["git", "status", "--porcelain"],
            capture_output=True,
            text=True,
            cwd=workspace,
        )
        changes = len(result.stdout.strip().split("\n")) if result.stdout.strip() else 0
        committed = changes == 0
        print(f"  Changes:  {'Committed' if committed else f'{changes} uncommitted files'}")
    except Exception:
        print("  Changes:  Unable to determine")

    print()
    if committed is False:
        print("Next: git add . && git commit -m 'solution' && janus-labs score")
    else:
        print("Next: janus-labs score")

    return 0


def cmd_diagnose(args) -> int:
    """Analyze instruction file impact on benchmark scores (JL-280)."""
    from cli.diagnose import diagnose, print_diagnose_report, diagnose_to_dict

    result_path = Path(args.result_file)
    if not result_path.exists():
        print(f"Result file not found: {result_path}", file=sys.stderr)
        print("Try: janus-labs diagnose <path-to-result.json>", file=sys.stderr)
        return 1

    result = diagnose(
        result_path=str(result_path),
        workspace_path=args.workspace,
        agent_override=args.agent,
        model_override=args.model,
    )

    if args.json_output:
        print(json.dumps(diagnose_to_dict(result), indent=2))
    else:
        print_diagnose_report(result)

    return 0


def cmd_smoke_test(args) -> int:
    """
    Quick validation with mock data (smoke test).

    Detects config, runs GEval scoring with mock bundles, and optionally submits.
    NOTE: This does NOT benchmark your actual agent config - use init/score for that.
    """
    from scaffold.workspace import init_workspace

    # Step 1: Detect config
    print_step(1, 4, "Detecting config")
    config_metadata = detect_config(Path.cwd())
    if config_metadata.config_source == "custom":
        print(f"    Found: {', '.join(config_metadata.config_files)} (hash: {config_metadata.config_hash})")
    else:
        print("    Using default config")

    # Step 2: Get suite and behavior
    print_step(2, 4, "Loading behavior")
    suite = get_suite(args.suite)
    if suite is None:
        print_error(f"Unknown suite: {args.suite}")
        return 1

    behavior = None
    for b in suite.behaviors:
        if b.behavior_id == args.behavior:
            behavior = b
            break

    if behavior is None:
        print_error(f"Unknown behavior: {args.behavior}")
        print(f"Available behaviors in {args.suite}:")
        for b in suite.behaviors:
            print(f"  - {b.behavior_id}: {b.name}")
        return 1

    print(f"    Behavior: {behavior.behavior_id} - {behavior.name}")

    # Step 3: Score with GEval
    print_step(3, 4, f"Scoring with GEval ({args.model})")

    # Run the suite for single behavior
    config = SuiteRunConfig(suite=suite, config_metadata=config_metadata)
    execute_fn = _create_geval_execute_fn(suite, args.model)

    try:
        result = run_suite(config, execute_fn)
    except Exception as e:
        print_error(f"Scoring failed: {e}")
        return 1

    # Save result to file
    if args.output:
        output_path = Path(args.output)
    else:
        # Use temp file if no output specified
        fd, tmp_path = tempfile.mkstemp(suffix=".json", prefix="janus-bench-")
        os.close(fd)
        output_path = Path(tmp_path)

    export_json(result, str(output_path))
    print(f"    Score: {result.headline_score:.1f} (Grade {result.grade})")

    # Step 4: Submit if requested
    if args.submit:
        print_step(4, 4, "Submitting to leaderboard")
        try:
            submit_data = submit_result(
                str(output_path),
                github_handle=args.github,
                dry_run=False,
            )

            if submit_data.get("status") == "success":
                # Print rich result
                print_benchmark_result(
                    score=submit_data["score"],
                    rank=submit_data.get("rank"),
                    percentile=submit_data.get("percentile"),
                    share_url=submit_data.get("share_url"),
                )

                # Copy to clipboard if available and not disabled
                if args.copy and submit_data.get("share_url"):
                    if is_clipboard_available():
                        if copy_to_clipboard(submit_data["share_url"]):
                            print("\nCopied to clipboard!")
                        else:
                            print_warning("Could not copy to clipboard")

            return 0

        except RuntimeError as e:
            print_error(f"Submit failed: {e}")
            # Still show local result
            print_benchmark_result(
                score=result.headline_score,
                grade=result.grade,
            )
            print(f"\nResult saved to: {output_path}")
            return 1
    else:
        # Not submitting - show local result only
        print_step(4, 4, "Complete (not submitting)")
        print_benchmark_result(
            score=result.headline_score,
            grade=result.grade,
        )
        print(f"\nResult saved to: {output_path}")
        print("\nTo submit to leaderboard, run:")
        print(f"  janus-labs submit {output_path}")
        print("Or use: janus-labs smoke-test --submit")
        return 0


def cmd_run_full(args) -> int:
    """Full pipeline: init -> agent -> score -> aggregate (JL-165)."""
    import time as _time
    from datetime import datetime, timezone
    from cli.submit import _truncate_bundle
    from cli.interactive import is_dumb_terminal, simple_confirm
    from cli.terminal_ui import ProgressBoard, _axis_key, _short_behavior_id, render_score_card
    from scaffold.workspace import init_workspace
    from scaffold.scorer import score_outcome
    from harness.agent_adapter import AgentAdapter, AGENT_PRESETS, run_agent_on_workspace
    from gauge.trust_elasticity import TrustElasticityMetric
    from suite.result import (
        BehaviorScore, BehaviorStatus, GovernanceFlags, SuiteResult,
    )
    interactive = getattr(args, "interactive", False)

    suite = get_suite(args.suite)
    if suite is None:
        print(f"Unknown suite '{args.suite}'", file=sys.stderr)
        return 1

    start_time = _time.perf_counter()

    # 1. Resolve agent adapter
    if args.agent_cmd:
        adapter = AgentAdapter(name="custom", command_template=args.agent_cmd, timeout_seconds=args.timeout)
    elif args.agent:
        adapter = AGENT_PRESETS[args.agent]
        adapter.timeout_seconds = args.timeout
    else:
        print("Error: --agent or --agent-cmd required with --full", file=sys.stderr)
        print("  Example: janus-labs run --full --suite refactor-storm --agent codex", file=sys.stderr)
        return 1

    # 2. Init workspaces (or reuse existing)
    if args.workspace_dir:
        base_dir = Path(args.workspace_dir).resolve()
        if not base_dir.exists():
            print(f"Error: workspace directory not found: {base_dir}", file=sys.stderr)
            return 1
        print(f"Reusing workspace: {base_dir}")
    else:
        base_dir = Path(tempfile.mkdtemp(prefix="janus-full-"))
        print(f"Initializing {len(suite.behaviors)} behavior workspaces...")
        for behavior in suite.behaviors:
            behavior_dir = base_dir / behavior.behavior_id
            init_workspace(
                target_dir=behavior_dir,
                suite=suite,
                behavior=behavior,
            )
        print(f"  Workspaces ready: {base_dir}")

    # 3. Run agent on each workspace + score
    config_meta = detect_config(base_dir)
    config_fingerprint = config_meta.config_hash

    print(f"\nRunning {adapter.name} agent on {len(suite.behaviors)} behaviors\n")
    if interactive:
        board = ProgressBoard(
            behavior_ids=[b.behavior_id for b in suite.behaviors],
            behavior_names=[b.name for b in suite.behaviors],
        )
        board.render()
    else:
        board = None
    behavior_scores = []
    bundles = []
    per_behavior_hashes = []

    for i, behavior in enumerate(suite.behaviors, 1):
        workspace_dir = base_dir / behavior.behavior_id
        readme = workspace_dir / "README.md"
        prompt = readme.read_text(encoding="utf-8") if readme.exists() else f"Complete the task for {behavior.name}"

        if board:
            board.start_behavior(behavior.behavior_id)
        else:
            print(f"Running {_short_behavior_id(behavior.behavior_id)} {behavior.name}...")

        agent_result = run_agent_on_workspace(
            adapter=adapter,
            workspace_dir=workspace_dir,
            prompt=prompt,
            behavior_id=behavior.behavior_id,
        )

        # Score workspace
        repo_diff = ""
        test_output = ""
        try:
            outcome = score_outcome(
                workspace_dir=workspace_dir,
                behavior_id=behavior.behavior_id,
                threshold=behavior.threshold,
                rubric=behavior.rubric,
            )
            score_100 = outcome.normalized_score * 100
            passed = outcome.passed_threshold
            grade = TrustElasticityMetric.score_to_grade(score_100)
            status = BehaviorStatus.PASSED if passed else BehaviorStatus.FAILED
            repo_diff = outcome.git_diff.get("patch", "") or ""
            test_output = str(outcome.test_results.get("output", "") or "")
            if outcome.workspace_hash:
                per_behavior_hashes.append(outcome.workspace_hash)
        except Exception as e:
            score_100 = 0.0
            passed = False
            grade = "F"
            status = BehaviorStatus.FAILED
            print(f"    Scoring error: {e}", file=sys.stderr)

        behavior_bundle = {
            "behavior_id": behavior.behavior_id,
            "behavior_name": behavior.name,
            "behavior_description": getattr(behavior, "description", None),
            "config_fingerprint": config_fingerprint,
            "rollouts": [
                {
                    "prompt": prompt,
                    "response": agent_result.stdout or "",
                    "repo_diff": repo_diff,
                    "test_output": test_output,
                }
            ],
        }
        truncated_bundle = _truncate_bundle(behavior_bundle)
        if truncated_bundle is None:
            truncated_bundle = {
                "behavior_id": behavior.behavior_id,
                "behavior_name": behavior.name,
                "behavior_description": getattr(behavior, "description", None),
                "config_fingerprint": config_fingerprint,
                "rollouts": [
                    {
                        "prompt": prompt[:200],
                        "response": (agent_result.stdout or "")[:200],
                        "repo_diff": "",
                        "test_output": "",
                    }
                ],
            }
        bundles.append(truncated_bundle)

        behavior_scores.append(
            BehaviorScore(
                behavior_id=behavior.behavior_id,
                name=behavior.name,
                score=score_100,
                trust_elasticity=score_100,
                grade=grade,
                passed=passed,
                halted=False,
                status=status,
            )
        )
        if board:
            board.finish_behavior(behavior.behavior_id, passed, agent_result.duration_ms)
        else:
            elapsed_s = agent_result.duration_ms / 1000
            label = "PASS" if passed else "FAIL"
            short_id = _short_behavior_id(behavior.behavior_id)
            print(f"{label} {short_id} {behavior.name} {score_100:.1f} ({elapsed_s:.1f}s)")

    # 4. Aggregate
    judged = [b for b in behavior_scores if b.status in (BehaviorStatus.PASSED, BehaviorStatus.FAILED)]
    scores = [b.score if b.score is not None else 0.0 for b in judged]
    headline = sum(scores) / len(scores) if scores else 0.0
    overall_grade = TrustElasticityMetric.score_to_grade(headline)
    total_ms = int((_time.perf_counter() - start_time) * 1000)
    workspace_hash = None
    if per_behavior_hashes:
        workspace_hash = hashlib.sha256(
            ":".join(sorted(per_behavior_hashes)).encode("utf-8")
        ).hexdigest()[:16]

    result = SuiteResult(
        suite_id=suite.suite_id,
        suite_version=suite.version,
        config_fingerprint=config_fingerprint,
        timestamp=datetime.now(timezone.utc).isoformat(),
        headline_score=headline,
        grade=overall_grade,
        behavior_scores=behavior_scores,
        governance_flags=GovernanceFlags(
            any_halted=False, halted_count=0, halted_behaviors=[], foundation_check_rate=0.0,
        ),
        comparability_key=suite.comparability_key,
        total_rollouts=len(behavior_scores),
        total_duration_ms=total_ms,
        config_metadata=config_meta,
        provenance="outcome-only",
        bundles=bundles,
        workspace_hash=workspace_hash,
        cli_version=__version__,
    )

    # 5. Export
    export_json(result, args.output)
    fmt = getattr(args, "format", "json")
    if fmt in ("html", "both"):
        html_path = str(Path(args.output).with_suffix(".html"))
        export_html(result, html_path)
        print(f"  HTML report: {html_path}")

    # 6. Summary — polished score card
    passed_count = sum(1 for b in behavior_scores if b.passed)

    baseline_delta = None
    try:
        from data.baseline_loader import find_baseline, load_baseline_as_suite_result

        baseline_path = find_baseline()
        if baseline_path:
            baseline = load_baseline_as_suite_result(baseline_path)
            baseline_delta = headline - baseline.headline_score
    except Exception:
        pass

    if interactive:
        render_score_card(
            agent_name=adapter.name,
            model_name=None,  # Model detection is a future story (JL-415)
            headline_score=headline,
            grade=overall_grade,
            behavior_scores=[
                {"behavior_id": b.behavior_id, "score": b.score, "name": b.name}
                for b in behavior_scores
            ],
            baseline_delta=baseline_delta,
            passed_count=passed_count,
            total_count=len(behavior_scores),
            duration_ms=total_ms,
        )
    else:
        scores_map = {
            _axis_key(b.behavior_id): b.score for b in behavior_scores if b.score is not None
        }
        cq_keys = ("BHV-001", "BHV-002")
        er_keys = ("BHV-003", "BHV-004")
        cq_scores = [scores_map[k] for k in cq_keys if k in scores_map]
        er_scores = [scores_map[k] for k in er_keys if k in scores_map]
        cq = sum(cq_scores) / len(cq_scores) if cq_scores else 0.0
        er = sum(er_scores) / len(er_scores) if er_scores else 0.0
        print(
            f"Score: {headline:.1f} ({overall_grade}) | CQ: {cq:.1f} | "
            f"ER: {er:.1f} | {passed_count}/{len(behavior_scores)} passed"
        )
        duration_s = total_ms / 1000
        print(f"Duration: {duration_s:.1f}s")
        if baseline_delta is not None:
            sign = "+" if baseline_delta >= 0 else ""
            print(f"Baseline: {sign}{baseline_delta:.1f}")

    print(f"Results: {args.output}")

    if interactive:
        print()
        try:
            if is_dumb_terminal():
                do_submit = simple_confirm("Submit to leaderboard?")
            else:
                try:
                    import questionary
                    do_submit = questionary.confirm("Submit to leaderboard?").ask()
                except ImportError:
                    do_submit = simple_confirm("Submit to leaderboard?")
            if do_submit:
                from cli.submit import submit_result
                url = submit_result(args.output, github_handle=None)
                if url:
                    print(f"  -> {url}")
                    from cli.clipboard import copy_to_clipboard, is_clipboard_available
                    if is_clipboard_available():
                        copy_to_clipboard(url)
                        print("  Share URL copied to clipboard (OK)")
        except (EOFError, KeyboardInterrupt):
            pass
    else:
        print(f"\nSubmit: janus-labs submit {args.output} --github <handle>")

    return 0


def cmd_run(args) -> int:
    """Run a benchmark suite."""
    # JL-165: Full pipeline mode
    if getattr(args, "full", False):
        return cmd_run_full(args)

    suite = get_suite(args.suite)
    if suite is None:
        print(f"RUN FAILED: Unknown suite '{args.suite}'", file=sys.stderr)
        print(f"\n  Available suites: refactor-storm", file=sys.stderr)
        print(f"\n  Try: janus-labs run --suite refactor-storm", file=sys.stderr)
        return 1

    config_metadata = detect_config(Path.cwd())
    config = SuiteRunConfig(suite=suite, config_metadata=config_metadata)

    # Select execute function based on flags
    # Priority: --mock > --judge > default (backend)
    if args.mock:
        # Explicit mock mode for offline testing
        print("=" * 60)
        print("  MOCK MODE: Scores are SIMULATED (deterministic hash)")
        print("=" * 60)
        print()
        execute_fn = _mock_execute_fn
        provenance = "mock-judged"
    elif args.judge:
        # Local LLM scoring (requires user's API key)
        print(f"Using local LLM-as-judge scoring (model: {args.model})")
        print("This requires OPENAI_API_KEY or ANTHROPIC_API_KEY.")
        print()
        execute_fn = _create_geval_execute_fn(suite, args.model)
        provenance = "geval-enriched"
    else:
        # Default: Backend-hosted judging (no local API key needed)
        print("Using backend-hosted LLM judging (no API key needed)")
        print()
        interactive = getattr(args, "interactive", False)
        request_delay = getattr(args, 'request_delay', 1.2)
        execute_fn = _create_backend_judge_execute_fn(suite, config_metadata, interactive=interactive, request_delay=request_delay)
        provenance = "backend-judged"

    result = run_suite(config, execute_fn)
    result.provenance = provenance

    output_path = Path(args.output)
    if args.format in ("json", "both"):
        export_json(result, str(output_path))
    if args.format in ("html", "both"):
        html_path = output_path.with_suffix(".html")
        export_html(result, str(html_path))

    # Gold standard output format
    from cli.output import print_verdict, print_detail, print_next_step
    from suite.result import BehaviorStatus

    # Check for judge failures (JL-210.1)
    judged_behaviors = [b for b in result.behavior_scores
                        if b.status in (BehaviorStatus.PASSED, BehaviorStatus.FAILED)]
    judge_failed_count = sum(1 for b in result.behavior_scores
                              if b.status == BehaviorStatus.JUDGE_FAILED)
    total_behaviors = len(result.behavior_scores)

    # Determine pass/fail based on governance and scores
    if judge_failed_count == total_behaviors:
        status = "INCOMPLETE"
    elif result.governance_flags.any_halted:
        status = "WARN"
    elif judge_failed_count > 0:
        status = "WARN"
    else:
        status = "PASS"
    print_verdict("RUN", status, f"Suite {suite.suite_id} complete")

    # Details breakdown
    if judge_failed_count > 0:
        print_detail(f"Headline score: {result.headline_score:.1f} ({result.grade}) - INCOMPLETE ({len(judged_behaviors)}/{total_behaviors} judged)")
    else:
        print_detail(f"Headline score: {result.headline_score:.1f} ({result.grade})")

    passed_count = sum(1 for b in result.behavior_scores if b.passed and b.status == BehaviorStatus.PASSED)
    print_detail(f"Behaviors: {passed_count}/{len(judged_behaviors)} passed")

    if result.governance_flags.any_halted:
        print_detail(f"Governance: {result.governance_flags.halted_count} halted")

    if args.judge:
        print_detail(f"Scored with GEval ({args.model})")

    print_detail(f"Results saved to: {args.output}")

    # Next step
    print_next_step(f"janus-labs compare baseline.json {args.output}")

    return 0


def cmd_compare(args) -> int:
    """Compare two results for regression."""
    # JL-160: Auto-baseline support
    if getattr(args, "auto_baseline", False):
        from data.baseline_loader import (
            find_baseline,
            load_baseline_as_suite_result,
            list_baselines,
        )

        current_path = args.current or args.baseline
        if not current_path:
            print("Error: provide current result JSON with --auto-baseline.", file=sys.stderr)
            print("Usage: janus-labs compare result.json --auto-baseline", file=sys.stderr)
            return 2

        current = load_json(current_path)
        baseline_path = find_baseline()
        if not baseline_path:
            print("No pre-computed baselines found.", file=sys.stderr)
            baselines = list_baselines()
            if baselines:
                print("Available baselines:", file=sys.stderr)
                for b in baselines:
                    print(f"  {b['agent']}/{b['model']} ({b['score']:.1f} {b['grade']})", file=sys.stderr)
            return 1
        baseline = load_baseline_as_suite_result(baseline_path)
        print(f"Using vanilla baseline: {baseline_path.name}")
    else:
        if not args.baseline or not args.current:
            print("Error: provide baseline and current result, or use --auto-baseline", file=sys.stderr)
            print("Usage: janus-labs compare baseline.json current.json", file=sys.stderr)
            print("   or: janus-labs compare result.json --auto-baseline", file=sys.stderr)
            return 2
        baseline = load_json(args.baseline)
        current = load_json(args.current)

    if args.config:
        config = load_thresholds(args.config)
    else:
        config = default_thresholds(baseline.suite_id)

    if config.suite_id != baseline.suite_id:
        print("Threshold suite_id does not match baseline suite.", file=sys.stderr)
        return 2

    result = compare_results(baseline, current, config)

    if args.output:
        export_comparison_json(result, args.output)

    if args.format == "text":
        print_comparison_text(result)
    elif args.format == "json":
        print(json.dumps(comparison_to_dict(result), indent=2))
    elif args.format == "github":
        print_github_annotations(result)
        summary_path = os.environ.get("GITHUB_STEP_SUMMARY")
        if summary_path:
            Path(summary_path).write_text(generate_github_summary(result), encoding="utf-8")

    return result.exit_code


def cmd_export(args) -> int:
    """Export result to different format."""
    result = load_json(args.input)
    output = args.output

    if args.format == "html":
        if output is None:
            output = str(Path(args.input).with_suffix(".html"))
        export_html(result, output)
        return 0

    if args.format == "json":
        if output is None:
            output = str(Path(args.input).with_suffix(".json"))
        export_json(result, output)
        return 0

    return 1


def cmd_profile(args) -> int:
    """Generate agent capability profile from baseline results (JL-311)."""
    from gauge.profile import baseline_to_profile, generate_profiles

    pkg_root = Path(__file__).parent.parent

    baseline_path = getattr(args, "baseline", None)
    baselines_dir = getattr(args, "baselines_dir", None)
    output_dir = Path(args.output_dir)
    output_format = "json" if getattr(args, "json", False) else getattr(args, "format", "text")

    # Resolve output_dir relative to package root if not absolute
    if not output_dir.is_absolute():
        output_dir = pkg_root / output_dir

    if baseline_path and baselines_dir:
        print("Use either --baseline or --baselines-dir, not both.", file=sys.stderr)
        return 2

    if baseline_path:
        # Single baseline mode
        p = Path(baseline_path)
        if not p.exists():
            print(f"Baseline not found: {p}", file=sys.stderr)
            return 1

        with open(p, encoding="utf-8") as f:
            try:
                baseline = json.load(f)
            except json.JSONDecodeError as e:
                print(f"Invalid baseline JSON: {p} ({e})", file=sys.stderr)
                return 1

        if "baseline_id" not in baseline:
            baseline["baseline_id"] = p.stem

        profile = baseline_to_profile(baseline)

        if output_format == "json":
            print(json.dumps(profile, indent=2))
        else:
            _print_profile_text(profile)

        # Save to output dir in both modes
        output_dir.mkdir(parents=True, exist_ok=True)
        agent_model = f"{profile.get('agent', 'unknown')}_{profile.get('model', 'unknown')}"
        out_path = output_dir / f"profile_{agent_model}.json"
        with open(out_path, "w", encoding="utf-8") as f:
            json.dump(profile, f, indent=2)
        if output_format == "text":
            print(f"\nSaved: {out_path}")
        return 0

    # Directory mode (explicit or default)
    if baselines_dir is None:
        baselines_dir = pkg_root / "data" / "baselines"
    else:
        baselines_dir = Path(baselines_dir)

    if not baselines_dir.exists():
        print(f"Baselines directory not found: {baselines_dir}", file=sys.stderr)
        print("Use --baseline <file> or --baselines-dir <dir>", file=sys.stderr)
        return 1

    reliability_mode = getattr(args, "reliability", False)
    profiles = generate_profiles(baselines_dir, output_dir, reliability=reliability_mode)

    if not profiles:
        print("No valid baselines found.", file=sys.stderr)
        return 1

    if output_format == "json":
        print(json.dumps(profiles, indent=2))
    else:
        # Print sorted table
        has_reliability = any(p.get("reliability") for p in profiles)
        print(f"Generated {len(profiles)} profiles to {output_dir}/\n")

        if has_reliability:
            print(f"{'Agent/Model':<40} {'Composite':>9} {'StdDev':>7} {'K':>3} {'Consist':>8} {'Grade':>6}")
            print("-" * 75)
            for p in sorted(profiles, key=lambda x: x["composite_score"], reverse=True):
                label = f"{p['agent']}/{p['model']}"
                if len(label) > 39:
                    label = label[:36] + "..."
                rel = p.get("reliability")
                if rel:
                    print(
                        f"{label:<40} {p['composite_score']:>8.1f} "
                        f"{rel['stddev_composite']:>7.2f} "
                        f"{rel['k']:>3} "
                        f"{rel['consistency_grade']:>8} "
                        f"{p['grade']:>6}"
                    )
                else:
                    print(
                        f"{label:<40} {p['composite_score']:>8.1f} "
                        f"{'--':>7} {'1':>3} {'--':>8} {p['grade']:>6}"
                    )
        else:
            print(f"{'Agent/Model':<45} {'Composite':>9} {'Code Q':>7} {'Err Res':>8} {'Grade':>6}")
            print("-" * 77)
            for p in sorted(profiles, key=lambda x: x["composite_score"], reverse=True):
                label = f"{p['agent']}/{p['model']}"
                if len(label) > 44:
                    label = label[:41] + "..."
                print(
                    f"{label:<45} {p['composite_score']:>8.1f} "
                    f"{p['axes']['code_quality']['score']:>7.1f} "
                    f"{p['axes']['error_resilience']['score']:>7.1f} "
                    f"{p['grade']:>6}"
                )

    # Generate leaderboard HTML if requested
    if getattr(args, "leaderboard", False):
        from gauge.leaderboard import profiles_to_leaderboard_html

        html = profiles_to_leaderboard_html(profiles)
        lb_path = output_dir / "leaderboard.html"
        with open(lb_path, "w", encoding="utf-8") as f:
            f.write(html)
        print(f"\nLeaderboard: {lb_path}")

    return 0


def _print_profile_text(profile: dict) -> None:
    """Print a single profile as formatted text with bar chart."""
    agent = profile.get("agent", "unknown")
    model = profile.get("model", "unknown")
    grade = profile.get("grade", "?")
    composite = profile.get("composite_score", 0.0)

    print(f"Agent Capability Profile: {agent} / {model}")
    print(f"Grade: {grade} ({composite:.1f})")
    print()

    axes = profile.get("axes", {})
    axis_labels = [
        ("code_quality", "Code Quality"),
        ("error_resilience", "Error Resilience"),
        ("instruction_resilience", "Instr. Resilience"),
    ]

    # Fallback to ASCII bars when terminal encoding can't render block chars.
    try:
        "\u2588".encode(sys.stdout.encoding or "utf-8")
        fill_char, empty_char = "\u2588", "\u2591"
    except (UnicodeEncodeError, LookupError):
        fill_char, empty_char = "#", "-"

    for key, label in axis_labels:
        axis = axes.get(key)
        if axis is None:
            print(f"  {label + ':':22s} (not available)")
        else:
            score = axis["score"]
            axis_grade = axis["grade"]
            filled = int(max(0, min(20, score / 100 * 20)))
            bar = fill_char * filled + empty_char * (20 - filled)
            print(f"  {label + ':':22s} {bar}  {score:.1f} ({axis_grade})")

    print()

    # Reliability section (K>=3)
    rel = profile.get("reliability")
    if rel:
        print(f"Reliability (K={rel['k']}):")
        print(f"  Consistency:   {rel['consistency_grade']} (stddev {rel['stddev_composite']:.2f})")
        print(f"  Pass^K rate:   {rel['pass_k_rate']:.1%}")
        pav = rel.get("per_axis_variance", {})
        for key, label in axis_labels:
            v = pav.get(key)
            if v is not None:
                print(f"  {label + ' var:':20s} {v:.2f}")
        print()

    source = profile.get("source", {})
    baseline_ids = source.get("baseline_ids", [])
    if baseline_ids:
        if len(baseline_ids) == 1:
            print(f"Source: {baseline_ids[0]}")
        else:
            print(f"Source: {len(baseline_ids)} baselines")
    suite_ver = source.get("suite_version", "unknown")
    print(f"Suite: refactor-storm v{suite_ver}")


def cmd_baseline(args) -> int:
    """Baseline management command."""
    if args.baseline_command == "update":
        return cmd_baseline_update(args)
    if args.baseline_command == "show":
        return cmd_baseline_show(args)
    if args.baseline_command == "list":
        return cmd_baseline_list(args)
    return 1


def cmd_baseline_update(args) -> int:
    """Promote result to baseline."""
    result = load_json(args.result)
    output = Path(args.output)

    if output.exists() and not args.force:
        print(f"Baseline exists: {output}. Use --force to overwrite.", file=sys.stderr)
        return 1

    export_json(result, str(output))
    print(f"Baseline updated: {output}")
    print(f"  Suite: {result.suite_id} v{result.suite_version}")
    print(f"  Score: {result.headline_score:.1f} ({result.grade})")
    return 0


def cmd_baseline_show(args) -> int:
    """Show baseline info."""
    result = load_json(args.baseline)
    print(f"Suite: {result.suite_id} v{result.suite_version}")
    print(f"Score: {result.headline_score:.1f} ({result.grade})")
    print(f"Comparability key: {result.comparability_key}")
    return 0


def cmd_baseline_list(args) -> int:
    """List pre-computed vanilla baselines."""
    from data.baseline_loader import list_baselines

    baselines = list_baselines()
    if not baselines:
        print("No pre-computed baselines found.", file=sys.stderr)
        return 1

    print(f"{'Agent':<10} {'Model':<35} {'Score':>6} {'Grade':>5} {'Suite':>8} {'Date':<10}")
    print("-" * 80)
    for b in baselines:
        print(
            f"{b['agent']:<10} {b['model']:<35} {b['score']:>6.1f} "
            f"{b['grade']:>5} {b['suite_version']:>8} {b['date']:<10}"
        )

    print(f"\n{len(baselines)} baselines available")
    print("Usage: janus-labs compare result.json --auto-baseline")
    return 0


if __name__ == "__main__":
    sys.exit(main())
