"""CLI entry point for clawbench pipeline."""

from __future__ import annotations

import json
import sys
from pathlib import Path

import click


@click.group()
def cli():
    """clawbench — Auto-improve agent skills via task completion optimization."""
    from clawbench._env import load_local_env_files

    load_local_env_files()


@cli.command("optimize")
@click.option(
    "--skill",
    required=True,
    type=click.Path(exists=True),
    help="Path to SKILL.md or meta-skill file to optimize",
)
@click.option(
    "--skills-dir",
    default=None,
    type=click.Path(exists=True),
    help="Skills directory for agent-native discovery (constant context, not optimized)",
)
@click.option(
    "--tasks-dir",
    default="tasks/",
    type=click.Path(exists=True),
    help="Tasks directory (default: tasks/)",
)
@click.option("--val-tasks", default=None, help="Comma-separated task names for validation (removed from train)")
@click.option("--test-tasks", default=None, help="Comma-separated task names for held-out test (removed from train)")
@click.option("--task", default=None, type=str, help="Filter by single task name")
@click.option("--task-tag", multiple=True, help="Filter by tag (OR across flags, comma for AND: --task-tag gmail,hard --task-tag gcal)")
@click.option("--agent", default="openclaw-gemini", help="Agent name (e.g. openclaw-gemini, gemini, claude-code)")
@click.option(
    "--agent-import-path",
    default=None,
    help="Import path for unregistered agents (e.g. mypackage.agents:MyAgent)",
)
@click.option("--model", default="google/gemini-3.1-flash-lite-preview", help="Model for agent execution")
@click.option(
    "-e",
    "--env",
    "environment_type",
    default="docker",
    type=click.Choice(["docker", "daytona", "e2b", "modal", "runloop", "gke"]),
    help="Execution environment (default: docker)",
)
@click.option(
    "--engine",
    default="gepa",
    type=click.Choice(["gepa"]),
    help="Optimization engine (default: gepa)",
)
@click.option("--max-iter", default=20, type=int, help="Max optimization iterations (proposals)")
@click.option("--workers", default=1, type=int, help="Concurrent evaluation workers (requires cloud env for N>1)")
@click.option(
    "--output-dir",
    default=None,
    type=click.Path(),
    help="Output directory (default: .local/clawbench-output/<timestamp>)",
)
@click.option("--seed", default=42, type=int, help="Random seed")
@click.option("--resume", is_flag=True, default=False, help="Resume from best_SKILL.md in output dir")
def optimize(skill, skills_dir, tasks_dir, val_tasks, test_tasks, task, task_tag, agent, agent_import_path, model, environment_type, engine, max_iter, workers, output_dir, seed, resume):
    """Run optimization loop to improve a skill."""
    from datetime import datetime
    from clawbench.optimize import run_optimization
    from clawbench.task_utils import resolve_tasks

    if workers < 1:
        click.echo("Error: --workers must be >= 1", err=True)
        sys.exit(1)
    if workers > 1 and environment_type == "docker":
        click.echo("Error: --workers > 1 requires a cloud environment (-e daytona, etc.)", err=True)
        click.echo("  Local Docker cannot run concurrent trial stacks reliably.", err=True)
        sys.exit(1)
    if workers == 1:
        click.echo("⚠ --workers 1 is debug mode (sequential, local). "
                    "For experiments, use --workers N -e daytona.", err=True)

    skill_path = Path(skill)
    tasks_dir = Path(tasks_dir)

    if output_dir is not None:
        output_dir = Path(output_dir)
    elif resume:
        # Find latest run to resume from
        base = Path(".local/clawbench-output")
        if base.is_dir():
            runs = sorted(base.iterdir(), reverse=True)
            for run_dir in runs:
                if (run_dir / "best_SKILL.md").exists():
                    output_dir = run_dir
                    break
        if output_dir is None:
            click.echo("No previous run found to resume from", err=True)
            sys.exit(1)
    else:
        timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
        output_dir = Path(f".local/clawbench-output/{timestamp}")

    # Resume: use best_SKILL.md from previous run as seed
    if resume:
        best_skill = output_dir / "best_SKILL.md"
        if best_skill.exists():
            skill_path = best_skill
            click.echo(f"Resuming from previous best: {best_skill}")
        else:
            click.echo(f"No best_SKILL.md found in {output_dir}, starting fresh")

    all_tasks = resolve_tasks(tasks_dir, task_name=task, task_tags=task_tag)

    # Build name -> task dict lookup
    task_by_name = {t["task_name"]: t for t in all_tasks}
    available_names = sorted(task_by_name.keys())

    # Resolve --val-tasks and --test-tasks to task dicts
    val_task_list = []
    test_task_list = []
    val_names = set()
    test_names = set()

    if val_tasks:
        val_names = {n.strip() for n in val_tasks.split(",")}
        unknown = val_names - set(available_names)
        if unknown:
            click.echo(f"Error: unknown --val-tasks: {', '.join(sorted(unknown))}", err=True)
            click.echo(f"  Available: {', '.join(available_names)}", err=True)
            sys.exit(1)
        val_task_list = [task_by_name[n] for n in available_names if n in val_names]

    if test_tasks:
        test_names = {n.strip() for n in test_tasks.split(",")}
        unknown = test_names - set(available_names)
        if unknown:
            click.echo(f"Error: unknown --test-tasks: {', '.join(sorted(unknown))}", err=True)
            click.echo(f"  Available: {', '.join(available_names)}", err=True)
            sys.exit(1)
        overlap = val_names & test_names
        if overlap:
            click.echo(f"Error: tasks in both --val-tasks and --test-tasks: {', '.join(sorted(overlap))}", err=True)
            sys.exit(1)
        test_task_list = [task_by_name[n] for n in available_names if n in test_names]

    # Train = all tasks minus val and test
    train_task_list = [t for t in all_tasks if t["task_name"] not in val_names | test_names]
    if not train_task_list:
        click.echo("Error: no train tasks remaining after --val-tasks / --test-tasks removal", err=True)
        sys.exit(1)

    agent_display = agent_import_path or agent
    click.echo(f"Optimization (engine={engine})")
    click.echo(f"  Skill: {skill_path}")
    click.echo(f"  Train: {len(train_task_list)} ({', '.join(t['task_name'] for t in train_task_list)})")
    if val_task_list:
        click.echo(f"  Val:   {len(val_task_list)} ({', '.join(t['task_name'] for t in val_task_list)})")
    if test_task_list:
        click.echo(f"  Test:  {len(test_task_list)} ({', '.join(t['task_name'] for t in test_task_list)})")
    click.echo(f"  Agent: {agent_display}")
    click.echo(f"  Model: {model}")
    click.echo(f"  Environment: {environment_type}")
    click.echo(f"  Max iterations: {max_iter}")
    click.echo(f"  Output: {output_dir}")
    click.echo()

    # Persist the launch command so the dashboard can display it
    output_dir.mkdir(parents=True, exist_ok=True)
    (output_dir / "run_config.json").write_text(json.dumps({
        "command": "clawbench " + " ".join(sys.argv[1:]),
        "skill": str(skill_path),
        "engine": engine,
        "agent": agent_import_path or agent,
        "model": model,
        "environment": environment_type,
        "max_iter": max_iter,
        "workers": workers,
        "seed": seed,
        "train_tasks": [t["task_name"] for t in train_task_list],
        "val_tasks": [t["task_name"] for t in val_task_list],
        "test_tasks": [t["task_name"] for t in test_task_list],
    }, indent=2))

    result = run_optimization(
        skill_path=skill_path,
        train_tasks=train_task_list,
        val_tasks=val_task_list,
        test_tasks=test_task_list,
        agent_name=agent,
        model_name=model,
        environment_type=environment_type,
        max_iter=max_iter,
        n_workers=workers,
        output_dir=output_dir,
        seed=seed,
        agent_import_path=agent_import_path,
        engine=engine,
        skills_dir=Path(skills_dir) if skills_dir else None,
    )

    click.echo(f"\nOptimization complete!")
    click.echo(f"  Best candidate index: {result.best_idx}")
    click.echo(f"  Total evaluator calls: {result.total_metric_calls}")
    if result.val_aggregate_scores:
        click.echo(f"  Best val score: {result.val_aggregate_scores[result.best_idx]:.3f}")
    click.echo(f"  Best skill saved to: {output_dir}/best_SKILL.md")
    click.echo(f"  Summary: {output_dir}/optimization_summary.json")


