import subprocess
import sys
import os
import time
import threading
from dataclasses import dataclass
from typing import Callable, Optional
from subprocess import CalledProcessError, TimeoutExpired


@dataclass
class CmdResult:
    """命令执行结果"""
    stdout: str
    stderr: str
    returncode: int
    duration: float
    command: str
    timed_out: bool = False


def execCmd(
    cmd: str,
    *,
    timeout: Optional[float] = None,
    cwd: Optional[str] = None,
    env: Optional[dict] = None,
    shell: str = "/bin/bash",
    echo: bool = False,
    on_stdout: Optional[Callable[[str], None]] = None,
    on_stderr: Optional[Callable[[str], None]] = None,
) -> CmdResult:
    """
    执行 shell 命令

    Args:
        cmd: 要执行的命令
        timeout: 超时秒数，None 表示无限制
        cwd: 工作目录
        env: 环境变量（合并到当前环境）
        shell: shell 路径，默认 /bin/bash
        echo: 是否实时打印到控制台
        on_stdout: stdout 回调函数，签名 (line: str) -> None
        on_stderr: stderr 回调函数，签名 (line: str) -> None

    Returns:
        CmdResult: 包含 stdout, stderr, returncode, duration, command, timed_out

    Raises:
        subprocess.CalledProcessError: 命令返回非零退出码
        subprocess.TimeoutExpired: 命令执行超时
    """
    # 合并环境变量
    process_env = os.environ.copy()
    if env:
        process_env.update(env)

    start_time = time.time()
    stdout_lines = []
    stderr_lines = []

    process = subprocess.Popen(
        cmd,
        shell=True,
        executable=shell,
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
        universal_newlines=True,
        bufsize=1,
        cwd=cwd,
        env=process_env,
    )

    def read_stream(stream, lines_buffer, callback, is_stderr=False):
        """读取流并处理输出"""
        for line in iter(stream.readline, ''):
            line = line.rstrip('\n\r')
            lines_buffer.append(line)
            if echo:
                if is_stderr:
                    print(line, file=sys.stderr)
                else:
                    print(line)
            if callback:
                callback(line)
        stream.close()

    # 使用线程并发读取 stdout 和 stderr
    stdout_thread = threading.Thread(
        target=read_stream,
        args=(process.stdout, stdout_lines, on_stdout, False)
    )
    stderr_thread = threading.Thread(
        target=read_stream,
        args=(process.stderr, stderr_lines, on_stderr, True)
    )

    stdout_thread.start()
    stderr_thread.start()

    # 等待进程完成或超时
    timed_out = False
    try:
        process.wait(timeout=timeout)
    except subprocess.TimeoutExpired:
        timed_out = True
        process.kill()
        # 等待线程完成读取剩余输出
        stdout_thread.join(timeout=1)
        stderr_thread.join(timeout=1)

        duration = time.time() - start_time
        stdout_str = '\n'.join(stdout_lines)
        stderr_str = '\n'.join(stderr_lines)

        raise TimeoutExpired(
            cmd=cmd,
            timeout=timeout,
            output=stdout_str,
            stderr=stderr_str,
        )

    # 等待线程完成
    stdout_thread.join()
    stderr_thread.join()

    duration = time.time() - start_time
    stdout_str = '\n'.join(stdout_lines)
    stderr_str = '\n'.join(stderr_lines)

    # 检查返回码
    if process.returncode != 0:
        raise CalledProcessError(
            returncode=process.returncode,
            cmd=cmd,
            output=stdout_str,
            stderr=stderr_str,
        )

    return CmdResult(
        stdout=stdout_str,
        stderr=stderr_str,
        returncode=process.returncode,
        duration=duration,
        command=cmd,
        timed_out=False,
    )


def runCmd(cmd):
    """
    执行命令，实时输出执行结果（向后兼容）

    内部调用 execCmd，失败时直接退出进程。
    推荐使用 execCmd 以获得更好的错误处理能力。

    Args:
        cmd: 要执行的命令

    Returns:
        CmdResult: 命令执行结果
    """
    if os.getenv("ADAM_OUTPUT_RAW"):
        return cmd

    return execCmd(cmd, echo=True)
