package rosa

import (
	"archive/tar"
	"bytes"
	"compress/gzip"
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"io/fs"
	"iter"
	"net/http"
	"path/filepath"
	"reflect"
	"runtime"
	"slices"
	"strconv"
	"strings"
	"sync"
	"sync/atomic"
	"unique"
	"unsafe"

	"hakurei.app/check"
	"hakurei.app/container"
	"hakurei.app/internal/pkg"
	"hakurei.app/internal/rosa/azalea"
)

// ArtifactH is a handle of the unique name of a prepared [pkg.Artifact].
type ArtifactH unique.Handle[string]

// H is a convenient function for acquiring an [ArtifactH] by name.
func H(name string) ArtifactH { return ArtifactH(unique.Make(name)) }

// String returns the name of p.
func (handle ArtifactH) String() string {
	return unique.Handle[string](handle).Value()
}

// MarshalJSON represents [ArtifactH] by its [Artifact.Name].
func (handle ArtifactH) MarshalJSON() ([]byte, error) {
	return json.Marshal(handle.String())
}

// UnmarshalJSON resolves [ArtifactH] by its [Artifact.Name].
func (handle *ArtifactH) UnmarshalJSON(data []byte) error {
	var name string
	if err := json.Unmarshal(data, &name); err != nil {
		return err
	}
	*handle = ArtifactH(unique.Make(name))
	return nil
}

// A HandleError is panicked when attempting to acquire an [Artifact] via an
// invalid [ArtifactH] with the Must suite of methods.
type HandleError ArtifactH

func (e HandleError) Error() string {
	if e == (HandleError{}) {
		return "invalid handle error"
	}
	return "package " + strconv.Quote(ArtifactH(e).String()) + " not available"
}

type (
	// Stage denotes the infrastructure to compile a [pkg.Artifact] on.
	Stage uint32
	// Toolchain refers to an instance of [S], and a [Stage] to compile on.
	Toolchain struct {
		stage Stage

		*S
	}
)

// P represents multiple [ArtifactH].
type P []ArtifactH

// Metadata is stage-agnostic immutable data around [Artifact] not directly
// representable in the resulting [pkg.Artifact].
type Metadata struct {
	// Unique package name.
	Name string `json:"name"`
	// Short user-facing description.
	Description string `json:"description"`
	// Project home page.
	Website string `json:"website,omitempty"`

	// Runtime dependencies.
	Dependencies P `json:"dependencies"`

	// Package version.
	Version string `json:"version"`
	// Project identifier on [Anitya].
	//
	// [Anitya]: https://release-monitoring.org/
	ID int `json:"-"`
	// Maintainer-facing reason of blocked update.
	Blocked string `json:"block,omitempty"`

	// Whether to exclude from exported functions.
	Exclude bool `json:"exclude,omitempty"`

	// Optional custom version checking behaviour.
	latest func(v *Versions) string
}

// GetLatest returns the latest version described by v.
func (meta *Metadata) GetLatest(v *Versions) string {
	if meta.latest != nil {
		return meta.latest(v)
	}
	return v.Latest
}

// Unversioned denotes an unversioned [Artifact].
const Unversioned = "\x00"

// UnpopulatedIDError is returned by [Artifact.GetLatest] for an instance of
// [Artifact] where ID is not populated.
type UnpopulatedIDError struct{}

func (UnpopulatedIDError) Unwrap() error { return errors.ErrUnsupported }
func (UnpopulatedIDError) Error() string { return "Anitya ID is not populated" }

// Versions are package versions returned by Anitya.
type Versions struct {
	// The latest version for the project, as determined by the version sorting algorithm.
	Latest string `json:"latest_version"`
	// List of all versions that aren’t flagged as pre-release.
	Stable []string `json:"stable_versions"`
	// List of all versions stored, sorted from newest to oldest.
	All []string `json:"versions"`
}

// getStable returns the first Stable version, or Latest if that is unavailable.
func (v *Versions) getStable() string {
	if len(v.Stable) == 0 {
		return v.Latest
	}
	return v.Stable[0]
}

// GetVersions returns versions fetched from Anitya.
func (meta *Metadata) GetVersions(ctx context.Context) (*Versions, error) {
	if meta.ID == 0 {
		return nil, UnpopulatedIDError{}
	}

	var resp *http.Response
	if req, err := http.NewRequestWithContext(
		ctx,
		http.MethodGet,
		"https://release-monitoring.org/api/v2/versions/?project_id="+
			strconv.Itoa(meta.ID),
		nil,
	); err != nil {
		return nil, err
	} else {
		req.Header.Set("User-Agent", "Rosa/1.1")
		if resp, err = http.DefaultClient.Do(req); err != nil {
			return nil, err
		}
	}

	var v Versions
	err := json.NewDecoder(resp.Body).Decode(&v)
	return &v, errors.Join(err, resp.Body.Close())
}