@cli.command("eval")
@click.option(
    "--skill",
    required=False,
    default=None,
    type=click.Path(exists=True),
    help="Path to SKILL.md file (omit for baseline — agent sees only instruction.md)",
)
@click.option(
    "--skills-dir",
    default=None,
    type=click.Path(exists=True),
    help="Skills directory for agent-native discovery (copied into container, symlinked to agent paths)",
)
@click.option(
    "--tasks-dir",
    default="tasks/",
    type=click.Path(exists=True),
    help="Tasks directory (default: tasks/)",
)
@click.option("--task", default=None, help="Single task name filter (omit for all tasks)")
@click.option("--task-tag", multiple=True, help="Filter by tag (OR across flags, comma for AND: --task-tag gmail,hard --task-tag gcal)")
@click.option("--agent", default="openclaw-gemini", help="Agent name (e.g. openclaw-gemini, gemini, claude-code)")
@click.option(
    "--agent-import-path",
    default=None,
    help="Import path for unregistered agents",
)
@click.option("--model", default="google/gemini-3.1-flash-lite-preview", help="Model for agent execution")
@click.option(
    "-e",
    "--env",
    "environment_type",
    default="docker",
    type=click.Choice(["docker", "daytona", "e2b", "modal", "runloop", "gke"]),
    help="Execution environment (default: docker)",
)
@click.option("--workers", default=1, type=int, help="Concurrent evaluation workers (requires cloud env for N>1)")
@click.option("--repeats", default=1, type=int, help="Run each task N times (for variance estimation)")
@click.option(
    "--output-dir",
    default=None,
    type=click.Path(),
    help="Write results.json + trial artifacts into this directory",
)
@click.option("--verbose", is_flag=True, help="Print per-task progress")
@click.option("--skip-verified", is_flag=True, help="Skip tasks with oracle_score=1.0 and unchanged files")
@click.option("--update-cache", is_flag=True, help="Save oracle scores to .verified_tasks.json after eval")
def eval_cmd(skill, skills_dir, tasks_dir, task, task_tag, agent, agent_import_path, model, environment_type, workers, repeats, output_dir, verbose, skip_verified, update_cache):
    """Evaluate a skill against tasks (no optimization loop)."""
    from clawbench.eval import run_eval

    if workers < 1:
        click.echo("Error: --workers must be >= 1", err=True)
        sys.exit(1)
    if repeats < 1:
        click.echo("Error: --repeats must be >= 1", err=True)
        sys.exit(1)
    if workers > 1 and environment_type == "docker":
        click.echo("Error: --workers > 1 requires a cloud environment (-e daytona, etc.)", err=True)
        click.echo("  Local Docker cannot run concurrent trial stacks reliably.", err=True)
        sys.exit(1)

    from clawbench.task_utils import resolve_tasks

    tasks = resolve_tasks(Path(tasks_dir), task_name=task, task_tags=task_tag)

    result = run_eval(
        skill_path=Path(skill) if skill else None,
        tasks_dir=Path(tasks_dir),
        tasks=tasks,
        agent_name=agent,
        agent_import_path=agent_import_path,
        model_name=model,
        environment_type=environment_type,
        n_workers=workers,
        n_repeats=repeats,
        verbose=verbose,
        skip_verified=skip_verified,
        update_cache=update_cache,
        skills_dir=Path(skills_dir) if skills_dir else None,
        output_dir=Path(output_dir) if output_dir else None,
    )

    # Print per-task results
    from clawbench._env import PASS_THRESHOLD
    n_total_runs = len(result.results)
    n_errors = sum(1 for r in result.results if r.status in ("sandbox_error", "trial_error"))
    if repeats > 1:
        header = f"Evaluated {result.skill} on {len(result.task_summary)} tasks x {repeats} repeats ({result.summary.total} runs"
        if n_errors:
            header += f", {n_errors} errors excluded"
        header += "):"
        click.echo(header + "\n")
        for ts in result.task_summary:
            status = "PASS" if ts["mean_score"] > PASS_THRESHOLD else "FAIL"
            n_requested = repeats
            n_valid = ts["n_runs"]
            runs_label = f"{n_valid}/{n_requested} runs" if n_valid < n_requested else f"{n_valid} runs"
            line = f"  [{status}] {ts['task_name']}: mean={ts['mean_score']:.2f} +/- {ts['std_score']:.2f} ({runs_label})"
            click.echo(line)
    else:
        click.echo(f"Evaluated {result.skill} on {result.summary.total} tasks:\n")
        for r in result.results:
            status = "PASS" if r.passed else "FAIL"
            line = f"  [{status}] {r.task_name}: reward={r.score:.2f} ({r.duration_sec}s)"
            if r.error:
                line += f"\n    error: {r.error}"
            click.echo(line)

    click.echo(f"\nResults: {result.summary.passed}/{result.summary.total} passed (threshold: {PASS_THRESHOLD})")
    click.echo(f"Avg reward: {result.summary.avg_reward:.3f}")
    click.echo(f"Pass rate: {result.summary.pass_rate:.1%}")
    if n_errors:
        click.echo(f"Infra errors: {n_errors}/{n_total_runs} runs ({n_errors / n_total_runs:.1%})")
    click.echo(f"Elapsed: {result.elapsed_sec:.1f}s")

    if output_dir:
        click.echo(f"\nResults saved to: {output_dir}")


