from __future__ import annotations

import os
import shlex
import subprocess
from dataclasses import dataclass
from pathlib import Path
from typing import cast

from .base import Patcher, PatchResult
from .payload_types import PatcherMeta


@dataclass(frozen=True)
class CommandPatcher(Patcher):
    """Run a local command that prints a unified diff to stdout."""

    cmd: str
    shell: bool = False
    timeout_sec: float = 30.0
    name: str = "command"

    def propose_patch(self, *, prompt: str, context_dir: Path, cwd: Path) -> PatchResult:
        timeout = max(0.01, float(self.timeout_sec))
        env = os.environ.copy()
        env["LLMDEBUG_EVAL_CONTEXT_DIR"] = str(context_dir)
        prompt_path = context_dir / "prompt.txt"
        env["LLMDEBUG_EVAL_PROMPT_FILE"] = str(prompt_path)
        try:
            context_dir.mkdir(parents=True, exist_ok=True)
            prompt_path.write_text(prompt, encoding="utf-8")
        except OSError as exc:
            message = f"{type(exc).__name__}: {exc}"
            return PatchResult(
                diff=None,
                meta=cast(
                    PatcherMeta,
                    {
                        "error": "prompt_file_write_failed",
                        "timed_out": False,
                        "timeout_sec": timeout,
                        "stderr": message,
                        "exc": message,
                        "prompt_file": str(prompt_path),
                    },
                ),
            )
        cmd: str | list[str]
        if self.shell:
            cmd = self.cmd
        else:
            try:
                cmd = shlex.split(self.cmd)
            except ValueError as exc:
                return PatchResult(
                    diff=None,
                    meta=cast(
                        PatcherMeta,
                        {
                            "error": "invalid_patch_cmd",
                            "stderr": f"{type(exc).__name__}: {exc}",
                            "timeout_sec": timeout,
                        },
                    ),
                )
            if not cmd:
                return PatchResult(
                    diff=None,
                    meta=cast(
                        PatcherMeta,
                        {
                            "error": "empty_patch_cmd",
                            "stderr": "Command resolved to no argv tokens.",
                            "timeout_sec": timeout,
                        },
                    ),
                )

        try:
            proc = subprocess.run(
                cmd,
                input=prompt,
                text=True,
                shell=self.shell,
                cwd=cwd,
                env=env,
                capture_output=True,
                timeout=timeout,
            )
        except subprocess.TimeoutExpired as exc:
            stdout = (exc.stdout or "").strip() if isinstance(exc.stdout, str) else ""
            stderr = (exc.stderr or "").strip() if isinstance(exc.stderr, str) else ""
            return PatchResult(
                diff=None,
                meta=cast(
                    PatcherMeta,
                    {
                        "error": "timeout",
                        "timed_out": True,
                        "timeout_sec": timeout,
                        "stdout": stdout,
                        "stderr": stderr,
                    },
                ),
            )
        except OSError as exc:
            message = f"{type(exc).__name__}: {exc}"
            return PatchResult(
                diff=None,
                meta=cast(
                    PatcherMeta,
                    {
                        "error": "spawn_failed",
                        "timed_out": False,
                        "timeout_sec": timeout,
                        "stderr": message,
                        "exc": message,
                    },
                ),
            )

        diff = proc.stdout.strip() or None
        meta = cast(
            PatcherMeta,
            {
                "returncode": int(proc.returncode),
                "stdout": proc.stdout.strip(),
                "stderr": proc.stderr.strip(),
                "timed_out": False,
                "timeout_sec": timeout,
            },
        )
        return PatchResult(diff=diff, meta=meta)


def _env_bool(name: str, default: bool = False) -> bool:
    raw = os.getenv(name)
    if raw is None:
        return default
    return raw.strip().lower() in {"1", "true", "yes", "on"}


def _env_float(name: str, default: float) -> float:
    raw = os.getenv(name)
    if raw is None:
        return default
    try:
        return float(raw)
    except ValueError:
        return default


def patcher_from_env() -> CommandPatcher | None:
    cmd = os.getenv("LLMDEBUG_EVAL_PATCH_CMD", "").strip()
    if not cmd:
        return None
    shell = _env_bool("LLMDEBUG_EVAL_PATCH_CMD_SHELL", default=False)
    timeout_sec = _env_float("LLMDEBUG_EVAL_PATCH_CMD_TIMEOUT_SEC", default=30.0)
    return CommandPatcher(cmd=cmd, shell=shell, timeout_sec=timeout_sec)
