package rosa

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"net/http"
	"runtime"
	"strconv"
	"sync"

	"hakurei.app/internal/pkg"
)

// PArtifact is a lazily-initialised [pkg.Artifact] preset.
type PArtifact int

const (
	LLVM PArtifact = iota

	// EarlyInit is the Rosa OS init program.
	EarlyInit
	// ImageSystem is the Rosa OS /system image.
	ImageSystem
	// ImageInitramfs is the Rosa OS initramfs archive.
	ImageInitramfs

	// Kernel is the generic Rosa OS Linux kernel.
	Kernel
	// KernelHeaders is an installation of kernel headers for [Kernel].
	KernelHeaders
	// KernelSource is a writable kernel source tree installed to [AbsUsrSrc].
	KernelSource
	// Firmware is firmware blobs for use with the Linux kernel.
	Firmware

	ACL
	ArgpStandalone
	Attr
	Autoconf
	Automake
	BC
	Bash
	Binutils
	Bison
	Bzip2
	CMake
	Connman
	Coreutils
	Curl
	DBus
	DTC
	Diffutils
	Elfutils
	Fakeroot
	Findutils
	Flex
	FontUtil
	Freetype
	Fuse
	GMP
	GLib
	Gawk
	GenInitCPIO
	Gettext
	Git
	Glslang
	GnuTLS
	Go
	Gperf
	Grep
	Gzip
	Hakurei
	HakureiDist
	Hwdata
	IPTables
	Kmod
	LIT
	LibX11
	LibXau
	LibXdmcp
	LibXext
	LibXfixes
	LibXfont2
	LibXrandr
	LibXrender
	LibXxf86vm
	Libarchive
	Libbsd
	Libcap
	Libconfig
	LibdisplayInfo
	Libdrm
	Libepoxy
	Libev
	Libexpat
	Libffi
	Libfontenc
	Libgd
	Libglvnd
	Libiconv
	Libmd
	Libmnl
	Libnftnl
	Libpciaccess
	Libpng
	Libpsl
	Libseccomp
	Libtasn1
	Libtirpc
	Libtool
	Libucontext
	Libunistring
	Libva
	LibxcbRenderUtil
	LibxcbUtil
	LibxcbUtilImage
	LibxcbUtilKeysyms
	LibxcbUtilWM
	Libxcvt
	Libxkbfile
	Libxml2
	Libxshmfence
	Libxslt
	Libxtrans
	LMSensors
	M4
	MPC
	MPFR
	Make
	Mesa
	Meson
	Mksh
	MuslFts
	MuslObstack
	NSS
	NSSCACert
	Ncurses
	Nettle
	Ninja
	OpenSSL
	P11Kit
	PCRE2
	Parallel
	Patch
	Perl
	PerlLocaleGettext
	PerlMIMECharset
	PerlModuleBuild
	PerlPodParser
	PerlSGMLS
	PerlTermReadKey
	PerlTestCmd
	PerlTextCharWidth
	PerlTextWrapI18N
	PerlUnicodeLineBreak
	PerlYAMLTiny
	Pixman
	PkgConfig
	Procps
	Python
	PythonFlitCore
	PythonHatchling
	PythonIniConfig
	PythonMako
	PythonMarkupSafe
	PythonPackaging
	PythonPathspec
	PythonPluggy
	PythonPyTest
	PythonPyYAML
	PythonPycparser
	PythonPygments
	PythonSetuptools
	PythonSetuptoolsSCM
	PythonTroveClassifiers
	PythonVCSVersioning
	PythonWheel
	QEMU
	Rdfind
	Readline
	Rsync
	Sed
	SPIRVHeaders
	SPIRVLLVMTranslator
	SPIRVTools
	SquashfsTools
	Strace
	TamaGo
	Tar
	Texinfo
	Toybox
	toyboxEarly
	Unzip
	UtilLinux
	VIM
	Wayland
	WaylandProtocols
	XCB
	XCBProto
	XDGDBusProxy
	XZ
	Xkbcomp
	XkeyboardConfig
	XorgProto
	Xserver
	Zlib
	Zstd

	// PresetUnexportedStart is the first unexported preset.
	PresetUnexportedStart

	stage0Dist = iota - 1
	llvmSource
	// earlyCompilerRT is an early, standalone compiler-rt installation for the
	// standalone runtimes build.
	//
	// earlyCompilerRT must only be loaded by [LLVM].
	earlyCompilerRT
	// earlyRuntimes is an early, standalone installation of LLVM runtimes to
	// work around the cmake build system leaking the system LLVM installation
	// when invoking the newly built toolchain.
	//
	// earlyRuntimes must only be loaded by [LLVM].
	earlyRuntimes

	buildcatrust
	utilMacros

	// Musl is a standalone libc that does not depend on the toolchain.
	Musl
	// muslHeaders is a system installation of [Musl] headers.
	muslHeaders

	// gcc is a hacked-to-pieces GCC toolchain meant for use in intermediate
	// stages only. This preset and its direct output must never be exposed.
	gcc

	// nettle3 is an older version of [Nettle].
	nettle3

	// Stage0 is a tarball containing all compile-time dependencies of artifacts
	// part of the [Std] toolchain.
	Stage0

	// PresetEnd is the total number of presets and does not denote a preset.
	PresetEnd
)

