import asyncio
import shutil
import logging
from pathlib import Path
from functools import lru_cache

logger = logging.getLogger(__name__)

@lru_cache(maxsize=None)
def _which(executable: str) -> str:
    return shutil.which(executable) or executable

async def await_command(cmd: list[str], cwd: Path, envs: dict | None = None) -> tuple[str, str, int]:
    logger.debug([_which(cmd[0]), *cmd[1:]])
    proc = await asyncio.create_subprocess_exec(
        _which(cmd[0]), *cmd[1:],
        cwd=cwd,
        env=envs,
        stdout=asyncio.subprocess.PIPE,
        stderr=asyncio.subprocess.PIPE,
    )
    stdout, stderr = await proc.communicate()
    return stdout.decode(), stderr.decode(), proc.returncode or 0


async def stream_command(
    cmd: list[str],
    cwd: Path,
    envs: dict | None = None,
    line_callback=None,
) -> tuple[str, str, int]:
    """Run a command streaming stdout line-by-line. Calls await line_callback(line) for each line."""
    logger.debug([_which(cmd[0]), *cmd[1:]])
    limit = 10 * 1024 * 1024  # 10 MB
    proc = await asyncio.create_subprocess_exec(
        _which(cmd[0]), *cmd[1:],
        cwd=cwd,
        env=envs,
        stdout=asyncio.subprocess.PIPE,
        stderr=asyncio.subprocess.PIPE,
        limit=limit,
    )

    stdout_lines: list[str] = []
    assert proc.stdout is not None
    async for raw_line in proc.stdout:
        line = raw_line.decode()
        stdout_lines.append(line)
        if line_callback is not None:
            await line_callback(line)

    _, stderr = await proc.communicate()
    return "".join(stdout_lines), stderr.decode(), proc.returncode or 0
