"""A minimal coding agent: one model, one `shell` tool, one working folder.

Backend: ChatGPT subscription OAuth -> Codex Responses endpoint (store=false,
encrypted reasoning replayed each turn). Every command runs under macOS
sandbox-exec: writes are allowed only inside the workspace and temp dirs, and
the experiment's ground-truth folders are unreadable.

Usage:
  mini_agent.py --workspace DIR --prompt-file FILE --log-dir DIR [--model gpt-6.1-sol]
"""
from __future__ import annotations

import argparse, json, os, subprocess, sys, time
from pathlib import Path

sys.path.insert(0, "<chatgpt-oauth>/src")
from chat_gpt_oauth.client import CODEX_RESPONSES_ENDPOINT, ensure_fresh_token  # noqa: E402
from chat_gpt_oauth.http import post_sse  # noqa: E402

EXPERIMENTS = Path(__file__).resolve().parents[2]  # .../textbook/experiments
SHELL_TOOL = {
    "type": "function",
    "name": "shell",
    "description": (
        "Run a bash command inside the working folder and return its exit code, stdout and stderr. "
        "Commands time out after 600 seconds. Network access is available."
    ),
    "parameters": {
        "type": "object",
        "properties": {"command": {"type": "string", "description": "The bash command to run."}},
        "required": ["command"],
        "additionalProperties": False,
    },
    "strict": True,
}
INSTRUCTIONS = (
    "You are a capable software agent working in a folder on the user's computer. "
    "You can run shell commands with the `shell` tool, read and write files, and look at the results before "
    "continuing. Work autonomously until the task is done; nobody will answer questions mid-task. "
    "When you are finished, reply with a short final message and do not call any more tools."
)
SECRET_ENV = ("AI_BUILDER_TOKEN",)


def redact(text: str) -> str:
    for name in SECRET_ENV:
        value = os.environ.get(name)
        if value and len(value) > 8:
            text = text.replace(value, "<redacted>")
    return text


def sandbox_profile(ws: Path) -> str:
    deny_read = [EXPERIMENTS, Path.home() / ".chatgpt_oauth", Path.home() / ".config" / "op"]
    rules = ["(version 1)", "(allow default)", "(deny file-write*)",
             f'(allow file-write* (subpath "{ws}") (subpath "/private/tmp") (subpath "/private/var/folders") (literal "/dev/null"))']
    rules += [f'(deny file-read* (subpath "{p}"))' for p in deny_read]
    return "".join(rules)


def run_shell(command: str, ws: Path, home: Path) -> str:
    env = {"PATH": "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin",
           "HOME": str(home), "LANG": "en_US.UTF-8", "TMPDIR": "/private/tmp",
           "UV_CACHE_DIR": str(home / ".uv-cache")}
    for name in SECRET_ENV:
        if os.environ.get(name):
            env[name] = os.environ[name]
    try:
        proc = subprocess.run(["sandbox-exec", "-p", sandbox_profile(ws), "bash", "-c", command],
                              cwd=ws, env=env, capture_output=True, text=True, timeout=600)
        out = f"exit_code: {proc.returncode}\nstdout:\n{proc.stdout}\nstderr:\n{proc.stderr}"
    except subprocess.TimeoutExpired as e:
        out = f"exit_code: timeout after 600s\nstdout:\n{e.stdout or ''}\nstderr:\n{e.stderr or ''}"
    out = redact(out if isinstance(out, str) else str(out))
    if len(out) > 16000:
        out = out[:8000] + f"\n...[{len(out) - 16000} characters truncated]...\n" + out[-8000:]
    return out


def clean_item(item: dict) -> dict:
    item = {k: v for k, v in item.items() if k not in ("id", "status")}
    if item.get("type") == "message":
        item["content"] = [{k: v for k, v in c.items() if k not in ("annotations", "logprobs")} for c in item["content"]]
    return item


