package policy

import (
	"context"

	"github.com/containerd/platforms"
	gwpb "github.com/moby/buildkit/frontend/gateway/pb"
	"github.com/moby/buildkit/solver/pb"
	ocispecs "github.com/opencontainers/image-spec/specs-go/v1"
	"github.com/pkg/errors"
	"github.com/sirupsen/logrus"
)

const maxMaterialDepth = 24

func SourceToInput(ctx context.Context, verifier PolicyVerifierProvider, src *gwpb.ResolveSourceMetaResponse, platform *ocispecs.Platform, logf func(logrus.Level, string)) (Input, error) {
	seen := map[string]struct{}{}
	return sourceToInputRecursive(ctx, verifier, src, platform, 0, seen, logf)
}

func sourceToInputRecursive(ctx context.Context, verifier PolicyVerifierProvider, src *gwpb.ResolveSourceMetaResponse, platform *ocispecs.Platform, depth int, seen map[string]struct{}, logf func(logrus.Level, string)) (Input, error) {
	if depth > maxMaterialDepth {
		return Input{}, errors.Errorf("provenance materials depth exceeds limit %d", maxMaterialDepth)
	}
	if src == nil || src.Source == nil {
		return Input{}, errors.New("source metadata response is required")
	}

	inp, unknowns, err := sourceToInput(ctx, verifier, src, platform, logf)
	if err != nil {
		return Input{}, err
	}
	inp.setUnknowns(unknowns)
	inp.Env.Depth = depth

	if inp.Image == nil || inp.Image.Provenance == nil || len(inp.Image.Provenance.materialsRaw) == 0 {
		return inp, nil
	}

	key := sourceUniqueIdentifier(src.Source, platform)
	if _, ok := seen[key]; ok {
		return Input{}, nil
	}
	seen[key] = struct{}{}
	defer delete(seen, key)

	materials := make([]Input, 0, len(inp.Image.Provenance.materialsRaw))
	for _, m := range inp.Image.Provenance.materialsRaw {
		matSrc, matPlatform, err := parseSLSAMaterial(m)
		if err != nil {
			materials = append(materials, Input{})
			continue
		}
		if matSrc == nil {
			materials = append(materials, Input{})
			continue
		}
		matResp := &gwpb.ResolveSourceMetaResponse{Source: matSrc}
		child, err := sourceToInputRecursive(ctx, verifier, matResp, firstNonNilPlatform(matPlatform, platform), depth+1, seen, logf)
		if err != nil {
			return Input{}, errors.Wrapf(err, "failed to build material input for %q", m.URI)
		}
		materials = append(materials, child)
	}
	inp.Image.Provenance.Materials = materials
	return inp, nil
}

// sourceUniqueIdentifier is used only for recursive cycle detection safety.
func sourceUniqueIdentifier(src *pb.SourceOp, platform *ocispecs.Platform) string {
	if src == nil {
		return ""
	}
	key := src.Identifier
	if platform != nil {
		key += "|" + platforms.Format(*platform)
	}
	return key
}

func firstNonNilPlatform(values ...*ocispecs.Platform) *ocispecs.Platform {
	for _, v := range values {
		if v != nil {
			return v
		}
	}
	return nil
}