// Artifact is a lazily initialised [pkg.Artifact] with associated [Metadata].
type Artifact func(t Toolchain) (meta *Metadata, a pkg.Artifact)

// A cachedArtifact caches satisfied [Artifact].
type cachedArtifact struct {
	meta *Metadata
	a    pkg.Artifact
}

const (
	// OptSkipCheck skips running all test suites.
	OptSkipCheck = 1 << iota
	// OptLLVMNoLTO disables LTO in all [LLVM] stages.
	OptLLVMNoLTO
)

// S holds a set of [Artifact].
type S struct {
	// [ArtifactH] to [Artifact].
	artifacts sync.Map
	// Size of artifacts.
	artifactCount atomic.Uint64

	// Target architecture.
	arch string
	// For initialising arch.
	archOnce sync.Once

	// Built-in functions.
	frame azalea.Frame
	// For initialising frame.
	frameOnce sync.Once
	// Must only be accessed after a call to getFrame.
	spool sync.Pool

	// Options for [pkg.Artifact] created against [S].
	opts int

	// Cached [pkg.Artifact].
	c [_stageEnd]sync.Map

	// Unpacked Gentoo LLVM stage3.
	gentooStage3 pkg.Artifact
}

// Clone returns a copy of s.
func (s *S) Clone() *S {
	v := S{arch: s.arch, opts: s.opts}
	s.artifacts.Range(func(key, value any) bool {
		v.artifacts.Store(key, value)
		v.artifactCount.Add(1)
		return true
	})
	return &v
}

// wantsArch must be called before accessing arch.
func (s *S) wantsArch() {
	s.archOnce.Do(func() {
		if s.arch == "" {
			s.arch = runtime.GOARCH
		}
	})
}

// Arch returns the target architecture.
func (s *S) Arch() string { s.wantsArch(); return s.arch }

// Arches returns an iterator over supported emulation targets.
func Arches(qemu *check.Absolute) iter.Seq2[string, container.BinfmtEntry] {
	return func(yield func(string, container.BinfmtEntry) bool) {
		for _, target := range []struct {
			arch, name  string
			offset      byte
			magic, mask string
		}{{
			arch:   "riscv64",
			offset: 0,
			magic:  "\x7fELF\x02\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\xf3\x00",
			mask:   "\xff\xff\xff\xff\xff\xff\xff\x00\xff\xff\xff\xff\xff\xff\xff\xff\xfe\xff\xff\xff",
		}, {
			arch:   "arm64",
			name:   "aarch64",
			offset: 0,
			magic:  "\x7fELF\x02\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\xb7\x00",
			mask:   "\xff\xff\xff\xff\xff\xff\xff\xfc\xff\xff\xff\xff\xff\xff\xff\xff\xfe\xff\xff\xff",
		}} {
			name := target.name
			if name == "" {
				name = target.arch
			}
			var interpreter *check.Absolute
			if qemu != nil {
				interpreter = qemu.Append(
					"system/bin",
					"qemu-"+name,
				)
			}

			if !yield(target.arch, container.BinfmtEntry{
				Offset:      target.offset,
				Magic:       target.magic,
				Mask:        target.mask,
				Interpreter: interpreter,
			}) {
				return
			}
		}
	}
}

// Flags returns the current preset flags.
func (s *S) Flags() int { return s.opts }

// DropCaches arranges for all cached [pkg.Artifact] to be freed some time after
// it returns. Must not be used concurrently with any other method.
func (s *S) DropCaches(targetArch string, flags int) {
	if targetArch == "" {
		targetArch = runtime.GOARCH
	}

	s.arch = targetArch
	s.opts = flags
	for i := range s.c {
		s.c[i].Clear()
	}
	s.frameOnce = sync.Once{}
}

// get returns the named [Artifact].
func (s *S) get(handle ArtifactH) (f Artifact) {
	s.wantsArch()
	v, ok := s.artifacts.Load(handle)
	if ok {
		f = v.(Artifact)
	}
	return
}

// mustGet is like get, but panics if the named [Artifact] is not registered.
func (s *S) mustGet(handle ArtifactH) (f Artifact) {
	f = s.get(handle)
	if f == nil {
		panic(HandleError(handle))
	}
	return
}

// New returns a [Toolchain] for the specified [Stage].
func (s *S) New(stage Stage) Toolchain {
	return Toolchain{S: s, stage: stage}
}

// Std is a convenience method that returns a [Toolchain] for the [Std] stage.
func (s *S) Std() Toolchain { return s.New(Std) }

// LoadError wraps panicked errors reaching [Toolchain.Load].
type LoadError struct {
	// Offending artifact handle.
	Handle ArtifactH
	// Recovered error.
	Err error
}

func (e LoadError) Unwrap() error { return e.Err }
func (e LoadError) Error() string {
	return "cannot load " + strconv.Quote(e.Handle.String()) + ": " + e.Err.Error()
}

