// Source: https://github.com/distribution/distribution

// Copyright 2014 https://github.com/distribution/distribution Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//    http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package ocischema

import (
	"encoding/json"
	"errors"
	"fmt"

	"github.com/harness/gitness/registry/app/manifest"

	"github.com/opencontainers/go-digest"
	v1 "github.com/opencontainers/image-spec/specs-go/v1"
)

// SchemaVersion provides a pre-initialized version structure for OCI Image
// Manifests.
var SchemaVersion = manifest.Versioned{
	SchemaVersion: 2,
	MediaType:     v1.MediaTypeImageManifest,
}

func init() {
	ocischemaFunc := func(b []byte) (manifest.Manifest, manifest.Descriptor, error) {
		if err := validateManifest(b); err != nil {
			return nil, manifest.Descriptor{}, err
		}
		m := new(DeserializedManifest)
		err := m.UnmarshalJSON(b)
		if err != nil {
			return nil, manifest.Descriptor{}, err
		}

		dgst := digest.FromBytes(b)
		return m, manifest.Descriptor{
			MediaType:   v1.MediaTypeImageManifest,
			Digest:      dgst,
			Size:        int64(len(b)),
			Annotations: m.Annotations(),
		}, err
	}
	err := manifest.RegisterManifestSchema(v1.MediaTypeImageManifest, ocischemaFunc)
	if err != nil {
		panic(fmt.Sprintf("Unable to register manifest: %s", err))
	}
}

// Manifest defines a ocischema manifest.
type Manifest struct {
	manifest.Versioned

	// This OPTIONAL property contains the type of an artifact when the
	// manifest is used for an artifact. This MUST be set when
	// config.mediaType is set to the empty value.
	ArtifactType string `json:"artifactType,omitempty"`

	// Config references the image configuration as a blob.
	Config manifest.Descriptor `json:"config"`

	// Layers lists descriptors for the layers referenced by the
	// configuration.
	Layers []manifest.Descriptor `json:"layers"`

	// This OPTIONAL property specifies a descriptor of another manifest.
	// This value, used by the referrers API, indicates a relationship to
	// the specified manifest.
	Subject *manifest.Descriptor `json:"subject,omitempty"`

	// Annotations contains arbitrary metadata for the image manifest.
	Annotations map[string]string `json:"annotations,omitempty"`
}

// References returns the descriptors of this manifests references.
func (m Manifest) References() []manifest.Descriptor {
	references := make([]manifest.Descriptor, 0, 1+len(m.Layers))
	references = append(references, m.Config)
	references = append(references, m.Layers...)
	return references
}

// Target returns the target of this manifest.
func (m Manifest) Target() manifest.Descriptor {
	return m.Config
}

// DeserializedManifest wraps Manifest with a copy of the original JSON.
// It satisfies the distribution.Manifest interface.
type DeserializedManifest struct {
	Manifest

	// canonical is the canonical byte representation of the Manifest.
	canonical []byte
}

// FromStruct takes a Manifest structure, marshals it to JSON, and returns a
// DeserializedManifest which contains the manifest and its JSON representation.
func FromStruct(m Manifest) (*DeserializedManifest, error) {
	var deserialized DeserializedManifest
	deserialized.Manifest = m

	var err error
	deserialized.canonical, err = json.MarshalIndent(&m, "", "   ")
	return &deserialized, err
}

// UnmarshalJSON populates a new Manifest struct from JSON data.
func (m *DeserializedManifest) UnmarshalJSON(b []byte) error {
	m.canonical = make([]byte, len(b))
	// store manifest in canonical
	copy(m.canonical, b)

	// Unmarshal canonical JSON into Manifest object
	var mfst Manifest
	if err := json.Unmarshal(m.canonical, &mfst); err != nil {
		return err
	}

	if mfst.MediaType != "" && mfst.MediaType != v1.MediaTypeImageManifest {
		return fmt.Errorf(
			"if present, mediaType in manifest should be '%s' not '%s'",
			v1.MediaTypeImageManifest, mfst.MediaType,
		)
	}

	m.Manifest = mfst

	return nil
}

// MarshalJSON returns the contents of canonical. If canonical is empty,
// marshals the inner contents.
func (m *DeserializedManifest) MarshalJSON() ([]byte, error) {
	if len(m.canonical) > 0 {
		return m.canonical, nil
	}

	return nil, errors.New("JSON representation not initialized in DeserializedManifest")
}

// Payload returns the raw content of the manifest. The contents can be used to
// calculate the content identifier.
func (m DeserializedManifest) Payload() (string, []byte, error) {
	return v1.MediaTypeImageManifest, m.canonical, nil
}

// validateManifest returns an error if the byte slice is invalid JSON or if it
// contains fields that belong to a index.
func validateManifest(b []byte) error {
	var doc struct {
		Manifests interface{} `json:"manifests,omitempty"`
	}
	if err := json.Unmarshal(b, &doc); err != nil {
		return err
	}
	if doc.Manifests != nil {
		return errors.New("ocimanifest: expected manifest but found index")
	}
	return nil
}

func (m *DeserializedManifest) Version() manifest.Versioned {
	// Media type can be either Docker (`application/vnd.docker.distribution.manifest.v2+json`) or OCI (empty).
	// We need to make it explicit if empty, otherwise we're not able to distinguish between media types.
	if m.Versioned.MediaType == "" {
		m.Versioned.MediaType = v1.MediaTypeImageManifest
	}

	return m.Versioned
}

func (m *DeserializedManifest) Config() manifest.Descriptor   { return m.Target() }
func (m *DeserializedManifest) Layers() []manifest.Descriptor { return m.Manifest.Layers }
func (m *DeserializedManifest) DistributableLayers() []manifest.Descriptor {
	var ll []manifest.Descriptor
	for _, l := range m.Layers() {
		switch l.MediaType {
		case v1.MediaTypeImageLayerNonDistributable, v1.MediaTypeImageLayerNonDistributableGzip:
			continue
		}
		ll = append(ll, l)
	}
	return ll
}
func (m *DeserializedManifest) ArtifactType() string { return m.Manifest.ArtifactType }
func (m *DeserializedManifest) Subject() manifest.Descriptor {
	if m.Manifest.Subject == nil {
		return manifest.Descriptor{}
	}
	return *m.Manifest.Subject
}

func (m *DeserializedManifest) Annotations() map[string]string {
	if m.Manifest.Annotations == nil {
		return map[string]string{}
	}
	return m.Manifest.Annotations
}

func (m *DeserializedManifest) TotalSize() int64 {
	var layersSize int64
	for _, layer := range m.Layers() {
		layersSize += layer.Size
	}

	return layersSize + m.Config().Size + int64(len(m.canonical))
}
