from __future__ import annotations

import os
import subprocess
from pathlib import Path

from llmdebug import _json as json

REPO_ROOT = Path(__file__).resolve().parents[1]
RUN_SCRIPT = REPO_ROOT / "evals" / "docker" / "run_eval_docker.sh"


def _install_docker_stub(bin_dir: Path) -> None:
    stub = bin_dir / "docker"
    stub.write_text(
        """#!/usr/bin/env python3
from llmdebug import _json as json
import os
import sys

log_path = os.environ.get("DOCKER_STUB_LOG", "")
if log_path:
    with open(log_path, "a", encoding="utf-8") as handle:
        handle.write(json.dumps({"argv": sys.argv[1:]}) + "\\n")

args = sys.argv[1:]
if len(args) >= 2 and args[0] == "image" and args[1] == "inspect":
    raise SystemExit(int(os.environ.get("DOCKER_STUB_IMAGE_INSPECT_RC", "0")))
if args and args[0] == "build":
    raise SystemExit(int(os.environ.get("DOCKER_STUB_BUILD_RC", "0")))
if args and args[0] == "run":
    is_probe = any("/repo/evals/results/.perm_probe" in item for item in args)
    code_var = "DOCKER_STUB_PROBE_RC" if is_probe else "DOCKER_STUB_RUN_RC"
    raise SystemExit(int(os.environ.get(code_var, "0")))
raise SystemExit(0)
""",
        encoding="utf-8",
    )
    stub.chmod(0o755)


def _install_uname_stub(bin_dir: Path) -> None:
    stub = bin_dir / "uname"
    stub.write_text(
        """#!/usr/bin/env bash
printf "%s\\n" "${DOCKER_STUB_UNAME:-Linux}"
""",
        encoding="utf-8",
    )
    stub.chmod(0o755)


def _arg_value(args: list[str], key: str) -> str | None:
    for index, arg in enumerate(args):
        if arg == key:
            if index + 1 < len(args):
                return args[index + 1]
            return None
        if arg.startswith(f"{key}="):
            return arg.split("=", 1)[1]
    return None


def _arg_values(args: list[str], key: str) -> list[str]:
    out: list[str] = []
    index = 0
    while index < len(args):
        arg = args[index]
        if arg == key:
            if index + 1 < len(args):
                out.append(args[index + 1])
            index += 2
            continue
        if arg.startswith(f"{key}="):
            out.append(arg.split("=", 1)[1])
        index += 1
    return out


def _run_wrapper(
    *,
    tmp_path: Path,
    args: list[str],
    uname_value: str,
    env_overrides: dict[str, str] | None = None,
) -> tuple[subprocess.CompletedProcess[str], list[list[str]]]:
    bin_dir = tmp_path / "bin"
    bin_dir.mkdir(parents=True, exist_ok=True)
    log_path = tmp_path / "docker_calls.jsonl"
    _install_docker_stub(bin_dir)
    _install_uname_stub(bin_dir)

    env = os.environ.copy()
    for key in (
        "OPENAI_MODEL",
        "OPENAI_BASE_URL",
        "OPENAI_API_KEY",
        "LLMDEBUG_EVAL_OPENAI_API_KEY",
        "Z_API",
        "DOCKER_EXTRA_ARGS",
    ):
        env.pop(key, None)

    env.update(
        {
            "PATH": f"{bin_dir}:{env.get('PATH', '')}",
            "DOCKER_STUB_LOG": str(log_path),
            "DOCKER_STUB_UNAME": uname_value,
            "DOCKER_STUB_IMAGE_INSPECT_RC": "0",
            "DOCKER_STUB_RUN_RC": "0",
            "LLMDEBUG_EVAL_DOCKER_USER": "1000:1000",
        }
    )
    if env_overrides:
        env.update(env_overrides)

    proc = subprocess.run(
        ["bash", str(RUN_SCRIPT), *args],
        cwd=REPO_ROOT,
        check=False,
        capture_output=True,
        text=True,
        env=env,
    )

    calls: list[list[str]] = []
    if log_path.exists():
        for line in log_path.read_text(encoding="utf-8").splitlines():
            line = line.strip()
            if not line:
                continue
            payload = json.loads(line)
            calls.append(list(payload["argv"]))
    return proc, calls