// Load satisfies an [Artifact] referred to by an [ArtifactH].
func (t Toolchain) Load(handle ArtifactH) (*Metadata, pkg.Artifact) {
	defer func() {
		r := recover()
		if r == nil {
			return
		}

		err, ok := r.(error)
		if !ok {
			panic(r)
		}

		if _, ok = err.(LoadError); ok {
			panic(err)
		}
		panic(LoadError{handle, err})
	}()

	t.wantsArch()
	e, ok := t.c[t.stage].Load(handle)
	if ok {
		r := e.(cachedArtifact)
		return r.meta, r.a
	}

	f := t.get(handle)
	if f == nil {
		return nil, nil
	}
	var r cachedArtifact
	r.meta, r.a = f(t)
	t.c[t.stage].Store(handle, r)
	return r.meta, r.a
}

// MustLoad is like Load, but panics if the named [Artifact] is not registered.
func (t Toolchain) MustLoad(handle ArtifactH) (*Metadata, pkg.Artifact) {
	meta, a := t.Load(handle)
	if meta == nil {
		panic(HandleError(handle))
	}
	return meta, a
}

// Register arranges for a new [Artifact] to be cured under s. It returns false
// if another [Artifact] is already registered under the same name.
func (s *S) Register(name string, f Artifact) bool {
	if name == "" {
		return false
	}
	p := ArtifactH(unique.Make(name))
	_, ok := s.artifacts.LoadOrStore(p, f)
	if !ok {
		s.artifactCount.Add(1)
	}
	return !ok
}

// RegisterError is returned or panicked when attempting to register multiple
// [Artifact] with the same name.
type RegisterError ArtifactH

func (e RegisterError) Error() string {
	if ArtifactH(e).String() == "" {
		return "attempting to register invalid name"
	}
	return "attempting to register " + strconv.Quote(ArtifactH(e).String()) + " twice"
}

// MustRegister is like Register, but panics if registration fails.
func (s *S) MustRegister(name string, f Artifact) {
	if !s.Register(name, f) {
		panic(RegisterError(H(name)))
	}
}

// count returns the number of [Artifact] registered to s.
func (s *S) count() int {
	return int(s.artifactCount.Load())
}

// CollectAll returns all [ArtifactH] registered to s.
func (s *S) CollectAll() (handles P) {
	handles = make(P, 0, s.count())
	s.artifacts.Range(func(key, _ any) bool {
		handles = append(handles, key.(ArtifactH))
		return true
	})
	slices.SortFunc(handles, func(a, b ArtifactH) int {
		return strings.Compare(a.String(), b.String())
	})
	return
}

// Collect returns all non-excluded [ArtifactH] registered to s.
func (s *S) Collect() (handles P) {
	handles = make(P, 0, s.count())
	s.artifacts.Range(func(key, _ any) bool {
		h := key.(ArtifactH)
		meta, _ := s.Std().MustLoad(h)
		if !meta.Exclude {
			handles = append(handles, h)
		}
		return true
	})
	slices.SortFunc(handles, func(a, b ArtifactH) int {
		return strings.Compare(a.String(), b.String())
	})
	return
}

// deferredGit is a call to Toolchain.newTagRemote from azalea.
type deferredGit struct {
	url      string
	tag      string
	checksum pkg.Checksum
}

