package libimage

import (
	"context"

	"github.com/containers/storage"
	ociv1 "github.com/opencontainers/image-spec/specs-go/v1"
	"github.com/sirupsen/logrus"
)

// layerTree is an internal representation of local layers.
type layerTree struct {
	// nodes is the actual layer tree with layer IDs being keys.
	nodes map[string]*layerNode
	// ociCache is a cache for Image.ID -> OCI Image. Translations are done
	// on-demand.
	ociCache map[string]*ociv1.Image
	// emptyImages do not have any top-layer so we cannot create a
	// *layerNode for them.
	emptyImages []*Image
}

// node returns a layerNode for the specified layerID.
func (t *layerTree) node(layerID string) *layerNode {
	node, exists := t.nodes[layerID]
	if !exists {
		node = &layerNode{}
		t.nodes[layerID] = node
	}
	return node
}

// toOCI returns an OCI image for the specified image.
func (t *layerTree) toOCI(ctx context.Context, i *Image) (*ociv1.Image, error) {
	var err error
	oci, exists := t.ociCache[i.ID()]
	if !exists {
		oci, err = i.toOCI(ctx)
		if err == nil {
			t.ociCache[i.ID()] = oci
		}
	}
	return oci, err
}

// layerNode is a node in a layerTree.  It's ID is the key in a layerTree.
type layerNode struct {
	children []*layerNode
	images   []*Image
	parent   *layerNode
	layer    *storage.Layer
}

// repoTags assemble all repo tags all of images of the layer node.
func (l *layerNode) repoTags() ([]string, error) {
	orderedTags := []string{}
	visitedTags := make(map[string]bool)

	for _, image := range l.images {
		repoTags, err := image.RepoTags()
		if err != nil {
			return nil, err
		}
		for _, tag := range repoTags {
			if _, visited := visitedTags[tag]; visited {
				continue
			}
			visitedTags[tag] = true
			orderedTags = append(orderedTags, tag)
		}
	}

	return orderedTags, nil
}

// layerTree extracts a layerTree from the layers in the local storage and
// relates them to the specified images.
func (r *Runtime) layerTree(images []*Image) (*layerTree, error) {
	layers, err := r.store.Layers()
	if err != nil {
		return nil, err
	}

	if images == nil {
		images, err = r.ListImages(context.Background(), nil, nil)
		if err != nil {
			return nil, err
		}
	}

	tree := layerTree{
		nodes:    make(map[string]*layerNode),
		ociCache: make(map[string]*ociv1.Image),
	}

	// First build a tree purely based on layer information.
	for i := range layers {
		node := tree.node(layers[i].ID)
		node.layer = &layers[i]
		if layers[i].Parent == "" {
			continue
		}
		parent := tree.node(layers[i].Parent)
		node.parent = parent
		parent.children = append(parent.children, node)
	}

	// Now assign the images to each (top) layer.
	for i := range images {
		img := images[i] // do not leak loop variable outside the scope
		topLayer := img.TopLayer()
		if topLayer == "" {
			tree.emptyImages = append(tree.emptyImages, img)
			continue
		}
		node, exists := tree.nodes[topLayer]
		if !exists {
			// Note: erroring out in this case has turned out having been a
			// mistake. Users may not be able to recover, so we're now
			// throwing a warning to guide them to resolve the issue and
			// turn the errors non-fatal.
			logrus.Warnf("Top layer %s of image %s not found in layer tree. The storage may be corrupted, consider running `podman system reset`.", topLayer, img.ID())
			continue
		}
		node.images = append(node.images, img)
	}

	return &tree, nil
}

// layersOf returns all storage layers of the specified image.
func (t *layerTree) layersOf(image *Image) []*storage.Layer {
	var layers []*storage.Layer
	node := t.node(image.TopLayer())
	for node != nil {
		layers = append(layers, node.layer)
		node = node.parent
	}
	return layers
}