def call_model(model: str, items: list, effort: str) -> dict:
    tokens = ensure_fresh_token()
    payload = {"model": model, "instructions": INSTRUCTIONS, "input": items, "tools": [SHELL_TOOL],
               "tool_choice": "auto", "parallel_tool_calls": False, "store": False, "stream": True,
               "include": ["reasoning.encrypted_content"], "reasoning": {"effort": effort}}
    for attempt in range(5):
        try:
            events = post_sse(CODEX_RESPONSES_ENDPOINT, body=json.dumps(payload).encode(), timeout=900, headers={
                "Authorization": f"Bearer {tokens.access_token}", "ChatGPT-Account-Id": tokens.account_id,
                "Content-Type": "application/json", "User-Agent": "chat-gpt-oauth-skill/0.1", "originator": "opencode"})
            done = next(e for e in reversed(events) if e.get("type") in ("response.completed", "response.failed", "error"))
            if done.get("type") != "response.completed":
                raise RuntimeError(json.dumps(done)[:800])
            return done["response"]
        except Exception as exc:  # retry transient backend errors
            print(f"[model call failed, attempt {attempt + 1}] {str(exc)[:300]}", file=sys.stderr, flush=True)
            time.sleep(10 * (attempt + 1))
    raise RuntimeError("model call failed 5 times")


def main() -> int:
    ap = argparse.ArgumentParser()
    ap.add_argument("--workspace", required=True)
    ap.add_argument("--prompt-file", required=True)
    ap.add_argument("--log-dir", required=True)
    ap.add_argument("--model", default="gpt-6.1-sol")
    ap.add_argument("--effort", default="medium")
    ap.add_argument("--max-turns", type=int, default=200)
    a = ap.parse_args()
    ws = Path(a.workspace).resolve(); log_dir = Path(a.log_dir).resolve(); log_dir.mkdir(parents=True, exist_ok=True)
    home = ws.parent / (ws.name + ".home"); home.mkdir(exist_ok=True)
    prompt = Path(a.prompt_file).read_text(encoding="utf-8")
    items: list = [{"role": "user", "content": [{"type": "input_text", "text": prompt}]}]
    log = (log_dir / "transcript.jsonl").open("w", encoding="utf-8")
    t0 = time.time(); usage_in = usage_out = 0; n_tool = 0; final = ""
    for turn in range(a.max_turns):
        resp = call_model(a.model, items, a.effort)
        u = resp.get("usage") or {}; usage_in += u.get("input_tokens", 0); usage_out += u.get("output_tokens", 0)
        calls = []
        for item in resp.get("output", []):
            items.append(clean_item(item))
            if item.get("type") == "function_call":
                calls.append(item)
            elif item.get("type") == "message":
                text = "".join(c.get("text", "") for c in item.get("content", []))
                final = text
                log.write(json.dumps({"t": round(time.time() - t0, 1), "turn": turn, "assistant": text}, ensure_ascii=False) + "\n")
        if not calls:
            break
        for call in calls:
            try:
                cmd = json.loads(call["arguments"]).get("command", "")
            except Exception:
                cmd = call.get("arguments", "")
            out = run_shell(cmd, ws, home); n_tool += 1
            items.append({"type": "function_call_output", "call_id": call["call_id"], "output": out})
            log.write(json.dumps({"t": round(time.time() - t0, 1), "turn": turn, "command": redact(cmd), "output": out[:4000]}, ensure_ascii=False) + "\n")
        log.flush()
    summary = {"model": a.model, "effort": a.effort, "turns": turn + 1, "tool_calls": n_tool,
               "seconds": round(time.time() - t0, 1), "input_tokens": usage_in, "output_tokens": usage_out,
               "hit_max_turns": bool(calls), "final_message": final}
    (log_dir / "summary.json").write_text(json.dumps(summary, ensure_ascii=False, indent=1), encoding="utf-8")
    print(json.dumps({k: v for k, v in summary.items() if k != "final_message"}))
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
