# Copyright (c) Advanced Micro Devices, Inc.
# SPDX-License-Identifier:  MIT

"""Get host/gpu specs."""

from __future__ import annotations

import argparse
import importlib
import os
import re
import socket
import subprocess
from dataclasses import dataclass, field, fields
from datetime import datetime
from math import ceil
from pathlib import Path as path
from typing import Any, Optional, TypeVar

import config
from utils.amdsmi_interface import (
    amdsmi_ctx,
    get_amdgpu_driver_version,
    get_gpu_compute_partition,
    get_gpu_memory_partition,
    get_gpu_vbios_part_number,
    get_gpu_vram_size,
)
from utils.logger import (
    console_debug,
    console_error,
    console_log,
    console_warning,
    demarcate,
)
from utils.mi_gpu_spec import mi_gpu_specs
from utils.utils_common import format_table_ascii, get_version

T = TypeVar("T")


def canonical_gpu_arch(gpu_arch: Optional[str]) -> Optional[str]:
    """Map LLVM GPU targets that share one SoC and analysis config tree."""
    if gpu_arch is None:
        return None
    if gpu_arch == "gfx1152":
        return "gfx1151"
    return gpu_arch


VERSION_LOC: list[str] = [
    "version",
    "version-dev",
    "version-hip-libraries",
    "version-hiprt",
    "version-hiprt-devel",
    "version-hip-sdk",
    "version-libs",
    "version-utils",
]


def detect_arch(rocminfo_lines: list[str]) -> Optional[tuple[str, int]]:
    supported_gpu_arch = mi_gpu_specs.get_gpu_series_dict()
    unsupported_gpu_arch: set[str] = set()

    for idx1, line_text in enumerate(rocminfo_lines):
        gpu_arch = search(
            r"^\s*Name\s*:\s* ([Gg][Ff][Xx][a-zA-Z0-9]+).*\s*$", line_text
        )
        if not gpu_arch:
            continue

        arch_for_support = canonical_gpu_arch(gpu_arch)
        if arch_for_support in supported_gpu_arch:
            return (arch_for_support, idx1)

        if gpu_arch not in unsupported_gpu_arch:
            unsupported_gpu_arch.add(gpu_arch)
            console_warning(
                "Detected GPU architecture: "
                f"{gpu_arch} is currently NOT supported by the profile mode."
            )

    if unsupported_gpu_arch:
        console_log(f"Supported architectures: {list(supported_gpu_arch.keys())}")

    console_error("Cannot find a supported arch in rocminfo.")


def detect_gpu_chip_id(rocminfo_lines: list[str]) -> Optional[str]:
    chip_id_dict = mi_gpu_specs.get_chip_id_dict()
    unknown_chips: list[str] = []

    for idx, line_text in enumerate(rocminfo_lines):
        chip_id = search(r"^\s*Chip ID\s*:\s* ([0-9]+).*\s*$", line_text)
        if chip_id:
            # Check if this chip ID is valid (known)
            if chip_id in chip_id_dict or int(chip_id) in chip_id_dict:
                return chip_id  # Return first valid chip ID found
            else:
                unknown_chips.append(chip_id)

    # Exhausted all lines - handle the cases where no valid chip was found
    if unknown_chips:
        for chip_id in unknown_chips:
            console_warning(f"Unknown Chip ID(s) detected: {chip_id}")
    else:
        console_warning("No Chip ID detected")

    return None


# Custom decorator to mimic the behavior of kw_only found in Python 3.10
def kw_only(cls: T) -> T:
    def __init__(self: Any, *args: Any, **kwargs: Any) -> None:  # noqa: ANN401
        for name, value in kwargs.items():
            setattr(self, name, value)

    cls.__init__ = __init__  # type: ignore
    return cls


