package libvirt

import (
	"strconv"
	"strings"

	golibvirt "github.com/digitalocean/go-libvirt"
	libvirtutils "github.com/thomasklein94/packer-plugin-libvirt/libvirt-utils"
)

type utils interface {
	GatherAllDomains() (domains []golibvirt.Domain, err error)
	GatherStatsForDomains(domains []golibvirt.Domain, metricNumber uint32) ([]golibvirt.DomainStatsRecord, error)
	GatherNumberOfPCPUs() (int, error)
	GatherVcpuMapping(domain golibvirt.Domain, pCPUs int, shouldGetCurrentPCPU bool) ([]vcpuAffinity, error)
	EnsureConnected(libvirtURI string) error
	Disconnect() error
}

type utilsImpl struct {
	libvirt *golibvirt.Libvirt
}

type vcpuAffinity struct {
	vcpuID        string
	coresAffinity string
	currentPCPUID int32
}

// GatherAllDomains gathers all domains on system
func (l *utilsImpl) GatherAllDomains() (domains []golibvirt.Domain, err error) {
	allDomainStatesFlag := golibvirt.ConnectListDomainsRunning + golibvirt.ConnectListDomainsPaused +
		golibvirt.ConnectListDomainsShutoff + golibvirt.ConnectListDomainsOther

	domains, _, err = l.libvirt.ConnectListAllDomains(1, allDomainStatesFlag)
	return domains, err
}

// GatherStatsForDomains gathers stats for given domains based on number that was previously calculated
func (l *utilsImpl) GatherStatsForDomains(domains []golibvirt.Domain, metricNumber uint32) ([]golibvirt.DomainStatsRecord, error) {
	if metricNumber == 0 {
		// do not need to do expensive call if no stats were set to gather
		return []golibvirt.DomainStatsRecord{}, nil
	}

	allDomainStatesFlag := golibvirt.ConnectGetAllDomainsStatsRunning + golibvirt.ConnectGetAllDomainsStatsPaused +
		golibvirt.ConnectGetAllDomainsStatsShutoff + golibvirt.ConnectGetAllDomainsStatsOther

	return l.libvirt.ConnectGetAllDomainStats(domains, metricNumber, allDomainStatesFlag)
}

func (l *utilsImpl) GatherNumberOfPCPUs() (int, error) {
	//nolint:dogsled //Using only needed values from library function
	_, _, _, _, nodes, sockets, cores, threads, err := l.libvirt.NodeGetInfo()
	if err != nil {
		return 0, err
	}

	return int(nodes * sockets * cores * threads), nil
}

// GatherVcpuMapping is based on official go-libvirt library:
// https://github.com/libvirt/libvirt-go-module/blob/268a5d02e00cc9b3d5d7fa6c08d753071e7d14b8/domain.go#L4516
// (this library cannot be used here because of C bindings)
func (l *utilsImpl) GatherVcpuMapping(domain golibvirt.Domain, pCPUs int, shouldGetCurrentPCPU bool) ([]vcpuAffinity, error) {
	//nolint:dogsled //Using only needed values from library function
	_, _, _, vCPUs, _, err := l.libvirt.DomainGetInfo(domain)
	if err != nil {
		return nil, err
	}

	bytesToHoldPCPUs := (pCPUs + 7) / 8

	cpuInfo, vcpuPinInfo, err := l.libvirt.DomainGetVcpus(domain, int32(vCPUs), int32(bytesToHoldPCPUs))
	if err != nil {
		// DomainGetVcpus gets not only affinity (1:N mapping from VCPU to PCPU)
		// but also realtime 1:1 mapping from VCPU to PCPU
		// Unfortunately it will return nothing (only error) for inactive domains -> for that case use
		// DomainGetVcpuPinInfo (which only gets affinity but even for inactive domains)

		vcpuPinInfo, _, err = l.libvirt.DomainGetVcpuPinInfo(domain, int32(vCPUs), int32(bytesToHoldPCPUs), uint32(golibvirt.DomainAffectCurrent))
		if err != nil {
			return nil, err
		}
	}

	var vcpuAffinities []vcpuAffinity
	for i := 0; i < int(vCPUs); i++ {
		var coresAffinity []string
		for j := 0; j < pCPUs; j++ {
			aByte := (i * bytesToHoldPCPUs) + (j / 8)
			aBit := j % 8

			if (vcpuPinInfo[aByte] & (1 << uint(aBit))) != 0 {
				coresAffinity = append(coresAffinity, strconv.Itoa(j))
			}
		}

		vcpu := vcpuAffinity{
			vcpuID:        strconv.FormatInt(int64(i), 10),
			coresAffinity: strings.Join(coresAffinity, ","),
			currentPCPUID: -1,
		}

		if shouldGetCurrentPCPU && i < len(cpuInfo) {
			vcpu.currentPCPUID = cpuInfo[i].CPU
		}

		if len(coresAffinity) > 0 {
			vcpuAffinities = append(vcpuAffinities, vcpu)
		}
	}

	return vcpuAffinities, nil
}

func (l *utilsImpl) EnsureConnected(libvirtURI string) error {
	if isConnected(l.libvirt) {
		return nil
	}

	driver, err := libvirtutils.ConnectByUriString(libvirtURI)
	if err != nil {
		return err
	}
	l.libvirt = driver
	return nil
}

func (l *utilsImpl) Disconnect() error {
	l.libvirt = nil
	return nil
}

func isConnected(driver *golibvirt.Libvirt) bool {
	if driver == nil {
		return false
	}

	select {
	case <-driver.Disconnected():
		return false
	default:
	}
	return true
}