// getFrame must be called before accessing s.
func (s *S) getFrame() azalea.Frame {
	s.frameOnce.Do(func() {
		s.spool.New = func() any {
			v := make([]azalea.Frame, 1, 1<<4)
			v[0] = s.getFrame()
			return v
		}

		s.wantsArch()
		k := func(name string) unique.Handle[azalea.Ident] {
			return unique.Make(azalea.Ident(name))
		}

		identDefault := k("default")
		identArch := unique.Make(azalea.Ident(s.arch))

		const compressOffset = 1 << 8
		enumCompress := map[unique.Handle[azalea.Ident]]any{
			k("uncompressed"): 0,
			k("gzip"):         pkg.Gzip + compressOffset,
			k("bzip2"):        pkg.Bzip2 + compressOffset,
			k("zstd"):         pkg.Zstd + compressOffset,
			k("xz"):           pkg.XZ + compressOffset,
		}

		s.frame.Val = map[unique.Handle[azalea.Ident]]any{
			k("unversioned"): Unversioned,

			k("triple"):       s.triple(),
			k("arch"):         s.arch,
			k("linuxArch"):    s.linuxArch(),
			k("earlyLDFLAGS"): s.earlyLDFLAGS(false),

			k("jobsE"):      jobsE,
			k("jobsFlagE"):  jobsFlagE,
			k("jobsLE"):     jobsLE,
			k("jobsLFlagE"): jobsLFlagE,

			k("loadE"):     loadE,
			k("loadFlagE"): loadFlagE,

			k("anityaFallback"): (*Versions).getStable,
			k("anityaLegacyCPAN"): func(v *Versions) string {
				for _, e := range v.Stable {
					_, m, ok := strings.Cut(e, ".")
					if !ok {
						continue
					}
					if len(m) > 1 && m[0] == '0' {
						continue
					}
					return e
				}
				return v.Latest
			},
			k("anityaFilterSPIRV"): func(v *Versions) string {
				for _, e := range v.Stable {
					fields := strings.SplitN(e, ".", 4)
					if len(fields) != 4 {
						continue
					}
					if slices.ContainsFunc(fields, func(f string) bool {
						return slices.ContainsFunc([]byte(f), func(d byte) bool {
							return d < '0' || d > '9'
						})
					}) {
						continue
					}
					return e
				}
				return v.Latest
			},
		}
		s.frame.Func = map[unique.Handle[azalea.Ident]]azalea.F{

			// library functions

			k("replace"): {F: func(
				args azalea.FArgs,
			) (v any, set bool, err error) {
				var _s, _old, _new string
				var n int
				if err = args.Apply(map[unique.Handle[azalea.Ident]]any{
					k("s"):   &_s,
					k("old"): &_old,
					k("new"): &_new,
					k("n"):   &n,
				}); err != nil {
					return
				}

				if n < 0 {
					n = 0
				} else if n == 0 {
					n = -1
				}

				v = strings.Replace(_s, _old, _new, n)
				set = true
				return
			}},

			k("slice"): {F: func(
				args azalea.FArgs,
			) (v any, set bool, err error) {
				var elems []string
				var start, end int64
				if err = args.Apply(map[unique.Handle[azalea.Ident]]any{
					k("elems"): &elems,
					k("start"): &start,
					k("end"):   &end,
				}); err != nil {
					return
				}

				v = elems[start:end]
				set = true
				return
			}},

			k("join"): {F: func(
				args azalea.FArgs,
			) (v any, set bool, err error) {
				var elems []string
				var sep string
				if err = args.Apply(map[unique.Handle[azalea.Ident]]any{
					k("elems"): &elems,
					k("sep"):   &sep,
				}); err != nil {
					return
				}
				v = strings.Join(elems, sep)
				set = true
				return
			}},

			k("split"): {F: func(
				args azalea.FArgs,
			) (v any, set bool, err error) {
				var p, sep string
				var n int64
				if err = args.Apply(map[unique.Handle[azalea.Ident]]any{
					k("s"):   &p,
					k("sep"): &sep,
					k("n"):   &n,
				}); err != nil {
					return
				}

				if n < 0 {
					n = 0
				} else if n == 0 {
					n = -1
				}

				v = strings.SplitN(p, sep, int(n))
				set = true
				return
			}},

			// formatting helpers

			k("litArgs"): {F: func(
				args azalea.FArgs,
			) (v any, set bool, err error) {
				var verbose bool
				var skipChecks []string
				if err = args.Apply(map[unique.Handle[azalea.Ident]]any{
					k("verbose"):    &verbose,
					k("skipChecks"): &skipChecks,
				}); err != nil {
					return
				}
				v = litArgs(verbose, skipChecks...)
				set = true
				return
			}},

			k("skipGNUTests"): {F: func(
				args azalea.FArgs,
			) (v any, set bool, err error) {
				var tests []int64
				if err = args.Apply(map[unique.Handle[azalea.Ident]]any{
					k("tests"): &tests,
				}); err != nil {
					return
				}
				v = skipGNUTests(tests...)
				set = true
				return
			}},

			// intenral/pkg built-ins

			k("remoteTar"): {F: func(
				args azalea.FArgs,
			) (v any, set bool, err error) {
				var url, checksum string
				var _compress int
				if err = args.Apply(map[unique.Handle[azalea.Ident]]any{
					k("url"):      &url,
					k("checksum"): &checksum,
					k("compress"): &_compress,
				}); err != nil {
					return
				}
				f := pkg.NewHTTPGet(nil, url, mustDecode(checksum))
				if _compress != 0 {
					f = pkg.NewDecompress(f, uint32(_compress-compressOffset))
				}
				v = pkg.NewTar(f)
				set = true
				return
			}, V: enumCompress},

			k("remoteFile"): {F: func(
				args azalea.FArgs,
			) (v any, set bool, err error) {
				var url, checksum string
				if err = args.Apply(map[unique.Handle[azalea.Ident]]any{
					k("url"):      &url,
					k("checksum"): &checksum,
				}); err != nil {
					return
				}
				v = pkg.NewHTTPGet(nil, url, mustDecode(checksum))
				set = true
				return
			}},

			// state helpers

			k("arch"): {F: func(
				args azalea.FArgs,
			) (v any, set bool, err error) {
				var fallback any
				for _, arg := range args {
					switch arg.K {
					case identDefault:
						fallback = arg.V
						continue

					case identArch:
						return arg.V, true, nil
					}
				}

				return fallback, fallback != nil, nil
			}},

			// convenience functions

			k("remoteGit"): {F: func(
				args azalea.FArgs,
			) (v any, set bool, err error) {
				var a deferredGit
				var checksum string
				if err = args.Apply(map[unique.Handle[azalea.Ident]]any{
					k("url"):      &a.url,
					k("tag"):      &a.tag,
					k("checksum"): &checksum,
				}); err != nil {
					return
				}
				a.checksum = mustDecode(checksum)
				v = a
				set = true
				return
			}},

			k("remoteGitLab"): {F: func(
				args azalea.FArgs,
			) (v any, set bool, err error) {
				var domain, suffix, ref, checksum string
				if err = args.Apply(map[unique.Handle[azalea.Ident]]any{
					k("domain"):   &domain,
					k("suffix"):   &suffix,
					k("ref"):      &ref,
					k("checksum"): &checksum,
				}); err != nil {
					return
				}
				v = newFromGitLab(domain, suffix, ref, checksum)
				set = true
				return
			}},

			k("remoteCPAN"): {F: func(
				args azalea.FArgs,
			) (v any, set bool, err error) {
				var author, name, version, checksum string
				if err = args.Apply(map[unique.Handle[azalea.Ident]]any{
					k("author"):   &author,
					k("name"):     &name,
					k("version"):  &version,
					k("checksum"): &checksum,
				}); err != nil {
					return
				}
				v = newFromCPAN(author, name, version, checksum)
				set = true
				return
			}},

			k("remoteGitHub"): {F: func(
				args azalea.FArgs,
			) (v any, set bool, err error) {
				var suffix, tag, checksum string
				if err = args.Apply(map[unique.Handle[azalea.Ident]]any{
					k("suffix"):   &suffix,
					k("tag"):      &tag,
					k("checksum"): &checksum,
				}); err != nil {
					return
				}
				v = newFromGitHub(suffix, tag, checksum)
				set = true
				return
			}},

			k("remoteGitHubRelease"): {F: func(
				args azalea.FArgs,
			) (v any, set bool, err error) {
				var suffix, tag, name, checksum string
				var compress uint32
				if err = args.Apply(map[unique.Handle[azalea.Ident]]any{
					k("suffix"):   &suffix,
					k("tag"):      &tag,
					k("name"):     &name,
					k("checksum"): &checksum,
					k("compress"): &compress,
				}); err != nil {
					return
				}
				v = newFromGitHubRelease(suffix, tag, name, checksum, compress)
				set = true
				return
			}, V: map[unique.Handle[azalea.Ident]]any{
				k("gzip"):  uint32(pkg.Gzip),
				k("bzip2"): uint32(pkg.Bzip2),
				k("zstd"):  uint32(pkg.Zstd),
				k("xz"):    uint32(pkg.XZ),
			}},

			// high-level helpers

			k("generic"): {F: func(
				args azalea.FArgs,
			) (v any, set bool, err error) {
				var attr GenericHelper
				chdir := true
				if err = args.Apply(map[unique.Handle[azalea.Ident]]any{
					k("chdir"):   &chdir,
					k("build"):   &attr.Build,
					k("check"):   &attr.Check,
					k("install"): &attr.Install,
				}); err != nil {
					return
				}
				attr.InPlace = !chdir

				v = &attr
				set = true
				return
			}},

			k("make"): {F: func(
				args azalea.FArgs,
			) (v any, set bool, err error) {
				var attr MakeHelper
				defaults, chdir, checkEarly := true, true, true
				attr.Configure, attr.Check = []KV{}, []string{}
				if err = args.Apply(map[unique.Handle[azalea.Ident]]any{
					k("defaults"):      &defaults,
					k("generate"):      &attr.Generate,
					k("preMake"):       &attr.ScriptMakeEarly,
					k("preCheck"):      &attr.ScriptCheckEarly,
					k("postInstall"):   &attr.Script,
					k("chdir"):         &chdir,
					k("configureName"): &attr.ConfigureName,
					k("forceAutoconf"): &attr.ForceAutoconf,
					k("configure"):     &attr.Configure,
					k("host"):          &attr.Host,
					k("build"):         &attr.Build,
					k("make"):          &attr.Make,
					k("check"):         &attr.Check,
					k("install"):       &attr.Install,

					k("checkEarly"): &checkEarly,
				}); err != nil {
					return
				}
				attr.OmitDefaults, attr.InPlace = !defaults, !chdir
				attr.SkipConfigure = attr.Configure == nil
				attr.SkipCheck = attr.Check == nil
				attr.SkipCheckEarly = !checkEarly

				v = &attr
				set = true
				return
			}},

			k("cmake"): {F: func(
				args azalea.FArgs,
			) (v any, set bool, err error) {
				var attr CMakeHelper
				var test azalea.AString
				if err = args.Apply(map[unique.Handle[azalea.Ident]]any{
					k("append"):      &attr.Append,
					k("buildType"):   &attr.BuildType,
					k("cache"):       &attr.Cache,
					k("postInstall"): &attr.Script,
					k("test"):        &test,
					k("make"):        &attr.Make,
				}); err != nil {
					return
				}
				attr.Test = test.V
				attr.SkipTest = test.Nil

				v = &attr
				set = true
				return
			}},

			k("meson"): {F: func(
				args azalea.FArgs,
			) (v any, set bool, err error) {
				var attr MesonHelper
				test := true
				if err = args.Apply(map[unique.Handle[azalea.Ident]]any{
					k("muon"):            &attr.Muon,
					k("preCompile"):      &attr.ScriptCompileEarly,
					k("postCompile"):     &attr.ScriptCompiled,
					k("postInstall"):     &attr.Script,
					k("setup"):           &attr.Setup,
					k("test"):            &test,
					k("skipTests"):       &attr.SkipTests,
					k("interactiveTest"): &attr.InteractiveTest,
				}); err != nil {
					return
				}
				attr.SkipTest = !test

				v = &attr
				set = true
				return
			}},

			k("makeMaker"): {F: func(
				args azalea.FArgs,
			) (v any, set bool, err error) {
				var attr MakeMakerHelper
				doCheck := true
				if err = args.Apply(map[unique.Handle[azalea.Ident]]any{
					k("check"): &doCheck,
				}); err != nil {
					return
				}
				attr.SkipCheck = !doCheck
				v = &attr
				set = true
				return
			}},

			k("pip"): {F: func(
				args azalea.FArgs,
			) (v any, set bool, err error) {
				var attr PipHelper
				var scriptCheck azalea.AString
				if err = args.Apply(map[unique.Handle[azalea.Ident]]any{
					k("append"):         &attr.Append,
					k("buildIsolation"): &attr.BuildIsolation,
					k("enterSource"):    &attr.EnterSource,
					k("install"):        &attr.Install,
					k("check"):          &scriptCheck,
					k("postInstall"):    &attr.Script,
				}); err != nil {
					return
				}
				attr.Check = scriptCheck.V
				attr.SkipCheck = scriptCheck.Nil

				v = &attr
				set = true
				return
			}},
		}
	})
	return s.frame
}