def generate_machine_specs(
    args: Optional[argparse.Namespace], sysinfo: Optional[dict[str, Any]] = None
) -> MachineSpecs:
    if sysinfo is not None:
        try:
            sysinfo_ver = str(sysinfo["version"])
            version = get_version(config.rocprof_compute_home)["version"]
            curr_ver = version[: version.find(".")]

            if sysinfo_ver != curr_ver:
                console_warning(
                    "Detected mismatch in sysinfo versioning. "
                    "You need to reprofile to update data."
                )

            # Normalize arch aliases to canonical config targets
            sysinfo_norm = dict(sysinfo)
            gpu_arch = sysinfo_norm.get("gpu_arch")
            sysinfo_norm["gpu_arch"] = {"gfx1152": "gfx1151"}.get(gpu_arch, gpu_arch)
            return MachineSpecs(**sysinfo_norm)
        except KeyError:
            console_error(
                "Detected mismatch in sysinfo versioning. You need to reprofile "
                "to update data."
            )

    # read timestamp info
    now = datetime.now()
    local_now = now.astimezone()
    local_tzname = local_now.tzinfo.tzname(local_now)  # type: ignore
    timestamp = f"{now.strftime('%c')} ({local_tzname})"

    # set specs version
    version = get_version(config.rocprof_compute_home)["version"]
    # NB: Just taking major as specs version.
    # May want to make this more specific in the future
    # version will always follow 'major.minor.patch' format
    specs_version = version[: version.find(".")]

    ##########################################
    ## A. Machine Specs
    ##########################################
    machine_info = extract_machine_info()

    ##########################################
    ## B. SoC Specs
    ##########################################
    soc_info = extract_soc_info()

    # FIXME: use device
    # Load amd-smi data
    gpu_info = extract_gpu_info(gpu_arch=soc_info["gpu_arch"])

    # Combine all specifications
    with amdsmi_ctx():
        specs = MachineSpecs(
            version=specs_version,
            timestamp=timestamp,
            rocminfo_lines=soc_info["rocminfo_lines"],
            hostname=socket.gethostname(),
            cpu_model=machine_info["cpu_model"],
            sbios=machine_info["sbios"],
            linux_kernel_version=machine_info["linux_kernel_version"],
            amd_gpu_kernel_version=get_amdgpu_driver_version(),
            cpu_memory=machine_info["cpu_memory"],
            gpu_memory=get_gpu_vram_size(),
            linux_distro=machine_info["linux_distro"],
            rocm_version=get_rocm_ver().strip(),
            vbios=gpu_info["vbios"],
            compute_partition=gpu_info["compute_partition"],
            memory_partition=gpu_info["memory_partition"],
            gpu_arch=soc_info["gpu_arch"],
            gpu_chip_id=soc_info["gpu_chip_id"],
        )

    # Load above SoC specs via module import
    try:
        soc_module = importlib.import_module(
            f"rocprof_compute_soc.soc_{specs.gpu_arch}"
        )
        soc_class = getattr(soc_module, f"{specs.gpu_arch}_soc")
        soc_obj = soc_class(args, specs)  # noqa: F841
    except ModuleNotFoundError as e:
        console_error(
            f"Arch {specs.gpu_arch} marked as supported,"
            f"but couldn't find class implementation {e}."
        )

    # Derive SoC-dependent fields
    if specs.rocminfo_lines is None:
        # Yaml-only / no rocminfo: skip get_gpu_model (avoids chip-id warnings).
        specs.gpu_model = specs.gpu_model or ""
    else:
        specs.gpu_model = (
            mi_gpu_specs.get_gpu_model(specs.gpu_arch, specs.gpu_chip_id) or ""
        )
    specs.num_xcd = str(
        mi_gpu_specs.get_num_xcds(
            specs.gpu_arch, specs.gpu_model or None, specs.compute_partition
        )
    )
    specs.total_l2_chan = totall2_banks(
        specs.gpu_arch,
        specs.gpu_model,
        specs.l2_banks,
        specs.compute_partition,
    )
    specs.num_hbm_channels = str(specs.get_hbm_channels())

    return specs


