/*
   Copyright The containerd 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 oci provides the importer and the exporter for OCI Image Spec.
package oci

import (
	"archive/tar"
	"context"
	"encoding/json"
	"fmt"
	"io"
	"io/ioutil"
	"path"
	"strings"

	"github.com/containerd/containerd/content"
	"github.com/containerd/containerd/errdefs"
	"github.com/containerd/containerd/images"
	digest "github.com/opencontainers/go-digest"
	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
	"github.com/pkg/errors"
)

// V1Importer implements OCI Image Spec v1.
type V1Importer struct {
	// ImageName is preprended to either `:` + OCI ref name or `@` + digest (for anonymous refs).
	// This field is mandatory atm, but may change in the future. maybe ref map[string]string as in moby/moby#33355
	ImageName string
}

var _ images.Importer = &V1Importer{}

// Import implements Importer.
func (oi *V1Importer) Import(ctx context.Context, store content.Store, reader io.Reader) ([]images.Image, error) {
	if oi.ImageName == "" {
		return nil, errors.New("ImageName not set")
	}
	tr := tar.NewReader(reader)
	var imgrecs []images.Image
	foundIndexJSON := false
	for {
		hdr, err := tr.Next()
		if err == io.EOF {
			break
		}
		if err != nil {
			return nil, err
		}
		if hdr.Typeflag != tar.TypeReg && hdr.Typeflag != tar.TypeRegA {
			continue
		}
		hdrName := path.Clean(hdr.Name)
		if hdrName == "index.json" {
			if foundIndexJSON {
				return nil, errors.New("duplicated index.json")
			}
			foundIndexJSON = true
			imgrecs, err = onUntarIndexJSON(tr, oi.ImageName)
			if err != nil {
				return nil, err
			}
			continue
		}
		if strings.HasPrefix(hdrName, "blobs/") {
			if err := onUntarBlob(ctx, tr, store, hdrName, hdr.Size); err != nil {
				return nil, err
			}
		}
	}
	if !foundIndexJSON {
		return nil, errors.New("no index.json found")
	}
	for _, img := range imgrecs {
		err := setGCRefContentLabels(ctx, store, img.Target)
		if err != nil {
			return imgrecs, err
		}
	}
	// FIXME(AkihiroSuda): set GC labels for unreferrenced blobs (i.e. with unknown media types)?
	return imgrecs, nil
}

func onUntarIndexJSON(r io.Reader, imageName string) ([]images.Image, error) {
	b, err := ioutil.ReadAll(r)
	if err != nil {
		return nil, err
	}
	var idx ocispec.Index
	if err := json.Unmarshal(b, &idx); err != nil {
		return nil, err
	}
	var imgrecs []images.Image
	for _, m := range idx.Manifests {
		ref, err := normalizeImageRef(imageName, m)
		if err != nil {
			return nil, err
		}
		imgrecs = append(imgrecs, images.Image{
			Name:   ref,
			Target: m,
		})
	}
	return imgrecs, nil
}

func normalizeImageRef(imageName string, manifest ocispec.Descriptor) (string, error) {
	digest := manifest.Digest
	if digest == "" {
		return "", errors.Errorf("manifest with empty digest: %v", manifest)
	}
	ociRef := manifest.Annotations[ocispec.AnnotationRefName]
	if ociRef == "" {
		return imageName + "@" + digest.String(), nil
	}
	return imageName + ":" + ociRef, nil
}

func onUntarBlob(ctx context.Context, r io.Reader, store content.Ingester, name string, size int64) error {
	// name is like "blobs/sha256/deadbeef"
	split := strings.Split(name, "/")
	if len(split) != 3 {
		return errors.Errorf("unexpected name: %q", name)
	}
	algo := digest.Algorithm(split[1])
	if !algo.Available() {
		return errors.Errorf("unsupported algorithm: %s", algo)
	}
	dgst := digest.NewDigestFromHex(algo.String(), split[2])
	return content.WriteBlob(ctx, store, "unknown-"+dgst.String(), r, ocispec.Descriptor{Size: size, Digest: dgst})
}

// GetChildrenDescriptors returns children blob descriptors for the following supported types:
// - images.MediaTypeDockerSchema2Manifest, ocispec.MediaTypeImageManifest
// - images.MediaTypeDockerSchema2ManifestList, ocispec.MediaTypeImageIndex
func GetChildrenDescriptors(r io.Reader, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) {
	switch desc.MediaType {
	case images.MediaTypeDockerSchema2Manifest, ocispec.MediaTypeImageManifest:
		var manifest ocispec.Manifest
		if err := json.NewDecoder(r).Decode(&manifest); err != nil {
			return nil, err
		}
		return append([]ocispec.Descriptor{manifest.Config}, manifest.Layers...), nil
	case images.MediaTypeDockerSchema2ManifestList, ocispec.MediaTypeImageIndex:
		var index ocispec.Index
		if err := json.NewDecoder(r).Decode(&index); err != nil {
			return nil, err
		}
		return index.Manifests, nil
	}
	return nil, nil
}

func setGCRefContentLabels(ctx context.Context, store content.Store, desc ocispec.Descriptor) error {
	info, err := store.Info(ctx, desc.Digest)
	if err != nil {
		if errdefs.IsNotFound(err) {
			// when the archive is created from multi-arch image,
			// it may contain only blobs for a certain platform.
			// So ErrNotFound (on manifest list) is expected here.
			return nil
		}
		return err
	}
	ra, err := store.ReaderAt(ctx, desc)
	if err != nil {
		return err
	}
	defer ra.Close()
	r := content.NewReader(ra)
	children, err := GetChildrenDescriptors(r, desc)
	if err != nil {
		return err
	}
	if info.Labels == nil {
		info.Labels = map[string]string{}
	}
	for i, child := range children {
		// Note: child blob is not guaranteed to be written to the content store. (multi-arch)
		info.Labels[fmt.Sprintf("containerd.io/gc.ref.content.%d", i)] = child.Digest.String()
	}
	if _, err := store.Update(ctx, info, "labels"); err != nil {
		return err
	}
	for _, child := range children {
		if err := setGCRefContentLabels(ctx, store, child); err != nil {
			return err
		}
	}
	return nil
}
