// Package unpack allows for unpacking OCI artifacts, rendering the contents using
// supported variables, and applying the artifacts to a K8s cluster.
package unpack

import (
	"fmt"

	v1 "github.com/google/go-containerregistry/pkg/v1"

	"edge-infra.dev/pkg/f8n/warehouse/cluster"
	"edge-infra.dev/pkg/f8n/warehouse/oci"
	"edge-infra.dev/pkg/f8n/warehouse/oci/layer"
	"edge-infra.dev/pkg/f8n/warehouse/oci/walk"
	"edge-infra.dev/pkg/f8n/warehouse/pallet"
)

// Fn is called for each visited package in Walk() with a Pallet instantiated
// from the root package node and the unpacked layers for that Pallet.
type Fn func(p pallet.Pallet, layers []layer.Layer) error

// Walk unpacks the input Artifact a and calls fn for each valid node in the
// traversal, based on the input options. The caller is passed the result of
// unpacking and processing each visited node via the unpacking function fn.
func Walk(a oci.Artifact, fn Fn, opts ...Option) error {
	options, err := makeOptions(opts...)
	if err != nil {
		return err
	}

	// short circuit if input is standalone image or backed by one
	switch a := a.(type) {
	// if artifact is not backed by v1.Image, options.provider must be set to
	// non-empty value
	case oci.Unwrapper:
		if img, ok := a.Unwrap().(v1.Image); ok {
			return unpackImg(img, fn, options)
		}
	// if its a v1.Image straight up, short-circuit directly
	case v1.Image:
		return unpackImg(a, fn, options)
	}

	// we now know we are dealing with something like v1.ImageIndex
	// so options.provider is required
	if options.provider == "" {
		return cluster.ErrNoProviders
	}

	// validate passed provider against artifact providers
	p, err := pallet.New(a)
	if err != nil {
		return err
	}
	if err := p.Providers().IsValid(options.provider); err != nil {
		return err
	}

	// create walker for indexes
	visited := map[string]v1.Hash{} // so we only call fn once per package
	walker := &walk.Fns{
		Image: func(img v1.Image, parent v1.ImageIndex) error {
			var (
				p   pallet.Pallet
				err error
			)

			isPkg, err := oci.IsPackage(img, parent)
			if !isPkg || err != nil {
				p, err = pallet.New(parent)
				if err != nil {
					return err
				}
			} else {
				p, err = pallet.New(img)
				if err != nil {
					return err
				}
			}

			// filter variants other than the one we are specifically looking for.
			// we know options.provider is present because its required for all
			// non-Image inputs
			if !p.Supports(options.provider) {
				return nil
			}

			// calculate digest because we need it for either comparison against
			// visited nodes or to mark a visited node
			digest, err := p.Digest()
			if err != nil {
				return err
			}

			if visited[p.Name()] != (v1.Hash{}) {
				if visited[p.Name()] != digest {
					return fmt.Errorf("unpack.Walk: %w", oci.NewConflictErr(
						p.Name(), visited[p.Name()], digest,
					))
				}

				// return early if we have already visited this package and there is not
				// a conflict
				return nil
			}

			layers, err := unpackPallet(p, *options)
			if err != nil {
				return err
			}

			// mark node as visited before we invoke the unpacking
			// function
			visited[p.Name()] = digest

			return fn(p, layers)
		},
	}

	return walk.Walk(a, walker)
}

// unpackImg is a helper when the artifact is a single image
func unpackImg(img v1.Image, fn Fn, opts *options) error {
	p, err := pallet.New(img)
	if err != nil {
		return err
	}

	layers, err := unpackPallet(p, *opts)
	if err != nil {
		return err
	}

	return fn(p, layers)
}