package backend

import (
	"fmt"
	"log/slog"
	"strings"

	"dagger.io/dagger"
)

// Distribution is a string that represents the GOOS and GOARCH environment variables joined by a "/".
// Optionally, if there is an extra argument specific to that architecture, it will be the last segment of the string.
// Examples:
// - "linux/arm/v6" = GOOS=linux, GOARCH=arm, GOARM=6
// - "linux/arm/v7" = GOOS=linux, GOARCH=arm, GOARM=7
// - "linux/amd64/v7" = GOOS=linux, GOARCH=arm, GOARM=7
// - "linux/amd64/v2" = GOOS=linux, GOARCH=amd64, GOAMD64=v2
// The list of distributions is built from the command "go tool dist list".
// While not all are used, it at least represents the possible combinations.
type Distribution string

const (
	DistDarwinAMD64   Distribution = "darwin/amd64"
	DistDarwinAMD64v1 Distribution = "darwin/amd64/v1"
	DistDarwinAMD64v2 Distribution = "darwin/amd64/v2"
	DistDarwinAMD64v3 Distribution = "darwin/amd64/v3"
	DistDarwinAMD64v4 Distribution = "darwin/amd64/v4"
	DistDarwinARM64   Distribution = "darwin/arm64"
)

const (
	DistFreeBSD386          Distribution = "freebsd/386"
	DistFreeBSD386SSE2      Distribution = "freebsd/386/sse2"
	DistFreeBSD386SoftFloat Distribution = "freebsd/386/softfloat"
	DistFreeBSDAMD64        Distribution = "freebsd/amd64"
	DistFreeBSDAMD64v1      Distribution = "freebsd/amd64/v1"
	DistFreeBSDAMD64v2      Distribution = "freebsd/amd64/v2"
	DistFreeBSDAMD64v3      Distribution = "freebsd/amd64/v3"
	DistFreeBSDAMD64v4      Distribution = "freebsd/amd64/v4"
	DistFreeBSDARM          Distribution = "freebsd/arm"
	DistFreeBSDARM64        Distribution = "freebsd/arm64"
	DistFreeBSDRISCV        Distribution = "freebsd/riscv64"
)

const (
	DistIllumosAMD64   Distribution = "illumos/amd64"
	DistIllumosAMD64v1 Distribution = "illumos/amd64/v1"
	DistIllumosAMD64v2 Distribution = "illumos/amd64/v2"
	DistIllumosAMD64v3 Distribution = "illumos/amd64/v3"
	DistIllumosAMD64v4 Distribution = "illumos/amd64/v4"
)
const (
	DistLinux386              Distribution = "linux/386"
	DistLinux386SSE2          Distribution = "linux/386/sse2"
	DistLinux386SoftFloat     Distribution = "linux/386/softfloat"
	DistLinuxAMD64            Distribution = "linux/amd64"
	DistLinuxAMD64v1          Distribution = "linux/amd64/v1"
	DistLinuxAMD64v2          Distribution = "linux/amd64/v2"
	DistLinuxAMD64v3          Distribution = "linux/amd64/v3"
	DistLinuxAMD64v4          Distribution = "linux/amd64/v4"
	DistLinuxAMD64Dynamic     Distribution = "linux/amd64/dynamic"
	DistLinuxAMD64DynamicMusl Distribution = "linux/amd64/dynamic-musl"
	DistLinuxARM              Distribution = "linux/arm"
	DistLinuxARMv6            Distribution = "linux/arm/v6"
	DistLinuxARMv7            Distribution = "linux/arm/v7"
	DistLinuxARM64            Distribution = "linux/arm64"
	DistLinuxARM64Dynamic     Distribution = "linux/arm64/dynamic"
	DistLinuxLoong64          Distribution = "linux/loong64"
	DistLinuxMips             Distribution = "linux/mips"
	DistLinuxMips64           Distribution = "linux/mips64"
	DistLinuxMips64le         Distribution = "linux/mips64le"
	DistLinuxMipsle           Distribution = "linux/mipsle"
	DistLinuxPPC64            Distribution = "linux/ppc64"
	DistLinuxPPC64le          Distribution = "linux/ppc64le"
	DistLinuxRISCV64          Distribution = "linux/riscv64"
	DistLinuxS390X            Distribution = "linux/s390x"
)

