#!/bin/sh /etc/rc.common

START=95
STOP=10
USE_PROCD=1

STATUS_DIR="/var/run/qemu-vms"
LOG_DIR="/var/log/qemu-vms"

host_log() {
	# Per-VM HOST-side lifecycle log: start/stop attempts, config
	# warnings (missing MAC, unknown disk_bus, VNC without a port,
	# passthrough bind issues), and QEMU's own stderr (KVM unavailable,
	# image not found, device init failures). Deliberately separate from
	# the guest's own serial console capture
	# (/var/log/qemu-$vm-console.log): that one shows what the GUEST OS
	# printed; this one shows whether the HOST managed to start/
	# configure the VM at all - meaningful for VNC-mode VMs too, which
	# have no serial console capture at all.
	local cfg="$1"
	shift
	mkdir -p "$LOG_DIR"
	echo "$(date '+%Y-%m-%d %H:%M:%S') $*" >> "$LOG_DIR/$cfg.log"
}

is_anonymous_section() {
	# UCI auto-generated names look like: cfgXXXXXX (cfg + hex)
	case "$1" in
		cfg[0-9a-f][0-9a-f][0-9a-f][0-9a-f]*) return 0 ;;
		*) return 1 ;;
	esac
}

warn_if_anonymous() {
	local cfg="$1"
	if is_anonymous_section "$cfg"; then
		logger -t qemu-vms "WARNING: VM section '$cfg' is anonymous (no name set). " \
			"Paths (console/qmp socket, pidfile) are derived from this auto-generated " \
			"UCI id and may change if the config is reordered or the section is recreated. " \
			"Please set an explicit section name, e.g. config vm 'myvm'."
	fi
}

build_netargs() {
	local cfg="$1"
	idx=0
	netdev_opts=""
	device_opts=""
	config_list_foreach "$cfg" network _add_net
}

_add_net() {
	local netcfg="$1"
	local mac ifname bridge

	config_get mac "$netcfg" mac
	config_get ifname "$netcfg" ifname
	config_get bridge "$netcfg" bridge
	config_get driver "$netcfg" driver "virtio-net-pci"

	local id="net${idx}"

	# QEMU's tap "script=" hook only ever calls the script with the tap
	# ifname as $1 - there is no way to pass a second argument (like the
	# target bridge) through QEMU itself. So instead of pointing directly
	# at /etc/qemu-ifup, generate a tiny per-interface wrapper that bakes
	# in this network section's bridge and calls /etc/qemu-ifup with both
	# arguments. If "bridge" is empty, the wrapper still runs but passes
	# an empty $2, and /etc/qemu-ifup simply skips the enslave step.
	local ifup_wrapper="$STATUS_DIR/qemu-ifup-$netcfg.sh"
	mkdir -p "$STATUS_DIR"
	cat > "$ifup_wrapper" <<-EOF
		#!/bin/sh
		exec /etc/qemu-ifup "\$1" "$bridge"
	EOF
	chmod +x "$ifup_wrapper"

	netdev_opts="$netdev_opts -netdev tap,id=$id,ifname=$ifname,script=$ifup_wrapper,downscript=no"

	local mac_opt=""
	[ -n "$mac" ] && mac_opt=",mac=$mac"
	device_opts="$device_opts -device ${driver}${mac_opt},netdev=$id"
	idx=$((idx + 1))
}

build_usbargs() {
	local cfg="$1"
	usb_opts=""
	usb_needed=0
	config_list_foreach "$cfg" usb_passthrough _add_usb
}

_add_usb() {
	local usbcfg="$1"
	local vendor_id product_id

	config_get vendor_id "$usbcfg" vendor_id
	config_get product_id "$usbcfg" product_id
	[ -n "$vendor_id" ] && [ -n "$product_id" ] || return 0

	usb_opts="$usb_opts -device usb-host,vendorid=0x$vendor_id,productid=0x$product_id"
	usb_needed=1
}