@cli.command("check")
@click.option(
    "--tasks-dir",
    default="tasks/",
    type=click.Path(exists=True),
    help="Tasks directory (default: tasks/)",
)
@click.option("--task", default=None, metavar="NAME", help="Check a single task by name.")
@click.option("--skip-verified", is_flag=True, help="Skip tasks with oracle_score=1.0 and unchanged files.")
@click.option("--check-base", is_flag=True, help="Also check Dockerfile.base against config.toml.")
@click.option("--oracle", is_flag=True, help="After static checks pass, run oracle eval and update cache.")
@click.option("--verbose", is_flag=True, help="Print per-task oracle eval progress.")
def check(tasks_dir, task, skip_verified, check_base, oracle, verbose):
    """Validate task files (format, evaluate.py, Dockerfile, task.toml)."""
    from clawbench.task_check import (
        check_format,
        check_evaluate_callable,
        check_rewards_contract,
        check_dockerfile_conventions,
        check_no_inline_skills,
        check_no_docker_compose,
        check_task_toml_schema,
        check_base_image_consistency,
        SKIP,
    )

    tasks_path = Path(tasks_dir)
    cache = {}
    if skip_verified:
        from clawbench.score_cache import load_cache, is_verified
        cache = load_cache(tasks_path)

    if task:
        task_dirs = [tasks_path / task]
    else:
        task_dirs = sorted(
            d for d in tasks_path.iterdir()
            if d.is_dir() and d.name not in SKIP
            and not d.name.startswith((".", "_"))
        )

    total = 0
    failures = 0
    warned = 0
    passed_tasks = []

    for task_dir in task_dirs:
        name = task_dir.name
        if skip_verified and cache:
            from clawbench.score_cache import is_verified
            if is_verified(name, task_dir, cache):
                click.echo(f"SKIP {name}  (verified, oracle=1.0)")
                continue

        issues: list[str] = []
        task_warnings: list[str] = []
        issues.extend(check_format(task_dir))
        issues.extend(check_evaluate_callable(task_dir))
        issues.extend(check_rewards_contract(task_dir))
        df_issues, df_warnings = check_dockerfile_conventions(task_dir)
        issues.extend(df_issues)
        task_warnings.extend(df_warnings)
        issues.extend(check_no_inline_skills(task_dir))
        issues.extend(check_no_docker_compose(task_dir))
        issues.extend(check_task_toml_schema(task_dir))

        total += 1
        if issues:
            failures += 1
            for issue in issues:
                click.echo(f"FAIL {name}: {issue}")
        else:
            for w in task_warnings:
                click.echo(f"WARN {name}: {w}")
            if task_warnings:
                warned += 1
                click.echo(f"OK   {name} ({len(task_warnings)} warning(s))")
            else:
                click.echo(f"OK   {name}")
            passed_tasks.append(name)

    # Cross-cutting: Dockerfile.base vs config.toml
    if check_base:
        base_issues = check_base_image_consistency()
        if base_issues:
            failures += 1
            for issue in base_issues:
                click.echo(f"FAIL [base-image]: {issue}")
        else:
            click.echo("OK   [base-image]")

    summary = f"\n{total - failures}/{total} tasks passed static checks."
    if warned:
        summary += f" ({warned} with warnings)"
    click.echo(summary)

    if failures > 0 or not oracle:
        sys.exit(0 if failures == 0 else 1)

    # Step 2: oracle eval on tasks that passed static checks
    if not passed_tasks:
        click.echo("No tasks to evaluate.")
        sys.exit(0)

    click.echo(f"\nRunning oracle eval on {len(passed_tasks)} task(s)...\n")

    from clawbench.eval import run_eval
    from clawbench._env import PASS_THRESHOLD

    result = run_eval(
        skill_path=None,
        tasks_dir=tasks_path,
        task_filter=task,
        task_tags=None,
        agent_name="oracle",
        agent_import_path=None,
        model_name=None,
        environment_type="docker",
        n_workers=1,
        n_repeats=1,
        verbose=verbose,
        skip_verified=skip_verified,
        update_cache=True,
        skills_dir=None,
    )

    for r in result.results:
        status = "PASS" if r.passed else "FAIL"
        line = f"  [{status}] {r.task_name}: reward={r.score:.2f} ({r.duration_sec}s)"
        if r.error:
            line += f"\n    error: {r.error}"
        click.echo(line)

    click.echo(f"\nOracle: {result.summary.passed}/{result.summary.total} passed (threshold: {PASS_THRESHOLD})")
    click.echo(f"Avg reward: {result.summary.avg_reward:.3f}")
    sys.exit(0 if result.summary.passed == result.summary.total else 1)