@demarcate
def extract_machine_info() -> dict[str, Any]:
    result: dict[str, Optional[str]] = {
        "cpu_model": None,
        "sbios": None,
        "linux_kernel_version": None,
        "cpu_memory": None,
        "linux_distro": None,
    }

    try:
        cpuinfo = path("/proc/cpuinfo").read_text()
        meminfo = path("/proc/meminfo").read_text()
        version = path("/proc/version").read_text()
        os_release = path("/etc/os-release").read_text()

        result["cpu_model"] = search(r"^model name\s*: (.*?)$", cpuinfo)
        result["sbios"] = (
            path("/sys/class/dmi/id/bios_vendor").read_text().strip()
            + path("/sys/class/dmi/id/bios_version").read_text().strip()
        )
        result["linux_kernel_version"] = search(r"version (\S*)", version)
        result["cpu_memory"] = search(r"MemTotal:\s*(\S*)", meminfo)
        result["linux_distro"] = search(r'PRETTY_NAME="(.*?)"', os_release) or ""

    except OSError as e:
        console_warning(f"Could not read system files: {e}")
    return result


@demarcate
def extract_gpu_info(gpu_arch: Optional[str]) -> dict[str, Any]:
    # Partition is only supported on >= MI 300 series
    # (gpu_arch should be gfx940 or higher for MI300+)
    is_partition_supported = False
    if gpu_arch and gpu_arch.startswith("gfx") and len(gpu_arch) >= 6:
        try:
            is_partition_supported = int(gpu_arch[3:6], 16) >= 0x940
        except ValueError:
            pass  # Invalid hex string, keep is_partition_supported as False

    result: dict[str, Optional[str]] = {
        "vbios": None,
        "compute_partition": None,
        "memory_partition": None,
    }

    with amdsmi_ctx():
        result["vbios"] = get_gpu_vbios_part_number()
        if is_partition_supported:
            result["compute_partition"] = get_gpu_compute_partition()
            result["memory_partition"] = get_gpu_memory_partition()
        else:
            result["compute_partition"] = "N/A"
            result["memory_partition"] = "N/A"

    # Apply defaults and warnings
    if is_partition_supported:
        if result["compute_partition"] == "N/A" or not result["compute_partition"]:
            console_warning("Cannot detect accelerator partition from amd-smi.")
            console_warning("Applying default accelerator partition: SPX")
            result["compute_partition"] = "SPX"

        if result["memory_partition"] == "N/A" or not result["memory_partition"]:
            console_warning("Cannot detect memory partition from amd-smi.")

    console_debug(
        f"vbios is {result['vbios']}, compute partition is "
        f"{result['compute_partition']}, memory partition is "
        f"{result['memory_partition']}"
    )

    return result


@demarcate
def extract_soc_info() -> dict[str, Any]:
    result: dict[str, Any] = {
        "rocminfo_lines": None,
        "gpu_arch": None,
        "gpu_chip_id": None,
    }

    # Read rocminfo
    rocminfo_full = run(["rocminfo"])
    if rocminfo_full is None:
        return result

    rocminfo_lines = rocminfo_full.split("\n")
    arch_result = detect_arch(rocminfo_lines)

    if arch_result is None:
        return result

    result["gpu_arch"], arch_idx = arch_result
    result["rocminfo_lines"] = rocminfo_lines[
        arch_idx + 1 :
    ]  # update rocminfo for target section
    result["gpu_chip_id"] = detect_gpu_chip_id(rocminfo_lines)

    return result