const (
	DistOpenBSD386          Distribution = "openbsd/386"
	DistOpenBSD386SSE2      Distribution = "openbsd/386/sse2"
	DistOpenBSD386SoftFLoat Distribution = "openbsd/386/softfloat"
	DistOpenBSDAMD64        Distribution = "openbsd/amd64"
	DistOpenBSDAMD64v1      Distribution = "openbsd/amd64/v1"
	DistOpenBSDAMD64v2      Distribution = "openbsd/amd64/v2"
	DistOpenBSDAMD64v3      Distribution = "openbsd/amd64/v3"
	DistOpenBSDAMD64v4      Distribution = "openbsd/amd64/v4"
	DistOpenBSDARM          Distribution = "openbsd/arm"
	DistOpenBSDARMv6        Distribution = "openbsd/arm/v6"
	DistOpenBSDARMv7        Distribution = "openbsd/arm/v7"
	DistOpenBSDARM64        Distribution = "openbsd/arm64"
	DistOpenBSDMips64       Distribution = "openbsd/mips64"
)

const (
	DistPlan9386          Distribution = "plan9/386"
	DistPlan9386SSE2      Distribution = "plan9/386/sse2"
	DistPlan9386SoftFloat Distribution = "plan9/386/softfloat"
	DistPlan9AMD64        Distribution = "plan9/amd64"
	DistPlan9AMD64v1      Distribution = "plan9/amd64/v1"
	DistPlan9AMD64v2      Distribution = "plan9/amd64/v2"
	DistPlan9AMD64v3      Distribution = "plan9/amd64/v3"
	DistPlan9AMD64v4      Distribution = "plan9/amd64/v4"
	DistPlan9ARM          Distribution = "plan9/arm/v6"
	DistPlan9ARMv6        Distribution = "plan9/arm/v6"
	DistPlan9ARMv7        Distribution = "plan9/arm/v7"
)

const (
	DistSolarisAMD64   Distribution = "solaris/amd64"
	DistSolarisAMD64v1 Distribution = "solaris/amd64/v1"
	DistSolarisAMD64v2 Distribution = "solaris/amd64/v2"
	DistSolarisAMD64v3 Distribution = "solaris/amd64/v3"
	DistSolarisAMD64v4 Distribution = "solaris/amd64/v4"
)

const (
	DistWindows386          Distribution = "windows/386"
	DistWindows386SSE2      Distribution = "windows/386/sse2"
	DistWindows386SoftFloat Distribution = "windows/386/softfloat"
	DistWindowsAMD64        Distribution = "windows/amd64"
	DistWindowsAMD64v1      Distribution = "windows/amd64/v1"
	DistWindowsAMD64v2      Distribution = "windows/amd64/v2"
	DistWindowsAMD64v3      Distribution = "windows/amd64/v3"
	DistWindowsAMD64v4      Distribution = "windows/amd64/v4"
	DistWindowsARM          Distribution = "windows/arm"
	DistWindowsARMv6        Distribution = "windows/arm/v6"
	DistWindowsARMv7        Distribution = "windows/arm/v7"
	DistWindowsARM64        Distribution = "windows/arm64"
)

func IsWindows(d Distribution) bool {
	return strings.Split(string(d), "/")[0] == "windows"
}

func OSAndArch(d Distribution) (string, string) {
	p := strings.Split(string(d), "/")
	if len(p) < 2 {
		return string(d), ""
	}
	return p[0], p[1]
}

func FullArch(d Distribution) string {
	p := strings.Split(string(d), "/")
	return strings.Join(p[1:], "/")
}

func ArchVersion(d Distribution) string {
	p := strings.Split(string(d), "/")
	if len(p) < 3 {
		return ""
	}

	// ARM specifically must be specified without a 'v' prefix.
	// GOAMD64, however, expects a 'v' prefix.
	// Specifying the ARM version with the 'v' prefix and without is supported in Docker's platform argument, however.
	if arch := p[1]; arch == "arm" {
		return strings.TrimPrefix(p[2], "v")
	}

	return p[2]
}

func PackageArch(d Distribution) string {
	_, arch := OSAndArch(d)

	if arch == "arm" {
		return "armhf"
	}

	return arch
}