// P represents multiple [PArtifact] and is stable through JSON.
type P []PArtifact

// MarshalJSON represents [PArtifact] by their [Metadata.Name].
func (s P) MarshalJSON() ([]byte, error) {
	names := make([]string, len(s))
	for i, p := range s {
		names[i] = GetMetadata(p).Name
	}
	return json.Marshal(names)
}

// UnmarshalJSON resolves the value created by MarshalJSON back to [P].
func (s *P) UnmarshalJSON(data []byte) error {
	var names []string
	if err := json.Unmarshal(data, &names); err != nil {
		return err
	}

	*s = make(P, len(names))
	for i, name := range names {
		if p, ok := ResolveName(name); !ok {
			return fmt.Errorf("unknown artifact %q", name)
		} else {
			(*s)[i] = p
		}
	}
	return nil
}

// Metadata is stage-agnostic information of a [PArtifact] not directly
// representable in the resulting [pkg.Artifact].
type Metadata struct {
	f func(t Toolchain) (a pkg.Artifact, version string)

	// 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"`

	// Project identifier on [Anitya].
	//
	// [Anitya]: https://release-monitoring.org/
	ID int `json:"-"`

	// 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 [PArtifact].
const Unversioned = "\x00"

// UnpopulatedIDError is returned by [Metadata.GetLatest] for an instance of
// [Metadata] 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())
}

var (
	// artifactsM is an array of [PArtifact] metadata.
	artifactsM [PresetEnd]Metadata

	// artifacts stores the result of Metadata.f.
	artifacts [_toolchainEnd][len(artifactsM)]struct {
		a pkg.Artifact
		v string
	}
	// artifactsOnce is for lazy initialisation of artifacts.
	artifactsOnce [_toolchainEnd][len(artifactsM)]sync.Once

	// arch is the target architecture.
	arch = runtime.GOARCH

	// presetOpts globally modifies behaviour of presets.
	presetOpts int
)

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

// Arch returns the target architecture.
func Arch() string { return arch }

// Flags returns the current preset flags
func Flags() int { return presetOpts }

// zero zeros the value pointed to by p.
func zero[T any](p *T) { var v T; *p = v }

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

	arch = targetArch
	presetOpts = flags
	zero(&artifacts)
	zero(&artifactsOnce)
}

// GetMetadata returns [Metadata] of a [PArtifact].
func GetMetadata(p PArtifact) *Metadata { return &artifactsM[p] }

// construct constructs a [pkg.Artifact] corresponding to a [PArtifact] once.
func (t Toolchain) construct(p PArtifact) {
	artifactsOnce[t][p].Do(func() {
		artifacts[t][p].a, artifacts[t][p].v = artifactsM[p].f(t)
	})
}

// Load returns the resulting [pkg.Artifact] of [PArtifact].
func (t Toolchain) Load(p PArtifact) pkg.Artifact {
	t.construct(p)
	return artifacts[t][p].a
}

// Version returns the version string of [PArtifact].
func (t Toolchain) Version(p PArtifact) string {
	t.construct(p)
	return artifacts[t][p].v
}

// ResolveName returns a [PArtifact] by name.
func ResolveName(name string) (p PArtifact, ok bool) {
	for i := range PresetUnexportedStart {
		if name == artifactsM[i].Name {
			return i, true
		}
	}
	return 0, false
}
