"""Agentic mode: a bounded tool loop tuned for small models.

One JSON action per turn (pclip's protocol, rebuilt fresh):

    {"action": "list_files"}
    {"action": "read_file",  "path": "..."}
    {"action": "write_file", "path": "...", "content": "..."}
    {"action": "run_tests"}
    {"action": "done"}

`done` is gated on a green test run; the gate result is fed back so the
model can keep repairing until the turn budget runs out.
"""

from __future__ import annotations

import json
import re
from pathlib import Path

from . import client, workspace
from .challenges import Challenge
from .config import Model
from .session import Session

SYSTEM = """You are a coding agent working in a workspace directory. Complete the SPEC by writing files and running the pytest suite until it passes.

Each turn, reply with ONE JSON object and nothing else:
  {{"action": "list_files"}}
  {{"action": "read_file", "path": "<relative path>"}}
  {{"action": "write_file", "path": "<relative path>", "content": "<complete file content>"}}
  {{"action": "run_tests"}}
  {{"action": "done"}}

Rules: write COMPLETE file contents (no diffs, no placeholders). Use run_tests to check your work. Only reply "done" after run_tests passes. Required deliverable files: {expected}

SPEC:
{spec}"""


def run(model: Model, challenge: Challenge, root: Path, session: Session,
        max_turns: int = 24, test_timeout: int = 120, retries: int = 3) -> None:
    messages = [
        {"role": "system", "content": SYSTEM.format(
            expected=", ".join(challenge.expected_files), spec=challenge.spec)},
        {"role": "user", "content": "Workspace ready. Begin."},
    ]
    for turn in range(1, max_turns + 1):
        completion = client.chat(model, messages, retries=retries)
        session.log("completion", turn=turn, tokens=completion.completion_tokens,
                    tokens_in=completion.prompt_tokens)
        messages.append({"role": "assistant", "content": completion.content})
        try:
            action = _parse_action(completion.content)
        except ValueError as err:
            session.log("bad_action", turn=turn, error=str(err), content=completion.content[:2000])
            messages.append({"role": "user", "content": f"Invalid action: {err}. Reply with one JSON action object."})
            continue

        session.log("action", turn=turn, **{k: v for k, v in action.items() if k != "content"},
                    content_bytes=len(action.get("content", "")))
        kind = action["action"]

        if kind == "done":
            outcome = workspace.run_tests(root, timeout=test_timeout)
            session.log("done_gate", green=outcome.green, passed=outcome.passed, failed=outcome.failed)
            if outcome.green:
                return
            messages.append({"role": "user", "content":
                             f"Not done — tests are not green:\n{outcome.output[-3000:]}\nKeep working."})
            continue

        result = _execute(kind, action, root, challenge, test_timeout, session)
        messages.append({"role": "user", "content": result})

    session.log("turn_budget_exhausted", max_turns=max_turns)


def _execute(kind: str, action: dict, root: Path, challenge: Challenge,
             test_timeout: int, session: Session) -> str:
    if kind == "list_files":
        files = sorted(str(p.relative_to(root)) for p in root.rglob("*")
                       if p.is_file() and "__pycache__" not in p.parts)
        return "Files:\n" + "\n".join(files)
    if kind == "read_file":
        path = root / action["path"]
        if not path.is_file():
            return f"No such file: {action['path']}"
        return path.read_text()[:20000]
    if kind == "write_file":
        try:
            workspace.write_file(root, action["path"], action["content"])
        except ValueError as err:
            return str(err)
        return f"Wrote {action['path']} ({len(action['content'])} bytes)."
    if kind == "run_tests":
        outcome = workspace.run_tests(root, timeout=test_timeout)
        session.log("run_tests", passed=outcome.passed, failed=outcome.failed, errored=outcome.errored)
        return f"pytest: {outcome.passed} passed, {outcome.failed} failed\n{outcome.output[-3000:]}"
    return f"Unknown action {kind!r}."


def _parse_action(content: str) -> dict:
    fenced = re.search(r"```(?:json)?\s*(\{.*?)```", content, re.DOTALL)
    text = fenced.group(1) if fenced else content
    start = text.find("{")
    if start < 0:
        raise ValueError("no JSON object found")
    try:
        obj, _ = json.JSONDecoder().raw_decode(text[start:])
    except json.JSONDecodeError as err:
        raise ValueError(f"invalid JSON: {err}") from err
    kind = obj.get("action")
    if kind not in {"list_files", "read_file", "write_file", "run_tests", "done"}:
        raise ValueError(f"unknown action {kind!r}")
    if kind in {"read_file", "write_file"} and not obj.get("path"):
        raise ValueError(f"{kind} requires a path")
    if kind == "write_file" and "content" not in obj:
        raise ValueError("write_file requires content")
    return obj
