package main

import (
	cryptorand "crypto/rand"
	"errors"
	"fmt"
	"math"
	"math/big"
	"math/rand"
	"strconv"
	"strings"

	"github.com/osbuild/images/pkg/arch"
	"github.com/osbuild/images/pkg/bib/osinfo"
	"github.com/osbuild/images/pkg/blueprint"
	"github.com/osbuild/images/pkg/container"
	"github.com/osbuild/images/pkg/customizations/anaconda"
	"github.com/osbuild/images/pkg/customizations/kickstart"
	"github.com/osbuild/images/pkg/customizations/users"
	"github.com/osbuild/images/pkg/disk"
	"github.com/osbuild/images/pkg/image"
	"github.com/osbuild/images/pkg/manifest"
	"github.com/osbuild/images/pkg/osbuild"
	"github.com/osbuild/images/pkg/pathpolicy"
	"github.com/osbuild/images/pkg/platform"
	"github.com/osbuild/images/pkg/policies"
	"github.com/osbuild/images/pkg/runner"
	"github.com/sirupsen/logrus"

	"github.com/particle-os/debian-bootc-image-builder/bib/internal/distrodef"
	"github.com/particle-os/debian-bootc-image-builder/bib/internal/imagetypes"
	"github.com/particle-os/debian-bootc-image-builder/bib/internal/debian-patch"
)

// TODO: Auto-detect this from container image metadata
const DEFAULT_SIZE = uint64(10 * GibiByte)

type ManifestConfig struct {
	// OCI image path (without the transport, that is always docker://)
	Imgref      string
	BuildImgref string

	ImageTypes imagetypes.ImageTypes

	// Build config
	Config *blueprint.Blueprint

	// CPU architecture of the image
	Architecture arch.Arch

	// The minimum size required for the root fs in order to fit the container
	// contents
	RootfsMinsize uint64

	// Paths to the directory with the distro definitions
	DistroDefPaths []string

	// Extracted information about the source container image
	SourceInfo      *osinfo.Info
	BuildSourceInfo *osinfo.Info

	// RootFSType specifies the filesystem type for the root partition
	RootFSType string

	// use librepo ad the rpm downlaod backend
	UseLibrepo bool
}

func Manifest(c *ManifestConfig) (*manifest.Manifest, error) {
	rng := createRand()

	if c.ImageTypes.BuildsISO() {
		return manifestForISO(c, rng)
	}
	return manifestForDiskImage(c, rng)
}

var (
	// The mountpoint policy for bootc images is more restrictive than the
	// ostree mountpoint policy defined in osbuild/images. It only allows /
	// (for sizing the root partition) and custom mountpoints under /var but
	// not /var itself.

	// Since our policy library doesn't support denying a path while allowing
	// its subpaths (only the opposite), we augment the standard policy check
	// with a simple search through the custom mountpoints to deny /var
	// specifically.
	mountpointPolicy = pathpolicy.NewPathPolicies(map[string]pathpolicy.PathPolicy{
		// allow all existing mountpoints (but no subdirs) to support size customizations
		"/":     {Deny: false, Exact: true},
		"/boot": {Deny: false, Exact: true},

		// /var is not allowed, but we need to allow any subdirectories that
		// are not denied below, so we allow it initially and then check it
		// separately (in checkMountpoints())
		"/var": {Deny: false},

		// /var subdir denials
		"/var/home":     {Deny: true},
		"/var/lock":     {Deny: true}, // symlink to ../run/lock which is on tmpfs
		"/var/mail":     {Deny: true}, // symlink to spool/mail
		"/var/mnt":      {Deny: true},
		"/var/roothome": {Deny: true},
		"/var/run":      {Deny: true}, // symlink to ../run which is on tmpfs
		"/var/srv":      {Deny: true},
		"/var/usrlocal": {Deny: true},
	})

	mountpointMinimalPolicy = pathpolicy.NewPathPolicies(map[string]pathpolicy.PathPolicy{
		// allow all existing mountpoints to support size customizations
		"/":     {Deny: false, Exact: true},
		"/boot": {Deny: false, Exact: true},
	})
)