def _main_run_call(calls: list[list[str]]) -> list[str]:
    run_calls = [call for call in calls if call and call[0] == "run"]
    assert run_calls
    return run_calls[-1]


def _split_docker_and_eval_args(
    docker_run_call: list[str], *, image: str = "llmdebug-eval:latest"
) -> tuple[list[str], list[str]]:
    assert docker_run_call and docker_run_call[0] == "run"
    image_index = docker_run_call.index(image)
    docker_args = docker_run_call[1:image_index]
    eval_args = docker_run_call[image_index + 1 :]
    return docker_args, eval_args


def test_run_eval_docker_forwards_openai_autoswitch_flags_and_forces_local_executor(
    tmp_path: Path,
) -> None:
    proc, calls = _run_wrapper(
        tmp_path=tmp_path,
        uname_value="Linux",
        args=[
            "--patcher",
            "openai",
            "--openai-model",
            "qwen-demo",
            "--openai-base-url",
            "http://127.0.0.1:1234/v1",
            "--openai-response-mode",
            "edits",
            "--openai-response-mode-autoswitch",
            "retry_to_diff",
            "--openai-auto-context-window",
            "--openai-force-patch-attempt",
            "--executor",
            "testcontainers",
            "--container-network",
            "none",
            "--k",
            "3",
        ],
    )

    assert proc.returncode == 0, proc.stderr
    docker_args, eval_args = _split_docker_and_eval_args(_main_run_call(calls))
    assert _arg_value(docker_args, "--network") == "host"
    assert "--openai-auto-context-window" in eval_args
    assert _arg_value(eval_args, "--openai-response-mode-autoswitch") == "retry_to_diff"
    assert _arg_value(eval_args, "--openai-base-url") == "http://127.0.0.1:1234/v1"
    assert "--container-network" not in eval_args
    assert "testcontainers" not in eval_args
    assert _arg_value(eval_args, "--executor") == "local"


def test_run_eval_docker_forwards_no_openai_http2_flag(tmp_path: Path) -> None:
    proc, calls = _run_wrapper(
        tmp_path=tmp_path,
        uname_value="Linux",
        args=[
            "--patcher",
            "openai",
            "--openai-model",
            "qwen-demo",
            "--openai-base-url",
            "http://127.0.0.1:1234/v1",
            "--no-openai-http2",
            "--k",
            "1",
        ],
    )

    assert proc.returncode == 0, proc.stderr
    _, eval_args = _split_docker_and_eval_args(_main_run_call(calls))
    assert "--no-openai-http2" in eval_args


def test_run_eval_docker_translates_localhost_url_on_darwin(tmp_path: Path) -> None:
    proc, calls = _run_wrapper(
        tmp_path=tmp_path,
        uname_value="Darwin",
        args=[
            "--patcher",
            "openai",
            "--openai-model",
            "qwen-demo",
            "--openai-base-url",
            "http://localhost:1234/v1",
            "--k",
            "1",
        ],
    )

    assert proc.returncode == 0, proc.stderr
    docker_args, eval_args = _split_docker_and_eval_args(_main_run_call(calls))
    assert not _arg_values(docker_args, "--network")
    assert _arg_value(eval_args, "--openai-base-url") == "http://host.docker.internal:1234/v1"
    assert "Translating localhost URL for Docker access" in proc.stderr