// getStack returns a new stack for azalea evaluation.
func (s *S) getStack() []azalea.Frame {
	s.getFrame()
	return s.spool.Get().([]azalea.Frame)
}

// putStack returns a stack to spool.
func (s *S) putStack(v []azalea.Frame) { s.spool.Put(v) }

// toHandles makes handles out of an [azalea.Array] of identifiers.
func toHandles(idents azalea.Array) (P, error) {
	handles := make(P, len(idents))
	for i, p := range idents {
		if len(p) != 1 {
			return nil, azalea.EvaluationError{
				Expr: p,
				Err:  errors.New("concatenation not allowed for handles"),
			}
		}
		s, ok := p[0].(azalea.Ident)
		if !ok {
			return nil, azalea.EvaluationError{
				Expr: p[0],
				Err:  errors.New("identifiers expected for handles"),
			}
		}
		handles[i] = H(string(s))
	}
	return handles, nil
}

// evalContext holds per-artifact context.
type evalContext struct {
	// Backing filesystem.
	b fs.FS
	// Pending azalea function.
	expr *azalea.Func
	// Current toolchain.
	t Toolchain
}

// HelperError describes a package with nil helper.
type HelperError string

func (e HelperError) Error() string {
	return "package " + strconv.Quote(string(e)) + " has no exec helper"
}

