// Package rosa provides Rosa OS toolchain artifacts and miscellaneous software.
package rosa

import (
	"errors"
	"path"
	"slices"
	"strconv"
	"strings"
	"sync"

	"hakurei.app/fhs"
	"hakurei.app/internal/pkg"
)

// Extension is the variant identification string of custom artifact
// implementations registered by package rosa.
const Extension = "rosa"

func init() { pkg.SetExtension(Extension) }

const (
	// kindEtc is the kind of [pkg.Artifact] of cureEtc.
	kindEtc = iota + pkg.KindCustomOffset

	// kindBusyboxBin is the kind of [pkg.Artifact] of busyboxBin.
	kindBusyboxBin
)

// mustDecode is like [pkg.MustDecode], but replaces the zero value and prints
// a warning.
func mustDecode(s string) pkg.Checksum {
	var fallback = pkg.Checksum{}
	if s == "" {
		println(
			"falling back to",
			pkg.Encode(fallback),
			"for unpopulated checksum",
		)
		return fallback
	}
	return pkg.MustDecode(s)
}

// KV is a key-value pair of strings.
type KV [2]string

var (
	// AbsUsrSrc is the conventional directory to place source code under.
	AbsUsrSrc = fhs.AbsUsr.Append("src")

	// AbsSystem is the Rosa OS installation prefix.
	AbsSystem = fhs.AbsRoot.Append("system")
)

// linuxArch returns the architecture name used by linux corresponding to arch.
func linuxArch() string {
	switch arch {
	case "amd64":
		return "x86_64"
	case "arm64":
		return "aarch64"
	case "riscv64":
		return "riscv64"

	default:
		panic("unsupported target " + arch)
	}
}

// triplet returns the Rosa OS host triple corresponding to arch.
func triplet() string {
	return linuxArch() + "-rosa-linux-musl"
}

// perArch is a value that differs per architecture.
type perArch[T any] map[string]T

// unwrap returns the value for the current architecture.
func (p perArch[T]) unwrap() T {
	v, ok := p[arch]
	if !ok {
		panic("unsupported target " + arch)
	}
	return v
}

const (
	// EnvTriplet holds the return value of triplet.
	EnvTriplet = "ROSA_TRIPLE"
)

// earlyLDFLAGS returns LDFLAGS corresponding to triplet.
func earlyLDFLAGS(static bool) string {
	s := "-fuse-ld=lld " +
		"-L/system/lib -Wl,-rpath=/system/lib " +
		"-L/system/lib/" + triplet() + " " +
		"-Wl,-rpath=/system/lib/" + triplet() + " " +
		"-rtlib=compiler-rt " +
		"-unwindlib=libunwind " +
		"-Wl,--as-needed"
	if !static {
		s += " -Wl,--dynamic-linker=/system/bin/linker"
	}
	return s
}

// Toolchain denotes the infrastructure to compile a [pkg.Artifact] on.
type Toolchain uint32

const (
	// _toolchainBusybox denotes a busybox installation from the busyboxBin
	// binary distribution. This is defined as a toolchain to make use of the
	// toolchain abstractions to preprocess toolchainGentoo and is not a real,
	// functioning toolchain. It does not contain any compilers.
	_toolchainBusybox Toolchain = iota

	// toolchainGentoo denotes the toolchain in a Gentoo stage3 tarball. Special
	// care must be taken to compile correctly against this toolchain.
	toolchainGentoo

	// toolchainIntermediateGentoo is like to toolchainIntermediate, but
	// compiled against toolchainGentoo.
	toolchainIntermediateGentoo

	// toolchainStdGentoo is like Std, but bootstrapped from toolchainGentoo.
	// This toolchain creates the first [Stage0] distribution.
	toolchainStdGentoo

	// toolchainStage0 denotes the stage0 toolchain. Special care must be taken
	// to compile correctly against this toolchain.
	toolchainStage0

	// toolchainIntermediate denotes the intermediate toolchain compiled against
	// toolchainStage0. This toolchain should be functionally identical to [Std]
	// and is used to bootstrap [Std].
	toolchainIntermediate

	// Std denotes the standard Rosa OS toolchain.
	Std

	// _toolchainEnd is the total number of toolchains available and does not
	// denote a valid toolchain.
	_toolchainEnd
)