_add_pci() {
	local pci_section="$1"
	local pci_id
	config_get pci_id "$pci_section" pci_id
	[ -n "$pci_id" ] || return 0

	local pci_path="/sys/bus/pci/devices/0000:$pci_id"
	local iommu_group=""
	if [ -L "$pci_path/iommu_group" ]; then
		iommu_group="$(basename "$(readlink -f "$pci_path/iommu_group")")"
	fi
	if [ -n "$iommu_group" ]; then
		host_log "$cfg" "PCI passthrough: device $pci_id is in IOMMU group $iommu_group"
		for dev in /sys/kernel/iommu_groups/$iommu_group/devices/*; do
			local dev_name="$(basename "$dev")"
			local current_driver="$(basename "$(readlink -f "$dev/driver" 2>/dev/null)" 2>/dev/null)"
			if [ -n "$current_driver" ] && [ "$current_driver" != "vfio-pci" ]; then
				local orig_file="$STATUS_DIR/pci-orig-driver-$dev_name"
				[ -f "$orig_file" ] || echo "$current_driver" > "$orig_file"
				echo -n "$dev_name" > "$dev/driver/unbind" 2>/dev/null
				host_log "$cfg" "PCI: unbound $dev_name from $current_driver"
			fi
			echo "vfio-pci" > "$dev/driver_override" 2>/dev/null
			echo "$dev_name" > /sys/bus/pci/drivers/vfio-pci/bind 2>/dev/null
			if [ -e "$dev/driver" ] && [ "$(basename "$(readlink -f "$dev/driver")")" = "vfio-pci" ]; then
				host_log "$cfg" "PCI: successfully bound $dev_name to vfio-pci"
			else
				host_log "$cfg" "ERROR: failed to bind $dev_name to vfio-pci"
			fi
		done
		sleep 3
		pci_opts="$pci_opts -device vfio-pci,host=$pci_id"
	else
		host_log "$cfg" "WARNING: Cannot determine IOMMU group for $pci_id"
	fi
}

_unbind_usb_host() {
	local usbcfg="$1"
	local vendor_id product_id
	config_get vendor_id "$usbcfg" vendor_id
	config_get product_id "$usbcfg" product_id
	logger -t qemu-vms "DEBUG unbind: usbcfg=$usbcfg vendor=$vendor_id product=$product_id"
	[ -n "$vendor_id" ] && [ -n "$product_id" ] || return 0
	for dev in /sys/bus/usb/devices/*; do
		if [ -f "$dev/idVendor" ] && [ -f "$dev/idProduct" ]; then
			local v=$(cat "$dev/idVendor") p=$(cat "$dev/idProduct")
			if [ "$v" = "$vendor_id" ] && [ "$p" = "$product_id" ]; then
				local devname=$(basename "$dev")
				if [ -L "$dev/driver" ]; then
					local driver_path=$(readlink -f "$dev/driver")
					echo -n "$devname" > "$driver_path/unbind" 2>/dev/null
					host_log "$cfg" "USB: unbound $devname from $(basename "$driver_path")"
				fi
				# interfaces are SIBLINGS of $dev, not children -
				# glob at the /sys/bus/usb/devices/ level, matching "$devname:*"
				for iface in /sys/bus/usb/devices/"$devname":*; do
					[ -e "$iface" ] || continue
					if [ -L "$iface/driver" ]; then
						local ifname=$(basename "$iface")
						local idriver_path=$(readlink -f "$iface/driver")
						echo -n "$ifname" > "$idriver_path/unbind" 2>/dev/null
						host_log "$cfg" "USB: unbound $ifname from $(basename "$idriver_path")"
					fi
				done
			fi
		fi
	done
}

_release_usb_node() {
	# Rebind a single sysfs usb node (device or interface) to whatever
	# driver it was recorded as being on before we ever touched it.
	local node="$1"
	local orig_file="$STATUS_DIR/usb-orig-driver-$node"

	[ -f "$orig_file" ] || return 0
	local orig_driver
	orig_driver="$(cat "$orig_file")"

	if [ -n "$orig_driver" ] && [ -e "/sys/bus/usb/drivers/$orig_driver" ]; then
		echo -n "$node" > "/sys/bus/usb/drivers/$orig_driver/bind" 2>/dev/null
		if [ -L "/sys/bus/usb/devices/$node/driver" ] && \
		   [ "$(basename "$(readlink -f "/sys/bus/usb/devices/$node/driver")")" = "$orig_driver" ]; then
			logger -t qemu-vms "release_usb: restored $node to $orig_driver"
		else
			logger -t qemu-vms "WARNING: release_usb: failed to rebind $node to $orig_driver"
		fi
	fi
	rm -f "$orig_file"
}

release_usb() {
	# Usage: release_usb <vendor_id> <product_id>
	local vendor_id="$1"
	local product_id="$2"

	[ -n "$vendor_id" ] && [ -n "$product_id" ] || {
		logger -t qemu-vms "release_usb: vendor_id/product_id required"
		return 1
	}

	for dev in /sys/bus/usb/devices/*; do
		[ -f "$dev/idVendor" ] && [ -f "$dev/idProduct" ] || continue
		local v=$(cat "$dev/idVendor") p=$(cat "$dev/idProduct")
		[ "$v" = "$vendor_id" ] && [ "$p" = "$product_id" ] || continue

		local devname=$(basename "$dev")
		_release_usb_node "$devname"

		for iface in /sys/bus/usb/devices/"$devname":*; do
			[ -e "$iface" ] || continue
			_release_usb_node "$(basename "$iface")"
		done
	done
}

release_usb_passthrough() {
	# Usage: release_usb_passthrough <usb-passthrough section name>
	local section="$1"
	config_load qemu-vms
	local vendor_id product_id
	config_get vendor_id "$section" vendor_id
	config_get product_id "$section" product_id
	if [ -z "$vendor_id" ] || [ -z "$product_id" ]; then
		logger -t qemu-vms "release_usb_passthrough: section '$section' has no vendor_id/product_id"
		return 1
	fi
	release_usb "$vendor_id" "$product_id"
}


build_customargs() {
	local cfg="$1"
	custom_opts=""
	config_list_foreach "$cfg" custom_arg _add_customarg
}

_add_customarg() {
	local arg="$1"
	# Deliberately unquoted below (word-splitting is intentional): a
	# single list entry like "-device foo,bar=baz" should become two
	# separate argv elements ("-device" and "foo,bar=baz"), same as if
	# the admin had typed it directly on a qemu command line. This is an
	# escape hatch for arbitrary flags we don't have a dedicated UCI
	# option for - the admin is trusted with their own local config.
	custom_opts="$custom_opts $arg"
}

build_diskargs() {
	local cfg="$1"
	local image="$2"
	local bus

	config_get bus "$cfg" disk_bus "virtio"

	disk_controller_opts=""
	disk_opts=""

	case "$bus" in
		virtio)
			#disk_opts="-drive file=$image,format=raw,if=virtio"
			disk_opts="-drive file=$image,if=virtio"
			;;
		ide)
			# Widest guest compatibility (old Windows/DOS/legacy kernels
			# without virtio drivers), at some cost to I/O throughput.
			#disk_opts="-drive file=$image,format=$format,if=ide"
			disk_opts="-drive file=$image,if=ide"
			;;
		scsi)
			disk_controller_opts="-device virtio-scsi-pci,id=scsi0"
			#disk_opts="-drive file=$image,format=$format,if=none,id=drive0 -device scsi-hd,drive=drive0,bus=scsi0.0"
			disk_opts="-drive file=$image,if=none,id=drive0 -device scsi-hd,drive=drive0,bus=scsi0.0"
			;;
		sata|ahci)
			disk_controller_opts="-device ahci,id=ahci0"
			#disk_opts="-drive file=$image,format=$format,if=none,id=drive0 -device ide-hd,drive=drive0,bus=ahci0.0"
			disk_opts="-drive file=$image,if=none,id=drive0 -device ide-hd,drive=drive0,bus=ahci0.0"
			;;
		*)
			logger -t qemu-vms "WARNING: VM '$cfg' has unknown disk_bus '$bus', falling back to virtio"
			#disk_opts="-drive file=$image,format=$format,if=virtio"
			disk_opts="-drive file=$image,if=virtio"
			host_log "$cfg" "WARNING: unknown disk_bus '$bus', falling back to virtio"
			;;
	esac
}



start_vm() {
	local cfg="$1"
	local enabled image memory smp cpu pci_passthrough

	config_get_bool enabled "$cfg" enabled 0
	[ "$enabled" = "1" ] || return 0

	local existing_pid
	existing_pid="$(cat "$STATUS_DIR/$cfg.pid" 2>/dev/null)"
	if [ -n "$existing_pid" ] && kill -0 "$existing_pid" 2>/dev/null && \
	   grep -aq "qemu-system-x86_64" "/proc/$existing_pid/cmdline" 2>/dev/null; then
		logger -t qemu-vms "start_vm: '$cfg' already running (pid $existing_pid), skipping"
		return 0
	fi

	host_log "$cfg" "=== start_vm: attempting to start ==="

	warn_if_anonymous "$cfg"

	config_get image "$cfg" image
	config_get cdrom "$cfg" cdrom
	config_get memory "$cfg" memory 128
	config_get smp "$cfg" smp 1
	config_get cpu "$cfg" cpu "host"
	config_list_foreach "$cfg" usb_passthrough _unbind_usb_host
	config_get pci_passthrough "$cfg" pci_passthrough
	config_get display_type "$cfg" display_type "serial"
	config_get vnc_port "$cfg" vnc_port
	config_get machine "$cfg" machine "q35"

	local console_sock="$STATUS_DIR/qemu-$cfg.sock"
	local console_log="$LOG_DIR/qemu-$cfg-console.log"
	local qmp_sock="$STATUS_DIR/qemu-$cfg.qmp"

	build_netargs "$cfg"
	build_usbargs "$cfg"
	build_customargs "$cfg"
	build_diskargs "$cfg" "$image"

	local pci_opts=""
	config_list_foreach "$cfg" pci_passthrough _add_pci

	local cdrom_opts=""
	[ -n "$cdrom" ] && cdrom_opts="-cdrom $cdrom"

	local usb_controller_opts=""
	[ "$usb_needed" = "1" ] && usb_controller_opts="-device qemu-xhci,id=usb-bus0"

	local vnc_opts=""
	if [ "$display_type" = "vnc" ] && [ -n "$vnc_port" ]; then
		local vnc_display=$((vnc_port - 5900))
		local vnc_ws_port=$((vnc_port + 1000))
		if [ "$vnc_display" -ge 0 ] 2>/dev/null; then
			vnc_opts="-vnc :${vnc_display},websocket=${vnc_ws_port}"
		else
			logger -t qemu-vms "WARNING: VM '$cfg' has vnc_port=$vnc_port, which is below 5900. Skipping VNC."
		fi
	elif [ "$display_type" = "vnc" ]; then
		logger -t qemu-vms "WARNING: VM '$cfg' has display_type=vnc but no vnc_port set. Skipping VNC."
	fi

	local display_opts="-display none -nographic"
	[ -n "$vnc_opts" ] && display_opts=""

	#rm -f "$console_sock" "$qmp_sock"
	mkdir -p "$STATUS_DIR"
	mkdir -p "$LOG_DIR"
	mkdir -p "$(dirname "$console_log")"

	procd_open_instance "qemu-$cfg"
	procd_set_param command /usr/libexec/qemu-vms/run-with-log.sh "$LOG_DIR/$cfg.log" \
		/usr/bin/qemu-system-x86_64 \
		-enable-kvm -cpu "$cpu" -smp "$smp" -m "${memory}M" \
		$disk_controller_opts $disk_opts \
		$device_opts $netdev_opts $pci_opts $cdrom_opts $usb_controller_opts $usb_opts $vnc_opts $custom_opts \
		-device virtio-rng-pci \
		-machine $machine \
		-chardev socket,id=con0,path="$console_sock",server=on,wait=off,logfile="$console_log",logappend=on \
		-serial chardev:con0 \
		-chardev socket,id=qmp0,path="$qmp_sock",server=on,wait=off \
		-qmp chardev:qmp0 \
		-monitor none $display_opts

	procd_set_param respawn 3600 5 5
	procd_set_param stdout 1
	procd_set_param stderr 1
	procd_set_param pidfile "$STATUS_DIR/$cfg.pid"
	procd_close_instance

	# NOTE: no persistent socat/console bridge here on purpose.
	# QEMU discards console output cleanly when nobody is connected to
	# console_sock (server=on,wait=off), while logfile= still captures
	# everything continuously (boot log never stalls). Interactive
	# access is attached on-demand via /usr/bin/vm-console, which only
	# connects for the duration of the minicom session.
}

release_pci() {
	# Usage: release_pci <pci_id, e.g. 04:00.0>
	local pci_id="$1"
	[ -n "$pci_id" ] || {
		logger -t qemu-vms "release_pci: pci_id required"
		return 1
	}

	local pci_path="/sys/bus/pci/devices/0000:$pci_id"
	local iommu_group=""
	[ -L "$pci_path/iommu_group" ] && iommu_group="$(basename "$(readlink -f "$pci_path/iommu_group")")"

	if [ -z "$iommu_group" ]; then
		logger -t qemu-vms "release_pci: cannot determine IOMMU group for $pci_id"
		return 1
	fi

	for dev in /sys/kernel/iommu_groups/$iommu_group/devices/*; do
		local dev_name="$(basename "$dev")"
		local current_driver="$(basename "$(readlink -f "$dev/driver" 2>/dev/null)" 2>/dev/null)"

		if [ "$current_driver" != "vfio-pci" ]; then
			logger -t qemu-vms "release_pci: $dev_name is not bound to vfio-pci (currently '$current_driver'), skipping"
			continue
		fi

		echo -n "" > "$dev/driver_override" 2>/dev/null
		echo -n "$dev_name" > "$dev/driver/unbind" 2>/dev/null

		local orig_file="$STATUS_DIR/pci-orig-driver-$dev_name"
		if [ -f "$orig_file" ]; then
			local orig_driver
			orig_driver="$(cat "$orig_file")"
			if [ -n "$orig_driver" ] && [ -e "/sys/bus/pci/drivers/$orig_driver" ]; then
				echo -n "$dev_name" > "/sys/bus/pci/drivers/$orig_driver/bind" 2>/dev/null
				if [ -e "$dev/driver" ] && [ "$(basename "$(readlink -f "$dev/driver")")" = "$orig_driver" ]; then
					logger -t qemu-vms "release_pci: restored $dev_name to $orig_driver"
				else
					logger -t qemu-vms "WARNING: release_pci: failed to rebind $dev_name to $orig_driver"
					logger -t qemu-vms "WARNING: release_pci: will need reoot host to bind original driver $$orig_driver"
					fi
			fi
			rm -f "$orig_file"
		else
			logger -t qemu-vms "release_pci: no recorded original driver for $dev_name, left unbound"
		fi
	done
}

release_pci_passthrough() {
	# Usage: release_pci_passthrough <pci-passthrough section name>
	local section="$1"
	config_load qemu-vms
	local pci_id
	config_get pci_id "$section" pci_id
	if [ -z "$pci_id" ]; then
		logger -t qemu-vms "release_pci_passthrough: section '$section' has no pci_id"
		return 1
	fi
	release_pci "$pci_id"
}


EXTRA_COMMANDS="release_pci_passthrough release_usb_passthrough"
EXTRA_HELP="        release_pci_passthrough <section>	Unbind a PCI device from vfio-pci and restore its original driver
        release_usb_passthrough <section>	Unbind a USB device from passthrough use and restore its original driver"


start_service() {
	config_load qemu-vms
	if [ -n "$1" ]; then
		start_vm "$1"
	else
		config_foreach start_vm vm
	fi
}

#service_triggers() {
#	procd_add_reload_trigger "qemu-vms"
#}