@kw_only
@dataclass
class MachineSpecs:
    ##########################################
    ## A. Workload / Spec info
    ##########################################

    # these three fields are special in that they're not included
    # when you use (e.g.,) --specs to view the machinespecs, but they
    # _are_ included in profiling/analysis, so we mark them as 'optional'
    # in the metadata to avoid erroring out on missing fields on
    # serialization
    workload_path: Optional[str] = field(
        default=None,
        metadata={
            "doc": "Path to the workload data directory.",
            "name": "Workload Path",
            "optional": True,
            "show_in_table": True,
        },
    )
    command: Optional[str] = field(
        default=None,
        metadata={
            "doc": "The command the workload was executed with.",
            "name": "Command",
            "optional": True,
            "show_in_table": True,
        },
    )
    ip_blocks: Optional[str] = field(
        default=None,
        metadata={
            "doc": "The hardware blocks profiling information was collected for.",
            "name": "IP Blocks",
            "optional": True,
        },
    )
    timestamp: Optional[str] = field(
        default=None,
        metadata={
            "doc": "The time (in local system time) when data was collected",
            "name": "Timestamp",
            "show_in_table": True,
        },
    )
    version: Optional[str] = field(
        default=None,
        metadata={
            "doc": "The version of the machine specification file format.",
            "name": "MachineSpecs Version",
            "intable": False,
            "show_in_table": True,
        },
    )
    timestamp: Optional[str] = field(
        default=None,
        metadata={
            "doc": "The time (in local system time) when data was collected",
            "name": "Timestamp",
            "show_in_table": True,
        },
    )
    rocminfo_lines: Optional[list] = field(
        default=None, metadata={"show_in_table": False}
    )
    ##########################################
    ## A. Machine Specs
    ##########################################
    hostname: Optional[str] = field(
        default=None,
        metadata={
            "doc": "The hostname of the machine.",
            "name": "Hostname",
            "show_in_table": True,
        },
    )
    cpu_model: Optional[str] = field(
        default=None,
        metadata={
            "doc": "The model name of the CPU used.",
            "name": "CPU Model",
            "show_in_table": True,
        },
    )
    sbios: Optional[str] = field(
        default=None,
        metadata={
            "doc": "The system management bios version and vendor.",
            "name": "SBIOS",
            "show_in_table": True,
        },
    )
    linux_distro: Optional[str] = field(
        default=None,
        metadata={
            "doc": "The Linux distribution installed on the machine.",
            "name": "Linux Distribution",
            "show_in_table": True,
        },
    )
    linux_kernel_version: Optional[str] = field(
        default=None,
        metadata={
            "doc": "The Linux kernel version running on the machine.",
            "name": "Linux Kernel Version",
            "show_in_table": True,
        },
    )
    amd_gpu_kernel_version: Optional[str] = field(
        default=None,
        metadata={
            "doc": ("The version of the AMDGPU driver installed on the machine."),
            "name": "AMD GPU Kernel Version",
            "show_in_table": True,
        },
    )
    cpu_memory: Optional[str] = field(
        default=None,
        metadata={
            "doc": "The total amount of memory available to the CPU.",
            "unit": "KB",
            "name": "CPU Memory",
            "show_in_table": True,
        },
    )
    gpu_memory: Optional[str] = field(
        default=None,
        metadata={
            "doc": (
                "The total amount of memory available to accelerators/GPUs "
                "in the system."
            ),
            "unit": "KB",
            "name": "GPU Memory",
            "show_in_table": True,
        },
    )
    rocm_version: Optional[str] = field(
        default=None,
        metadata={
            "doc": "The ROCm version used during data-collection.",
            "name": "ROCm Version",
            "show_in_table": True,
        },
    )
    vbios: Optional[str] = field(
        default=None,
        metadata={
            "doc": "The version of the accelerators/GPUs video bios in the system.",
            "name": "VBIOS",
            "show_in_table": True,
        },
    )
    compute_partition: Optional[str] = field(
        default=None,
        metadata={
            "doc": (
                "The compute partitioning mode active on the accelerators/GPUs in the "
                "system (MI300 only)."
            ),
            "name": "Compute Partition",
            "show_in_table": True,
        },
    )
    memory_partition: Optional[str] = field(
        default=None,
        metadata={
            "doc": (
                "The memory partitioning mode active on the accelerators/GPUs in the "
                "system (MI300 only)."
            ),
            "name": "Memory Partition",
            "show_in_table": True,
        },
    )

    ##########################################
    ## B. SoC Specs
    ##########################################
    gpu_series: Optional[str] = field(
        default=None,
        metadata={
            "doc": "The series of the accelerators/GPUs in the system.",
            "name": "GPU Series",
            "show_in_table": True,
        },
    )
    gpu_model: Optional[str] = field(
        default=None,
        metadata={
            "doc": "The product name of the accelerators/GPUs in the system.",
            "name": "GPU Model",
            "show_in_table": True,
        },
    )
    gpu_arch: Optional[str] = field(
        default=None,
        metadata={
            "doc": "The architecture name of the accelerators/GPUs in the system,\n"
            "as used by (e.g.,) the AMDGPU backed of LLVM.",
            "name": "GPU Arch",
            "show_in_table": True,
        },
    )
    gpu_chip_id: Optional[str] = field(
        default=None,
        metadata={
            "doc": "The Chip ID of the accelerators/GPUs in the system.",
            "name": "Chip ID",
            "optional": True,
            "show_in_table": True,
        },
    )
    gpu_l1: Optional[str] = field(
        default=None,
        metadata={
            "doc": (
                "The size of the vL1D cache (per compute-unit) on the "
                "accelerators/GPUs."
            ),
            "name": "GPU L1",
            "unit": "KiB",
            "show_in_table": True,
        },
    )
    gpu_l2: Optional[str] = field(
        default=None,
        metadata={
            "doc": (
                "The size of the vL1D cache (per compute-unit) on the "
                "accelerators/GPUs."
            ),
            "name": "GPU L2",
            "unit": "KiB",
            "show_in_table": True,
        },
    )
    cu_per_gpu: Optional[str] = field(
        default=None,
        metadata={
            "doc": (
                "The total number of compute units per accelerator/GPU in the system. "
                "On systems with configurable partitioning, (e.g., MI300) this is "
                "the total number of compute units in a partition."
            ),
            "name": "CU per GPU",
            "show_in_table": True,
        },
    )
    simd_per_cu: Optional[str] = field(
        default=None,
        metadata={
            "doc": (
                "The number of SIMD processors in a compute unit for the "
                "accelerators/GPUs in the system."
            ),
            "name": "SIMD per CU",
            "show_in_table": True,
        },
    )
    se_per_gpu: Optional[str] = field(
        default=None,
        metadata={
            "doc": (
                "The number of shader engines on the accelerators/GPUs in the system. "
                "On systems with configurable partitioning, (e.g., MI300) this is "
                "the total number of shader engines in a partition."
            ),
            "name": "SE per GPU",
            "show_in_table": True,
        },
    )
    wave_size: Optional[str] = field(
        default=None,
        metadata={
            "doc": (
                "The number work-items in a wavefront on the accelerators/GPUs in "
                "the system."
            ),
            "name": "Wave Size",
            "show_in_table": True,
        },
    )
    workgroup_max_size: Optional[str] = field(
        default=None,
        metadata={
            "doc": (
                "The maximum number of work-items in a workgroup on the "
                "accelerators/GPUs in the system."
            ),
            "name": "Workgroup Max Size",
            "show_in_table": True,
        },
    )
    max_waves_per_cu: Optional[str] = field(
        default=None,
        metadata={
            "doc": (
                "The maximum number of wavefronts that can be resident on a "
                "compute unit on the accelerators/GPUs in the system"
            ),
            "name": "Max Waves per CU",
            "show_in_table": True,
        },
    )
    max_sclk: Optional[str] = field(
        default=None,
        metadata={
            "doc": (
                "The maximum engine (compute-unit) clock rate of the "
                "accelerators/GPUs in the system."
            ),
            "name": "Max SCLK",
            "unit": "MHz",
            "show_in_table": True,
        },
    )
    max_mclk: Optional[str] = field(
        default=None,
        metadata={
            "doc": (
                "The maximum memory clock rate of the accelerators/GPUs in the system."
            ),
            "name": "Max MCLK",
            "unit": "MHz",
            "show_in_table": True,
        },
    )
    cur_sclk: Optional[str] = field(
        default=None,
        metadata={
            "doc": (
                "[RESERVED] The current engine (compute unit) clock rate of the "
                "accelerators/GPUs in the system. Unused."
            ),
            "name": "Cur SCLK",
            "unit": "MHz",
            "show_in_table": True,
        },
    )
    cur_mclk: Optional[str] = field(
        default=None,
        metadata={
            "doc": (
                "[RESERVED] The current memory clock rate of the accelerators/GPUs "
                "in the system. Unused."
            ),
            "name": "Cur MCLK",
            "unit": "MHz",
            "show_in_table": True,
        },
    )
    l2_banks: Optional[str] = field(
        default=None,
        metadata={
            "show_in_table": True,
        },
    )
    total_l2_chan: Optional[str] = field(
        default=None,
        metadata={
            "doc": (
                "The maximum number of L2 cache channels on the accelerators/GPUs "
                "in the system. On systems with configurable partitioning, "
                "(e.g., MI300) this is the total number of L2 cache channels "
                "in a partition."
            ),
            "name": "Total L2 Channels",
            "show_in_table": True,
        },
    )
    lds_banks_per_cu: Optional[str] = field(
        default=None,
        metadata={
            "doc": (
                "The number of banks in the LDS for a compute unit on the "
                "accelerators/GPUs in the system."
            ),
            "name": "LDS Banks per CU",
            "show_in_table": True,
        },
    )
    sqc_per_gpu: Optional[str] = field(
        default=None,
        metadata={
            "doc": (
                "The number of L1I/sL1D caches on the accelerators/GPUs in the "
                "system. On systems with configurable partitioning, (e.g., MI300) "
                "this is the total number of L1I/sL1D caches in a partition."
            ),
            "name": "SQC per GPU",
            "show_in_table": True,
        },
    )
    pipes_per_gpu: Optional[str] = field(
        default=None,
        metadata={
            "doc": (
                "The number of scheduler-pipes on the accelerators/GPUs in the system."
            ),
            "name": "Pipes per GPU",
            "show_in_table": True,
        },
    )
    num_xcd: Optional[str] = field(
        default=None,
        metadata={
            "doc": (
                "The total number of accelerator complex dies in a compute partition "
                "on the accelerators/GPUs in the system. For accelerators without "
                "partitioning (i.e., pre-MI300), this is considered to be one."
            ),
            "name": "Num XCDs",
            "unit": "XCDs",
            "show_in_table": True,
        },
    )
    num_hbm_channels: Optional[str] = field(
        default=None,
        metadata={
            "doc": "Number of HBM channels",
            "name": "HBM channels",
            "show_in_table": True,
        },
    )

    def get_hbm_channels(self) -> Optional[str]:
        if self.memory_partition and self.memory_partition.lower().startswith("nps"):
            hbmchannels = 128
            if self.memory_partition.lower() == "nps4":
                hbmchannels //= 4
            elif self.memory_partition.lower() == "nps8":
                hbmchannels //= 8
            return str(hbmchannels)
        else:
            return self.total_l2_chan

    def get_class_members(self) -> dict[str, Any]:
        """Return class members as a dictionary."""
        data: dict[str, Any] = {}
        missing_required_fields: list[str] = []

        for class_field in fields(self):
            if not class_field.metadata.get("show_in_table", True):
                continue

            name = class_field.name
            value = getattr(self, name)
            data[name] = value

            # Check for missing required fields
            if value is None and not class_field.metadata.get("optional", False):
                missing_required_fields.append(name)

        # Handle warnings after processing all fields
        if missing_required_fields:
            for field_name in missing_required_fields:
                console_warning(
                    f"Incomplete class definition for {self.gpu_arch}. "
                    f"Expecting populated {field_name} but detected None."
                )
            console_warning(f"Missing specs fields for {self.gpu_arch}")

        return data

    def __repr__(self) -> str:
        topstr = (
            "Machine Specifications: describing the state of the machine that "
            "ROCm Compute Profiler data was collected on.\n"
        )
        data: list[dict[str, Any]] = []
        has_description = False
        has_unit = False

        for class_field in fields(self):
            name = class_field.name
            if class_field.metadata.get("show_in_table", True):
                _data: dict[str, Any] = {}
                value = getattr(self, name)
                if class_field.metadata:
                    # check out of table before any re-naming for pretty-printing
                    if (
                        "intable" in class_field.metadata
                        and not class_field.metadata["intable"]
                    ):
                        if name == "version":
                            topstr += f"Output version: {value}\n"
                        else:
                            console_error(
                                f"Unknown out of table printing field: {name}"
                            )
                        continue
                    if "name" in class_field.metadata:
                        name = class_field.metadata["name"]
                    if "unit" in class_field.metadata:
                        _data["Unit"] = class_field.metadata["unit"]
                        has_unit = True
                    if "doc" in class_field.metadata:
                        _data["Description"] = class_field.metadata["doc"]
                        has_description = True
                _data["Spec"] = name
                _data["Value"] = value if value is not None else ""
                data.append(_data)

        # Build columns list based on what data is present
        columns = ["Spec", "Value"]
        if has_description:
            columns.append("Description")
        if has_unit:
            columns.append("Unit")

        return topstr + format_table_ascii(data, columns, decimal=2)


