from dataclasses import dataclass
import enum
import glob
import os
from os import PathLike
import pathlib
import re
import shutil
import stat
from typing import Any, Final, Iterator, TypedDict

from ...config import GlobalConfig
from ...i18n import _
from ...log import RuyiLogger, humanize_list
from ...ruyipkg.atom import Atom
from ...ruyipkg.pkg_manifest import BoundPackageManifest, EmulatorProgDecl
from ...ruyipkg.profile import ProfileProxy
from ...utils.global_mode import ProvidesGlobalMode
from ...utils.l10n import match_lang_code
from ...utils.templating import render_template_str
from .emulator_cfg import ResolvedEmulatorProg


class ConfiguredTargetTuple(TypedDict):
    target: str
    toolchain_root: PathLike[Any]
    toolchain_sysroot: PathLike[Any] | None
    toolchain_flags: str
    binutils_flavor: str
    cc_flavor: str
    gcc_install_dir: PathLike[Any] | None


class ResolvedSysrootPkgSource(TypedDict):
    sysroot_dir: PathLike[Any]
    pkg_manifest: BoundPackageManifest
    gcc_install_dir: PathLike[Any] | None


class SysrootProvisionMode(enum.Enum):
    COPY_TREE = "copy-tree"
    SYMLINK_TREE = "symlink-tree"
    PROJECT_ROOTFS = "project-rootfs"


class VenvProvisionError(Exception):
    pass


PROJECTED_SYSROOT_ROOTS: Final = (
    "include",
    "lib",
    "lib32",
    "lib64",
    "usr/include",
    "usr/lib",
    "usr/lib32",
    "usr/lib64",
    "usr/libexec",
    "usr/share",
    "bin",
    "sbin",
)


@dataclass
class SysrootProjectionReport:
    copied_entries: int = 0
    skipped_entries: int = 0
    included_roots: int = 0


def _count_copytree_failures(exc: shutil.Error) -> int:
    if not exc.args:
        return 1

    failures = exc.args[0]
    if isinstance(failures, list):
        return len(failures)

    return 1


def _is_projected_rootfs_relpath(relpath: pathlib.PurePosixPath) -> bool:
    for root in PROJECTED_SYSROOT_ROOTS:
        projected_root = pathlib.PurePosixPath(root)
        if relpath == projected_root or projected_root in relpath.parents:
            return True

    return False


def _copy_projected_sysroot_symlink(
    source_root: pathlib.Path,
    dest_root: pathlib.Path,
    src: pathlib.Path,
    dest: pathlib.Path,
) -> None:
    target = os.readlink(src)
    if not target.startswith("/"):
        os.symlink(target, dest)
        return

    target_relpath = pathlib.PurePosixPath(target.removeprefix("/"))
    target_in_source = source_root.joinpath(*target_relpath.parts)
    if (
        _is_projected_rootfs_relpath(target_relpath)
        and (target_in_source.exists() or target_in_source.is_symlink())
    ):
        target_in_dest = dest_root.joinpath(*target_relpath.parts)
        os.symlink(os.path.relpath(target_in_dest, dest.parent), dest)
        return

    os.symlink(target, dest)


def _copy_projected_sysroot_entry(
    logger: RuyiLogger,
    source_root: pathlib.Path,
    dest_root: pathlib.Path,
    src: pathlib.Path,
    dest: pathlib.Path,
    report: SysrootProjectionReport,
) -> None:
    try:
        mode = src.lstat().st_mode
    except OSError as e:
        report.skipped_entries += 1
        logger.D(f"skipping sysroot entry {src}: {e}")
        return

    try:
        if stat.S_ISLNK(mode):
            dest.parent.mkdir(parents=True, exist_ok=True)
            _copy_projected_sysroot_symlink(source_root, dest_root, src, dest)
            report.copied_entries += 1
            return

        if stat.S_ISDIR(mode):
            dest.mkdir(parents=True, exist_ok=True)
            report.copied_entries += 1
            try:
                children = list(src.iterdir())
            except OSError as e:
                report.skipped_entries += 1
                logger.D(f"skipping sysroot directory contents {src}: {e}")
                return

            for child in children:
                _copy_projected_sysroot_entry(
                    logger,
                    source_root,
                    dest_root,
                    child,
                    dest / child.name,
                    report,
                )
            return

        if stat.S_ISREG(mode):
            dest.parent.mkdir(parents=True, exist_ok=True)
            shutil.copy2(src, dest, follow_symlinks=False)
            report.copied_entries += 1
            return
    except OSError as e:
        report.skipped_entries += 1
        logger.D(f"skipping sysroot entry {src}: {e}")
        return

    report.skipped_entries += 1
    logger.D(f"skipping unsupported sysroot entry {src}")