// pf implements [azalea.PF].
func (ctx *evalContext) pf(
	name azalea.Ident,
	args azalea.FArgs,
) (v any, set bool, err error) {
	k := func(name string) unique.Handle[azalea.Ident] {
		return unique.Make(azalea.Ident(name))
	}

	meta := Metadata{Name: string(name)}
	var (
		attr    PackageAttr
		patches []string
		files   []KV

		anitya  int64
		kc      any
		output  pkg.Artifact
		sourceA any
		helper  Helper
		overlay string

		inputs, runtimes, extra azalea.Array

		bootstrap = true
	)
	if err = args.Apply(map[unique.Handle[azalea.Ident]]any{
		k("description"): &meta.Description,
		k("website"):     &meta.Website,
		k("version"):     &meta.Version,
		k("exclude"):     &meta.Exclude,
		k("anitya"):      &anitya,
		k("block"):       &meta.Blocked,
		k("latest"):      &meta.latest,

		k("writable"):       &attr.Writable,
		k("chmod"):          &attr.Chmod,
		k("enterSource"):    &attr.EnterSource,
		k("env"):            &attr.Env,
		k("early"):          &attr.ScriptEarly,
		k("bin"):            &attr.Bin,
		k("populateUsrBin"): &attr.PopulateUsrBin,
		k("patches"):        &patches,
		k("files"):          &files,
		k("exclusive"):      &attr.Exclusive,
		k("toyboxEarly"):    &attr.Early,
		k("minimal"):        &attr.NoToolchain,
		k("bootstrap"):      &bootstrap,

		k("checksum"): &kc,
		k("output"):   &output,
		k("source"):   &sourceA,
		k("exec"):     &helper,
		k("inputs"):   &inputs,
		k("runtime"):  &runtimes,
		k("extra"):    &extra,
		k("overlay"):  &overlay,
	}); err != nil {
		return
	}
	attr.Std = !bootstrap
	var inputsH, extraH P
	if inputsH, err = toHandles(inputs); err != nil {
		return
	} else if meta.Dependencies, err = toHandles(runtimes); err != nil {
		return
	} else if extraH, err = toHandles(extra); err != nil {
		return
	}

	meta.ID = int(anitya)
	for _, pathname := range patches {
		var p []byte
		p, err = fs.ReadFile(ctx.b, pathname)
		if err != nil {
			return
		}
		attr.Patches = append(attr.Patches, KV{
			filepath.Base(pathname),
			unsafe.String(unsafe.SliceData(p), len(p)),
		})
	}

	if output != nil {
		if len(attr.Patches) != 0 || attr.Chmod {
			rn := string(name)
			if meta.Version != Unversioned {
				rn = string(name) + "-" + meta.Version
			}
			output = ctx.t.NewPatchedSource(rn, output, !attr.Chmod, attr.Patches...)
		}
		v = cachedArtifact{&meta, output}
		set = true
		return
	}

	if overlay == "" {
		for _, h := range extraH {
			_, a := ctx.t.MustLoad(h)
			attr.Paths = append(attr.Paths, pkg.Path(
				AbsUsrSrc.Append("extra", h.String()),
				attr.Writable,
				a,
			))
		}
	} else {
		var o pkg.ExecPath
		if o.P, err = check.NewAbs(overlay); err != nil {
			o.P = AbsUsrSrc.Append("extra", overlay)
		}
		extraA := ctx.t.Append(nil, extraH...)
		o.A = make([]pkg.Artifact, len(extraA))
		for i, a := range extraA {
			o.A[i] = a
		}
		attr.Paths = append(attr.Paths, o)
	}

	for _, pair := range files {
		var pathname *check.Absolute
		if pathname, err = check.NewAbs(pair[0]); err != nil {
			pathname = AbsUsrSrc.Append(pair[0])
			err = nil
		}

		if pair[1] == "" {
			pair[1] = pair[0]
		}

		var p []byte
		p, err = fs.ReadFile(ctx.b, pair[1])
		if err != nil {
			return
		}

		attr.Paths = append(attr.Paths, pkg.Path(
			pathname,
			false,
			pkg.NewFile(filepath.Base(pair[0]), p)),
		)
	}

	if kc != nil {
		checksum, ok := kc.(string)
		if !ok {
			err = azalea.TypeError{
				Concrete: reflect.TypeOf(kc),
				Asserted: reflect.TypeFor[string](),
			}
			return
		}
		attr.KnownChecksum = new(mustDecode(checksum))
	}

	var source pkg.Artifact
	switch p := sourceA.(type) {
	case pkg.Artifact:
		source = p

	case deferredGit:
		source = ctx.t.NewViaGit(p.url, "refs/tags/"+p.tag, p.checksum)

	case azalea.Ident:
		var _meta *Metadata
		_meta, source = ctx.t.Load(H(string(p)))
		if meta.Version == "" {
			meta.Version = _meta.Version
		}

	default:
		if sourceA != nil {
			panic(azalea.TypeError{
				Concrete: reflect.TypeOf(sourceA),
				Asserted: reflect.TypeFor[pkg.Artifact](),
			})
		}
	}

	if helper == nil {
		panic(HelperError(name))
	}

	v = cachedArtifact{&meta, ctx.t.New(
		meta.Name,
		meta.Version,
		source,
		&attr,
		helper,
		inputsH...,
	)}
	set = true
	return
}