func checkMountpoints(filesystems []blueprint.FilesystemCustomization, policy *pathpolicy.PathPolicies) error {
	errs := []error{}
	for _, fs := range filesystems {
		if err := policy.Check(fs.Mountpoint); err != nil {
			errs = append(errs, err)
		}
		if fs.Mountpoint == "/var" {
			// this error message is consistent with the errors returned by policy.Check()
			// TODO: remove trailing space inside the quoted path when the function is fixed in osbuild/images.
			errs = append(errs, fmt.Errorf(`path "/var" is not allowed`))
		}
	}
	if len(errs) > 0 {
		return fmt.Errorf("the following errors occurred while validating custom mountpoints:\n%w", errors.Join(errs...))
	}
	return nil
}

func checkFilesystemCustomizations(fsCustomizations []blueprint.FilesystemCustomization, ptmode disk.PartitioningMode) error {
	var policy *pathpolicy.PathPolicies
	switch ptmode {
	case disk.BtrfsPartitioningMode:
		// btrfs subvolumes are not supported at build time yet, so we only
		// allow / and /boot to be customized when building a btrfs disk (the
		// minimal policy)
		policy = mountpointMinimalPolicy
	default:
		policy = mountpointPolicy
	}
	if err := checkMountpoints(fsCustomizations, policy); err != nil {
		return err
	}
	return nil
}

// updateFilesystemSizes updates the size of the root filesystem customization
// based on the minRootSize. The new min size whichever is larger between the
// existing size and the minRootSize. If the root filesystem is not already
// configured, a new customization is added.
func updateFilesystemSizes(fsCustomizations []blueprint.FilesystemCustomization, minRootSize uint64) []blueprint.FilesystemCustomization {
	updated := make([]blueprint.FilesystemCustomization, len(fsCustomizations), len(fsCustomizations)+1)
	hasRoot := false
	for idx, fsc := range fsCustomizations {
		updated[idx] = fsc
		if updated[idx].Mountpoint == "/" {
			updated[idx].MinSize = max(updated[idx].MinSize, minRootSize)
			hasRoot = true
		}
	}

	if !hasRoot {
		// no root customization found: add it
		updated = append(updated, blueprint.FilesystemCustomization{Mountpoint: "/", MinSize: minRootSize})
	}
	return updated
}

// setFSTypes sets the filesystem types for all mountable entities to match the
// selected rootfs type.
// If rootfs is 'btrfs', the function will keep '/boot' to its default.
func setFSTypes(pt *disk.PartitionTable, rootfs string) error {
	if rootfs == "" {
		return fmt.Errorf("root filesystem type is empty")
	}

	return pt.ForEachMountable(func(mnt disk.Mountable, _ []disk.Entity) error {
		switch mnt.GetMountpoint() {
		case "/boot/efi":
			// never change the efi partition's type
			return nil
		case "/boot":
			// change only if we're not doing btrfs
			if rootfs == "btrfs" {
				return nil
			}
			fallthrough
		default:
			switch elem := mnt.(type) {
			case *disk.Filesystem:
				elem.Type = rootfs
			case *disk.BtrfsSubvolume:
				// nothing to do
			default:
				return fmt.Errorf("the mountable disk entity for %q of the base partition table is not an ordinary filesystem but %T", mnt.GetMountpoint(), mnt)
			}
			return nil
		}
	})
}

func genPartitionTable(c *ManifestConfig, customizations *blueprint.Customizations, rng *rand.Rand) (*disk.PartitionTable, error) {
	fsCust := customizations.GetFilesystems()
	diskCust, err := customizations.GetPartitioning()
	if err != nil {
		return nil, fmt.Errorf("error reading disk customizations: %w", err)
	}

	// Embedded disk customization applies if there was no local customization
	if fsCust == nil && diskCust == nil && c.SourceInfo != nil && c.SourceInfo.ImageCustomization != nil {
		imageCustomizations := c.SourceInfo.ImageCustomization

		fsCust = imageCustomizations.GetFilesystems()
		diskCust, err = imageCustomizations.GetPartitioning()
		if err != nil {
			return nil, fmt.Errorf("error reading disk customizations: %w", err)
		}
	}

	var partitionTable *disk.PartitionTable
	switch {
	// XXX: move into images library
	case fsCust != nil && diskCust != nil:
		return nil, fmt.Errorf("cannot combine disk and filesystem customizations")
	case diskCust != nil:
		partitionTable, err = genPartitionTableDiskCust(c, diskCust, rng)
		if err != nil {
			return nil, err
		}
	default:
		partitionTable, err = genPartitionTableFsCust(c, fsCust, rng)
		if err != nil {
			return nil, err
		}
	}

	// Ensure ext4 rootfs has fs-verity enabled
	rootfs := partitionTable.FindMountable("/")
	if rootfs != nil {
		switch elem := rootfs.(type) {
		case *disk.Filesystem:
			if elem.Type == "ext4" {
				elem.MkfsOptions = append(elem.MkfsOptions, []disk.MkfsOption{disk.MkfsVerity}...)
			}
		}
	}

	return partitionTable, nil
}