def _project_rootfs_sysroot(
    logger: RuyiLogger,
    src: pathlib.Path,
    dest: pathlib.Path,
) -> SysrootProjectionReport:
    report = SysrootProjectionReport()
    dest.mkdir(parents=True, exist_ok=False)

    for root in PROJECTED_SYSROOT_ROOTS:
        root_src = src / root
        if not root_src.exists() and not root_src.is_symlink():
            continue

        report.included_roots += 1
        _copy_projected_sysroot_entry(
            logger,
            src,
            dest,
            root_src,
            dest / root,
            report,
        )

    return report


def provision_sysroot(
    logger: RuyiLogger,
    src: pathlib.Path,
    dest: pathlib.Path,
    mode: SysrootProvisionMode,
    target_tuple: str,
) -> None:
    if mode == SysrootProvisionMode.SYMLINK_TREE:
        logger.D(f"symlinking sysroot for {target_tuple}")
        os.symlink(src, dest)
        return

    if mode == SysrootProvisionMode.COPY_TREE:
        logger.D(f"copying sysroot for {target_tuple}")
        try:
            shutil.copytree(
                src,
                dest,
                symlinks=True,
                ignore_dangling_symlinks=True,
            )
        except shutil.Error as e:
            failure_count = _count_copytree_failures(e)
            if failure_count == 1:
                reason = _("one entry could not be copied")
            else:
                reason = _("{count} entries could not be copied").format(
                    count=failure_count,
                )
            logger.F(
                _(
                    "cannot copy sysroot from [yellow]{src}[/]: {reason}"
                ).format(
                    src=src,
                    reason=reason,
                )
            )
            logger.I(
                _(
                    "Ruyi does not elevate privileges when creating virtual environments; use a sysroot readable by the current user, --symlink-sysroot-from-dir, or --project-sysroot-from-rootfs"
                )
            )
            logger.D(f"sysroot copy failure details: {e}")
            raise VenvProvisionError from e
        except OSError as e:
            logger.F(
                _("cannot copy sysroot from [yellow]{src}[/] to [green]{dest}[/]: {err}").format(
                    src=src,
                    dest=dest,
                    err=e,
                )
            )
            raise VenvProvisionError from e
        return

    if mode == SysrootProvisionMode.PROJECT_ROOTFS:
        logger.D(f"projecting rootfs sysroot for {target_tuple}")
        try:
            report = _project_rootfs_sysroot(logger, src, dest)
        except OSError as e:
            logger.F(
                _("cannot project sysroot from [yellow]{src}[/] to [green]{dest}[/]: {err}").format(
                    src=src,
                    dest=dest,
                    err=e,
                )
            )
            raise VenvProvisionError from e

        if report.included_roots == 0:
            logger.F(
                _(
                    "cannot project sysroot from [yellow]{src}[/]: no supported sysroot directories were found"
                ).format(src=src)
            )
            raise VenvProvisionError

        logger.I(
            _(
                "projected sysroot from [yellow]{src}[/]: copied {copied_count} entries, skipped {skipped_count} entries"
            ).format(
                src=src,
                copied_count=report.copied_entries,
                skipped_count=report.skipped_entries,
            )
        )
        if report.skipped_entries:
            logger.W(
                _(
                    "some unreadable or unsupported files were skipped; the projected sysroot may be incomplete"
                )
            )
        return

    raise NotImplementedError(mode)


def _resolve_sysroot_pkg_source(
    config: GlobalConfig,
    host: str,
    sysroot_atom_str: str,
) -> ResolvedSysrootPkgSource | int:
    """Resolve a sysroot package source from an atom string.

    Returns ResolvedSysrootPkgSource on success, or an error exit code (int).
    """
    logger = config.logger
    mr = config.repo

    gcc_pkg_atom = Atom.parse(sysroot_atom_str)
    gcc_pkg_pm = gcc_pkg_atom.match_in_repo(mr, config.include_prereleases)
    if gcc_pkg_pm is None:
        logger.F(
            _("cannot match a toolchain package with [yellow]{atom}[/]").format(
                atom=sysroot_atom_str,
            )
        )
        return 1

    if gcc_pkg_pm.toolchain_metadata is None:
        logger.F(
            _("the package [yellow]{atom}[/] is not a toolchain").format(
                atom=sysroot_atom_str,
            )
        )
        return 1

    gcc_pkg_root = config.lookup_binary_install_dir(
        host,
        gcc_pkg_pm.name_for_installation,
    )
    if gcc_pkg_root is None:
        logger.F(
            _("cannot find the installed directory for the sysroot package")
        )
        return 1

    tc_sysroot_relpath = gcc_pkg_pm.toolchain_metadata.included_sysroot
    if tc_sysroot_relpath is None:
        logger.F(
            _("sysroot is requested but the package [yellow]{atom}[/] does not contain one").format(
                atom=sysroot_atom_str,
            )
        )
        return 1

    sysroot_dir = pathlib.Path(gcc_pkg_root) / tc_sysroot_relpath

    # also figure the GCC include/libs path out for Clang to be able to
    # locate them
    gcc_install_dir = find_gcc_install_dir(
        gcc_pkg_root,
        # we should use the GCC-providing package's target tuple as that's
        # not guaranteed to be the same as llvm's
        gcc_pkg_pm.toolchain_metadata.target,
    )

    # for now, require this directory to be present (or clang would barely work)
    if gcc_install_dir is None:
        logger.F(
            _("cannot find a GCC include & lib directory in the sysroot package")
        )
        return 1

    return ResolvedSysrootPkgSource(
        sysroot_dir=sysroot_dir,
        pkg_manifest=gcc_pkg_pm,
        gcc_install_dir=gcc_install_dir,
    )