// isStage0 returns whether t is a stage0 toolchain.
func (t Toolchain) isStage0() bool {
	switch t {
	case toolchainGentoo, toolchainStage0:
		return true
	default:
		return false
	}
}

// isIntermediate returns whether t is an intermediate toolchain.
func (t Toolchain) isIntermediate() bool {
	switch t {
	case toolchainIntermediateGentoo, toolchainIntermediate:
		return true
	default:
		return false
	}
}

// isStd returns whether t is considered functionally equivalent to [Std].
func (t Toolchain) isStd() bool {
	switch t {
	case toolchainStdGentoo, Std:
		return true
	default:
		return false
	}
}

// lastIndexFunc is like [strings.LastIndexFunc] but for [slices].
func lastIndexFunc[S ~[]E, E any](s S, f func(E) bool) (i int) {
	if i = slices.IndexFunc(s, f); i < 0 {
		return
	}
	if i0 := lastIndexFunc[S](s[i+1:], f); i0 >= 0 {
		i = i0
	}
	return
}

// fixupEnviron fixes up PATH, prepends extras and returns the resulting slice.
func fixupEnviron(env, extras []string, paths ...string) []string {
	// some python tools try to be clever and buffers their output, making the
	// build process appear to hang
	env = append(env, "PYTHONUNBUFFERED=1")

	const pathPrefix = "PATH="
	pathVal := strings.Join(paths, ":")

	if i := lastIndexFunc(env, func(s string) bool {
		return strings.HasPrefix(s, pathPrefix)
	}); i < 0 {
		env = append(env, pathPrefix+pathVal)
	} else {
		if len(env[i]) == len(pathPrefix) {
			env[i] = pathPrefix + pathVal
		} else {
			env[i] += ":" + pathVal
		}
	}

	return append(extras, env...)
}

// absCureScript is the absolute pathname [Toolchain.New] places the fixed-up
// build script under.
var absCureScript = AbsSystem.Append(".cure-script")

const (
	// TExclusive denotes an exclusive [pkg.Artifact].
	TExclusive = 1 << iota
	// TEarly hints for an early variant of [Toybox] to be used when available.
	TEarly
	// TNoToolchain excludes the LLVM toolchain.
	TNoToolchain
	// THostNet arranges for a [pkg.KindExecNet] to be created.
	THostNet
)

var (
	// gentooStage3 is the url of a Gentoo stage3 tarball.
	gentooStage3 string
	// gentooStage3Checksum is the expected checksum of gentooStage3.
	gentooStage3Checksum pkg.Checksum
)

// SetGentooStage3 sets the Gentoo stage3 tarball url and checksum. It panics
// if given zero values or if these values have already been set.
func SetGentooStage3(url string, checksum pkg.Checksum) {
	if gentooStage3 != "" {
		panic(errors.New("attempting to set Gentoo stage3 url twice"))
	}
	if url == "" {
		panic(errors.New("attempting to set Gentoo stage3 url to the zero value"))
	}
	gentooStage3, gentooStage3Checksum = url, checksum
}

