//
// Copyright 2021 The Sigstore 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 attach

import (
	"context"
	"errors"
	"fmt"
	"io"
	"net/http"
	"os"
	"path/filepath"

	"github.com/google/go-containerregistry/pkg/logs"
	"github.com/google/go-containerregistry/pkg/name"
	v1 "github.com/google/go-containerregistry/pkg/v1"
	"github.com/google/go-containerregistry/pkg/v1/empty"
	"github.com/google/go-containerregistry/pkg/v1/mutate"
	"github.com/google/go-containerregistry/pkg/v1/remote"
	"github.com/google/go-containerregistry/pkg/v1/remote/transport"
	ocistatic "github.com/google/go-containerregistry/pkg/v1/static"
	ocitypes "github.com/google/go-containerregistry/pkg/v1/types"
	"github.com/sigstore/cosign/v3/cmd/cosign/cli/options"
	ociexperimental "github.com/sigstore/cosign/v3/internal/pkg/oci/remote"
	"github.com/sigstore/cosign/v3/internal/ui"
	ociremote "github.com/sigstore/cosign/v3/pkg/oci/remote"
	"github.com/sigstore/cosign/v3/pkg/oci/static"
)

func SBOMCmd(ctx context.Context, regOpts options.RegistryOptions, regExpOpts options.RegistryExperimentalOptions, sbomRef string, sbomType ocitypes.MediaType, imageRef string) error {
	if regExpOpts.RegistryReferrersMode == options.RegistryReferrersModeOCI11 {
		return sbomCmdOCIExperimental(ctx, regOpts, sbomRef, sbomType, imageRef)
	}

	ref, err := name.ParseReference(imageRef, regOpts.NameOptions()...)
	if err != nil {
		return err
	}

	b, err := sbomBytes(sbomRef)
	if err != nil {
		return err
	}

	remoteOpts, err := regOpts.ClientOpts(ctx)
	if err != nil {
		return err
	}

	dstRef, err := ociremote.SBOMTag(ref, remoteOpts...)
	if err != nil {
		return err
	}

	ui.Infof(ctx, "Uploading SBOM file for [%s] to [%s] with mediaType [%s].\n", ref.Name(), dstRef.Name(), sbomType)
	img, err := static.NewFile(b, static.WithLayerMediaType(sbomType))
	if err != nil {
		return err
	}
	return remote.Write(dstRef, img, regOpts.GetRegistryClientOpts(ctx)...)
}

func sbomCmdOCIExperimental(ctx context.Context, regOpts options.RegistryOptions, sbomRef string, sbomType ocitypes.MediaType, imageRef string) error {
	var dig name.Digest
	ref, err := name.ParseReference(imageRef, regOpts.NameOptions()...)
	if err != nil {
		return err
	}
	if digr, ok := ref.(name.Digest); ok {
		dig = digr
	} else {
		desc, err := remote.Head(ref, regOpts.GetRegistryClientOpts(ctx)...)
		if err != nil {
			return err
		}
		dig = ref.Context().Digest(desc.Digest.String())
	}

	artifactType := ociexperimental.ArtifactType("sbom")

	desc, err := remote.Head(dig, regOpts.GetRegistryClientOpts(ctx)...)
	var terr *transport.Error
	if errors.As(err, &terr) && terr.StatusCode == http.StatusNotFound {
		h, err := v1.NewHash(dig.DigestStr())
		if err != nil {
			return err
		}
		// The subject doesn't exist, attach to it as if it's an empty OCI image.
		logs.Progress.Println("subject doesn't exist, attaching to empty image")
		desc = &v1.Descriptor{
			ArtifactType: artifactType,
			MediaType:    ocitypes.OCIManifestSchema1,
			Size:         0,
			Digest:       h,
		}
	} else if err != nil {
		return err
	}

	b, err := sbomBytes(sbomRef)
	if err != nil {
		return err
	}

	empty := mutate.MediaType(
		mutate.ConfigMediaType(empty.Image, ocitypes.MediaType(artifactType)),
		ocitypes.OCIManifestSchema1)
	att, err := mutate.AppendLayers(empty, ocistatic.NewLayer(b, sbomType))
	if err != nil {
		return err
	}
	att = mutate.Subject(att, *desc).(v1.Image)
	attdig, err := att.Digest()
	if err != nil {
		return err
	}
	dstRef := ref.Context().Digest(attdig.String())

	fmt.Fprintf(os.Stderr, "Uploading SBOM file for [%s] to [%s] with config.mediaType [%s] layers[0].mediaType [%s].\n",
		ref.Name(), dstRef.String(), artifactType, sbomType)
	return remote.Write(dstRef, att, regOpts.GetRegistryClientOpts(ctx)...)
}

func sbomBytes(sbomRef string) ([]byte, error) {
	// sbomRef can be "-", a string or a file.
	switch signatureType(sbomRef) {
	case StdinSignature:
		return io.ReadAll(os.Stdin)
	case RawSignature:
		return []byte(sbomRef), nil
	case FileSignature:
		return os.ReadFile(filepath.Clean(sbomRef))
	default:
		return nil, errors.New("unknown SBOM arg type")
	}
}
