import os
import subprocess


class Chroot:
    """
    Sets up mounts for the virtual filesystems inside a root tree, preparing it for running commands using chroot. This
    should be used whenever a stage needs to run a command against the root tree but doesn't support a --root option or
    similar.
    Cleans up mounts when done.

    This mounts /proc, /dev, and /sys.
    """

    def __init__(self, root: str, bind_mounts=None):
        self.root = root
        self._bind_mounts = bind_mounts or []

    def __enter__(self):
        for d in ["/proc", "/dev", "/sys"]:
            if not os.path.exists(self.root + d):
                print(f"Making missing chroot directory: {d}")
                os.makedirs(self.root + d)

        subprocess.run(["mount", "-t", "proc", "-o", "nosuid,noexec,nodev",
                        "proc", f"{self.root}/proc"],
                       check=True)

        try:
            subprocess.run(["mount", "-t", "devtmpfs", "-o", "mode=0755,noexec,nosuid,strictatime",
                            "devtmpfs", f"{self.root}/dev"],
                           check=True)
        except subprocess.CalledProcessError:
            # If we're not allowed to mount a new devtmpfs, try bind-mounting the host /dev
            subprocess.run(["mount", "--bind", "/dev", f"{self.root}/dev"],
                           check=True)

        subprocess.run(["mount", "-t", "sysfs", "-o", "nosuid,noexec,nodev",
                        "sysfs", f"{self.root}/sys"],
                       check=True)

        for d in self._bind_mounts:
            target_path = os.path.join(self.root, d.lstrip("/"))
            if not os.path.exists(target_path):
                print(f"Making missing chroot directory: {d}")
                os.makedirs(target_path)
            subprocess.run(["mount", "--rbind", d, target_path], check=True)

        return self

    def __exit__(self, exc_type, exc_value, tracebk):
        failed_umounts = []
        for d in ["/proc", "/dev", "/sys"]:
            if subprocess.run(["umount", "--lazy", self.root + d], check=False).returncode != 0:
                failed_umounts.append(d)
        for d in self._bind_mounts[::-1]:
            target_path = os.path.join(self.root, d.lstrip("/"))
            if subprocess.run(["umount", "--lazy", target_path], check=False).returncode != 0:
                failed_umounts.append(d)
        if failed_umounts:
            print(f"Error unmounting paths from chroot: {failed_umounts}")

    def run(self, cmd, **kwargs):
        cmd = ["chroot", self.root] + cmd
        # pylint: disable=subprocess-run-check
        return subprocess.run(cmd, **kwargs)  # noqa: PLW1510