// New returns a [pkg.Artifact] compiled on this toolchain.
func (t Toolchain) New(
	name string,
	flag int,
	extra []pkg.Artifact,
	knownChecksum *pkg.Checksum,
	env []string,
	script string,

	paths ...pkg.ExecPath,
) pkg.Artifact {
	const lcMessages = "LC_MESSAGES=C.UTF-8"

	var support []pkg.Artifact
	switch t {
	case _toolchainBusybox:
		name += "-early"
		support = slices.Concat([]pkg.Artifact{newBusyboxBin()}, extra)
		env = fixupEnviron(env, nil, "/system/bin")

	case toolchainGentoo, toolchainStage0:
		name += "-boot"
		support = append(support, extra...)
		support = append(support, cureEtc{})
		if t == toolchainStage0 {
			support = append(support, t.Load(stage0Dist))
		} else {
			support = append(support, _toolchainBusybox.New("gentoo", 0, nil, nil, nil, `
tar -C /work -xf /usr/src/stage3.tar.xz
rm -rf /work/dev/ /work/proc/
ln -vs ../usr/bin /work/bin
mkdir -vp /work/system/bin
(cd /work/system/bin && ln -vs \
	../../bin/sh \
	../../usr/lib/llvm/*/bin/* \
	.)
`, pkg.Path(AbsUsrSrc.Append("stage3.tar.xz"), false,
				pkg.NewHTTPGet(
					nil, gentooStage3,
					gentooStage3Checksum,
				),
			)))
		}
		env = fixupEnviron(env, []string{
			EnvTriplet + "=" + triplet(),
			lcMessages,
			"LDFLAGS=" + earlyLDFLAGS(true),
		}, "/system/bin",
			"/usr/bin",
		)

	case toolchainIntermediateGentoo, toolchainStdGentoo,
		toolchainIntermediate, Std:
		if t.isIntermediate() {
			name += "-std"
		}

		toybox := Toybox
		if flag&TEarly != 0 {
			toybox = toyboxEarly
		}

		base := LLVM
		if flag&TNoToolchain != 0 {
			base = Musl
		}

		support = slices.Concat(extra, (t-1).AppendPresets([]pkg.Artifact{
			cureEtc{newIANAEtc()},
		},
			base,
			Mksh,
			toybox,
		))
		env = fixupEnviron(env, []string{
			EnvTriplet + "=" + triplet(),
			lcMessages,
		}, "/system/bin", "/bin")

	default:
		panic("unsupported toolchain " + strconv.Itoa(int(t)))
	}

	return pkg.NewExec(
		name, arch, knownChecksum, pkg.ExecTimeoutMax,
		flag&THostNet != 0,
		flag&TExclusive != 0,
		fhs.AbsRoot, env,
		AbsSystem.Append("bin", "sh"),
		[]string{"sh", absCureScript.String()},

		slices.Concat([]pkg.ExecPath{pkg.Path(
			fhs.AbsRoot, true,
			support...,
		), pkg.Path(
			absCureScript, false,
			pkg.NewFile(".cure-script", []byte("set -eu -o pipefail\n"+script)),
		)}, paths)...,
	)
}

// NewPatchedSource returns [pkg.Artifact] of source with patches applied. If
// passthrough is true, source is returned as is for zero length patches.
func (t Toolchain) NewPatchedSource(
	name, version string,
	source pkg.Artifact,
	passthrough bool,
	patches ...KV,
) pkg.Artifact {
	if passthrough && len(patches) == 0 {
		return source
	}

	paths := make([]pkg.ExecPath, len(patches)+1)
	for i, p := range patches {
		paths[i+1] = pkg.Path(
			AbsUsrSrc.Append(name+"-patches", p[0]+".patch"), false,
			pkg.NewFile(p[0]+".patch", []byte(p[1])),
		)
	}
	paths[0] = pkg.Path(AbsUsrSrc.Append(name), false, source)

	aname := name + "-" + version + "-src"
	script := `
cp -r /usr/src/` + name + `/. /work/.
chmod -R +w /work && cd /work
`
	if len(paths) > 1 {
		script += `
cat /usr/src/` + name + `-patches/* | \
	patch \
	-p 1 \
	--ignore-whitespace
`
		aname += "-patched"
	}
	return t.New(aname, 0, t.AppendPresets(nil,
		Patch,
	), nil, nil, script, paths...)
}