class VenvPackageInfo(TypedDict):
    repo_id: str
    category: str
    name: str
    version: str


class VenvMetadata(TypedDict):
    emulator_pkgs: dict[str, VenvPackageInfo]
    extra_pkgs: list[VenvPackageInfo]
    sysroot_pkg: VenvPackageInfo | None
    toolchain_pkgs: dict[str, VenvPackageInfo]


def _venv_pkg_info_from_pkg(pkg: BoundPackageManifest) -> VenvPackageInfo:
    return VenvPackageInfo(
        repo_id=pkg.repo_id,
        category=pkg.category,
        name=pkg.name,
        version=pkg.ver,
    )


def do_make_venv(
    config: GlobalConfig,
    host: str,
    profile_name: str,
    dest: pathlib.Path,
    with_sysroot: bool,
    override_name: str | None = None,
    tc_atoms_str: list[str] | None = None,
    emu_atom_str: str | None = None,
    sysroot_atom_str: str | None = None,
    copy_sysroot_dir_str: str | None = None,
    symlink_sysroot_dir_str: str | None = None,
    project_sysroot_dir_str: str | None = None,
    extra_cmd_atoms_str: list[str] | None = None,
) -> int:
    logger = config.logger

    explicit_sysroot_dir: pathlib.Path | None = None
    explicit_sysroot_gcc_install_dir: PathLike[Any] | None = None
    explicit_sysroot_pkg: VenvPackageInfo | None = None
    sysroot_provision_mode = SysrootProvisionMode.COPY_TREE

    if sysroot_atom_str is not None:
        result = _resolve_sysroot_pkg_source(config, host, sysroot_atom_str)
        if isinstance(result, int):
            return result

        explicit_sysroot_dir = pathlib.Path(result["sysroot_dir"])
        explicit_sysroot_gcc_install_dir = result["gcc_install_dir"]
        explicit_sysroot_pkg = _venv_pkg_info_from_pkg(result["pkg_manifest"])
    elif copy_sysroot_dir_str is not None:
        explicit_sysroot_dir = pathlib.Path(copy_sysroot_dir_str).resolve()
        if not explicit_sysroot_dir.is_dir():
            logger.F(
                _("the sysroot directory [yellow]{path}[/] does not exist").format(
                    path=copy_sysroot_dir_str,
                )
            )
            return 1
    elif symlink_sysroot_dir_str is not None:
        explicit_sysroot_dir = pathlib.Path(symlink_sysroot_dir_str).resolve()
        if not explicit_sysroot_dir.is_dir():
            logger.F(
                _("the sysroot directory [yellow]{path}[/] does not exist").format(
                    path=symlink_sysroot_dir_str,
                )
            )
            return 1
        sysroot_provision_mode = SysrootProvisionMode.SYMLINK_TREE
    elif project_sysroot_dir_str is not None:
        explicit_sysroot_dir = pathlib.Path(project_sysroot_dir_str).resolve()
        if not explicit_sysroot_dir.is_dir():
            logger.F(
                _("the rootfs directory [yellow]{path}[/] does not exist").format(
                    path=project_sysroot_dir_str,
                )
            )
            return 1
        sysroot_provision_mode = SysrootProvisionMode.PROJECT_ROOTFS

    # TODO: support omitting this if user only has one toolchain installed
    # this should come after implementation of local state cache
    if tc_atoms_str is None:
        logger.F(
            _("You have to specify at least one toolchain atom for now, e.g. [yellow]`-t gnu-plct`[/]")
        )
        return 1

    mr = config.repo

    profile = mr.get_profile(profile_name)
    if profile is None:
        logger.F(_("profile '{profile}' not found").format(profile=profile_name))
        return 1

    target_arch = ""
    seen_target_tuples: set[str] = set()
    targets: list[ConfiguredTargetTuple] = []
    warn_differing_target_arch = False
    venv_metadata = VenvMetadata(
        emulator_pkgs={},
        extra_pkgs=[],
        sysroot_pkg=None,
        toolchain_pkgs={},
    )

    for tc_atom_str in tc_atoms_str:
        tc_atom = Atom.parse(tc_atom_str)
        tc_pm = tc_atom.match_in_repo(mr, config.include_prereleases)
        if tc_pm is None:
            logger.F(_("cannot match a toolchain package with [yellow]{atom}[/]").format(
                atom=tc_atom_str,
            ))
            return 1

        if tc_pm.toolchain_metadata is None:
            logger.F(_("the package [yellow]{atom}[/] is not a toolchain").format(
                atom=tc_atom_str,
            ))
            return 1

        if not tc_pm.toolchain_metadata.satisfies_quirk_set(profile.need_quirks):
            logger.F(_(
                "the package [yellow]{atom}[/] does not support all necessary features for the profile [yellow]{profile}[/]"
                ).format(
                    atom=tc_atom_str,
                    profile=profile_name,
                    )
            )
            logger.I(
                _("quirks needed by profile:   {humanized_list}").format(
                humanized_list=humanize_list(profile.need_quirks, item_color='cyan'),
                )
            )
            logger.I(
                _("quirks provided by package: {humanized_list}").format(
                humanized_list=humanize_list(tc_pm.toolchain_metadata.quirks, item_color='yellow'),
                )
            )
            return 1

        target_tuple = tc_pm.toolchain_metadata.target
        if target_tuple in seen_target_tuples:
            logger.F(
                _("the target tuple [yellow]{target_tuple}[/] is already covered by one of the requested toolchains").format(
                    target_tuple=target_tuple,
                    )
            )
            logger.I(
                _("for now, only toolchains with differing target tuples can co-exist in one virtual environment")
            )
            return 1

        toolchain_root = config.lookup_binary_install_dir(
            host,
            tc_pm.name_for_installation,
        )
        if toolchain_root is None:
            logger.F(_("cannot find the installed directory for the toolchain"))
            return 1

        tc_sysroot_dir: PathLike[Any] | None = None
        gcc_install_dir: PathLike[Any] | None = None
        if with_sysroot:
            if explicit_sysroot_dir is not None:
                tc_sysroot_dir = explicit_sysroot_dir
                gcc_install_dir = explicit_sysroot_gcc_install_dir
                if explicit_sysroot_pkg is not None:
                    venv_metadata["sysroot_pkg"] = explicit_sysroot_pkg
            elif tc_sysroot_relpath := tc_pm.toolchain_metadata.included_sysroot:
                tc_sysroot_dir = pathlib.Path(toolchain_root) / tc_sysroot_relpath
                venv_metadata["sysroot_pkg"] = _venv_pkg_info_from_pkg(tc_pm)
            else:
                logger.F(
                    _("sysroot is requested but the toolchain package does not include one, and no explicit sysroot source is given")
                )
                return 1

        # derive flags for (the quirks of) this toolchain
        tc_flags = profile.get_common_flags(tc_pm.toolchain_metadata.quirks)

        # record the target tuple info to configure in the venv
        configured_target: ConfiguredTargetTuple = {
            "target": target_tuple,
            "toolchain_root": toolchain_root,
            "toolchain_sysroot": tc_sysroot_dir,
            "toolchain_flags": tc_flags,
            # assume clang is preferred if package contains clang
            # this is mostly true given most packages don't contain both
            "cc_flavor": "clang" if tc_pm.toolchain_metadata.has_clang else "gcc",
            # same for binutils provider flavor
            "binutils_flavor": (
                "llvm" if tc_pm.toolchain_metadata.has_llvm else "binutils"
            ),
            "gcc_install_dir": gcc_install_dir,
        }
        logger.D(f"configuration for {target_tuple}: {configured_target}")
        targets.append(configured_target)
        seen_target_tuples.add(target_tuple)
        venv_metadata["toolchain_pkgs"][target_tuple] = _venv_pkg_info_from_pkg(tc_pm)

        # record the target architecture for use in emulator package matching
        if not target_arch:
            target_arch = tc_pm.toolchain_metadata.target_arch
        elif target_arch != tc_pm.toolchain_metadata.target_arch:
            # first one wins
            warn_differing_target_arch = True

    if warn_differing_target_arch:
        logger.W(_("multiple toolchains specified with differing target architecture"))
        logger.I(
            _("using the target architecture of the first toolchain: [yellow]{arch}[/]").format(
                arch=target_arch,
                )
        )

    # Now handle the emulator.
    emu_progs = None
    emu_root: PathLike[Any] | None = None
    if emu_atom_str:
        emu_atom = Atom.parse(emu_atom_str)
        emu_pm = emu_atom.match_in_repo(mr, config.include_prereleases)
        if emu_pm is None:
            logger.F(_("cannot match an emulator package with [yellow]{atom}[/]").format(
                atom=emu_atom_str,
            ))
            return 1

        if emu_pm.emulator_metadata is None:
            logger.F(_("the package [yellow]{atom}[/] is not an emulator").format(
                atom=emu_atom_str,
            ))
            return 1

        emu_progs = list(emu_pm.emulator_metadata.list_for_arch(target_arch))
        if not emu_progs:
            logger.F(
                _("the emulator package [yellow]{atom}[/] does not support the target architecture [yellow]{arch}[/]").format(
                    atom=emu_atom_str,
                    arch=target_arch,
                )
            )
            return 1

        for prog in emu_progs:
            if not profile.check_emulator_flavor(
                prog.flavor,
                emu_pm.emulator_metadata.quirks,
            ):
                logger.F(
                    _("the package [yellow]{atom}[/] does not support all necessary features for the profile [yellow]{profile}[/]").format(
                        atom=emu_atom_str,
                        profile=profile_name,
                    )
                )
                logger.I(
                    _("quirks needed by profile:   {humanized_list}").format(
                        humanized_list=humanize_list(profile.get_needed_emulator_pkg_flavors(prog.flavor), item_color='cyan'),
                    )
                )
                logger.I(
                    _("quirks provided by package: {humanized_list}").format(
                        humanized_list=humanize_list(emu_pm.emulator_metadata.quirks or [], item_color='yellow'),
                    )
                )
                return 1

        emu_root = config.lookup_binary_install_dir(
            host,
            emu_pm.name_for_installation,
        )
        if emu_root is None:
            logger.F(_("cannot find the installed directory for the emulator"))
            return 1

        venv_metadata["emulator_pkgs"][target_arch] = _venv_pkg_info_from_pkg(emu_pm)

    # Now resolve extra commands to provide in the venv.
    extra_cmds: dict[str, str] = {}
    if extra_cmd_atoms_str:
        for extra_cmd_atom_str in extra_cmd_atoms_str:
            extra_cmd_atom = Atom.parse(extra_cmd_atom_str)
            extra_cmd_pm = extra_cmd_atom.match_in_repo(
                mr,
                config.include_prereleases,
            )
            if extra_cmd_pm is None:
                logger.F(
                    _("cannot match an extra command package with [yellow]{atom}[/]").format(
                        atom=extra_cmd_atom_str,
                    )
                )
                return 1

            extra_cmd_bm = extra_cmd_pm.binary_metadata
            if not extra_cmd_bm:
                logger.F(
                    _("the package [yellow]{atom}[/] is not a binary-providing package").format(
                        atom=extra_cmd_atom_str,
                    )
                )
                return 1

            extra_cmds_decl = extra_cmd_bm.get_commands_for_host(host)
            if not extra_cmds_decl:
                logger.W(
                    _("the package [yellow]{atom}[/] does not provide any command for host [yellow]{host}[/], ignoring").format(
                        atom=extra_cmd_atom_str,
                        host=host,
                    )
                )
                continue

            cmd_root = config.lookup_binary_install_dir(
                host,
                extra_cmd_pm.name_for_installation,
            )
            if cmd_root is None:
                logger.F(
                    _("cannot find the installed directory for the package [yellow]{pkg}[/]").format(
                        pkg=extra_cmd_pm.name_for_installation,
                    )
                )
                return 1
            cmd_root = pathlib.Path(cmd_root)

            venv_metadata["extra_pkgs"].append(_venv_pkg_info_from_pkg(extra_cmd_pm))

            for cmd, cmd_rel_path in extra_cmds_decl.items():
                # resolve the command path
                cmd_path = (cmd_root / cmd_rel_path).resolve()
                if not cmd_path.is_relative_to(cmd_root):
                    # we don't allow commands to resolve outside of the
                    # providing package's install root
                    logger.F(
                        _("internal error: resolved command path is outside of the providing package")
                    )
                    return 1

                # add the command to the list
                extra_cmds[cmd] = str(cmd_path)

    if override_name is not None:
        logger.I(
            _("Creating a Ruyi virtual environment [cyan]'{name}'[/] at [green]{dest}[/]...").format(
                name=override_name,
                dest=dest,
            )
        )
    else:
        logger.I(
            _("Creating a Ruyi virtual environment at [green]{dest}[/]...").format(
                dest=dest,
            )
        )

    maker = VenvMaker(
        config,
        profile,
        targets,
        dest.resolve(),
        emu_progs,
        emu_root,
        extra_cmds,
        venv_metadata,
        override_name,
        sysroot_provision_mode,
    )
    try:
        maker.provision()
    except VenvProvisionError:
        return 1

    # TODO: move the template to PO
    locale = match_lang_code(config.lang_code, avail=("en", "zh_CN"))
    logger.I(
        render_template_str(
            f"prompt.venv-created.{locale}.txt",
            {
                "sysroot": maker.sysroot_destdir(None),
            },
        )
    )

    return 0


