//go:build mage

package main

import (
	"fmt"
	"os"
	"strings"
	"text/template"

	"github.com/magefile/mage/mg"
	"github.com/magefile/mage/sh"
	"github.com/samber/lo"
	"gitlab.com/gitlab-org/gitlab-runner/magefiles/build"
	"gitlab.com/gitlab-org/gitlab-runner/magefiles/packages"
)

// Package namespace for handling deb and rpm packages
type Package mg.Namespace

// Deb builds deb package
func (p Package) Deb(arch, packageArch string) error {
	return p.createPackage(packages.Deb, arch, packageArch)
}

// Rpm builds rpm package
func (p Package) Rpm(arch, packageArch string) error {
	return p.createPackage(packages.Rpm, arch, packageArch)
}

// RpmFips builds rpm package for fips
func (p Package) RpmFips() error {
	return p.createPackage(packages.RpmFips, "amd64", "amd64")
}

func (p Package) createPackage(pkgType packages.Type, arch, packageArch string) error {
	blueprint, err := build.PrintBlueprint(packages.Assemble(pkgType, arch, packageArch))
	if err != nil {
		return err
	}

	artifactsPath := fmt.Sprintf("%s_%s.%s", arch, packageArch, pkgType)
	if err := build.Export(blueprint.Artifacts(), build.ReleaseArtifactsPath(artifactsPath)); err != nil {
		return err
	}

	return packages.Create(blueprint)
}

// We use the go generate statement to call the package:generate target through mage
// We don't need any kind of ast parsing as all the data is already available
// This is just an easier way to generate files through mage as we can just run
// go generate for the mage tags
//
//go:generate mage package:generate
var packageBuilds = packages.Builds{
	"deb": {
		{"Deb64", []string{"amd64"}, []string{"amd64"}, []string{"amd64"}},
		{"Deb32", []string{"386"}, []string{"i686"}, []string{"i686"}},
		{"DebArm64", []string{"arm64", "arm64"}, []string{"aarch64", "arm64"}, []string{"aarch64", "arm64"}},
		{"DebArm32", []string{"arm", "arm"}, []string{"armel", "armhf"}, []string{"armel", "armhf"}},
		{"DebRiscv64", []string{"riscv64"}, []string{"riscv64"}, []string{"riscv64"}},
		{"DebIbm", []string{"s390x", "ppc64le"}, []string{"s390x", "ppc64el"}, []string{"s390x", "ppc64le"}},
	},
	"rpm": {
		{"Rpm64", []string{"amd64"}, []string{"amd64"}, []string{"x86_64"}},
		{"Rpm32", []string{"386"}, []string{"i686"}, []string{"i686"}},
		{"RpmArm64", []string{"arm64", "arm64"}, []string{"aarch64", "arm64"}, []string{"aarch64", "arm64"}},
		{"RpmArm32", []string{"arm", "arm"}, []string{"arm", "armhf"}, []string{"arm", "armhf"}},
		{"RpmRiscv64", []string{"riscv64"}, []string{"riscv64"}, []string{"riscv64"}},
		{"RpmIbm", []string{"s390x", "ppc64le"}, []string{"s390x", "ppc64el"}, []string{"s390x", "ppc64le"}},
	},
}

// Archs prints the list of architectures as they appear in the final package's filename
// for either "deb" or "rpm"
func (p Package) Archs(dist string) {
	archs := lo.Flatten(lo.Map(packageBuilds[dist], func(p packages.Build, index int) []string {
		return p.PackageFileArchs
	}))

	fmt.Println(strings.Join(archs, " "))
}

// Filenames prints the final names of the packages for all supported architectures for a version and a distribution
func (p Package) Filenames(dist, version string) error {
	fmt.Println(strings.Join(packages.Filenames(packageBuilds, dist, version), " "))
	return nil
}

type templateContext struct {
	Dist   string
	Builds []packages.Build
}

// HelpersDeb creates a deb package with the exported runner-helper images
func (p Package) HelpersDeb() error { return p.packageHelpers(packages.Deb) }

// HelpersRpm creates an rpm package with the exported runner-helper images
func (p Package) HelpersRpm() error { return p.packageHelpers(packages.Rpm) }

func (p Package) packageHelpers(pkgType packages.Type) error {
	blueprint, err := build.PrintBlueprint(packages.AssembleHelpers(pkgType))
	if err != nil {
		return err
	}

	artifactsPath := fmt.Sprintf("noarch.%s", pkgType)
	if err := build.Export(blueprint.Artifacts(), build.ReleaseArtifactsPath(artifactsPath)); err != nil {
		return err
	}

	return packages.CreateHelper(blueprint)
}

// Generate generates the Mage package build targets
func (p Package) Generate() error {
	tmpl := `// Code generated by mage package:generate. DO NOT EDIT.
//go:build mage

package main
{{ range .Builds }}
// {{ .Name }} builds {{ $.Dist }} package for {{ .Archs | Join }}
func (p Package) {{ .Name }}() error {
	var err error
	{{ $pkg_archs := .PackageArchs -}}
	{{ range $index, $arch := .Archs -}}
	err = p.{{ $.Dist | Capitalize }}("{{ $arch }}", "{{ index $pkg_archs $index }}")
	if err != nil {
		return err
	}

	{{ end -}}

	return nil
}
{{ end -}}
`

	fns := template.FuncMap{
		"Capitalize": func(in string) string {
			return strings.ToUpper(in[:1]) + in[1:]
		},
		"Join": func(in []string) string {
			return strings.Join(in, " ")
		},
	}

	template, err := template.New("packages").Funcs(fns).Parse(tmpl)
	if err != nil {
		return err
	}

	for dist, b := range packageBuilds {
		f, err := os.OpenFile(fmt.Sprintf("package_%s.go", dist), os.O_RDWR|os.O_TRUNC|os.O_CREATE, 0666)
		if err != nil {
			return err
		}

		defer f.Close()

		if err := template.Execute(f, templateContext{
			Dist:   dist,
			Builds: b,
		}); err != nil {
			return err
		}
	}

	return nil
}

// Deps makes sure the packages needed to build rpm and deb packages are available on the system
func (p Package) Deps() error {
	if err := sh.Run("fpm", "--help"); err != nil {
		return sh.RunV("gem", "install", "rake", "fpm:1.15.1", "--no-document")
	}

	return nil
}

// Prepare prepares the filesystem permissions for packages
func (p Package) Prepare() error {
	err := sh.RunV("bash", "-c", "chmod 755 packaging/root/usr/share/gitlab-runner/")
	if err != nil {
		return err
	}

	err = sh.RunV("bash", "-c", "chmod 755 packaging/root/usr/share/gitlab-runner/*")
	if err != nil {
		return err
	}

	return nil
}

// VerifyIterationVariable verifies that the PACKAGES_ITERATION variable is set correctly.
// When on the `main` branch it's allowed to be only ever set to `1`. Only in stable branches
// and tags it is allowed to be changed.
// This restriction exists to prevent stable branches to be created from `main` when the iteration
// is not set to `1`. Preventing us from releasing a package with an iteration with no preceeding packages.
func (Package) VerifyIterationVariable() error {
	return packages.VerifyIterationVariable()
}

// Docs generates user documentation listing the linux distribution/versions for which runner packages are published for
// the stable branch.
func (p Package) Docs() error {
	return packages.GenerateSupportedOSDocs(supportedOSVersions)
}
