"""Local agent execution adapter for full benchmark pipeline.

JL-165: Non-Docker equivalent of benchmarks/runners/*.py for local execution.
"""

import os
import shlex
import shutil
import subprocess
import sys
import time
from dataclasses import dataclass, field
from pathlib import Path
from typing import Optional


@dataclass
class AgentAdapter:
    """Wraps an agent CLI for local execution."""
    name: str
    command_template: str
    timeout_seconds: int = 300
    env_overrides: dict = field(default_factory=dict)


@dataclass
class AgentRunResult:
    """Result of running an agent on a behavior workspace."""
    behavior_id: str
    success: bool
    duration_ms: int
    stdout: str = ""
    stderr: str = ""
    error: Optional[str] = None


# Built-in adapter presets (mirrors benchmarks/runners/ patterns)
AGENT_PRESETS = {
    "codex": AgentAdapter(
        name="codex",
        command_template="codex exec --dangerously-bypass-approvals-and-sandbox {stdin}",
        timeout_seconds=300,
    ),
    "gemini": AgentAdapter(
        name="gemini",
        command_template="gemini -y -p {prompt_content}",
        timeout_seconds=300,
    ),
    "copilot": AgentAdapter(
        name="copilot",
        command_template="copilot -p {prompt_content} --yolo",
        timeout_seconds=300,
    ),
    "claude": AgentAdapter(
        name="claude",
        command_template="claude --dangerously-skip-permissions -p {prompt_content}",
        timeout_seconds=300,
    ),
}


def detect_available_agents() -> list[str]:
    """Detect which agent CLIs are available on PATH.

    Returns list of preset keys (e.g., ["codex", "claude"]).
    """
    available = []
    cli_binaries = {
        "codex": ["codex"],
        "gemini": ["gemini"],
        "copilot": ["copilot"],
        "claude": ["claude"],
    }
    for key, binaries in cli_binaries.items():
        for binary in binaries:
            if shutil.which(binary):
                available.append(key)
                break
    return available


def _auto_commit_if_needed(workspace_dir: Path) -> bool:
    """Auto-commit uncommitted changes after agent run."""
    try:
        status = subprocess.run(
            ["git", "status", "--porcelain"],
            cwd=str(workspace_dir),
            capture_output=True,
            text=True,
            encoding="utf-8",
            errors="replace",
        )
        if status.stdout.strip():
            subprocess.run(
                ["git", "add", "-A"],
                cwd=str(workspace_dir),
                capture_output=True,
                check=True,
            )
            subprocess.run(
                ["git", "commit", "-m", "Agent solution"],
                cwd=str(workspace_dir),
                capture_output=True,
                check=True,
            )
            return True
    except (subprocess.CalledProcessError, FileNotFoundError):
        pass
    return False


def run_agent_on_workspace(
    adapter: AgentAdapter,
    workspace_dir: Path,
    prompt: str,
    behavior_id: str = "",
) -> AgentRunResult:
    """Execute agent CLI in a workspace directory.

    Writes the prompt to a temp file for adapters that consume file paths,
    then runs the agent command with cwd=workspace_dir.
    After agent exits, auto-commits if uncommitted changes exist.
    """
    prompt_file = workspace_dir / ".janus-prompt.txt"
    prompt_file.write_text(prompt, encoding="utf-8")

    template = adapter.command_template

    # {stdin} mode: pipe prompt text via subprocess stdin (e.g., Codex CLI
    # reads prompt from stdin when invoked with "-" as the prompt arg).
    stdin_mode = "{stdin}" in template
    if stdin_mode:
        cmd = template.replace("{stdin}", "-").replace("{prompt_file}", str(prompt_file))
        command: str | list[str] = cmd
        use_shell = True
    elif "{prompt_content}" in template:
        cmd_parts = shlex.split(
            template.replace("{prompt_file}", str(prompt_file))
        )
        command = [
            prompt if part == "{prompt_content}" else part
            for part in cmd_parts
        ]
        use_shell = False
    else:
        command = (
            template.replace("{prompt_file}", str(prompt_file))
            .replace("{prompt_content}", prompt)
        )
        use_shell = True

    start = time.perf_counter()
    try:
        env = {**os.environ, **adapter.env_overrides}
        # On Windows, CREATE_NEW_PROCESS_GROUP ensures timeout can kill the
        # entire process tree (otherwise shell=True children survive SIGTERM).
        extra_kwargs = {}
        if sys.platform == "win32":
            extra_kwargs["creationflags"] = subprocess.CREATE_NEW_PROCESS_GROUP
        result = subprocess.run(
            command,
            shell=use_shell,
            cwd=str(workspace_dir),
            capture_output=True,
            input=prompt if stdin_mode else None,
            text=True,
            encoding="utf-8",
            errors="replace",
            timeout=adapter.timeout_seconds,
            env=env,
            **extra_kwargs,
        )

        duration_ms = int((time.perf_counter() - start) * 1000)
        _auto_commit_if_needed(workspace_dir)
        prompt_file.unlink(missing_ok=True)

        return AgentRunResult(
            behavior_id=behavior_id,
            success=result.returncode == 0,
            duration_ms=duration_ms,
            stdout=result.stdout[-2000:] if result.stdout else "",
            stderr=result.stderr[-2000:] if result.stderr else "",
            error=None if result.returncode == 0 else f"Exit code {result.returncode}",
        )

    except subprocess.TimeoutExpired:
        duration_ms = int((time.perf_counter() - start) * 1000)
        prompt_file.unlink(missing_ok=True)
        _auto_commit_if_needed(workspace_dir)
        return AgentRunResult(
            behavior_id=behavior_id,
            success=False,
            duration_ms=duration_ms,
            error=f"Timeout after {adapter.timeout_seconds}s",
        )
    except Exception as e:
        duration_ms = int((time.perf_counter() - start) * 1000)
        prompt_file.unlink(missing_ok=True)
        return AgentRunResult(
            behavior_id=behavior_id,
            success=False,
            duration_ms=duration_ms,
            error=str(e),
        )