// helperInPlace is a special directory value for omitting the cd statement.
const helperInPlace = "\x00"

// Helper is a build system helper for [Toolchain.NewPackage].
type Helper interface {
	// extra returns helper-specific dependencies.
	extra(flag int) P

	// wantsChmod returns whether the source directory should be made writable.
	wantsChmod() bool
	// wantsWrite returns whether the source directory should be mounted writable.
	wantsWrite() bool
	// scriptEarly returns the helper-specific segment of cure script that goes
	// before the cd statement.
	scriptEarly() string
	// createDir returns whether the path returned by wantsDir should be created.
	createDir() bool
	// wantsDir returns the directory to enter before script.
	//
	// The zero value implies source directory if [PackageAttr.ScriptEarly] is
	// also empty. The special value helperInPlace omits the cd statement.
	wantsDir() string
	// script returns the helper-specific segment of cure script.
	script(name string) string
}

// PackageAttr holds build-system-agnostic attributes.
type PackageAttr struct {
	// Measure output if populated. Required by [THostNet].
	KnownChecksum *pkg.Checksum

	// Mount the source tree writable.
	Writable bool
	// Do not pass through [Toolchain.NewPatchedSource].
	Chmod bool
	// Unconditionally enter source directory early.
	EnterSource bool

	// Additional environment variables.
	Env []string
	// Runs before script emitted by [Helper]. Enters source if non-empty.
	ScriptEarly string

	// Passed to [Toolchain.NewPatchedSource].
	Patches []KV

	// Passed through to [Toolchain.New], before source.
	Paths []pkg.ExecPath
	// Passed through to [Toolchain.New].
	Flag int
}

// pa holds whether a [PArtifact] is present.
type pa = [PresetEnd]bool

// paPool holds addresses of pa.
var paPool = sync.Pool{New: func() any { return new(pa) }}

// paGet returns the address of a new pa.
func paGet() *pa { return paPool.Get().(*pa) }

// paPut returns a pa to paPool.
func paPut(pv *pa) { *pv = pa{}; paPool.Put(pv) }

// appendPreset recursively appends a [PArtifact] and its runtime dependencies.
func (t Toolchain) appendPreset(
	a []pkg.Artifact,
	pv *pa, p PArtifact,
) []pkg.Artifact {
	if pv[p] {
		return a
	}
	pv[p] = true

	for _, d := range GetMetadata(p).Dependencies {
		a = t.appendPreset(a, pv, d)
	}
	return append(a, t.Load(p))
}

// AppendPresets recursively appends multiple [PArtifact] and their runtime
// dependencies.
func (t Toolchain) AppendPresets(
	a []pkg.Artifact,
	presets ...PArtifact,
) []pkg.Artifact {
	pv := paGet()
	for _, p := range presets {
		a = t.appendPreset(a, pv, p)
	}
	paPut(pv)
	return a
}