def get_rocm_ver() -> str:
    # Check for version files in ROCm installation
    rocm_base_path = path(os.getenv("ROCM_PATH", "/opt/rocm"))

    for version_file_name in VERSION_LOC:
        version_file_path = rocm_base_path / ".info" / version_file_name
        if version_file_path.exists():
            return version_file_path.read_text().strip()

    # Fallback to environment variable
    ROCM_VER_USER = os.getenv("ROCM_VER")
    if ROCM_VER_USER:
        console_log(
            "profiling",
            "Overriding missing ROCm version detection with "
            f"ROCM_VER = {ROCM_VER_USER}",
        )
        return ROCM_VER_USER

    # No version found - log error and return empty string
    console_warning("Unable to detect a complete local ROCm installation.")
    console_warning(
        f"The expected {rocm_base_path}/.info/ versioning directory is missing."
    )
    console_error("Ensure you have valid ROCm installation.", exit=False)
    return ""


def run(cmd: list[str], exit_on_error: bool = False) -> str:
    try:
        p = subprocess.run(cmd, capture_output=True)
    except FileNotFoundError as e:
        console_error(
            f"Unable to parse specs. Can't find ROCm asset: {e.filename}\n"
            'Try passing a path to an existing workload results in "analyze" mode.'
        )

    if exit_on_error and p.returncode != 0:  # type: ignore
        console_error(f"Command {cmd} failed with non-zero exit code")
    return p.stdout.decode("utf-8")  # type: ignore