// calcRequiredDirectorySizes will calculate the minimum sizes for /
// for disk customizations. We need this because with advanced partitioning
// we never grow the rootfs to the size of the disk (unlike the tranditional
// filesystem customizations).
//
// So we need to go over the customizations and ensure the min-size for "/"
// is at least rootfsMinSize.
//
// Note that a custom "/usr" is not supported in image mode so splitting
// rootfsMinSize between / and /usr is not a concern.
func calcRequiredDirectorySizes(distCust *blueprint.DiskCustomization, rootfsMinSize uint64) (map[string]uint64, error) {
	// XXX: this has *way* too much low-level knowledge about the
	// inner workings of blueprint.DiskCustomizations plus when
	// a new type it needs to get added here too, think about
	// moving into "images" instead (at least partly)
	mounts := map[string]uint64{}
	for _, part := range distCust.Partitions {
		switch part.Type {
		case "", "plain":
			mounts[part.Mountpoint] = part.MinSize
		case "lvm":
			for _, lv := range part.LogicalVolumes {
				mounts[lv.Mountpoint] = part.MinSize
			}
		case "btrfs":
			for _, subvol := range part.Subvolumes {
				mounts[subvol.Mountpoint] = part.MinSize
			}
		default:
			return nil, fmt.Errorf("unknown disk customization type %q", part.Type)
		}
	}
	// ensure rootfsMinSize is respected
	return map[string]uint64{
		"/": max(rootfsMinSize, mounts["/"]),
	}, nil
}

func genPartitionTableDiskCust(c *ManifestConfig, diskCust *blueprint.DiskCustomization, rng *rand.Rand) (*disk.PartitionTable, error) {
	if err := diskCust.ValidateLayoutConstraints(); err != nil {
		return nil, fmt.Errorf("cannot use disk customization: %w", err)
	}

	diskCust.MinSize = max(diskCust.MinSize, c.RootfsMinsize)

	basept, ok := partitionTables[c.Architecture.String()]
	if !ok {
		return nil, fmt.Errorf("pipelines: no partition tables defined for %s", c.Architecture)
	}
	defaultFSType, err := disk.NewFSType(c.RootFSType)
	if err != nil {
		return nil, err
	}
	requiredMinSizes, err := calcRequiredDirectorySizes(diskCust, c.RootfsMinsize)
	if err != nil {
		return nil, err
	}
	partOptions := &disk.CustomPartitionTableOptions{
		PartitionTableType: basept.Type,
		// XXX: not setting/defaults will fail to boot with btrfs/lvm
		BootMode:         platform.BOOT_HYBRID,
		DefaultFSType:    defaultFSType,
		RequiredMinSizes: requiredMinSizes,
		Architecture:     c.Architecture,
	}
	return disk.NewCustomPartitionTable(diskCust, partOptions, rng)
}

func genPartitionTableFsCust(c *ManifestConfig, fsCust []blueprint.FilesystemCustomization, rng *rand.Rand) (*disk.PartitionTable, error) {
	basept, ok := partitionTables[c.Architecture.String()]
	if !ok {
		return nil, fmt.Errorf("pipelines: no partition tables defined for %s", c.Architecture)
	}

	partitioningMode := disk.RawPartitioningMode
	if c.RootFSType == "btrfs" {
		partitioningMode = disk.BtrfsPartitioningMode
	}
	if err := checkFilesystemCustomizations(fsCust, partitioningMode); err != nil {
		return nil, err
	}
	fsCustomizations := updateFilesystemSizes(fsCust, c.RootfsMinsize)

	pt, err := disk.NewPartitionTable(&basept, fsCustomizations, DEFAULT_SIZE, partitioningMode, c.Architecture, nil, rng)
	if err != nil {
		return nil, err
	}

	if err := setFSTypes(pt, c.RootFSType); err != nil {
		return nil, fmt.Errorf("error setting root filesystem type: %w", err)
	}
	return pt, nil
}