def test_run_eval_docker_uses_openai_env_defaults_with_key_precedence(tmp_path: Path) -> None:
    proc, calls = _run_wrapper(
        tmp_path=tmp_path,
        uname_value="Linux",
        args=[
            "--patcher",
            "openai",
            "--k",
            "1",
        ],
        env_overrides={
            "OPENAI_MODEL": "env-model",
            "OPENAI_BASE_URL": "http://127.0.0.1:9999/v1",
            "OPENAI_API_KEY": "openai-key",
            "LLMDEBUG_EVAL_OPENAI_API_KEY": "llmdebug-key",
            "Z_API": "z-key",
        },
    )

    assert proc.returncode == 0, proc.stderr
    docker_args, eval_args = _split_docker_and_eval_args(_main_run_call(calls))
    assert _arg_value(eval_args, "--openai-model") == "env-model"
    assert _arg_value(eval_args, "--openai-base-url") == "http://127.0.0.1:9999/v1"
    assert _arg_value(eval_args, "--openai-api-key") == "z-key"
    assert _arg_value(docker_args, "--network") == "host"


def test_run_eval_docker_keeps_explicit_network_from_docker_extra_args(tmp_path: Path) -> None:
    proc, calls = _run_wrapper(
        tmp_path=tmp_path,
        uname_value="Linux",
        args=[
            "--patcher",
            "openai",
            "--openai-model",
            "qwen-demo",
            "--openai-base-url",
            "http://localhost:1234/v1",
            "--k",
            "1",
        ],
        env_overrides={"DOCKER_EXTRA_ARGS": "--network bridge"},
    )

    assert proc.returncode == 0, proc.stderr
    docker_args, _ = _split_docker_and_eval_args(_main_run_call(calls))
    assert _arg_values(docker_args, "--network") == ["bridge"]


def test_run_eval_docker_forwards_case_list_and_transport_retry_flags(tmp_path: Path) -> None:
    case_list_path = tmp_path / "failed_cases.txt"
    case_list_path.write_text("case_a\ncase_b\n", encoding="utf-8")

    proc, calls = _run_wrapper(
        tmp_path=tmp_path,
        uname_value="Linux",
        args=[
            "--patcher",
            "openai",
            "--openai-model",
            "qwen-demo",
            "--openai-base-url",
            "http://127.0.0.1:1234/v1",
            "--case-list-file",
            str(case_list_path),
            "--openai-transport-max-retries",
            "9",
            "--openai-transport-retry-backoff-base-sec",
            "1.5",
            "--openai-transport-retry-backoff-max-sec",
            "12.0",
            "--k",
            "1",
        ],
    )

    assert proc.returncode == 0, proc.stderr
    _, eval_args = _split_docker_and_eval_args(_main_run_call(calls))
    assert _arg_value(eval_args, "--case-list-file") == str(case_list_path)
    assert _arg_value(eval_args, "--openai-transport-max-retries") == "9"
    assert _arg_value(eval_args, "--openai-transport-retry-backoff-base-sec") == "1.5"
    assert _arg_value(eval_args, "--openai-transport-retry-backoff-max-sec") == "12.0"


def test_run_eval_docker_rewrites_config_path_for_container(tmp_path: Path) -> None:
    config_path = tmp_path / "run_eval.toml"
    config_path.write_text(
        """
[run_eval]
patcher = "null"
conditions = "traceback_only"
k = 1
""".strip()
        + "\n",
        encoding="utf-8",
    )

    proc, calls = _run_wrapper(
        tmp_path=tmp_path,
        uname_value="Linux",
        args=[
            "--config",
            str(config_path),
            "--run-id",
            "docker-config-path-rewrite",
        ],
    )

    assert proc.returncode == 0, proc.stderr
    _, eval_args = _split_docker_and_eval_args(_main_run_call(calls))
    rewritten_path = _arg_value(eval_args, "--config")
    assert rewritten_path is not None
    assert rewritten_path.startswith("/repo/evals/artifacts/config_mounts/")
    assert str(config_path) not in eval_args


def test_run_eval_docker_reports_missing_config_file(tmp_path: Path) -> None:
    missing_path = tmp_path / "missing.toml"
    proc, _calls = _run_wrapper(
        tmp_path=tmp_path,
        uname_value="Linux",
        args=[
            "--config",
            str(missing_path),
        ],
    )

    assert proc.returncode == 2
    assert "Config file not found" in proc.stderr
