"""Worker task executors."""

from __future__ import annotations

from dataclasses import dataclass
import os
import signal
import shutil
import subprocess
import sys

from a2a_agent.config import AgentConfig


DEFAULT_EXECUTOR_TIMEOUT_SECONDS = 540
EXECUTOR_TIMEOUT_ENV = "A2A_EXECUTOR_TIMEOUT_SECONDS"
PROCESS_TREE_REAP_TIMEOUT_SECONDS = 10


@dataclass(frozen=True)
class ExecutorResult:
    ok: bool
    text: str


def selected_executor(config: AgentConfig) -> str:
    return os.environ.get("A2A_EXECUTOR", config.executor).strip().lower() or "echo"


def executor_timeout_seconds() -> int:
    raw_timeout = os.environ.get(EXECUTOR_TIMEOUT_ENV)
    if raw_timeout is None:
        return DEFAULT_EXECUTOR_TIMEOUT_SECONDS

    try:
        timeout = int(raw_timeout)
        if timeout <= 0:
            raise ValueError
    except ValueError:
        print(
            f"warning: invalid {EXECUTOR_TIMEOUT_ENV}={raw_timeout!r}; "
            f"using default {DEFAULT_EXECUTOR_TIMEOUT_SECONDS}",
            file=sys.stderr,
        )
        return DEFAULT_EXECUTOR_TIMEOUT_SECONDS

    return timeout


def execute_text(text: str, config: AgentConfig) -> ExecutorResult:
    executor = selected_executor(config)
    if executor == "echo":
        return ExecutorResult(ok=True, text=text)
    if executor == "opencode":
        return _run_opencode(text)
    if executor == "cursor":
        return _run_cursor(text)
    if executor == "grok":
        return _run_grok(text)
    return ExecutorResult(ok=False, text=f"unknown executor: {executor}")


def _build_command(program: str, args: list[str]) -> list[str] | None:
    """PATH에서 program 실제 경로를 찾아 실행 커맨드를 만든다.

    윈도우 npm/설치본은 program.cmd 같은 셸 스크립트라 bare 이름으로는
    파이썬 subprocess가 실행 파일을 못 찾는다. shutil.which는 PATHEXT를 존중해
    .cmd를 찾아주고, .cmd/.bat는 cmd.exe를 통해 실행해야 한다.
    """
    executable = shutil.which(program)
    if executable is None:
        return None
    if os.name == "nt" and executable.lower().endswith((".cmd", ".bat")):
        return ["cmd", "/c", executable, *args]
    return [executable, *args]


def _kill_process_tree(
    proc: subprocess.Popen[str], *, platform: str | None = None
) -> None:
    current_platform = os.name if platform is None else platform
    tree_killed = False

    if current_platform == "nt":
        try:
            completed = subprocess.run(
                ["taskkill", "/F", "/T", "/PID", str(proc.pid)],
                check=False,
                stdout=subprocess.DEVNULL,
                stderr=subprocess.DEVNULL,
                timeout=PROCESS_TREE_REAP_TIMEOUT_SECONDS,
            )
            tree_killed = completed.returncode == 0
        except (OSError, subprocess.SubprocessError):
            pass
    else:
        try:
            os.killpg(os.getpgid(proc.pid), signal.SIGKILL)
            tree_killed = True
        except OSError:
            pass

    if not tree_killed:
        try:
            proc.kill()
        except OSError:
            pass


def _kill_and_reap(proc: subprocess.Popen[str]) -> None:
    try:
        proc.kill()
    except OSError:
        pass

    try:
        proc.wait(timeout=PROCESS_TREE_REAP_TIMEOUT_SECONDS)
    except (OSError, subprocess.SubprocessError):
        pass

    # 손자 프로세스가 파이프를 계속 잡는 비정상 상황에서도 로컬 핸들을 닫는다.
    for stream in (proc.stdout, proc.stderr):
        if stream is not None:
            try:
                stream.close()
            except OSError:
                pass


def _run_external(
    program: str,
    args: list[str],
    timeout: int | None = None,
    label: str = "external executor",
) -> ExecutorResult:
    command = _build_command(program, args)
    if command is None:
        return ExecutorResult(
            ok=False,
            text=(
                f"{program}을(를) PATH에서 찾지 못했습니다. {program} 설치 후 "
                "새 터미널에서 워커를 다시 실행하세요."
            ),
        )

    effective_timeout = executor_timeout_seconds() if timeout is None else timeout
    popen_kwargs: dict[str, object] = {
        "stdout": subprocess.PIPE,
        "stderr": subprocess.PIPE,
        "text": True,
        # 윈도우 한국어 로케일은 기본 코덱이 cp949라 UTF-8 출력을 못 읽고 깨진다.
        # 외부 AI CLI 출력은 UTF-8이므로 명시적으로 UTF-8로 디코딩한다.
        "encoding": "utf-8",
        "errors": "replace",
    }
    if os.name != "nt":
        popen_kwargs["start_new_session"] = True

    try:
        proc = subprocess.Popen(command, **popen_kwargs)
    except FileNotFoundError:
        return ExecutorResult(ok=False, text=f"{program} executable was not found")

    try:
        stdout, stderr = proc.communicate(timeout=effective_timeout)
    except subprocess.TimeoutExpired:
        _kill_process_tree(proc)
        try:
            proc.communicate(timeout=PROCESS_TREE_REAP_TIMEOUT_SECONDS)
        except subprocess.TimeoutExpired:
            _kill_and_reap(proc)
        return ExecutorResult(
            ok=False,
            text=(
                f"{label} timed out after {effective_timeout} seconds; "
                "process tree killed"
            ),
        )

    stdout = stdout.strip()
    stderr = stderr.strip()
    if proc.returncode == 0:
        return ExecutorResult(ok=True, text=stdout or f"({label} returned no output)")

    detail = stderr or stdout or "no output"
    return ExecutorResult(
        ok=False,
        text=f"{label} failed with exit code {proc.returncode}: {detail}",
    )


def _run_opencode(prompt: str) -> ExecutorResult:
    return _run_external(
        "opencode",
        ["run", "-m", "opencode/big-pickle", prompt],
        label="opencode",
    )


def _run_cursor(prompt: str) -> ExecutorResult:
    return _run_external(
        "cursor-agent",
        ["-p", "--yolo", "--trust", prompt],
        label="cursor-agent",
    )


def _run_grok(prompt: str) -> ExecutorResult:
    # 일회용 호출: -c(대화 지속) 없이. --always-approve로 자동 승인.
    return _run_external(
        "grok",
        ["--always-approve", "-p", prompt],
        label="grok",
    )