func manifestForDiskImage(c *ManifestConfig, rng *rand.Rand) (*manifest.Manifest, error) {
	if c.Imgref == "" {
		return nil, fmt.Errorf("pipeline: no base image defined")
	}
	
	// Add Debian-specific pre-validation
	if err := debianpatch.PreValidateImage(c.Imgref); err != nil {
		return nil, fmt.Errorf("debian pre-validation failed: %w", err)
	}
	
	containerSource := container.SourceSpec{
		Source: c.Imgref,
		Name:   c.Imgref,
		Local:  true,
	}
	buildContainerSource := container.SourceSpec{
		Source: c.BuildImgref,
		Name:   c.BuildImgref,
		Local:  true,
	}

	var customizations *blueprint.Customizations
	if c.Config != nil {
		customizations = c.Config.Customizations
	}

	// Use the standard NewBootcDiskImage for all images, including Debian
	// We'll handle Debian-specific package installation at a different level
	img := image.NewBootcDiskImage(containerSource, buildContainerSource)
	
	// For Debian images, we might need to add some basic packages
	// that are expected by the bootc system
	if c.SourceInfo.OSRelease.ID == "debian" {
		// TODO: Add Debian-specific package handling here
		// This might involve setting ExtraBasePackages or similar fields
		// once we understand how the NewBootcDiskImage works internally
	}
	
	img.OSCustomizations.Users = users.UsersFromBP(customizations.GetUsers())
	img.OSCustomizations.Groups = users.GroupsFromBP(customizations.GetGroups())
	img.OSCustomizations.SELinux = c.SourceInfo.SELinuxPolicy
	img.OSCustomizations.BuildSELinux = img.OSCustomizations.SELinux
	if c.BuildSourceInfo != nil {
		img.OSCustomizations.BuildSELinux = c.BuildSourceInfo.SELinuxPolicy
	}

	img.OSCustomizations.KernelOptionsAppend = []string{
		"rw",
		// TODO: Drop this as we expect kargs to come from the container image,
		// xref https://github.com/CentOS/centos-bootc-layered/blob/main/cloud/usr/lib/bootc/install/05-cloud-kargs.toml
		"console=tty0",
		"console=ttyS0",
	}

	switch c.Architecture {
	case arch.ARCH_X86_64:
		img.Platform = &platform.X86{
			BasePlatform: platform.BasePlatform{},
			BIOS:         true,
		}
	case arch.ARCH_AARCH64:
		img.Platform = &platform.Aarch64{
			UEFIVendor: "debian",
			BasePlatform: platform.BasePlatform{
				QCOW2Compat: "1.1",
			},
		}
	case arch.ARCH_S390X:
		img.Platform = &platform.S390X{
			BasePlatform: platform.BasePlatform{
				QCOW2Compat: "1.1",
			},
			Zipl: true,
		}
	case arch.ARCH_PPC64LE:
		img.Platform = &platform.PPC64LE{
			BasePlatform: platform.BasePlatform{
				QCOW2Compat: "1.1",
			},
			BIOS: true,
		}
	}

	if kopts := customizations.GetKernel(); kopts != nil && kopts.Append != "" {
		img.OSCustomizations.KernelOptionsAppend = append(img.OSCustomizations.KernelOptionsAppend, kopts.Append)
	}

	pt, err := genPartitionTable(c, customizations, rng)
	if err != nil {
		return nil, err
	}
	img.PartitionTable = pt

	// Check Directory/File Customizations are valid
	dc := customizations.GetDirectories()
	fc := customizations.GetFiles()
	if err := blueprint.ValidateDirFileCustomizations(dc, fc); err != nil {
		return nil, err
	}
	if err := blueprint.CheckDirectoryCustomizationsPolicy(dc, policies.OstreeCustomDirectoriesPolicies); err != nil {
		return nil, err
	}
	if err := blueprint.CheckFileCustomizationsPolicy(fc, policies.OstreeCustomFilesPolicies); err != nil {
		return nil, err
	}
	img.OSCustomizations.Files, err = blueprint.FileCustomizationsToFsNodeFiles(fc)
	if err != nil {
		return nil, err
	}
	img.OSCustomizations.Directories, err = blueprint.DirectoryCustomizationsToFsNodeDirectories(dc)
	if err != nil {
		return nil, err
	}

	// For the bootc-disk image, the filename is the basename and the extension
	// is added automatically for each disk format
	img.Filename = "disk"

	mf := manifest.New()
	mf.Distro = manifest.DISTRO_FEDORA
	runner := &runner.Linux{}

	if err := img.InstantiateManifestFromContainers(&mf, []container.SourceSpec{containerSource}, runner, rng); err != nil {
		return nil, err
	}

	return &mf, nil
}



