// Copyright Project Harbor 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 artifact

import (
	"context"
	"encoding/json"
	"fmt"

	"github.com/docker/distribution/manifest/manifestlist"
	"github.com/docker/distribution/manifest/schema1"
	"github.com/docker/distribution/manifest/schema2"
	v1 "github.com/opencontainers/image-spec/specs-go/v1"

	"github.com/goharbor/harbor/src/controller/artifact/processor"
	"github.com/goharbor/harbor/src/controller/artifact/processor/wasm"
	"github.com/goharbor/harbor/src/lib/log"
	"github.com/goharbor/harbor/src/lib/q"
	"github.com/goharbor/harbor/src/pkg"
	"github.com/goharbor/harbor/src/pkg/artifact"
	"github.com/goharbor/harbor/src/pkg/blob"
	"github.com/goharbor/harbor/src/pkg/registry"
)

// Abstractor abstracts the metadata of artifact
type Abstractor interface {
	// AbstractMetadata abstracts the metadata for the specific artifact type into the artifact model,
	AbstractMetadata(ctx context.Context, artifact *artifact.Artifact) error
}

// NewAbstractor creates a new abstractor
func NewAbstractor() Abstractor {
	return &abstractor{
		artMgr:  pkg.ArtifactMgr,
		blobMgr: blob.Mgr,
		regCli:  registry.Cli,
	}
}

type abstractor struct {
	artMgr  artifact.Manager
	blobMgr blob.Manager
	regCli  registry.Client
}

func (a *abstractor) AbstractMetadata(ctx context.Context, artifact *artifact.Artifact) error {
	// read manifest content
	manifest, _, err := a.regCli.PullManifest(artifact.RepositoryName, artifact.Digest)
	if err != nil {
		return err
	}
	manifestMediaType, content, err := manifest.Payload()
	if err != nil {
		return err
	}
	artifact.ManifestMediaType = manifestMediaType

	switch artifact.ManifestMediaType {
	case "", "application/json", schema1.MediaTypeSignedManifest:
		if err := a.abstractManifestV1Metadata(ctx, artifact, content); err != nil {
			return err
		}
	case v1.MediaTypeImageManifest, schema2.MediaTypeManifest:
		if err = a.abstractManifestV2Metadata(artifact, content); err != nil {
			return err
		}
	case v1.MediaTypeImageIndex, manifestlist.MediaTypeManifestList:
		if err = a.abstractIndexMetadata(ctx, artifact, content); err != nil {
			return err
		}
	default:
		return fmt.Errorf("unsupported manifest media type: %s", artifact.ManifestMediaType)
	}
	return processor.Get(artifact.ResolveArtifactType()).AbstractMetadata(ctx, artifact, content)
}

// the artifact is enveloped by docker manifest v1
func (a *abstractor) abstractManifestV1Metadata(ctx context.Context, artifact *artifact.Artifact, content []byte) error {
	// unify the media type of v1 manifest to "schema1.MediaTypeSignedManifest"
	artifact.ManifestMediaType = schema1.MediaTypeSignedManifest
	// as no config layer in the docker v1 manifest, use the "schema1.MediaTypeSignedManifest"
	// as the media type of artifact
	artifact.MediaType = schema1.MediaTypeSignedManifest

	manifest := &schema1.Manifest{}
	if err := json.Unmarshal(content, manifest); err != nil {
		return err
	}

	var ol q.OrList
	for _, fsLayer := range manifest.FSLayers {
		ol.Values = append(ol.Values, fsLayer.BlobSum.String())
	}

	// there is no layer size in v1 manifest, compute the artifact size from the blobs
	blobs, err := a.blobMgr.List(ctx, q.New(q.KeyWords{"digest": &ol}))
	if err != nil {
		log.G(ctx).Errorf("failed to get blobs of the artifact %s, error %v", artifact.Digest, err)
		return err
	}

	artifact.Size = int64(len(content))
	for _, blob := range blobs {
		artifact.Size += blob.Size
	}

	return nil
}

// the artifact is enveloped by OCI manifest or docker manifest v2
func (a *abstractor) abstractManifestV2Metadata(artifact *artifact.Artifact, content []byte) error {
	manifest := &v1.Manifest{}
	if err := json.Unmarshal(content, manifest); err != nil {
		return err
	}
	// use the "manifest.config.mediatype" as the media type of the artifact
	artifact.MediaType = manifest.Config.MediaType
	if manifest.Annotations[wasm.AnnotationVariantKey] == wasm.AnnotationVariantValue || manifest.Annotations[wasm.AnnotationHandlerKey] == wasm.AnnotationHandlerValue {
		artifact.MediaType = wasm.MediaType
	}
	/*
		https://github.com/opencontainers/distribution-spec/blob/v1.1.0/spec.md#listing-referrers
		For referrers list, if the artifactType is empty or missing in the image manifest, the value of artifactType MUST be set to the config descriptor mediaType value
	*/
	if manifest.ArtifactType != "" {
		artifact.ArtifactType = manifest.ArtifactType
	} else {
		artifact.ArtifactType = manifest.Config.MediaType
	}

	// set size
	artifact.Size = int64(len(content)) + manifest.Config.Size
	for _, layer := range manifest.Layers {
		artifact.Size += layer.Size
	}
	// set annotations
	artifact.Annotations = manifest.Annotations
	return nil
}

// the artifact is enveloped by OCI index or docker manifest list
func (a *abstractor) abstractIndexMetadata(ctx context.Context, art *artifact.Artifact, content []byte) error {
	// the identity of index is still in progress, we use the manifest mediaType
	// as the media type of artifact
	art.MediaType = art.ManifestMediaType

	index := &v1.Index{}
	if err := json.Unmarshal(content, index); err != nil {
		return err
	}

	/*
		https://github.com/opencontainers/distribution-spec/blob/v1.1.0/spec.md#listing-referrers
		For referrers list, If the artifactType is empty or missing in an index, the artifactType MUST be omitted.
	*/
	if index.ArtifactType != "" {
		art.ArtifactType = index.ArtifactType
	} else {
		art.ArtifactType = ""
	}

	// set annotations
	art.Annotations = index.Annotations

	art.Size += int64(len(content))
	// populate the referenced artifacts
	for _, mani := range index.Manifests {
		digest := mani.Digest.String()
		// make sure the child artifact exist
		ar, err := a.artMgr.GetByDigest(ctx, art.RepositoryName, digest)
		if err != nil {
			return err
		}
		art.Size += ar.Size
		art.References = append(art.References, &artifact.Reference{
			ChildID:     ar.ID,
			ChildDigest: digest,
			Platform:    mani.Platform,
			URLs:        mani.URLs,
			Annotations: mani.Annotations,
		})
	}

	// Currently, CNAB put its media type inside the annotations
	// try to parse the artifact media type from the annotations
	if art.Annotations != nil {
		mediaType := art.Annotations["org.opencontainers.artifactType"]
		if len(mediaType) > 0 {
			art.MediaType = mediaType
		}
	}

	return nil
}