// From the distribution, try to assume the docker platform (used in Docker's --platform argument or the (dagger.ContainerOpts).Platform field
func Platform(d Distribution) dagger.Platform {
	p := strings.ReplaceAll(string(d), "/dynamic-musl", "")
	p = strings.ReplaceAll(p, "/dynamic", "")
	p = strings.ReplaceAll(p, "arm/v6", "arm/v7")
	// for now let's just try to use the distro name as the platform and see if that works...
	return dagger.Platform(p)
}

type DistroBuildOptsFunc func(distro Distribution, experiments []string, tags []string) *GoBuildOpts

func LDFlagsStatic(info *VCSInfo) []LDFlag {
	return []LDFlag{
		{"-w", nil},
		{"-s", nil},
		{"-X", info.X()},
		{"-linkmode=external", nil},
		{"-extldflags=-static", nil},
	}
}

func LDFlagsDynamic(info *VCSInfo) []LDFlag {
	return []LDFlag{
		{"-X", info.X()},
	}
}

func ZigCC(distro Distribution) string {
	target, ok := ZigTargets[distro]
	if !ok {
		target = "x86_64-linux-musl" // best guess? should probably retun an error but i don't want to
	}

	return fmt.Sprintf("zig cc -target %s", target)
}

func ZigCXX(distro Distribution) string {
	target, ok := ZigTargets[distro]
	if !ok {
		target = "x86_64-linux-musl" // best guess? should probably retun an error but i don't want to
	}

	return fmt.Sprintf("zig c++ -target %s", target)
}

var DefaultBuildOpts = func(distro Distribution, experiments []string, tags []string) *GoBuildOpts {
	os, arch := OSAndArch(distro)

	return &GoBuildOpts{
		CC:                ZigCC(distro),
		CXX:               ZigCXX(distro),
		ExperimentalFlags: experiments,
		OS:                os,
		Arch:              arch,
		CGOEnabled:        true,
	}
}

// BuildOptsStaticARM builds Grafana statically for the armv6/v7 architectures (not aarch64/arm64)
func BuildOptsStaticARM(distro Distribution, experiments []string, tags []string) *GoBuildOpts {
	var (
		os, _ = OSAndArch(distro)
		arm   = ArchVersion(distro)
	)

	return &GoBuildOpts{
		CC:                "/toolchain/arm-linux-musleabihf-cross/bin/arm-linux-musleabihf-gcc",
		CXX:               "/toolchain/arm-linux-musleabihf-cross/bin/arm-linux-musleabihf-cpp",
		ExperimentalFlags: experiments,
		OS:                os,
		Arch:              "arm",
		GoARM:             GoARM(arm),
		CGOEnabled:        true,
	}
}

// BuildOptsStaticS390X builds Grafana statically for the s390x arch
func BuildOptsStaticS390X(distro Distribution, experiments []string, tags []string) *GoBuildOpts {
	var (
		os, _ = OSAndArch(distro)
	)

	return &GoBuildOpts{
		CC:                "/toolchain/s390x-linux-musl-cross/bin/s390x-linux-musl-gcc",
		CXX:               "/toolchain/s390x-linux-musl-cross/bin/s390x-linux-musl-cpp",
		ExperimentalFlags: experiments,
		OS:                os,
		Arch:              "s390x",
		CGOEnabled:        true,
	}
}

// BuildOptsStaticRiscv64 builds Grafana statically for the riscv64 arch
func BuildOptsStaticRiscv64(distro Distribution, experiments []string, tags []string) *GoBuildOpts {
	var (
		os, _ = OSAndArch(distro)
	)

	return &GoBuildOpts{
		CC:                "/toolchain/riscv64-linux-musl-cross/bin/riscv64-linux-musl-gcc",
		CXX:               "/toolchain/riscv64-linux-musl-cross/bin/riscv64-linux-musl-cpp",
		ExperimentalFlags: experiments,
		OS:                os,
		Arch:              "riscv64",
		CGOEnabled:        true,
	}
}

// BuildOptsStaticWindows builds Grafana statically for Windows on amd64
func BuildOptsStaticWindows(distro Distribution, experiments []string, tags []string) *GoBuildOpts {
	var (
		os, _ = OSAndArch(distro)
	)

	return &GoBuildOpts{
		CC:                "/toolchain/x86_64-w64-mingw32-cross/bin/x86_64-w64-mingw32-gcc",
		CXX:               "/toolchain/x86_64-w64-mingw32-cross/bin/x86_64-w64-mingw32-cpp",
		ExperimentalFlags: experiments,
		OS:                os,
		Arch:              "amd64",
		CGOEnabled:        true,
	}
}