def search(pattern: str, string: str) -> Optional[str]:
    m = re.search(pattern, string, re.MULTILINE)
    if m is not None:
        return m.group(1)
    return None


def total_sqc(archname: str, num_compute_units: str, num_shader_engines: str) -> int:
    cu_per_se = float(num_compute_units) / float(num_shader_engines)
    sq_per_se = cu_per_se / 2.0
    if archname.lower() in ["mi50", "mi100"]:
        sq_per_se = cu_per_se / 3
    sq_per_se = ceil(sq_per_se)
    return int(sq_per_se) * int(num_shader_engines)


def totall2_banks(
    gpu_arch: Optional[str] = None,
    gpu_model: Optional[str] = None,
    L2banks: Optional[str] = None,
    compute_partition: Optional[str] = None,
) -> Optional[str]:
    xcd_count = mi_gpu_specs.get_num_xcds(gpu_arch, gpu_model, compute_partition)

    # TODO: MachineSpecs and OmniSoC mspec should converge...
    if L2banks is not None and xcd_count is not None:
        return str(int(L2banks) * int(xcd_count))
    return None


if __name__ == "__main__":
    specs = generate_machine_specs(None, None)
    if specs:
        print(specs)
    else:
        console_error("specs", "Failed to generate machine specifications", exit=False)
