#!/bin/bash
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements.  See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership.  The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License.  You may obtain a copy of the License at
#
#   http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied.  See the License for the
# specific language governing permissions and limitations
# under the License.

#
# Enumerate GPUs (NVIDIA, Intel, AMD) and output JSON for libvirt,
# including:
#   - PCI metadata (address, vendor/device IDs, driver, pci_class)
#   - IOMMU group
#   - PCI root (for PCIe topology grouping)
#   - NUMA node
#   - SR-IOV VF counts
#   - full_passthrough block (with VM usage)
#   - vGPU (MDEV) instances (fetching profile “name” and “max_instance” from description)
#   - VF (SR-IOV / MIG) instances (with VM usage)
#
# Uses `lspci -nnm` for GPU discovery and `virsh` to detect VM attachments.
# Compatible with Ubuntu (20.04+, 22.04+) and RHEL/CentOS (7/8), Bash ≥4.
#
#
# Sample JSON:
# {
#   "gpus": [
#     {
#       "pci_address": "00:03.0",
#       "vendor_id": "10de",
#       "device_id": "2484",
#       "vendor": "NVIDIA Corporation",
#       "device": "GeForce RTX 3070",
#       "driver": "nvidia",
#       "pci_class": "VGA compatible controller",
#       "iommu_group": "8",
#       "sriov_totalvfs": 0,
#       "sriov_numvfs": 0,

#       "full_passthrough": {
#         "enabled": true,
#         "libvirt_address": {
#           "domain": "0x0000",
#           "bus": "0x00",
#           "slot": "0x03",
#           "function": "0x0"
#         },
#         "used_by_vm": "win10"
#       },

#       "vgpu_instances": [],

#       "vf_instances": []
#     },
#     {
#       "pci_address": "00:AF.0",
#       "vendor_id": "10de",
#       "device_id": "1EB8",
#       "vendor": "NVIDIA Corporation",
#       "device": "Tesla T4",
#       "driver": "nvidia",
#       "pci_class": "3D controller",
#       "iommu_group": "12",
#       "sriov_totalvfs": 0,
#       "sriov_numvfs": 0,

#       "full_passthrough": {
#         "enabled": false,
#         "libvirt_address": {
#           "domain": "0x0000",
#           "bus": "0x00",
#           "slot": "0xAF",
#           "function": "0x0"
#         },
#         "used_by_vm": null
#       },

#       "vgpu_instances": [
#         {
#           "mdev_uuid": "a1b2c3d4-5678-4e9a-8b0c-d1e2f3a4b5c6",
#           "profile_name": "grid_t4-16c",
#           "max_instances": 4,
#           "libvirt_address": {
#             "domain": "0x0000",
#             "bus": "0x00",
#             "slot": "0xAF",
#             "function": "0x0"
#           },
#           "used_by_vm": "vm1"
#         },
#         {
#           "mdev_uuid": "b2c3d4e5-6789-4f0a-9c1d-e2f3a4b5c6d7",
#           "profile_name": "grid_t4-8c",
#           "max_instances": 8,
#           "libvirt_address": {
#             "domain": "0x0000",
#             "bus": "0x00",
#             "slot": "0xAF",
#             "function": "0x1"
#           },
#           "used_by_vm": "vm2"
#         }
#       ],

#       "vf_instances": []
#     },
#     {
#       "pci_address": "00:65.0",
#       "vendor_id": "10de",
#       "device_id": "20B0",
#       "vendor": "NVIDIA Corporation",
#       "device": "A100-SXM4-40GB",
#       "driver": "nvidia",
#       "pci_class": "VGA compatible controller",
#       "iommu_group": "15",
#       "sriov_totalvfs": 7,
#       "sriov_numvfs": 7,

#       "full_passthrough": {
#         "enabled": false,
#         "libvirt_address": {
#           "domain": "0x0000",
#           "bus": "0x00",
#           "slot": "0x65",
#           "function": "0x0"
#         },
#         "used_by_vm": null
#       },

#       "vgpu_instances": [
#         {
#           "mdev_uuid": "f4a2c8de-1234-4b3a-8c9d-0a1b2c3d4e5f",
#           "profile_name": "grid_a100-8c",
#           "max_instances": 8,
#           "libvirt_address": {
#             "domain": "0x0000",
#             "bus": "0x00",
#             "slot": "0x65",
#             "function": "0x0"
#           },
#           "used_by_vm": null
#         },
#         {
#           "mdev_uuid": "e5b3d9ef-5678-4c2b-9d0e-1b2c3d4e5f6a",
#           "profile_name": "grid_a100-5c",
#           "max_instances": 5,
#           "libvirt_address": {
#             "domain": "0x0000",
#             "bus": "0x00",
#             "slot": "0x65",
#             "function": "0x1"
#           },
#           "used_by_vm": null
#         }
#       ],