def _pipeline_config() -> dict:
    """Read [clawbench] section from config.toml."""
    from clawbench._paths import _PROJECT_ROOT
    import tomllib
    cfg_path = _PROJECT_ROOT / "config.toml"
    if cfg_path.exists():
        with open(cfg_path, "rb") as f:
            return tomllib.load(f).get("clawbench", {})
    return {}


@cli.command("dashboard")
@click.option(
    "--output-dir",
    default=None,
    type=click.Path(),
    help="Path to optimization output directory (default: from config.toml)",
)
@click.option("--port", default=None, type=int, help="Dashboard port (default: from config.toml)")
@click.option("--host", default="0.0.0.0", help="Dashboard host")
@click.option("--reload", is_flag=True, default=False, help="Enable auto-reload on code changes")
def dashboard(output_dir, port, host, reload):
    """Launch the Skill Evolution dashboard UI."""
    import uvicorn
    from clawbench.dashboard import create_app
    from clawbench._paths import _PROJECT_ROOT

    pcfg = _pipeline_config()
    if port is None:
        port = pcfg.get("port", 9050)
    if output_dir is None:
        default_dir = pcfg.get("output_dir", ".local/clawbench-output")
        output_dir = str(_PROJECT_ROOT / default_dir)

    output_dir = Path(output_dir)
    if not output_dir.exists():
        click.echo(f"Error: output directory {output_dir} does not exist", err=True)
        sys.exit(1)

    click.echo(f"clawbench dashboard: http://{host}:{port}")
    click.echo(f"Output dir: {output_dir}")

    if reload:
        import os
        os.environ["CLAWBENCH_OUTPUT_DIR"] = str(output_dir)
        uvicorn.run(
            "clawbench.dashboard:app",
            host=host, port=port, log_level="warning",
            reload=True,
            reload_dirs=[str(Path(__file__).resolve().parent)],
        )
    else:
        app = create_app(output_dir)
        uvicorn.run(app, host=host, port=port, log_level="warning")


if __name__ == "__main__":
    cli()
