package policy

import (
	"path"
	"strconv"
	"strings"

	"github.com/distribution/reference"
	"github.com/docker/buildx/util/urlutil"
	slsa1 "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v1"
	"github.com/moby/buildkit/solver/pb"
	"github.com/moby/buildkit/util/gitutil"
	"github.com/moby/buildkit/util/purl"
	"github.com/opencontainers/go-digest"
	ocispecs "github.com/opencontainers/image-spec/specs-go/v1"
	"github.com/pkg/errors"
)

func isMaterialKey(key string) (idx int, rest string, ok bool) {
	const prefix = "image.provenance.materials["
	if !strings.HasPrefix(key, prefix) {
		return 0, "", false
	}
	rest = strings.TrimPrefix(key, prefix)
	end := strings.IndexByte(rest, ']')
	if end < 0 {
		return 0, "", false
	}
	n, err := strconv.Atoi(rest[:end])
	if err != nil {
		return 0, "", false
	}
	rest = strings.TrimPrefix(rest[end+1:], ".")
	return n, rest, true
}

func parseSLSAMaterial(m slsa1.ResourceDescriptor) (*pb.SourceOp, *ocispecs.Platform, error) {
	uri := m.URI
	dgst := m.Digest
	if strings.HasPrefix(uri, "pkg:docker/") {
		return dockerMaterialSource(uri, dgst)
	}

	if gu, err := gitutil.ParseURL(uri); err == nil {
		if strings.HasSuffix(strings.ToLower(gu.Path), ".git") || gu.Scheme != gitutil.HTTPProtocol && gu.Scheme != gitutil.HTTPSProtocol {
			return gitMaterialSource(uri)
		}
	}

	if urlutil.IsHTTPURL(uri) {
		return &pb.SourceOp{Identifier: uri}, nil, nil
	}

	return nil, nil, errors.Errorf("unsupported material URI %q", uri)
}

func dockerMaterialSource(uri string, dgst map[string]string) (*pb.SourceOp, *ocispecs.Platform, error) {
	refStr, platform, err := purl.PURLToRef(uri)
	if err != nil {
		return nil, nil, err
	}

	named, err := reference.ParseNormalizedNamed(refStr)
	if err != nil {
		return nil, nil, errors.Wrapf(err, "invalid docker reference %q from %q", refStr, uri)
	}
	if checksum := strings.TrimSpace(dgst["sha256"]); checksum != "" {
		dgstRef, err := digest.Parse(checksum)
		if err != nil {
			dgstRef, err = digest.Parse("sha256:" + checksum)
		}
		if err != nil {
			return nil, nil, errors.Wrapf(err, "invalid material digest %q for %q", checksum, uri)
		}
		if canonical, ok := named.(reference.Canonical); ok {
			if canonical.Digest() != dgstRef {
				return nil, nil, errors.Errorf("material digest mismatch for %q: ref has %s but provenance has %s", uri, canonical.Digest(), dgstRef)
			}
		} else {
			named, err = reference.WithDigest(named, dgstRef)
			if err != nil {
				return nil, nil, errors.Wrapf(err, "failed to add digest %q to %q", dgstRef, refStr)
			}
		}
	}

	return &pb.SourceOp{Identifier: "docker-image://" + named.String()}, platform, nil
}

func gitMaterialSource(uri string) (*pb.SourceOp, *ocispecs.Platform, error) {
	gu, err := gitutil.ParseURL(uri)
	if err != nil {
		return nil, nil, err
	}

	switch gu.Scheme {
	case gitutil.HTTPSProtocol, gitutil.HTTPProtocol, gitutil.SSHProtocol, gitutil.GitProtocol:
	default:
		return nil, nil, errors.Errorf("unsupported git material URI %q", uri)
	}

	id := gu.Host + path.Join("/", gu.Path)
	if gu.Opts != nil && (gu.Opts.Ref != "" || gu.Opts.Subdir != "") {
		id += "#" + gu.Opts.Ref
		if gu.Opts.Subdir != "" {
			id += ":" + gu.Opts.Subdir
		}
	}

	return &pb.SourceOp{
		Identifier: "git://" + id,
		Attrs: map[string]string{
			pb.AttrFullRemoteURL: uri,
		},
	}, nil, nil
}