#       "vf_instances": [
#         {
#           "vf_pci_address": "65:00.2",
#           "vf_profile": "1g.5gb",
#           "libvirt_address": {
#             "domain": "0x0000",
#             "bus": "0x00",
#             "slot": "0x65",
#             "function": "0x2"
#           },
#           "used_by_vm": "ml"
#         },
#         {
#           "vf_pci_address": "65:00.3",
#           "vf_profile": "2g.10gb",
#           "libvirt_address": {
#             "domain": "0x0000",
#             "bus": "0x00",
#             "slot": "0x65",
#             "function": "0x3"
#           },
#           "used_by_vm": null
#         }
#       ]
#     },
#     {
#       "pci_address": "00:02.0",
#       "vendor_id": "8086",
#       "device_id": "46A6",
#       "vendor": "Intel Corporation",
#       "device": "Alder Lake-P GT2 [Iris Xe Graphics]",
#       "driver": "i915",
#       "pci_class": "VGA compatible controller",
#       "iommu_group": "0",
#       "sriov_totalvfs": 4,
#       "sriov_numvfs": 4,

#       "full_passthrough": {
#         "enabled": false,
#         "libvirt_address": {
#           "domain": "0x0000",
#           "bus": "0x00",
#           "slot": "0x02",
#           "function": "0x0"
#         },
#         "used_by_vm": null
#       },

#       "vgpu_instances": [
#         {
#           "mdev_uuid": "b7c8d9fe-1111-2222-3333-444455556666",
#           "profile_name": "i915-GVTg_V5_4",
#           "max_instances": 4,
#           "libvirt_address": {
#             "domain": "0x0000",
#             "bus": "0x00",
#             "slot": "0x02",
#             "function": "0x0"
#           },
#           "used_by_vm": null
#         },
#         {
#           "mdev_uuid": "c8d9e0af-7777-8888-9999-000011112222",
#           "profile_name": "i915-GVTg_V5_8",
#           "max_instances": 8,
#           "libvirt_address": {
#             "domain": "0x0000",
#             "bus": "0x00",
#             "slot": "0x02",
#             "function": "0x1"
#           },
#           "used_by_vm": null
#         }
#       ],

#       "vf_instances": [
#         {
#           "vf_pci_address": "00:02.1",
#           "vf_profile": "Intel SR-IOV VF 1",
#           "libvirt_address": {
#             "domain": "0x0000",
#             "bus": "0x00",
#             "slot": "0x02",
#             "function": "0x1"
#           },
#           "used_by_vm": "linux01"
#         },
#         {
#           "vf_pci_address": "00:02.2",
#           "vf_profile": "Intel SR-IOV VF 2",
#           "libvirt_address": {
#             "domain": "0x0000",
#             "bus": "0x00",
#             "slot": "0x02",
#             "function": "0x2"
#           },
#           "used_by_vm": null
#         }
#       ]
#     },
#     {
#       "pci_address": "00:03.0",
#       "vendor_id": "1002",
#       "device_id": "7340",
#       "vendor": "AMD",
#       "device": "Instinct MI210",
#       "driver": "amdgpu",
#       "pci_class": "3D controller",
#       "iommu_group": "8",
#       "sriov_totalvfs": 8,
#       "sriov_numvfs": 8,

#       "full_passthrough": {
#         "enabled": false,
#         "libvirt_address": {
#           "domain": "0x0000",
#           "bus": "0x00",
#           "slot": "0x03",
#           "function": "0x0"
#         },
#         "used_by_vm": null
#       },

#       "vgpu_instances": [],

#       "vf_instances": [
#         {
#           "vf_pci_address": "03:00.1",
#           "vf_profile": "mi210-4c",
#           "libvirt_address": {
#             "domain": "0x0000",
#             "bus": "0x00",
#             "slot": "0x03",
#             "function": "0x1"
#           },
#           "used_by_vm": null
#         },
#         {
#           "vf_pci_address": "03:00.2",
#           "vf_profile": "mi210-2c",
#           "libvirt_address": {
#             "domain": "0x0000",
#             "bus": "0x00",
#             "slot": "0x03",
#             "function": "0x2"
#           },
#           "used_by_vm": null
#         },
#         {
#           "vf_pci_address": "03:00.3",
#           "vf_profile": "mi210-1c",
#           "libvirt_address": {
#             "domain": "0x0000",
#             "bus": "0x00",
#             "slot": "0x03",
#             "function": "0x3"
#           },
#           "used_by_vm": null
#         }
#       ]
#     },
#	  {
#	   "pci_address":"05:00.0",
#   	"vendor_id":"1002",
#	   "device_id":"74a5",
#   	"vendor":"Advanced Micro Devices, Inc. [AMD/ATI]",
#	   "device":"Aqua Vanjaram [Instinct MI325X]",
#   	"driver":"amdgpu",
#	   "pci_class":"Processing accelerators [1200]",
#   	"iommu_group":"null",
#	   "pci_root":"0000:05:00.0",
#   	"numa_node":-1,
#	   "sriov_totalvfs":0,
#   	"sriov_numvfs":0,
#	   "max_instances":null,
#   	"video_ram":null,
#	   "max_heads":null,
#   	"max_resolution_x":null,
#	   "max_resolution_y":null,
#
#   	"full_passthrough": {
#     	  "enabled":1,
#		  "libvirt_address": {
#         "domain":"0x0000",
#         "bus":"0x05",
#         "slot":"0x00",
#         "function":"0x0"
#     	},
#     	"used_by_vm":null
#   	},

