package schema1

import (
	"context"
	"errors"
	"fmt"

	"github.com/distribution/distribution/v3"
	"github.com/distribution/distribution/v3/manifest"
	"github.com/distribution/distribution/v3/reference"
	"github.com/docker/libtrust"
	"github.com/opencontainers/go-digest"
)

// referenceManifestBuilder is a type for constructing manifests from schema1
// dependencies.
type referenceManifestBuilder struct {
	Manifest
	pk libtrust.PrivateKey
}

// NewReferenceManifestBuilder is used to build new manifests for the current
// schema version using schema1 dependencies.
func NewReferenceManifestBuilder(pk libtrust.PrivateKey, ref reference.Named, architecture string) distribution.ManifestBuilder {
	tag := ""
	if tagged, isTagged := ref.(reference.Tagged); isTagged {
		tag = tagged.Tag()
	}

	return &referenceManifestBuilder{
		Manifest: Manifest{
			Versioned: manifest.Versioned{
				SchemaVersion: 1,
			},
			Name:         ref.Name(),
			Tag:          tag,
			Architecture: architecture,
		},
		pk: pk,
	}
}

func (mb *referenceManifestBuilder) Build(ctx context.Context) (distribution.Manifest, error) {
	m := mb.Manifest
	if len(m.FSLayers) == 0 {
		return nil, errors.New("cannot build manifest with zero layers or history")
	}

	m.FSLayers = make([]FSLayer, len(mb.Manifest.FSLayers))
	m.History = make([]History, len(mb.Manifest.History))
	copy(m.FSLayers, mb.Manifest.FSLayers)
	copy(m.History, mb.Manifest.History)

	return Sign(&m, mb.pk)
}

// AppendReference adds a reference to the current ManifestBuilder
func (mb *referenceManifestBuilder) AppendReference(d distribution.Describable) error {
	r, ok := d.(Reference)
	if !ok {
		return fmt.Errorf("unable to add non-reference type to v1 builder")
	}

	// Entries need to be prepended
	mb.Manifest.FSLayers = append([]FSLayer{{BlobSum: r.Digest}}, mb.Manifest.FSLayers...)
	mb.Manifest.History = append([]History{r.History}, mb.Manifest.History...)
	return nil

}

// References returns the current references added to this builder
func (mb *referenceManifestBuilder) References() []distribution.Descriptor {
	refs := make([]distribution.Descriptor, len(mb.Manifest.FSLayers))
	for i := range mb.Manifest.FSLayers {
		layerDigest := mb.Manifest.FSLayers[i].BlobSum
		history := mb.Manifest.History[i]
		ref := Reference{layerDigest, 0, history}
		refs[i] = ref.Descriptor()
	}
	return refs
}

// Reference describes a manifest v2, schema version 1 dependency.
// An FSLayer associated with a history entry.
type Reference struct {
	Digest  digest.Digest
	Size    int64 // if we know it, set it for the descriptor.
	History History
}

// Descriptor describes a reference
func (r Reference) Descriptor() distribution.Descriptor {
	return distribution.Descriptor{
		MediaType: MediaTypeManifestLayer,
		Digest:    r.Digest,
		Size:      r.Size,
	}
}