# pyenv.py
from __future__ import annotations

import os
import subprocess
from dataclasses import dataclass, field
from pathlib import Path
from typing import Sequence, Union

from ..waiting_config import WaitingConfig, WaitingConfigArg

__all__ = ["SystemCommandError", "SystemCommand"]


def _is_windows() -> bool:
    return os.name == "nt"

def _format_cmd(args: Sequence[str]) -> str:
    return " ".join(map(str, args))


@dataclass(slots=True)
class SystemCommand:
    """
    Lazy command result:
    - created with a running Popen
    - call wait() to collect stdout/stderr and returncode
    - call raise_on_status() to error if non-zero
    """
    args: tuple[str, ...]
    cwd: Path | None
    env: dict[str, str] | None
    popen: subprocess.Popen[str]
    completed: subprocess.CompletedProcess[str] | None = field(default=None, init=False, repr=False)

    @staticmethod
    def run_sync(
        args: Sequence[str],
        *,
        cwd: Path | None = None,
        env: dict[str, str] | None = None,
        check: bool = True,
    ) -> subprocess.CompletedProcess[str]:
        proc = subprocess.run(
            list(args),
            cwd=str(cwd) if cwd else None,
            env=env,
            text=True,
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
        )
        if check and proc.returncode != 0:
            raise SystemCommandError(
                f"Command failed ({proc.returncode}): {_format_cmd(args)}\n"
                f"--- stdout ---\n{proc.stdout}\n"
                f"--- stderr ---\n{proc.stderr}\n"
            )
        return proc

    @staticmethod
    def run_lazy(
        args: Sequence[str],
        *,
        cwd: Path | None = None,
        env: dict[str, str] | None = None,
    ) -> SystemCommand:
        popen = subprocess.Popen(
            list(args),
            cwd=str(cwd) if cwd else None,
            env=env,
            text=True,
            stdin=subprocess.PIPE,
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
        )

        return SystemCommand(
            args=tuple(map(str, args)),
            cwd=cwd,
            env=env,
            popen=popen,
        )

    def poll(self) -> int | None:
        return self.popen.poll()

    @property
    def returncode(self) -> int | None:
        return self.popen.returncode if self.completed is None else self.completed.returncode

    @property
    def stdout(self) -> str | None:
        return None if self.completed is None else self.completed.stdout

    @property
    def stderr(self) -> str | None:
        return None if self.completed is None else self.completed.stderr

    def wait(
        self,
        wait: WaitingConfigArg | None = True,
        raise_error: bool = True
    ) -> Union["SystemCommand", "SystemCommandError"]:
        if self.completed is not None:
            return self.completed  # type: ignore[return-value]

        wait_cfg = WaitingConfig.check_arg(wait)

        if wait_cfg.timeout:
            out, err = self.popen.communicate(timeout=wait_cfg.timeout_total_seconds)
            self.completed = subprocess.CompletedProcess(
                args=list(self.args),
                returncode=self.popen.returncode or 0,
                stdout=out,
                stderr=err,
            )

            return self.raise_for_status(raise_error=raise_error)
        return self

    def raise_for_status(
        self,
        wait: WaitingConfigArg | None = False,
        raise_error: bool = True
    ) -> Union["SystemCommand", "SystemCommandError"]:
        if self.returncode != 0:
            e = SystemCommandError(command=self)

            if raise_error:
                raise e
            return e

        return self


@dataclass(frozen=True, slots=True)
class SystemCommandError(RuntimeError):
    """Raised when a subprocess command fails."""
    command: SystemCommand

    def __str__(self):
        cp = self.command

        return (
            f"{cp} failed:\n"
            f"--- stdout ---\n{cp.stdout}\n"
            f"--- stderr ---\n{cp.stderr}\n"
        )

    def __repr__(self):
        cp = self.command

        return (
            f"{cp} failed:\n"
            f"--- stdout ---\n{cp.stdout}\n"
            f"--- stderr ---\n{cp.stderr}\n"
        )