// children returns the child images of parent. Child images are images with
// either the same top layer as parent or parent being the true parent layer.
// Furthermore, the history of the parent and child images must match with the
// parent having one history item less.  If all is true, all images are
// returned.  Otherwise, the first image is returned.  Note that manifest lists
// do not have children.
func (t *layerTree) children(ctx context.Context, parent *Image, all bool) ([]*Image, error) {
	if parent.TopLayer() == "" {
		if isManifestList, _ := parent.IsManifestList(ctx); isManifestList {
			return nil, nil
		}
	}

	parentID := parent.ID()
	parentOCI, err := t.toOCI(ctx, parent)
	if err != nil {
		return nil, err
	}

	// checkParent returns true if child and parent are in such a relation.
	checkParent := func(child *Image) (bool, error) {
		if parentID == child.ID() {
			return false, nil
		}
		childOCI, err := t.toOCI(ctx, child)
		if err != nil {
			return false, err
		}
		// History check.
		return areParentAndChild(parentOCI, childOCI), nil
	}

	var children []*Image

	// Empty images are special in that they do not have any physical layer
	// but yet can have a parent-child relation.  Hence, compare the
	// "parent" image to all other known empty images.
	if parent.TopLayer() == "" {
		for i := range t.emptyImages {
			empty := t.emptyImages[i]
			isParent, err := checkParent(empty)
			if err != nil {
				return nil, err
			}
			if isParent {
				children = append(children, empty)
				if !all {
					break
				}
			}
		}
		return children, nil
	}

	parentNode, exists := t.nodes[parent.TopLayer()]
	if !exists {
		// Note: erroring out in this case has turned out having been a
		// mistake. Users may not be able to recover, so we're now
		// throwing a warning to guide them to resolve the issue and
		// turn the errors non-fatal.
		logrus.Warnf("Layer %s not found in layer tree. The storage may be corrupted, consider running `podman system reset`.", parent.TopLayer())
		return children, nil
	}

	// addChildrenFrom adds child images of parent to children.  Returns
	// true if any image is a child of parent.
	addChildrenFromNode := func(node *layerNode) (bool, error) {
		foundChildren := false
		for i, childImage := range node.images {
			isChild, err := checkParent(childImage)
			if err != nil {
				return foundChildren, err
			}
			if isChild {
				foundChildren = true
				children = append(children, node.images[i])
				if all {
					return foundChildren, nil
				}
			}
		}
		return foundChildren, nil
	}

	// First check images where parent's top layer is also the parent
	// layer.
	for _, childNode := range parentNode.children {
		found, err := addChildrenFromNode(childNode)
		if err != nil {
			return nil, err
		}
		if found && all {
			return children, nil
		}
	}

	// Now check images with the same top layer.
	if _, err := addChildrenFromNode(parentNode); err != nil {
		return nil, err
	}

	return children, nil
}

// parent returns the parent image or nil if no parent image could be found.
// Note that manifest lists do not have parents.
func (t *layerTree) parent(ctx context.Context, child *Image) (*Image, error) {
	if child.TopLayer() == "" {
		if isManifestList, _ := child.IsManifestList(ctx); isManifestList {
			return nil, nil
		}
	}

	childID := child.ID()
	childOCI, err := t.toOCI(ctx, child)
	if err != nil {
		return nil, err
	}

	// Empty images are special in that they do not have any physical layer
	// but yet can have a parent-child relation.  Hence, compare the
	// "child" image to all other known empty images.
	if child.TopLayer() == "" {
		for _, empty := range t.emptyImages {
			if childID == empty.ID() {
				continue
			}
			emptyOCI, err := t.toOCI(ctx, empty)
			if err != nil {
				return nil, err
			}
			// History check.
			if areParentAndChild(emptyOCI, childOCI) {
				return empty, nil
			}
		}
		return nil, nil
	}

	node, exists := t.nodes[child.TopLayer()]
	if !exists {
		// Note: erroring out in this case has turned out having been a
		// mistake. Users may not be able to recover, so we're now
		// throwing a warning to guide them to resolve the issue and
		// turn the errors non-fatal.
		logrus.Warnf("Layer %s not found in layer tree. The storage may be corrupted, consider running `podman system reset`.", child.TopLayer())
		return nil, nil
	}

	// Check images from the parent node (i.e., parent layer) and images
	// with the same layer (i.e., same top layer).
	images := node.images
	if node.parent != nil {
		images = append(images, node.parent.images...)
	}
	for _, parent := range images {
		if parent.ID() == childID {
			continue
		}
		parentOCI, err := t.toOCI(ctx, parent)
		if err != nil {
			return nil, err
		}
		// History check.
		if areParentAndChild(parentOCI, childOCI) {
			return parent, nil
		}
	}

	return nil, nil
}