var (
	// ErrToplevel is returned by [S.RegisterAzalea] when encountering a
	// toplevel expression other than a package declaration.
	ErrToplevel = errors.New("top level must only contain package declarations")
)

// RegisterAzalea registers all package declarations from r. The backing
// filesystem is directly exposed to azalea pathnames.
func (s *S) RegisterAzalea(r io.Reader, b fs.FS) error {
	var pending []*azalea.Func
	if expressions, err := azalea.Parse(r); err != nil {
		return err
	} else {
		pending = make([]*azalea.Func, len(expressions))
		for i, expr := range expressions {
			f, ok := expr.(azalea.Func)
			if !ok || !f.Package {
				return ErrToplevel
			}
			pending[i] = &f
		}
	}

	for _, f := range pending {
		if !s.Register(string(f.Ident), func(t Toolchain) (*Metadata, pkg.Artifact) {
			v, set, err := azalea.Evaluate[cachedArtifact](
				(&evalContext{b, f, t}).pf,
				t.getStack(),
				*f,
			)
			if err != nil {
				panic(err)
			} else if !set {
				panic(errors.New("unexpected unset"))
			}
			return v.meta, v.a
		}) {
			return RegisterError(H(string(f.Ident)))
		}
	}
	return nil
}

// RegisterFS registers from azalea files discovered in fsys. A file is
// evaluated if it is at the top level and its name has the suffix ".az", or it
// is in a top-level directory with the exact file name "package.az". The
// backing filesystem is directly exposed to azalea pathnames.
func (s *S) RegisterFS(fsys fs.FS) error {
	dents, err := fs.ReadDir(fsys, ".")
	if err != nil {
		return err
	}

	var r fs.File
	for _, dent := range dents {
		if dent.IsDir() {
			var sub fs.FS
			sub, err = fs.Sub(fsys, dent.Name())
			if err != nil {
				return err
			}

			r, err = sub.Open("package.az")
			if errors.Is(err, fs.ErrNotExist) {
				continue
			}
			if err != nil {
				return err
			}

			err = s.RegisterAzalea(r, sub)
			if _err := r.Close(); err == nil {
				err = _err
			}
			if err != nil {
				return err
			}
			continue
		}

		if !dent.Type().IsRegular() ||
			!strings.HasSuffix(dent.Name(), ".az") {
			continue
		}

		r, err = fsys.Open(dent.Name())
		if err != nil {
			return err
		}

		err = s.RegisterAzalea(r, fsys)
		if _err := r.Close(); err == nil {
			err = _err
		}
		if err != nil {
			return err
		}
	}
	return nil
}