def find_gcc_install_dir(
    install_root: PathLike[Any],
    target_tuple: str,
) -> PathLike[Any] | None:
    # check $PREFIX/lib/gcc/$TARGET/*
    search_root = pathlib.Path(install_root) / "lib" / "gcc" / target_tuple
    try:
        for p in search_root.iterdir():
            # only want the first one (should be the only one)
            return p
    except FileNotFoundError:
        pass

    # nothing?
    return None


class VenvMaker:
    """Performs the actual creation of a Ruyi virtual environment."""

    def __init__(
        self,
        gc: GlobalConfig,
        profile: ProfileProxy,
        targets: list[ConfiguredTargetTuple],
        dest: PathLike[Any],
        emulator_progs: list[EmulatorProgDecl] | None,
        emulator_root: PathLike[Any] | None,
        extra_cmds: dict[str, str] | None,
        metadata: VenvMetadata,
        override_name: str | None = None,
        sysroot_provision_mode: SysrootProvisionMode = SysrootProvisionMode.COPY_TREE,
    ) -> None:
        self.gc = gc
        self.profile = profile
        self.targets = targets
        self.venv_root = pathlib.Path(dest)
        self.emulator_progs = emulator_progs
        self.emulator_root = emulator_root
        self.extra_cmds = extra_cmds or {}
        self.metadata = metadata
        self.override_name = override_name
        self.sysroot_provision_mode = sysroot_provision_mode

        self.bindir = self.venv_root / "bin"

    @property
    def logger(self) -> RuyiLogger:
        return self.gc.logger

    def render_and_write(
        self,
        dest: PathLike[Any],
        template_name: str,
        data: dict[str, Any],
    ) -> None:
        self.logger.D(f"rendering template '{template_name}' with data {data}")
        content = render_template_str(template_name, data).encode("utf-8")
        self.logger.D(f"writing {dest}")
        with open(dest, "wb") as fp:
            fp.write(content)

    def sysroot_srcdir(self, target_tuple: str | None) -> pathlib.Path | None:
        if target_tuple is None:
            # check the primary target
            if s := self.targets[0]["toolchain_sysroot"]:
                return pathlib.Path(s)
            return None

        # check if we have this target
        for t in self.targets:
            if t["target"] != target_tuple:
                continue
            if s := t["toolchain_sysroot"]:
                return pathlib.Path(s)

        return None

    def has_sysroot_for(self, target_tuple: str | None) -> bool:
        return self.sysroot_srcdir(target_tuple) is not None

    def sysroot_destdir(self, target_tuple: str | None) -> pathlib.Path | None:
        if not self.has_sysroot_for(target_tuple):
            return None

        dirname = f"sysroot.{target_tuple}" if target_tuple is not None else "sysroot"
        return self.venv_root / dirname

    def provision(self) -> None:
        venv_root = self.venv_root
        bindir = self.bindir

        venv_root.mkdir()
        bindir.mkdir()

        env_data = {
            "profile": self.profile.id,
            "sysroot": self.sysroot_destdir(None),
            "metadata": self.metadata,
        }
        self.render_and_write(
            venv_root / "ruyi-venv.toml",
            "ruyi-venv.toml",
            env_data,
        )

        for i, tgt in enumerate(self.targets):
            is_primary = i == 0
            self.provision_target(tgt, is_primary)

        if self.extra_cmds:
            symlink_binaries(
                self.gc,
                self.logger,
                bindir,
                src_cmds_names=list(self.extra_cmds.keys()),
            )

        template_data = {
            "RUYI_VENV": str(venv_root),
            "RUYI_VENV_NAME": self.override_name,
        }

        self.render_and_write(
            bindir / "ruyi-activate",
            "ruyi-activate.bash",
            template_data,
        )

        self.render_and_write(
            bindir / "ruyi-activate.fish",
            "ruyi-activate.fish",
            template_data,
        )

        qemu_bin: PathLike[Any] | None = None
        profile_emu_env: dict[str, str] | None = None
        if self.emulator_root is not None and self.emulator_progs:
            resolved_emu_progs = [
                ResolvedEmulatorProg.new(
                    p,
                    self.emulator_root,
                    self.profile,
                    self.sysroot_destdir(None),
                )
                for p in self.emulator_progs
            ]
            binfmt_data = {
                "resolved_progs": resolved_emu_progs,
            }
            self.render_and_write(
                venv_root / "binfmt.conf",
                "binfmt.conf",
                binfmt_data,
            )

            for i, p in enumerate(self.emulator_progs):
                if not p.is_qemu:
                    continue

                qemu_bin = pathlib.Path(self.emulator_root) / p.relative_path
                profile_emu_env = resolved_emu_progs[i].env

                self.logger.D("symlinking the ruyi-qemu wrapper")
                os.symlink(self.gc.self_exe, bindir / "ruyi-qemu")

        # provide initial cached configuration to venv
        self.render_and_write(
            venv_root / "ruyi-cache.v2.toml",
            "ruyi-cache.toml",
            self.make_venv_cache_data(
                qemu_bin,
                self.extra_cmds,
                profile_emu_env,
            ),
        )

    def make_venv_cache_data(
        self,
        qemu_bin: PathLike[Any] | None,
        extra_cmds: dict[str, str],
        profile_emu_env: dict[str, str] | None,
    ) -> dict[str, object]:
        targets_cache_data: dict[str, object] = {
            tgt["target"]: {
                "toolchain_bindir": str(pathlib.Path(tgt["toolchain_root"]) / "bin"),
                "toolchain_sysroot": self.sysroot_destdir(tgt["target"]),
                "toolchain_flags": tgt["toolchain_flags"],
                "gcc_install_dir": tgt["gcc_install_dir"],
            }
            for tgt in self.targets
        }

        cmd_metadata_map = make_cmd_metadata_map(self.logger, self.targets)

        # add extra cmds that are not associated with any target
        for cmd, dest in extra_cmds.items():
            if cmd in cmd_metadata_map:
                self.logger.W(
                    _("extra command {cmd} is already provided by another package, overriding it").format(
                        cmd=cmd,
                        )
                )
            cmd_metadata_map[cmd] = {
                "dest": dest,
                "target_tuple": "",
            }

        return {
            "profile_emu_env": profile_emu_env,
            "qemu_bin": qemu_bin,
            "targets": targets_cache_data,
            "cmd_metadata_map": cmd_metadata_map,
        }

    def provision_target(
        self,
        tgt: ConfiguredTargetTuple,
        is_primary: bool,
    ) -> None:
        venv_root = self.venv_root
        bindir = self.bindir
        target_tuple = tgt["target"]

        # getting the destdir this way ensures it's suffixed with the target
        # tuple
        if sysroot_destdir := self.sysroot_destdir(target_tuple):
            sysroot_srcdir = tgt["toolchain_sysroot"]
            assert sysroot_srcdir is not None
            sysroot_srcdir = pathlib.Path(sysroot_srcdir)

            provision_sysroot(
                self.logger,
                sysroot_srcdir,
                sysroot_destdir,
                self.sysroot_provision_mode,
                target_tuple,
            )

            if is_primary:
                self.logger.D("symlinking primary sysroot into place")
                primary_sysroot_destdir = self.sysroot_destdir(None)
                assert primary_sysroot_destdir is not None
                os.symlink(sysroot_destdir.name, primary_sysroot_destdir)

        self.logger.D(f"symlinking {target_tuple} binaries into venv")
        toolchain_bindir = pathlib.Path(tgt["toolchain_root"]) / "bin"
        symlink_binaries(self.gc, self.logger, bindir, src_bindir=toolchain_bindir)

        make_llvm_tool_aliases(
            self.logger,
            bindir,
            target_tuple,
            tgt["binutils_flavor"] == "llvm",
            tgt["cc_flavor"] == "clang",
        )

        # CMake toolchain file & Meson cross file
        if tgt["cc_flavor"] == "clang":
            cc_path = bindir / "clang"
            cxx_path = bindir / "clang++"
        elif tgt["cc_flavor"] == "gcc":
            cc_path = bindir / f"{target_tuple}-gcc"
            cxx_path = bindir / f"{target_tuple}-g++"
        else:
            raise NotImplementedError

        if tgt["binutils_flavor"] == "binutils":
            meson_additional_binaries = {
                "ar": bindir / f"{target_tuple}-ar",
                "nm": bindir / f"{target_tuple}-nm",
                "objcopy": bindir / f"{target_tuple}-objcopy",
                "objdump": bindir / f"{target_tuple}-objdump",
                "ranlib": bindir / f"{target_tuple}-ranlib",
                "readelf": bindir / f"{target_tuple}-readelf",
                "strip": bindir / f"{target_tuple}-strip",
            }
        elif tgt["binutils_flavor"] == "llvm":
            meson_additional_binaries = {
                "ar": bindir / "llvm-ar",
                "nm": bindir / "llvm-nm",
                "objcopy": bindir / "llvm-objcopy",
                "objdump": bindir / "llvm-objdump",
                "ranlib": bindir / "llvm-ranlib",
                "readelf": bindir / "llvm-readelf",
                "strip": bindir / "llvm-strip",
            }
        else:
            raise NotImplementedError

        cmake_toolchain_file_path = venv_root / f"toolchain.{target_tuple}.cmake"
        toolchain_file_data = {
            "cc": cc_path,
            "cxx": cxx_path,
            "processor": self.profile.arch,
            "sysroot": self.sysroot_destdir(target_tuple),
            "venv_root": venv_root,
            "cmake_toolchain_file": str(cmake_toolchain_file_path),
            "meson_additional_binaries": meson_additional_binaries,
        }
        self.render_and_write(
            cmake_toolchain_file_path,
            "toolchain.cmake",
            toolchain_file_data,
        )

        meson_cross_file_path = venv_root / f"meson-cross.{target_tuple}.ini"
        self.render_and_write(
            meson_cross_file_path,
            "meson-cross.ini",
            toolchain_file_data,
        )

        if is_primary:
            self.logger.D(
                f"making cmake & meson file symlinks to primary target {target_tuple}"
            )
            primary_cmake_toolchain_file_path = venv_root / "toolchain.cmake"
            primary_meson_cross_file_path = venv_root / "meson-cross.ini"
            os.symlink(
                cmake_toolchain_file_path.name,
                primary_cmake_toolchain_file_path,
            )
            os.symlink(meson_cross_file_path.name, primary_meson_cross_file_path)


