#!/usr/bin/env python3
"""Adapter that lets agent-eval.py measure Part 24's hand-written agent loop.

Purpose: the control condition for the reality check. It wraps minimal-agent.py from
    Part 24's first lab in the entry-point contract agent-eval.py expects, so the same
    fifteen tasks, the same checks and the same trajectory format apply to the loop you
    wrote by hand and to every framework scaffold. It adds no capability of its own: the
    loop, the tools and the stopping conditions are Part 24's, unchanged.
Platform: all (pure Python over HTTP; the model may be served on any track)
Minimum memory: 16 GB on the machine serving the model; this script needs almost none
Assumes: Python 3.9 or later. minimal-agent.py and toolbox.py from
    src/labs/part-24-tools-mcp-and-the-agent-loop/ copied into the same directory as this
    file, or that directory passed as --option part24_dir=<path> to agent-eval.py. An
    OpenAI-compatible endpoint already serving the model alias.

Usage: python3 agent-eval.py --agent scaffold-minimal.py --tasks agent-tasks.json \\
           --model local/answer --workspace ./agent-workspace --out results-minimal.json
"""

from __future__ import annotations

import argparse
import importlib.util
import os
import sys
from pathlib import Path
from typing import Any, Dict, Optional

SCAFFOLD_NAME = "part-24-minimal-loop"

_STATE: Dict[str, Any] = {}


def _load(name: str, directory: Path):
    """Import a hyphenated file by path, the way agent-eval.py imports this one."""
    path = directory / f"{name}.py"
    if not path.exists():
        sys.exit(f"{path} is missing. Copy minimal-agent.py and toolbox.py from Part 24's "
                 f"lab directory beside this file, or pass --option part24_dir=<path>.")
    spec = importlib.util.spec_from_file_location(name.replace("-", "_"), path)
    if spec is None or spec.loader is None:
        sys.exit(f"cannot load {path}")
    module = importlib.util.module_from_spec(spec)
    sys.path.insert(0, str(directory.resolve()))
    spec.loader.exec_module(module)
    return module


def build(options: dict) -> None:
    directory = Path(options.get("part24_dir") or Path(__file__).resolve().parent)
    minimal = _load("minimal-agent", directory)
    toolbox = _load("toolbox", directory)

    # Part 24's run_task() takes an argparse.Namespace of stopping conditions. Building one
    # here rather than parsing a command line keeps every limit visible in one place.
    args = argparse.Namespace(
        base_url=options.get("base_url", "http://127.0.0.1:4000/v1"),
        model=options["model"],
        api_key=options.get("api_key") or os.environ.get("OPENAI_API_KEY"),
        max_turns=int(options.get("max_turns", 12)),
        max_tokens=int(options.get("max_tokens", 1024)),
        max_total_tokens=int(options.get("max_total_tokens", 60000)),
        max_seconds=int(options.get("max_seconds", 600)),
        max_repeats=int(options.get("max_repeats", 2)),
        temperature=float(options.get("temperature", 0.7)),
        no_think=str(options.get("no_think", "")).lower() in ("1", "true", "yes"),
        timeout=int(options.get("timeout", 300)),
        verbose=False,
    )
    box = toolbox.Toolbox(
        workspace=Path(options.get("workspace") or "."),
        index=Path(options["index"]) if options.get("index") else None,
        allowed_commands=toolbox.DEFAULT_ALLOWED_COMMANDS,
    )
    _STATE.update({"minimal": minimal, "box": box, "args": args})


def run_task(task: str, options: dict) -> dict:
    """One task through Part 24's loop, reshaped into agent-eval.py's record."""
    if not _STATE:
        build(options)
    result = _STATE["minimal"].run_task(task, _STATE["box"], _STATE["args"], None)

    # Part 24 counts assistant turns; agent-eval.py counts steps, which here means one per
    # model reply plus one per tool result. Both numbers come from the same transcript.
    trajectory = result.get("transcript", [])
    tool_rows = [row for row in trajectory if row.get("tool")]
    return {
        "answer": result.get("answer") or "",
        "steps": int(result.get("turns") or 0) + len(tool_rows),
        "tokens": int(result.get("tokens") or 0),
        "seconds": result.get("seconds"),
        "stopped": result.get("stopped", ""),
        "trajectory": trajectory,
    }


def close() -> None:
    _STATE.clear()


def main() -> None:
    parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
    parser.add_argument("task")
    parser.add_argument("--base-url", default="http://127.0.0.1:4000/v1")
    parser.add_argument("--model", required=True)
    parser.add_argument("--workspace", required=True)
    parser.add_argument("--index", default=None)
    parser.add_argument("--part24-dir", default=None)
    args = parser.parse_args()
    options: Dict[str, Optional[str]] = {
        "base_url": args.base_url, "model": args.model, "workspace": args.workspace,
        "index": args.index, "part24_dir": args.part24_dir,
    }
    record = run_task(args.task, options)
    print(record["answer"])
    print(f"\n{record['steps']} step(s), {record['tokens']} token(s), {record['seconds']} s "
          f"({record['stopped']})")


if __name__ == "__main__":
    main()