func manifestForISO(c *ManifestConfig, rng *rand.Rand) (*manifest.Manifest, error) {
	if c.Imgref == "" {
		return nil, fmt.Errorf("pipeline: no base image defined")
	}

	imageDef, err := distrodef.LoadImageDef(c.DistroDefPaths, c.SourceInfo.OSRelease.ID, c.SourceInfo.OSRelease.VersionID, "anaconda-iso")
	if err != nil {
		return nil, err
	}

	containerSource := container.SourceSpec{
		Source: c.Imgref,
		Name:   c.Imgref,
		Local:  true,
	}

	// The ref is not needed and will be removed from the ctor later
	// in time
	img := image.NewAnacondaContainerInstaller(containerSource, "")
	img.ContainerRemoveSignatures = true
	img.RootfsCompression = "zstd"

	img.Product = c.SourceInfo.OSRelease.Name
	img.OSVersion = c.SourceInfo.OSRelease.VersionID

	img.ExtraBasePackages = debianpatch.ConvertDebianPackageSetToRPM(debianpatch.DebianPackageSet{
		Include: imageDef.Packages,
	})

	img.ISOLabel = labelForISO(&c.SourceInfo.OSRelease, &c.Architecture)

	var customizations *blueprint.Customizations
	if c.Config != nil {
		customizations = c.Config.Customizations
	}
	img.FIPS = customizations.GetFIPS()
	img.Kickstart, err = kickstart.New(customizations)
	if err != nil {
		return nil, err
	}
	img.Kickstart.Path = osbuild.KickstartPathOSBuild
	if kopts := customizations.GetKernel(); kopts != nil && kopts.Append != "" {
		img.Kickstart.KernelOptionsAppend = append(img.Kickstart.KernelOptionsAppend, kopts.Append)
	}
	img.Kickstart.NetworkOnBoot = true

	instCust, err := customizations.GetInstaller()
	if err != nil {
		return nil, err
	}
	if instCust != nil && instCust.Modules != nil {
		img.AdditionalAnacondaModules = append(img.AdditionalAnacondaModules, instCust.Modules.Enable...)
		img.DisabledAnacondaModules = append(img.DisabledAnacondaModules, instCust.Modules.Disable...)
	}
	img.AdditionalAnacondaModules = append(img.AdditionalAnacondaModules,
		anaconda.ModuleUsers,
		anaconda.ModuleServices,
		anaconda.ModuleSecurity,
	)

	img.Kickstart.OSTree = &kickstart.OSTree{
		OSName: "default",
	}
	img.UseRHELLoraxTemplates = needsRHELLoraxTemplates(c.SourceInfo.OSRelease)

	switch c.Architecture {
	case arch.ARCH_X86_64:
		img.Platform = &platform.X86{
			BasePlatform: platform.BasePlatform{
				ImageFormat: platform.FORMAT_ISO,
			},
			BIOS:       true,
			UEFIVendor: c.SourceInfo.UEFIVendor,
		}
		img.ISOBoot = manifest.Grub2ISOBoot
	case arch.ARCH_AARCH64:
		// aarch64 always uses UEFI, so let's enforce the vendor
		if c.SourceInfo.UEFIVendor == "" {
			return nil, fmt.Errorf("UEFI vendor must be set for aarch64 ISO")
		}
		img.Platform = &platform.Aarch64{
			BasePlatform: platform.BasePlatform{
				ImageFormat: platform.FORMAT_ISO,
			},
			UEFIVendor: c.SourceInfo.UEFIVendor,
		}
	case arch.ARCH_S390X:
		img.Platform = &platform.S390X{
			Zipl: true,
			BasePlatform: platform.BasePlatform{
				ImageFormat: platform.FORMAT_ISO,
			},
		}
	case arch.ARCH_PPC64LE:
		img.Platform = &platform.PPC64LE{
			BIOS: true,
			BasePlatform: platform.BasePlatform{
				ImageFormat: platform.FORMAT_ISO,
			},
		}
	default:
		return nil, fmt.Errorf("unsupported architecture %v", c.Architecture)
	}
	// see https://github.com/osbuild/bootc-image-builder/issues/733
	img.RootfsType = manifest.SquashfsRootfs
	img.Filename = "install.iso"

	installRootfsType, err := disk.NewFSType(c.RootFSType)
	if err != nil {
		return nil, err
	}
	img.InstallRootfsType = installRootfsType

	mf := manifest.New()

	foundDistro, foundRunner, err := getDistroAndRunner(c.SourceInfo.OSRelease)
	if err != nil {
		return nil, fmt.Errorf("failed to infer distro and runner: %w", err)
	}
	mf.Distro = foundDistro

	_, err = img.InstantiateManifest(&mf, nil, foundRunner, rng)
	return &mf, err
}