#   	"vgpu_instances":[],
#   	"vf_instances":[]
# 	  },
# 	  {
#       "pci_address": "0001:65:00.0",
#       "vendor_id": "10de",
#       "device_id": "20B0",
#       "vendor": "NVIDIA Corporation",
#       "device": "A100-SXM4-40GB",
#       "driver": "nvidia",
#       "pci_class": "3D controller",
#       "iommu_group": "20",
#       "sriov_totalvfs": 0,
#       "sriov_numvfs": 0,
#
#       "full_passthrough": {
#         "enabled": true,
#         "libvirt_address": {
#           "domain": "0x0001",
#           "bus": "0x65",
#           "slot": "0x00",
#           "function": "0x0"
#         },
#         "used_by_vm": "ml-train"
#       },
#
#       "vgpu_instances": [],
#       "vf_instances": []
#     }
#   ]
# }
#

set -euo pipefail

# === Utility Functions ===

# Escape a string for JSON
json_escape() {
	local str="$1"
	str=${str//\\/\\\\}
	str=${str//\"/\\\"}
	str=${str//
/\\n}
	str=${str//
/\\r}
	str=${str//	/\\t}
	printf '"%s"' "$str"
}

# Cache for nodedev XML data to avoid repeated virsh calls
declare -A nodedev_cache

# Cache for nvidia-smi vgpu profile data
declare -A nvidia_vgpu_profiles

# Parse nvidia-smi vgpu -s -v output and populate profile cache
parse_nvidia_vgpu_profiles() {
	local gpu_address=""
	local profile_id=""
	local profile_name=""
	local max_instances=""
	local fb_memory=""
	local max_heads=""
	local max_x_res=""
	local max_y_res=""

	# Function to store current profile data
	store_profile_data() {
		if [[ -n "$gpu_address" && -n "$profile_id" && -n "$profile_name" ]]; then
			local key="${gpu_address}:${profile_id}"
			nvidia_vgpu_profiles["$key"]="$profile_name|${max_instances:-0}|${fb_memory:-0}|${max_heads:-0}|${max_x_res:-0}|${max_y_res:-0}"
		fi
	}

	# Skip if nvidia-smi is not available
	if ! command -v nvidia-smi >/dev/null 2>&1; then
		return
	fi

	while IFS= read -r line; do
		# Match GPU address line
		if [[ $line =~ ^GPU[[:space:]]+([0-9A-Fa-f:]+\.[0-9A-Fa-f]+) ]]; then
			# Store previous profile data before starting new GPU
			store_profile_data

			gpu_address="${BASH_REMATCH[1]}"
			# nvidia-smi reports addresses in the form "00000000:AF:00.0"
			# (8-digit domain). Reformat to the canonical 4-digit
			# "dddd:bb:ss.f" lowercase form so cache keys line up with the
			# normalize_pci_address output used at lookup time. Short
			# addresses ("AF:00.0") are widened by prepending domain 0.
			if [[ $gpu_address =~ ^([0-9A-Fa-f]+):([0-9A-Fa-f]+):([0-9A-Fa-f]+)\.([0-9A-Fa-f]+)$ ]]; then
				gpu_address=$(printf "%04x:%02x:%02x.%x" \
					"0x${BASH_REMATCH[1]}" "0x${BASH_REMATCH[2]}" \
					"0x${BASH_REMATCH[3]}" "0x${BASH_REMATCH[4]}")
			elif [[ $gpu_address =~ ^([0-9A-Fa-f]+):([0-9A-Fa-f]+)\.([0-9A-Fa-f]+)$ ]]; then
				gpu_address=$(printf "0000:%02x:%02x.%x" \
					"0x${BASH_REMATCH[1]}" "0x${BASH_REMATCH[2]}" "0x${BASH_REMATCH[3]}")
			else
				gpu_address="${gpu_address,,}"
			fi
			# Reset profile variables for new GPU
			profile_id=""
			profile_name=""
			max_instances=""
			fb_memory=""
			max_heads=""
			max_x_res=""
			max_y_res=""
		elif [[ $line =~ ^[[:space:]]*vGPU[[:space:]]+Type[[:space:]]+ID[[:space:]]*:[[:space:]]*0x([0-9A-Fa-f]+) ]]; then
			# Store previous profile data before starting new profile
			store_profile_data

			# Normalize to lowercase hex without 0x prefix
			profile_id="${BASH_REMATCH[1],,}"
			# Reset profile-specific variables
			profile_name=""
			max_instances=""
			fb_memory=""
			max_heads=""
			max_x_res=""
			max_y_res=""
		elif [[ $line =~ ^[[:space:]]*Name[[:space:]]*:[[:space:]]*(.+)$ ]]; then
			profile_name="${BASH_REMATCH[1]}"
		elif [[ $line =~ ^[[:space:]]*Max[[:space:]]+Instances[[:space:]]*:[[:space:]]*([0-9]+) ]]; then
			max_instances="${BASH_REMATCH[1]}"
		elif [[ $line =~ ^[[:space:]]*FB[[:space:]]+Memory[[:space:]]*:[[:space:]]*([0-9]+)[[:space:]]*MiB ]]; then
			fb_memory="${BASH_REMATCH[1]}"
		elif [[ $line =~ ^[[:space:]]*Display[[:space:]]+Heads[[:space:]]*:[[:space:]]*([0-9]+) ]]; then
			max_heads="${BASH_REMATCH[1]}"
		elif [[ $line =~ ^[[:space:]]*Maximum[[:space:]]+X[[:space:]]+Resolution[[:space:]]*:[[:space:]]*([0-9]+) ]]; then
			max_x_res="${BASH_REMATCH[1]}"
		elif [[ $line =~ ^[[:space:]]*Maximum[[:space:]]+Y[[:space:]]+Resolution[[:space:]]*:[[:space:]]*([0-9]+) ]]; then
			max_y_res="${BASH_REMATCH[1]}"
		fi
	done < <(nvidia-smi vgpu -s -v 2>/dev/null || true)

	# Store the last profile data after processing all lines
	store_profile_data
}

# Get current vGPU type ID for a VF from sysfs
get_current_vgpu_type() {
	local vf_path="$1"
	local current_type_file="$vf_path/nvidia/current_vgpu_type"

	if [[ -f "$current_type_file" ]]; then
		local type_id
		type_id=$(<"$current_type_file")

		# Remove any whitespace
		type_id="${type_id// /}"

		# Handle different input formats and normalize to lowercase hex without 0x
		if [[ $type_id =~ ^0x([0-9A-Fa-f]+)$ ]]; then
			# Input is hex with 0x prefix (e.g., "0x252")
			echo "${BASH_REMATCH[1],,}"
		elif [[ $type_id =~ ^[0-9]+$ ]]; then
			# Input is decimal (e.g., "594")
			printf "%x" "$type_id"
		elif [[ $type_id =~ ^[0-9A-Fa-f]+$ ]]; then
			# Input is hex without 0x prefix (e.g., "252")
			echo "${type_id,,}"
		else
			# Fallback for unknown format
			echo "0"
		fi
	else
		echo "0"
	fi
}

# Get profile information from nvidia-smi cache
get_nvidia_profile_info() {
	local gpu_address="$1"
	local profile_id="$2"
	local key="${gpu_address}:${profile_id}"

	if [[ -n "${nvidia_vgpu_profiles[$key]:-}" ]]; then
		echo "${nvidia_vgpu_profiles[$key]}"
	else
		echo "|0|0|0|0|0"  # Default empty values
	fi
}

# Parse a PCI address and assign the libvirt-formatted DOMAIN/BUS/SLOT/FUNC
# values into the caller's scope. Accepts both full ("0000:00:02.0") and short
# ("00:02.0") formats; short addresses are assumed to be in PCI domain 0.
#
# IMPORTANT: bash uses dynamic scoping, so callers that don't want these four
# values to leak into the global scope MUST declare them `local` before
# invoking this function, e.g.:
#     local DOMAIN BUS SLOT FUNC
#     parse_pci_address "$addr"
parse_pci_address() {
	local addr="$1"
	if [[ $addr =~ ^([0-9A-Fa-f]+):([0-9A-Fa-f]+):([0-9A-Fa-f]+)\.([0-9A-Fa-f]+)$ ]]; then
		DOMAIN=$(printf "0x%04x" "0x${BASH_REMATCH[1]}")
		BUS=$(printf "0x%02x" "0x${BASH_REMATCH[2]}")
		SLOT=$(printf "0x%02x" "0x${BASH_REMATCH[3]}")
		FUNC=$(printf "0x%x" "0x${BASH_REMATCH[4]}")
	elif [[ $addr =~ ^([0-9A-Fa-f]+):([0-9A-Fa-f]+)\.([0-9A-Fa-f]+)$ ]]; then
		DOMAIN="0x0000"
		BUS=$(printf "0x%02x" "0x${BASH_REMATCH[1]}")
		SLOT=$(printf "0x%02x" "0x${BASH_REMATCH[2]}")
		FUNC=$(printf "0x%x" "0x${BASH_REMATCH[3]}")
	else
		DOMAIN="0x0000"
		BUS="0x00"
		SLOT="0x00"
		FUNC="0x0"
	fi
}

# Normalize a PCI address to its canonical full domain-qualified form
# ("dddd:bb:ss.f", lowercase, zero-padded). Both "dddd:bb:ss.f" (full) and
# "bb:ss.f" (short, domain 0) inputs are accepted.
normalize_pci_address() {
	local addr="$1"
	if [[ $addr =~ ^([0-9A-Fa-f]+):([0-9A-Fa-f]+):([0-9A-Fa-f]+)\.([0-9A-Fa-f]+)$ ]]; then
		printf "%04x:%02x:%02x.%x\n" \
			"0x${BASH_REMATCH[1]}" "0x${BASH_REMATCH[2]}" \
			"0x${BASH_REMATCH[3]}" "0x${BASH_REMATCH[4]}"
	elif [[ $addr =~ ^([0-9A-Fa-f]+):([0-9A-Fa-f]+)\.([0-9A-Fa-f]+)$ ]]; then
		printf "0000:%02x:%02x.%x\n" \
			"0x${BASH_REMATCH[1]}" "0x${BASH_REMATCH[2]}" "0x${BASH_REMATCH[3]}"
	else
		echo "$addr"
	fi
}

# Get nodedev name for a PCI address (e.g. "0000:00:02.0" -> "pci_0000_00_02_0").
# Short addresses are widened to domain 0 first.
get_nodedev_name() {
	local addr
	addr=$(normalize_pci_address "$1")
	echo "pci_$(echo "$addr" | sed 's/[:.]/\_/g')"
}

# Get cached nodedev XML for a PCI address
get_nodedev_xml() {
	local addr="$1"
	local nodedev_name
	nodedev_name=$(get_nodedev_name "$addr")

	if [[ -z "${nodedev_cache[$nodedev_name]:-}" ]]; then
		if nodedev_cache[$nodedev_name]=$(virsh nodedev-dumpxml "$nodedev_name" 2>/dev/null); then
			true # Cache populated successfully
		else
			nodedev_cache[$nodedev_name]="" # Cache empty result to avoid retries
		fi
	fi

	echo "${nodedev_cache[$nodedev_name]}"
}

# Given a PCI address (e.g. "00:02.0"), return its IOMMU group or "null"
get_iommu_group() {
	local addr="$1"
	local xml
	xml=$(get_nodedev_xml "$addr")
	local group
	group=$(echo "$xml" | xmlstarlet sel -t -v "//iommuGroup/@number" 2>/dev/null || true)
	echo "${group:-null}"
}

# Given a PCI address, output "TOTALVFS NUMVFS"
get_sriov_counts() {
	local addr="$1"
	local xml
	xml=$(get_nodedev_xml "$addr")

	local totalvfs=0
	local numvfs=0

	if [[ -n "$xml" ]]; then
		# Check for SR-IOV capability before parsing
		local cap_xml
		cap_xml=$(echo "$xml" | xmlstarlet sel -t -c "//capability[@type='virt_functions']" 2>/dev/null || true)

		if [[ -n "$cap_xml" ]]; then
			totalvfs=$(echo "$cap_xml" | xmlstarlet sel -t -v "/capability/@maxCount" 2>/dev/null || true)
			numvfs=$(echo "$cap_xml" | xmlstarlet sel -t -v "count(/capability/address)" 2>/dev/null || true)
		fi
	fi

	echo "${totalvfs:-0} ${numvfs:-0}"
}

# Given a PCI address, return its NUMA node (or -1 if none)
get_numa_node() {
	local addr="$1"
	local xml
	xml=$(get_nodedev_xml "$addr")
	local node
	node=$(echo "$xml" | xmlstarlet sel -t -v "//numa/@node" 2>/dev/null || true)
	echo "${node:--1}"
}

# Given a PCI address, return its PCI root (the top‐level bridge ID, e.g.
# "0000:00:03.0"). Both full ("0000:00:02.0") and short ("00:02.0") inputs are
# accepted; output is always domain-qualified.
get_pci_root() {
	local addr
	addr=$(normalize_pci_address "$1")
	local xml
	xml=$(get_nodedev_xml "$addr")

	if [[ -n "$xml" ]]; then
		# Extract the parent device from XML
		local parent
		parent=$(echo "$xml" | xmlstarlet sel -t -v "/device/parent" 2>/dev/null || true)
		if [[ -n "$parent" ]]; then
			# If parent is a PCI device, recursively find its root.
			# libvirt nodedev names look like pci_<domain>_<bus>_<slot>_<func>
			# where <domain> is one or more hex digits (typically 4).
			if [[ $parent =~ ^pci_([0-9A-Fa-f]+)_([0-9A-Fa-f]{2})_([0-9A-Fa-f]{2})_([0-9A-Fa-f])$ ]]; then
				local parent_addr="${BASH_REMATCH[1]}:${BASH_REMATCH[2]}:${BASH_REMATCH[3]}.${BASH_REMATCH[4]}"
				get_pci_root "$parent_addr"
				return
			else
				# Parent is not a PCI device, so current device is the root
				echo "$addr"
				return
			fi
		fi
	fi

	# fallback
	echo "$addr"
}

# Build VM → hostdev maps:
#   pci_to_vm[BDF] = VM name that attaches that BDF
#   mdev_to_vm[UUID] = VM name that attaches that MDEV UUID
declare -A pci_to_vm mdev_to_vm

# Gather all VM names (including inactive)
mapfile -t VMS < <(virsh list --all --name | grep -v '^$')
for VM in "${VMS[@]}"; do
	# Skip if dumpxml fails
	if ! xml=$(virsh dumpxml "$VM" 2>/dev/null); then
		continue
	fi

	# -- PCI hostdevs: use xmlstarlet to extract the full domain:bus:slot.func
	# for all PCI host devices. libvirt's <address> element may omit the domain
	# attribute, in which case we default to 0.
	while IFS=: read -r dom bus slot func; do
		[[ -n "$bus" && -n "$slot" && -n "$func" ]] || continue
		[[ -n "$dom" ]] || dom="0"
		# Format to match lspci -D output (e.g., 0000:01:00.0) by padding with zeros
		dom_fmt=$(printf "%04x" "0x$dom")
		bus_fmt=$(printf "%02x" "0x$bus")
		slot_fmt=$(printf "%02x" "0x$slot")
		func_fmt=$(printf "%x" "0x$func")
		BDF="${dom_fmt}:${bus_fmt}:${slot_fmt}.${func_fmt}"
		pci_to_vm["$BDF"]="$VM"
	done < <(echo "$xml" | xmlstarlet sel -T -t -m "//hostdev[@type='pci']/source/address" \
		-v "substring-after(@domain, '0x')" -o ":" \
		-v "substring-after(@bus, '0x')" -o ":" \
		-v "substring-after(@slot, '0x')" -o ":" \
		-v "substring-after(@function, '0x')" -n 2>/dev/null || true)

	# -- MDEV hostdevs: use xmlstarlet to extract UUIDs --
	while IFS= read -r UUID; do
		[[ -n "$UUID" ]] && mdev_to_vm["$UUID"]="$VM"
	done < <(echo "$xml" | xmlstarlet sel -T -t -m "//hostdev[@type='mdev']/source/address" -v "@uuid" -n 2>/dev/null || true)
done

# Helper: convert a VM name to JSON value (quoted string or null)
to_json_vm() {
	local vm="$1"
	if [[ -z "$vm" ]]; then
		echo "null"
	else
		json_escape "$vm"
	fi
}

# Parse a "description" file for GPU properties and set global variables
# Expects one argument: the path to the description file
parse_and_add_gpu_properties() {
    local desc_file="$1"
    # Reset properties to null defaults
    MAX_INSTANCES="null"
    VIDEO_RAM="null"
    MAX_HEADS="null"
    MAX_RESOLUTION_X="null"
    MAX_RESOLUTION_Y="null"

    if [[ -f "$desc_file" ]]; then
        local desc
        desc=$(<"$desc_file")
        if [[ $desc =~ max_instance=([0-9]+) ]]; then
            MAX_INSTANCES="${BASH_REMATCH[1]}"
        fi
        if [[ $desc =~ framebuffer=([0-9]+)M? ]]; then # Support with or without 'M' suffix
            VIDEO_RAM="${BASH_REMATCH[1]}"
        fi
        if [[ $desc =~ num_heads=([0-9]+) ]]; then
            MAX_HEADS="${BASH_REMATCH[1]}"
        fi
        if [[ $desc =~ max_resolution=([0-9]+)x([0-9]+) ]]; then
            MAX_RESOLUTION_X="${BASH_REMATCH[1]}"
            MAX_RESOLUTION_Y="${BASH_REMATCH[2]}"
        fi
    fi
}

# Finds and formats mdev instances for a given PCI device (PF or VF).
# Appends JSON strings for each found mdev instance to the global 'vlist' array.
# Arguments:
#   $1: mdev_base_path (e.g., /sys/bus/pci/devices/.../mdev_supported_types)
#   $2: bdf — short "bb:ss.f" (e.g. 01:00.0) or full
#       "dddd:bb:ss.f" (e.g. 0001:65:00.0) for non-zero PCI domains
process_mdev_instances() {
	local mdev_base_path="$1"
	local bdf="$2"

	if [[ ! -d "$mdev_base_path" ]]; then
		return
	fi

	for PROF_DIR in "$mdev_base_path"/*; do
		[[ -d "$PROF_DIR" ]] || continue

		local PROFILE_NAME
		if [[ -f "$PROF_DIR/name" ]]; then
			PROFILE_NAME=$(<"$PROF_DIR/name")
		else
			PROFILE_NAME=$(basename "$PROF_DIR")
		fi

		parse_and_add_gpu_properties "$PROF_DIR/description"

		local DEVICE_DIR="$PROF_DIR/devices"
		if [[ -d "$DEVICE_DIR" ]]; then
			for UDIR in "$DEVICE_DIR"/*; do
				[[ -d "$UDIR" ]] || continue
				local MDEV_UUID
				MDEV_UUID=$(basename "$UDIR")

				local DOMAIN BUS SLOT FUNC
				parse_pci_address "$bdf"

				local raw
				raw="${mdev_to_vm[$MDEV_UUID]:-}"
				local USED_JSON
				USED_JSON=$(to_json_vm "$raw")

				vlist+=(
					"{\"mdev_uuid\":\"$MDEV_UUID\",\"profile_name\":$(json_escape "$PROFILE_NAME"),\"max_instances\":$MAX_INSTANCES,\"video_ram\":$VIDEO_RAM,\"max_heads\":$MAX_HEADS,\"max_resolution_x\":$MAX_RESOLUTION_X,\"max_resolution_y\":$MAX_RESOLUTION_Y,\"libvirt_address\":{\"domain\":\"$DOMAIN\",\"bus\":\"$BUS\",\"slot\":\"$SLOT\",\"function\":\"$FUNC\"},\"used_by_vm\":$USED_JSON}")
			done
		fi
	done
}

# === GPU Discovery ===

# Parse nvidia-smi vgpu profiles once at the beginning
parse_nvidia_vgpu_profiles

# `lspci -nnm` keeps the existing output format for devices in PCI domain 0
# (short "bb:ss.f" form) and includes the domain prefix only for devices on
# non-zero PCI segments (multi-IIO servers, some ARM systems). The helpers
# below (parse_pci_address, normalize_pci_address) accept both formats.
mapfile -t LINES < <(lspci -nnm)

echo '{ "gpus": ['

first_gpu=true
for LINE in "${LINES[@]}"; do
	# Parse lspci -nnm fields: SLOT "CLASS [CODE]" "VENDOR [VID]" "DEVICE [DID]" ...
	if [[ $LINE =~ ^([^[:space:]]+)[[:space:]]\"([^\"]+)\"[[:space:]]\"([^\"]+)\"[[:space:]]\"([^\"]+)\" ]]; then
		PCI_ADDR="${BASH_REMATCH[1],,}"  # Normalize to lowercase
		PCI_CLASS="${BASH_REMATCH[2]}"
		VENDOR_FIELD="${BASH_REMATCH[3]}"
		DEVICE_FIELD="${BASH_REMATCH[4]}"
	else
		continue
	fi

	# sysfs paths always require the full domain-qualified form. PCI_ADDR may
	# be short ("00:02.0") for domain 0 or full ("0001:65:00.0") otherwise, so
	# we derive a separate SYSFS_ADDR just for filesystem lookups.
	SYSFS_ADDR=$(normalize_pci_address "$PCI_ADDR")

	# If this is a VF, skip it. It will be processed under its PF.
	if [[ -e "/sys/bus/pci/devices/$SYSFS_ADDR/physfn" ]]; then
		continue
	fi

	# Only process PCI classes - 3D controller, Processing accelerators, Display controller
	if [[ ! "$PCI_CLASS" =~ (3D\ controller|Processing\ accelerators|Display\ controller) ]]; then
		continue
	fi

	# Extract vendor name and ID
	VENDOR=$(sed -E 's/ \[[0-9A-Fa-f]{4}\]$//' <<<"$VENDOR_FIELD")
	VENDOR_ID=$(sed -E 's/.*\[([0-9A-Fa-f]{4})\]$/\1/' <<<"$VENDOR_FIELD")
	# Extract device name and ID
	DEVICE=$(sed -E 's/ \[[0-9A-Fa-f]{4}\]$//' <<<"$DEVICE_FIELD")
	DEVICE_ID=$(sed -E 's/.*\[([0-9A-Fa-f]{4})\]$/\1/' <<<"$DEVICE_FIELD")

	# Kernel driver
	DRV_PATH="/sys/bus/pci/devices/$SYSFS_ADDR/driver"
	if [[ -L $DRV_PATH ]]; then
		DRIVER=$(basename "$(readlink "$DRV_PATH")")
	else
		DRIVER="unknown"
	fi

	# IOMMU group
	IOMMU=$(get_iommu_group "$PCI_ADDR")

	# PCI root (to group GPUs under same PCIe switch/root complex)
	PCI_ROOT=$(get_pci_root "$PCI_ADDR")

	# NUMA node
	NUMA_NODE=$(get_numa_node "$PCI_ADDR")

	# SR-IOV counts
	read -r TOTALVFS NUMVFS < <(get_sriov_counts "$PCI_ADDR")

	# Get Physical GPU properties from its own description file, if available
	PF_DESC_PATH="/sys/bus/pci/devices/$SYSFS_ADDR/description"
	parse_and_add_gpu_properties "$PF_DESC_PATH"
	# Save physical function's properties before they are overwritten by vGPU/VF processing
	PF_MAX_INSTANCES=$MAX_INSTANCES
	PF_VIDEO_RAM=$VIDEO_RAM
	PF_MAX_HEADS=$MAX_HEADS
	PF_MAX_RESOLUTION_X=$MAX_RESOLUTION_X
	PF_MAX_RESOLUTION_Y=$MAX_RESOLUTION_Y

	# === full_passthrough usage ===
	# pci_to_vm is keyed by the full domain-qualified form, so normalize the
	# lspci-style PCI_ADDR (which may be short for domain 0) before lookup.
	raw="${pci_to_vm[$(normalize_pci_address "$PCI_ADDR")]:-}"
	FULL_USED_JSON=$(to_json_vm "$raw")

	# === vGPU (MDEV) instances ===
	VGPU_ARRAY="[]"
	declare -a vlist=()
	# Process mdev on the Physical Function
	MDEV_BASE="/sys/bus/pci/devices/$SYSFS_ADDR/mdev_supported_types"
	process_mdev_instances "$MDEV_BASE" "$PCI_ADDR"

	# === VF instances (SR-IOV / MIG) ===
	VF_ARRAY="[]"
	declare -a flist=()
	if ((TOTALVFS > 0)); then
		for VF_LINK in /sys/bus/pci/devices/"$SYSFS_ADDR"/virtfn*; do
			[[ -L $VF_LINK ]] || continue
			VF_PATH=$(readlink -f "$VF_LINK")
			# Keep the full domain-qualified address (e.g. "0000:65:00.2")
			VF_BDF=${VF_PATH##*/}

			# For NVIDIA SR-IOV, check for vGPU (mdev) on the VF itself
			if [[ "$VENDOR_ID" == "10de" ]]; then
				VF_MDEV_BASE="$VF_PATH/mdev_supported_types"
				process_mdev_instances "$VF_MDEV_BASE" "$VF_BDF"
			fi

			parse_pci_address "$VF_BDF"

			# Determine vf_profile using nvidia-smi information
			VF_PROFILE=""
			VF_PROFILE_NAME=""
			VF_MAX_INSTANCES="null"
			VF_VIDEO_RAM="null"
			VF_MAX_HEADS="null"
			VF_MAX_RESOLUTION_X="null"
			VF_MAX_RESOLUTION_Y="null"

			if [[ "$VENDOR_ID" == "10de" ]]; then
				# For NVIDIA GPUs, check current vGPU type
				current_vgpu_type=$(get_current_vgpu_type "$VF_PATH")
				if [[ "$current_vgpu_type" != "0" ]]; then
					# nvidia_vgpu_profiles is keyed by the canonical full
					# "dddd:bb:ss.f" form, so widen PCI_ADDR (which may be
					# short for domain 0) before the cache lookup.
					profile_info=$(get_nvidia_profile_info "$(normalize_pci_address "$PCI_ADDR")" "$current_vgpu_type")
					IFS='|' read -r VF_PROFILE_NAME VF_MAX_INSTANCES VF_VIDEO_RAM VF_MAX_HEADS VF_MAX_RESOLUTION_X VF_MAX_RESOLUTION_Y <<< "$profile_info"
					VF_PROFILE="$VF_PROFILE_NAME"
				fi
			fi

			# Fallback to lspci parsing if no nvidia-smi profile found
			if [[ -z "$VF_PROFILE" ]]; then
				if VF_LINE=$(lspci -nnm -s "$VF_BDF" 2>/dev/null); then
					if [[ $VF_LINE =~ \"([^\"]+)\"[[:space:]]\"([^\"]+)\"[[:space:]]\"([^\"]+)\"[[:space:]]\"([^\"]+)\" ]]; then
						VF_DEVICE_FIELD="${BASH_REMATCH[4]}"
						VF_PROFILE=$(sed -E 's/ \[[0-9A-Fa-f]{4}\]$//' <<<"$VF_DEVICE_FIELD")
					fi
				fi
			fi
			VF_PROFILE_JSON=$(json_escape "$VF_PROFILE")

			# Determine which VM uses this VF_BDF (normalize for map lookup)
			raw="${pci_to_vm[$(normalize_pci_address "$VF_BDF")]:-}"
			USED_JSON=$(to_json_vm "$raw")

			# For backward-compat JSON output, strip the default "0000:" domain
			# prefix so domain-0 VFs still print as "65:00.2" rather than the
			# full "0000:65:00.2". Non-zero domains are preserved unchanged.
			VF_DISPLAY_ADDR="${VF_BDF#0000:}"

			flist+=(
				"{\"vf_pci_address\":\"$VF_DISPLAY_ADDR\",\"vf_profile\":$VF_PROFILE_JSON,\"max_instances\":$VF_MAX_INSTANCES,\"video_ram\":$VF_VIDEO_RAM,\"max_heads\":$VF_MAX_HEADS,\"max_resolution_x\":$VF_MAX_RESOLUTION_X,\"max_resolution_y\":$VF_MAX_RESOLUTION_Y,\"libvirt_address\":{\"domain\":\"$DOMAIN\",\"bus\":\"$BUS\",\"slot\":\"$SLOT\",\"function\":\"$FUNC\"},\"used_by_vm\":$USED_JSON}")
		done
		if [ ${#flist[@]} -gt 0 ]; then
			VF_ARRAY="[$(
				IFS=,
				echo "${flist[*]}"
			)]"
		fi
	fi

	# Consolidate all vGPU instances (from PF and VFs)
	if [ ${#vlist[@]} -gt 0 ]; then
		VGPU_ARRAY="[$(
			IFS=,
			echo "${vlist[*]}"
		)]"
	fi

	# === full_passthrough block ===
	# If vgpu_instances and vf_instances are empty, we can assume full passthrough
	FP_ENABLED=0
	if [[ ${#vlist[@]} -eq 0 && ${#flist[@]} -eq 0 ]]; then
		FP_ENABLED=1
	fi

	# Sets global DOMAIN/BUS/SLOT/FUNC for JSON output below
	parse_pci_address "$PCI_ADDR"

	# Emit JSON
	if $first_gpu; then
		first_gpu=false
	else
		echo ","
	fi

	cat <<JSON
    {
      "pci_address":$(json_escape "$PCI_ADDR"),
      "vendor_id":$(json_escape "$VENDOR_ID"),
      "device_id":$(json_escape "$DEVICE_ID"),
      "vendor":$(json_escape "$VENDOR"),
      "device":$(json_escape "$DEVICE"),
      "driver":$(json_escape "$DRIVER"),
      "pci_class":$(json_escape "$PCI_CLASS"),
      "iommu_group":$(json_escape "$IOMMU"),
      "pci_root":$(json_escape "$PCI_ROOT"),
      "numa_node":$NUMA_NODE,
      "sriov_totalvfs":$TOTALVFS,
      "sriov_numvfs":$NUMVFS,
      "max_instances":$PF_MAX_INSTANCES,
      "video_ram":$PF_VIDEO_RAM,
      "max_heads":$PF_MAX_HEADS,
      "max_resolution_x":$PF_MAX_RESOLUTION_X,
      "max_resolution_y":$PF_MAX_RESOLUTION_Y,

      "full_passthrough": {
        "enabled":$FP_ENABLED,
        "libvirt_address": {
          "domain":$(json_escape "$DOMAIN"),
          "bus":$(json_escape "$BUS"),
          "slot":$(json_escape "$SLOT"),
          "function":$(json_escape "$FUNC")
        },
        "used_by_vm":$FULL_USED_JSON
      },

      "vgpu_instances":$VGPU_ARRAY,
      "vf_instances":$VF_ARRAY
    }
JSON

done

echo ""
echo "]}"