def iter_binaries_to_symlink(
    logger: RuyiLogger,
    bindir: pathlib.Path,
) -> Iterator[pathlib.Path]:
    for filename in glob.iglob("*", root_dir=bindir):
        src_cmd_path = bindir / filename
        if not is_executable(src_cmd_path):
            logger.D(f"skipping non-executable {filename} in src bindir")
            continue

        if should_ignore_symlinking(filename):
            logger.D(f"skipping command {filename} explicitly")
            continue

        yield bindir / filename


class CmdMetadataEntry(TypedDict):
    dest: str
    target_tuple: str


def make_cmd_metadata_map(
    logger: RuyiLogger,
    targets: list[ConfiguredTargetTuple],
) -> dict[str, CmdMetadataEntry]:
    result: dict[str, CmdMetadataEntry] = {}
    for tgt in targets:
        # TODO: dedup this and provision_target
        toolchain_bindir = pathlib.Path(tgt["toolchain_root"]) / "bin"
        for cmd in iter_binaries_to_symlink(logger, toolchain_bindir):
            result[cmd.name] = {
                "dest": str(cmd),
                "target_tuple": tgt["target"],
            }
    return result


def symlink_binaries(
    gm: ProvidesGlobalMode,
    logger: RuyiLogger,
    dest_bindir: PathLike[Any],
    *,
    src_bindir: PathLike[Any] | None = None,
    src_cmds_names: list[str] | None = None,
) -> None:
    dest_binpath = pathlib.Path(dest_bindir)
    self_exe_path = gm.self_exe

    if src_bindir is not None:
        src_binpath = pathlib.Path(src_bindir)
        for src_cmd_path in iter_binaries_to_symlink(logger, src_binpath):
            filename = src_cmd_path.name

            # symlink self to dest with the name of this command
            dest_path = dest_binpath / filename
            logger.D(f"making ruyi symlink to {self_exe_path} at {dest_path}")
            os.symlink(self_exe_path, dest_path)
        return

    if src_cmds_names is not None:
        for cmd in src_cmds_names:
            # symlink self to dest with the name of this command
            dest_path = dest_binpath / cmd
            logger.D(f"making ruyi symlink to {self_exe_path} at {dest_path}")
            os.symlink(self_exe_path, dest_path)
        return

    raise ValueError(
        "internal error: either src_bindir or src_cmds_names must be provided"
    )