func StdZigBuildOpts(distro Distribution, experiments []string, tags []string) *GoBuildOpts {
	var (
		os, arch = OSAndArch(distro)
	)

	return &GoBuildOpts{
		CC:                ZigCC(distro),
		CXX:               ZigCXX(distro),
		ExperimentalFlags: experiments,
		OS:                os,
		Arch:              arch,
		CGOEnabled:        true,
	}
}

func BuildOptsWithoutZig(distro Distribution, experiments []string, tags []string) *GoBuildOpts {
	var (
		os, arch = OSAndArch(distro)
	)

	return &GoBuildOpts{
		ExperimentalFlags: experiments,
		OS:                os,
		Arch:              arch,
		CGOEnabled:        true,
	}
}

func ViceroyBuildOpts(distro Distribution, experiments []string, tags []string) *GoBuildOpts {
	var (
		os, arch = OSAndArch(distro)
	)

	return &GoBuildOpts{
		CC:                "viceroycc",
		ExperimentalFlags: experiments,
		OS:                os,
		Arch:              arch,
		CGOEnabled:        true,
	}
}

func BuildOptsNoCGO(distro Distribution, experiments []string, tags []string) *GoBuildOpts {
	var (
		os, arch = OSAndArch(distro)
	)

	return &GoBuildOpts{
		ExperimentalFlags: experiments,
		OS:                os,
		Arch:              arch,
		CGOEnabled:        false,
	}
}

var ZigTargets = map[Distribution]string{
	DistLinuxAMD64:            "x86_64-linux-musl",
	DistLinuxAMD64Dynamic:     "x86_64-linux-gnu",
	DistLinuxAMD64DynamicMusl: "x86_64-linux-musl",
	DistLinuxARM64:            "aarch64-linux-musl",
	DistLinuxARM64Dynamic:     "aarch64-linux-musl",
	DistLinuxARM:              "arm-linux-musleabihf",
	DistLinuxARMv6:            "arm-linux-musleabihf",
	DistLinuxARMv7:            "arm-linux-musleabihf",
	DistLinuxRISCV64:          "riscv64-linux-musl",
	DistWindowsAMD64:          "x86_64-windows-gnu",
	DistWindowsARM64:          "aarch64-windows-gnu",
}

var DistributionGoOpts = map[Distribution]DistroBuildOptsFunc{
	// The Linux distros should all have an equivalent zig target in the ZigTargets map
	DistLinuxARM:          BuildOptsStaticARM,
	DistLinuxARMv6:        BuildOptsStaticARM,
	DistLinuxARMv7:        BuildOptsStaticARM,
	DistLinuxS390X:        BuildOptsStaticS390X,
	DistLinuxARM64:        StdZigBuildOpts,
	DistLinuxARM64Dynamic: StdZigBuildOpts,
	DistLinuxAMD64:        StdZigBuildOpts,
	DistLinuxAMD64Dynamic: StdZigBuildOpts,
	DistPlan9AMD64:        StdZigBuildOpts,
	DistLinuxRISCV64:      BuildOptsStaticRiscv64,

	// Non-Linux distros can have whatever they want in CC and CXX; it'll get overridden
	// but it's probably not best to rely on that.
	DistWindowsAMD64: BuildOptsStaticWindows,
	DistWindowsARM64: StdZigBuildOpts,
	DistDarwinAMD64:  ViceroyBuildOpts,
	DistDarwinARM64:  ViceroyBuildOpts,

	DistLinuxAMD64DynamicMusl: BuildOptsWithoutZig,
}

func DistroOptsLogger(log *slog.Logger, fn DistroBuildOptsFunc) func(distro Distribution, experiments []string, tags []string) *GoBuildOpts {
	return func(distro Distribution, experiments []string, tags []string) *GoBuildOpts {
		opts := fn(distro, experiments, tags)
		log.Debug("Building with options", "distribution", distro, "experiments", experiments, "tags", tags, "os", opts.OS, "arch", opts.Arch, "arm", opts.GoARM, "CGO", opts.CGOEnabled, "386", opts.Go386, "CC", opts.CC, "CXX", opts.CXX)
		return opts
	}
}
