package pkgserver

import (
	"cmp"
	"errors"
	"slices"
	"strings"

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

const (
	declarationAscending = iota
	declarationDescending
	nameAscending
	nameDescending
	sizeAscending
	sizeDescending

	sortOrderEnd = iota - 1
)

// packageIndex refers to metadata by name and various sort orders.
type packageIndex struct {
	sorts  [sortOrderEnd + 1][]*metadata
	names  map[string]*metadata
	search searchCache
	// Taken from [rosa.Report] if available.
	handleAccess func(*error) func()
}

// metadata holds [rosa.Metadata] extended with additional information.
type metadata struct {
	handle rosa.ArtifactH
	*rosa.Metadata

	// Copied from [rosa.Metadata], [rosa.Unversioned] is equivalent to the zero
	// value. Otherwise, the zero value is invalid.
	Version string `json:"version,omitempty"`
	// Output data size, available if present in report.
	Size int64 `json:"size,omitempty"`
	// Whether the underlying [pkg.Artifact] is present in the report.
	HasReport bool `json:"report"`

	// Ident string encoded ahead of time.
	ids string
	// Backed by [rosa.Report], access must be prepared by HandleAccess.
	status []byte
}

// populate deterministically populates packageIndex, optionally with a report.
func (index *packageIndex) populate(report *rosa.Report) (err error) {
	if report != nil {
		defer report.HandleAccess(&err)()
		index.handleAccess = report.HandleAccess
	}

	handles := rosa.Collect()
	work := make([]*metadata, len(handles))
	index.names = make(map[string]*metadata)
	ir := pkg.NewIR()
	for i, handle := range handles {
		meta, a := rosa.MustLoad(handle)
		m := metadata{
			handle: handle,

			Metadata: meta,
			Version:  meta.Version,
		}
		if m.Version == "" {
			return errors.New("invalid version from " + m.Name)
		}
		if m.Version == rosa.Unversioned {
			m.Version = ""
		}

		if report != nil {
			id := ir.Ident(a)
			m.ids = pkg.Encode(id.Value())
			m.status, m.Size = report.ArtifactOf(id)
			m.HasReport = m.Size >= 0
		}

		work[i] = &m
		index.names[m.Name] = &m
	}

	index.sorts[declarationAscending] = work
	index.sorts[declarationDescending] = slices.Clone(work)
	slices.Reverse(index.sorts[declarationDescending][:])

	index.sorts[nameAscending] = slices.Clone(work)
	slices.SortFunc(index.sorts[nameAscending][:], func(a, b *metadata) int {
		return strings.Compare(a.Name, b.Name)
	})
	index.sorts[nameDescending] = slices.Clone(index.sorts[nameAscending])
	slices.Reverse(index.sorts[nameDescending][:])

	index.sorts[sizeAscending] = slices.Clone(work)
	slices.SortFunc(index.sorts[sizeAscending][:], func(a, b *metadata) int {
		return cmp.Compare(a.Size, b.Size)
	})
	index.sorts[sizeDescending] = slices.Clone(index.sorts[sizeAscending])
	slices.Reverse(index.sorts[sizeDescending][:])

	return
}
