package policy

import (
	"context"
	"slices"
	"strings"

	"github.com/docker/buildx/util/sourcemeta"
	"github.com/moby/buildkit/client/llb/sourceresolver"
	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"
)

type SourceMetadataResolver interface {
	ResolveSourceMetadata(context.Context, *pb.SourceOp, sourceresolver.Opt) (*sourceresolver.MetaResponse, error)
}

func ResolveInputUnknowns(ctx context.Context, input *Input, rootSource *pb.SourceOp, unknowns []string, rootPlatform *pb.Platform, defaultPlatform *ocispecs.Platform, resolver SourceMetadataResolver, verifier PolicyVerifierProvider, logf func(logrus.Level, string)) (bool, *gwpb.ResolveSourceMetaRequest, error) {
	if input == nil || len(unknowns) == 0 {
		return false, nil, nil
	}
	return resolveNodeUnknowns(ctx, input, rootSource, defaultPlatform, normalizeNodeUnknowns(unknowns), rootPlatform, defaultPlatform, resolver, verifier, logf, nil)
}

func resolveNodeUnknowns(ctx context.Context, node *Input, source *pb.SourceOp, nodePlatform *ocispecs.Platform, unknowns []string, rootPlatform *pb.Platform, defaultPlatform *ocispecs.Platform, resolver SourceMetadataResolver, verifier PolicyVerifierProvider, logf func(logrus.Level, string), setNode func(Input) error) (bool, *gwpb.ResolveSourceMetaRequest, error) {
	directUnknowns, childUnknowns := splitNodeUnknowns(unknowns)
	if len(directUnknowns) > 0 {
		req, err := sourceResolveRequest(source, nodePlatform, setNode == nil, directUnknowns, rootPlatform, logf)
		if err != nil {
			return false, nil, err
		}
		if req != nil {
			if setNode == nil {
				return false, req, nil
			}
			if resolver == nil {
				return false, nil, errors.Errorf("material metadata resolution requires source resolver")
			}
			resp, err := resolveSourceMetaWithResolver(ctx, resolver, req, defaultPlatform)
			if err != nil {
				return false, nil, errors.Wrap(err, "failed to resolve source metadata for material")
			}
			nextInput, err := SourceToInput(ctx, verifier, resp, nodePlatform, logf)
			if err != nil {
				return false, nil, errors.Wrap(err, "failed to rebuild material input")
			}
			if err := setNode(nextInput); err != nil {
				return false, nil, err
			}
			return true, nil, nil
		}
	}

	for idx, childNodeUnknowns := range childUnknowns {
		if node == nil || node.Image == nil || node.Image.Provenance == nil {
			continue
		}
		if idx < 0 || idx >= len(node.Image.Provenance.Materials) || idx >= len(node.Image.Provenance.materialsRaw) {
			continue
		}
		raw := node.Image.Provenance.materialsRaw[idx]
		childSource, childNodePlatform, err := parseSLSAMaterial(raw)
		if err != nil {
			continue
		}
		childPlatform := firstNonNilPlatform(childNodePlatform, nodePlatform)
		retry, next, err := resolveNodeUnknowns(ctx, &node.Image.Provenance.Materials[idx], childSource, childPlatform, childNodeUnknowns, rootPlatform, defaultPlatform, resolver, verifier, logf, func(next Input) error {
			node.Image.Provenance.Materials[idx] = next
			return nil
		})
		if err != nil {
			return false, nil, err
		}
		if retry || next != nil {
			return retry, next, nil
		}
	}
	return false, nil, nil
}

func splitNodeUnknowns(unknowns []string) ([]string, map[int][]string) {
	child := map[int][]string{}
	var direct []string
	for _, u := range unknowns {
		if u == "" {
			continue
		}
		if idx, rest, ok := isMaterialKey(u); ok {
			if rest == "" {
				continue
			}
			if !slices.Contains(child[idx], rest) {
				child[idx] = append(child[idx], rest)
			}
			continue
		}
		if !slices.Contains(direct, u) {
			direct = append(direct, u)
		}
	}
	return direct, child
}

func normalizeNodeUnknowns(unknowns []string) []string {
	out := make([]string, 0, len(unknowns))
	for _, u := range unknowns {
		v := strings.TrimPrefix(u, "input.")
		if v == "" {
			continue
		}
		if !slices.Contains(out, v) {
			out = append(out, v)
		}
	}
	return out
}

func sourceResolveRequest(source *pb.SourceOp, nodePlatform *ocispecs.Platform, rootNode bool, fields []string, rootPlatform *pb.Platform, logf func(logrus.Level, string)) (*gwpb.ResolveSourceMetaRequest, error) {
	if source == nil {
		return nil, nil
	}
	req := &gwpb.ResolveSourceMetaRequest{Source: source}
	if nodePlatform != nil {
		req.Platform = &pb.Platform{OS: nodePlatform.OS, Architecture: nodePlatform.Architecture, Variant: nodePlatform.Variant}
	} else if rootNode {
		req.Platform = rootPlatform
	}
	if err := AddUnknownsWithLogger(logf, req, fields); err != nil {
		return nil, err
	}
	if req.Image == nil && req.Git == nil && !hasHTTPUnknowns(fields) {
		return nil, nil
	}
	return req, nil
}

func resolveSourceMetaWithResolver(ctx context.Context, resolver SourceMetadataResolver, req *gwpb.ResolveSourceMetaRequest, defaultPlatform *ocispecs.Platform) (*gwpb.ResolveSourceMetaResponse, error) {
	if resolver == nil {
		return nil, errors.New("source resolver is not configured")
	}
	if req == nil || req.Source == nil {
		return nil, errors.New("source metadata request is missing source")
	}
	resp, err := resolver.ResolveSourceMetadata(ctx, req.Source, sourcemeta.ToResolverOpt(req, defaultPlatform))
	if err != nil {
		return nil, err
	}
	return sourcemeta.ToGatewayMetaResponse(resp), nil
}
