import base64
import time
import types
from dataclasses import dataclass

from clan_lib.errors import ClanError

from clan_cli.qemu.qmp import QEMUMonitorProtocol


@dataclass
class VmCommandResult:
    returncode: int
    stdout: str | None
    stderr: str | None


# qga is almost like qmp, but not quite, because:
#   - server doesn't send initial message
#   - no need to initialize by asking for capabilities
#   - results need to be base64 decoded
class QgaSession:
    def __init__(self, address: str) -> None:
        self.client = QEMUMonitorProtocol(address)
        self.client.connect(negotiate=False)

    def __enter__(self) -> "QgaSession":
        # Implement context manager enter function.
        return self

    def __exit__(
        self,
        exc_type: type[BaseException] | None,
        exc_value: BaseException | None,
        traceback: types.TracebackType | None,
    ) -> None:
        # Implement context manager exit function.
        self.client.close()

    def run_nonblocking(self, cmd: list[str]) -> int:
        result_pid = self.client.cmd(
            "guest-exec", {"path": cmd[0], "arg": cmd[1:], "capture-output": True}
        )
        if result_pid is None:
            msg = "Could not get PID from QGA"
            raise ClanError(msg)
        try:
            return result_pid["return"]["pid"]
        except KeyError as e:
            if "error" in result_pid:
                msg = f"Could not run command: {result_pid['error']['desc']}"
                raise ClanError(msg) from e
            msg = f"PID could not be found: {result_pid}"
            raise ClanError(msg) from e

    # run, wait for result, return exitcode and output
    def run(self, cmd: list[str], check: bool = True) -> VmCommandResult:
        pid = self.run_nonblocking(cmd)
        # loop until exited=true
        while True:
            result = self.client.cmd("guest-exec-status", {"pid": pid})
            if result is None:
                msg = "Could not get status from QGA"
                raise ClanError(msg)
            if "error" in result and result["error"]["desc"].startswith("PID"):
                msg = "PID could not be found"
                raise ClanError(msg)
            if result["return"]["exited"]:
                break
            time.sleep(0.1)

        exitcode = result["return"]["exitcode"]
        err_data = result["return"].get("err-data")
        stdout = None
        stderr = None
        if out_data := result["return"].get("out-data"):
            stdout = base64.b64decode(out_data).decode("utf-8")
        if err_data is not None:
            stderr = base64.b64decode(err_data).decode("utf-8")
        if check and exitcode != 0:
            msg = f"Command on guest failed\nCommand: {cmd}\nExitcode {exitcode}\nStdout: {stdout}\nStderr: {stderr}"
            raise ClanError(msg)
        return VmCommandResult(exitcode, stdout, stderr)