func labelForISO(os *osinfo.OSRelease, arch *arch.Arch) string {
	switch os.ID {
	case "debian":
		return fmt.Sprintf("Debian-%s-%s", os.VersionID, arch)
	default:
		return fmt.Sprintf("Container-Installer-%s", arch)
	}
}

func needsRHELLoraxTemplates(si osinfo.OSRelease) bool {
	// This function is Red Hat specific and not needed for Debian
	// Always return false since we don't use RHEL Lorax templates
	return false
}

func getDistroAndRunner(osRelease osinfo.OSRelease) (manifest.Distro, runner.Runner, error) {
	switch osRelease.ID {
	case "fedora":
		version, err := strconv.ParseUint(osRelease.VersionID, 10, 64)
		if err != nil {
			return manifest.DISTRO_NULL, nil, fmt.Errorf("cannot parse Fedora version (%s): %w", osRelease.VersionID, err)
		}

		return manifest.DISTRO_FEDORA, &runner.Fedora{
			Version: version,
		}, nil
	case "centos":
		version, err := strconv.ParseUint(osRelease.VersionID, 10, 64)
		if err != nil {
			return manifest.DISTRO_NULL, nil, fmt.Errorf("cannot parse CentOS version (%s): %w", osRelease.VersionID, err)
		}
		r := &runner.CentOS{
			Version: version,
		}
		switch version {
		case 9:
			return manifest.DISTRO_EL9, r, nil
		case 10:
			return manifest.DISTRO_EL10, r, nil
		default:
			logrus.Warnf("Unknown CentOS version %d, using default distro for manifest generation", version)
			return manifest.DISTRO_NULL, r, nil
		}

	case "rhel":
		versionParts := strings.Split(osRelease.VersionID, ".")
		if len(versionParts) != 2 {
			return manifest.DISTRO_NULL, nil, fmt.Errorf("invalid RHEL version format: %s", osRelease.VersionID)
		}
		major, err := strconv.ParseUint(versionParts[0], 10, 64)
		if err != nil {
			return manifest.DISTRO_NULL, nil, fmt.Errorf("cannot parse RHEL major version (%s): %w", versionParts[0], err)
		}
		minor, err := strconv.ParseUint(versionParts[1], 10, 64)
		if err != nil {
			return manifest.DISTRO_NULL, nil, fmt.Errorf("cannot parse RHEL minor version (%s): %w", versionParts[1], err)
		}
		r := &runner.RHEL{
			Major: major,
			Minor: minor,
		}
		switch major {
		case 9:
			return manifest.DISTRO_EL9, r, nil
		case 10:
			return manifest.DISTRO_EL10, r, nil
		default:
			logrus.Warnf("Unknown RHEL version %d, using default distro for manifest generation", major)
			return manifest.DISTRO_NULL, r, nil
		}
	case "debian":
		version, err := strconv.ParseUint(osRelease.VersionID, 10, 64)
		if err != nil {
			return manifest.DISTRO_NULL, nil, fmt.Errorf("cannot parse Debian version (%s): %w", osRelease.VersionID, err)
		}

		// For Debian, we'll use DISTRO_NULL since there's no specific Debian distro constant
		// but we'll use the Linux runner which should work for Debian
		logrus.Infof("Detected Debian version %d, using Linux runner", version)
		return manifest.DISTRO_NULL, &runner.Linux{}, nil
	}

	logrus.Warnf("Unknown distro %s, using default runner", osRelease.ID)
	return manifest.DISTRO_NULL, &runner.Linux{}, nil
}

func createRand() *rand.Rand {
	seed, err := cryptorand.Int(cryptorand.Reader, big.NewInt(math.MaxInt64))
	if err != nil {
		panic("Cannot generate an RNG seed.")
	}

	// math/rand is good enough in this case
	/* #nosec G404 */
	return rand.New(rand.NewSource(seed.Int64()))
}