LLVM_BINUTILS_ALIASES: Final = {
    "addr2line": "llvm-addr2line",
    "ar": "llvm-ar",
    "as": "llvm-as",
    "c++filt": "llvm-cxxfilt",
    "gcc-ar": "llvm-ar",
    "gcc-nm": "llvm-nm",
    "gcc-ranlib": "llvm-ranlib",
    # 'gcov': 'llvm-cov',  # I'm not sure if this is correct
    "ld": "ld.lld",
    "nm": "llvm-nm",
    "objcopy": "llvm-objcopy",
    "objdump": "llvm-objdump",
    "ranlib": "llvm-ranlib",
    "readelf": "llvm-readelf",
    "size": "llvm-size",
    "strings": "llvm-strings",
    "strip": "llvm-strip",
}

CLANG_GCC_ALIASES: Final = {
    "c++": "clang++",
    "cc": "clang",
    "cpp": "clang-cpp",
    "g++": "clang++",
    "gcc": "clang",
}


def make_llvm_tool_aliases(
    logger: RuyiLogger,
    dest_bindir: PathLike[Any],
    target_tuple: str,
    do_binutils: bool,
    do_clang: bool,
) -> None:
    if do_binutils:
        make_compat_symlinks(logger, dest_bindir, target_tuple, LLVM_BINUTILS_ALIASES)
    if do_clang:
        make_compat_symlinks(logger, dest_bindir, target_tuple, CLANG_GCC_ALIASES)


def make_compat_symlinks(
    logger: RuyiLogger,
    dest_bindir: PathLike[Any],
    target_tuple: str,
    aliases: dict[str, str],
) -> None:
    destdir = pathlib.Path(dest_bindir)
    for compat_basename, symlink_target in aliases.items():
        compat_name = f"{target_tuple}-{compat_basename}"
        logger.D(f"making compat symlink: {compat_name} -> {symlink_target}")
        os.symlink(symlink_target, destdir / compat_name)


def is_executable(p: PathLike[Any]) -> bool:
    return os.access(p, os.F_OK | os.X_OK)


def should_ignore_symlinking(c: str) -> bool:
    return is_command_specific_to_ct_ng(c) or is_command_versioned_cc(c)


def is_command_specific_to_ct_ng(c: str) -> bool:
    return c.endswith("populate") or c.endswith("ct-ng.config")


VERSIONED_CC_RE: Final = re.compile(
    r"(?:^|-)(?:g?cc|c\+\+|g\+\+|cpp|clang|clang\+\+)-[0-9.]+$"
)


def is_command_versioned_cc(c: str) -> bool:
    return VERSIONED_CC_RE.search(c) is not None