// NewPackage constructs a [pkg.Artifact] via a build system helper.
func (t Toolchain) NewPackage(
	name, version string,
	source pkg.Artifact,
	attr *PackageAttr,
	helper Helper,
	extra ...PArtifact,
) pkg.Artifact {
	if attr == nil {
		attr = new(PackageAttr)
	}

	if name == "" || version == "" {
		panic("name must be non-empty")
	}
	if source == nil {
		panic("source must be non-nil")
	}
	wantsChmod, wantsWrite := helper.wantsChmod(), helper.wantsWrite()
	extraRes := make([]pkg.Artifact, 0, 1<<3+len(extra))
	{
		pv := paGet()
		for _, p := range helper.extra(attr.Flag) {
			extraRes = t.appendPreset(extraRes, pv, p)
		}
		for _, p := range extra {
			extraRes = t.appendPreset(extraRes, pv, p)
		}
		paPut(pv)
	}

	var (
		scriptEarly  string
		sourceSuffix string
	)

	if _, ok := source.(pkg.FileArtifact); ok {
		if attr.Writable || attr.Chmod ||
			wantsChmod || wantsWrite ||
			len(attr.Patches) > 0 {
			panic("source processing requested on a xz-compressed tarball")
		}

		sourceSuffix = ".tar.xz"
		scriptEarly += `
tar -C /usr/src/ -xf '/usr/src/` + name + `.tar.xz'
mv '/usr/src/` + name + `-` + version + `' '/usr/src/` + name + `'
`
	}

	dir := helper.wantsDir()
	helperScriptEarly := helper.scriptEarly()
	if attr.EnterSource ||
		dir == "" ||
		attr.ScriptEarly != "" ||
		helperScriptEarly != "" {
		scriptEarly += `
cd '/usr/src/` + name + `/'
`
	}
	scriptEarly += attr.ScriptEarly + helperScriptEarly
	if dir != "" && dir != helperInPlace {
		if helper.createDir() {
			scriptEarly += "\nmkdir -p " + dir
		}
		scriptEarly += "\ncd " + dir + "\n"
	} else if !attr.EnterSource && attr.ScriptEarly == "" {
		panic("cannot remain in root")
	}

	return t.New(
		name+"-"+version,
		attr.Flag,
		extraRes,
		attr.KnownChecksum,
		attr.Env,
		scriptEarly+helper.script(name),
		slices.Concat(attr.Paths, []pkg.ExecPath{
			pkg.Path(AbsUsrSrc.Append(
				name+sourceSuffix,
			), attr.Writable || wantsWrite, t.NewPatchedSource(
				name, version, source, !attr.Chmod && !wantsChmod, attr.Patches...,
			)),
		})...,
	)
}

const (
	// jobsE is expression for preferred job count set by [pkg].
	jobsE = `"$` + pkg.EnvJobs + `"`
	// jobsE is expression for flag with preferred job count.
	jobsFlagE = `"-j$` + pkg.EnvJobs + `"`
	// jobsLE is expression for twice of preferred job count set by [pkg].
	jobsLE = `"$(expr ` + jobsE + ` '*' 2)"`
	// jobsE is expression for flag with double of preferred job count.
	jobsLFlagE = `"-j$(expr ` + jobsE + ` '*' 2)"`
)

// newTar wraps [pkg.NewHTTPGetTar] with a simpler function signature.
func newTar(url, checksum string, compression uint32) pkg.Artifact {
	return pkg.NewHTTPGetTar(nil, url, mustDecode(checksum), compression)
}

// newFromCPAN is a helper for downloading release from CPAN.
func newFromCPAN(author, name, version, checksum string) pkg.Artifact {
	return newTar(
		"https://cpan.metacpan.org/authors/id/"+
			author[:1]+"/"+author[:2]+"/"+author+"/"+
			name+"-"+version+".tar.gz",
		checksum,
		pkg.TarGzip,
	)
}

// newFromGitLab is a helper for downloading source from GitLab.
func newFromGitLab(domain, suffix, ref, checksum string) pkg.Artifact {
	return newTar(
		"https://"+domain+"/"+suffix+"/-/archive/"+
			ref+"/"+path.Base(suffix)+"-"+
			strings.ReplaceAll(ref, "/", "-")+".tar.bz2",
		checksum,
		pkg.TarBzip2,
	)
}

// newFromGitHub is a helper for downloading source from Microsoft Github.
func newFromGitHub(suffix, tag, checksum string) pkg.Artifact {
	return newTar(
		"https://github.com/"+suffix+
			"/archive/refs/tags/"+tag+".tar.gz",
		checksum,
		pkg.TarGzip,
	)
}

// newFromGitHubRelease is a helper for downloading release tarball from
// Microsoft Github.
func newFromGitHubRelease(
	suffix, tag, name, checksum string,
	compression uint32,
) pkg.Artifact {
	return newTar(
		"https://github.com/"+suffix+
			"/releases/download/"+tag+"/"+name,
		checksum,
		compression,
	)
}