// SetSource overrides the hakurei-source package with a cached tarball of fsys.
// The resulting IR is curable on the daemon. Must not be used concurrently with
// any other method.
func (s *S) SetSource(fsys fs.FS) error {
	var version string
	if p, err := fs.ReadFile(fsys, "cmd/dist/VERSION"); err != nil {
		return err
	} else if len(p) < 2 {
		return fmt.Errorf("invalid version string %q", string(p))
	} else {
		version = unsafe.String(unsafe.SliceData(p), len(p))
	}
	version = version[1:len(version)-1] + "-CURRENT"

	var buf bytes.Buffer
	w, err := gzip.NewWriterLevel(&buf, gzip.BestSpeed)
	if err != nil {
		return err
	}
	tw := tar.NewWriter(w)

	if err = fs.WalkDir(fsys, ".", func(path string, d fs.DirEntry, err error) error {
		if err != nil {
			return err
		}

		if d.IsDir() && d.Name() == ".git" {
			return fs.SkipDir
		}

		var fi fs.FileInfo
		if fi, err = d.Info(); err != nil {
			return err
		}

		var linkname string
		if fi.Mode()&fs.ModeSymlink != 0 {
			if linkname, err = fs.ReadLink(fsys, path); err != nil {
				return err
			}
		}

		var h *tar.Header
		if h, err = tar.FileInfoHeader(fi, linkname); err != nil {
			return err
		}
		h.Name = path

		if err = tw.WriteHeader(h); err != nil {
			return err
		}

		if fi.Mode().IsRegular() {
			var f io.ReadCloser
			if f, err = fsys.Open(path); err != nil {
				return err
			}

			_, err = io.Copy(tw, f)
			if _err := f.Close(); err == nil {
				err = _err
			}
			if err != nil {
				return err
			}
		}
		return nil
	}); err != nil {
		return err
	}

	if err = tw.Close(); err != nil {
		return err
	} else if err = w.Close(); err != nil {
		return err
	}

	const name = "hakurei-source"
	a := pkg.NewFile("hakurei-src-current.tar.gz", buf.Bytes())
	s.artifacts.Store(
		H(name),
		Artifact(func(t Toolchain) (*Metadata, pkg.Artifact) {
			return &Metadata{
				Name:        name,
				Description: "hakurei source tree (current)",
				Version:     version,
				Exclude:     true,
			}, pkg.NewTar(pkg.NewDecompress(a, pkg.Gzip))
		}),
	)
	s.DropCaches(s.Arch(), s.Flags())
	return nil
}

// SetGentooStage3 sets the Gentoo stage3 tarball url and checksum. It panics
// if given zero values or if these values have already been set.
func (s *S) SetGentooStage3(url string, checksum pkg.Checksum) {
	if s.gentooStage3 != nil {
		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"))
	}
	s.gentooStage3 = pkg.NewTar(pkg.NewDecompress(
		pkg.NewHTTPGet(nil, url, checksum),
		pkg.XZ,
	))
	s.DropCaches(s.Arch(), s.Flags())
}
