package pack

import (
	"context"
	"fmt"
	"math/rand"
	"net/url"
	"os"
	"path/filepath"
	"runtime"
	"sort"
	"strings"

	"github.com/Masterminds/semver"
	"github.com/buildpacks/imgutil"
	"github.com/buildpacks/imgutil/local"
	"github.com/buildpacks/imgutil/remote"
	"github.com/docker/docker/api/types"
	"github.com/docker/docker/volume/mounts"
	"github.com/google/go-containerregistry/pkg/name"
	"github.com/pkg/errors"
	ignore "github.com/sabhiram/go-gitignore"

	"github.com/buildpacks/pack/config"
	"github.com/buildpacks/pack/internal/blob"
	"github.com/buildpacks/pack/internal/build"
	"github.com/buildpacks/pack/internal/builder"
	"github.com/buildpacks/pack/internal/buildpack"
	"github.com/buildpacks/pack/internal/buildpackage"
	internalConfig "github.com/buildpacks/pack/internal/config"
	"github.com/buildpacks/pack/internal/dist"
	"github.com/buildpacks/pack/internal/layer"
	"github.com/buildpacks/pack/internal/paths"
	"github.com/buildpacks/pack/internal/stack"
	"github.com/buildpacks/pack/internal/stringset"
	"github.com/buildpacks/pack/internal/style"
	"github.com/buildpacks/pack/logging"
	"github.com/buildpacks/pack/pkg/archive"
	"github.com/buildpacks/pack/project"
)

const (
	minLifecycleVersionSupportingCreator = "0.7.4"
	prevLifecycleVersionSupportingImage  = "0.6.1"
	minLifecycleVersionSupportingImage   = "0.7.5"
)

// LifecycleExecutor executes the lifecycle which satisfies the Cloud Native Buildpacks Lifecycle specification.
// Implementations of the Lifecycle must execute the following phases by calling the
// phase-specific lifecycle binary in order:
//
//  Detection:         /cnb/lifecycle/detector
//  Analysis:          /cnb/lifecycle/analyzer
//  Cache Restoration: /cnb/lifecycle/restorer
//  Build:             /cnb/lifecycle/builder
//  Export:            /cnb/lifecycle/exporter
//
// or invoke the single creator binary:
//
//  Creator:            /cnb/lifecycle/creator
//
type LifecycleExecutor interface {
	// Execute is responsible for invoking each of these binaries
	// with the desired configuration.
	Execute(ctx context.Context, opts build.LifecycleOptions) error
}

// BuildOptions defines configuration settings for a Build.
type BuildOptions struct {
	// The base directory to use to resolve relative assets
	RelativeBaseDir string

	// required. Name of output image.
	Image string

	// required. Builder image name.
	Builder string

	// Name of the buildpack registry. Used to
	// add buildpacks to a build.
	Registry string

	// AppPath is the path to application bits.
	// If unset it defaults to current working directory.
	AppPath string

	// Specify the run image the Image will be
	// built atop.
	RunImage string

	// Address of docker daemon exposed to build container
	// e.g. tcp://example.com:1234, unix:///run/user/1000/podman/podman.sock
	DockerHost string

	// Used to determine a run-image mirror if Run Image is empty.
	// Used in combination with Builder metadata to determine to the the 'best' mirror.
	// 'best' is defined as:
	//  - if Publish is true, the best mirror matches registry we are publishing to.
	//  - if Publish is false, the best mirror matches a registry specified in Image.
	//  - otherwise if both of the above did not match, use mirror specified in
	//    the builder metadata
	AdditionalMirrors map[string][]string

	// User provided environment variables to the buildpacks.
	// Buildpacks may both read and overwrite these values.
	Env map[string]string

	// Option only valid if Publish is true
	// Create an additional image that contains cache=true layers and push it to the registry.
	CacheImage string

	// Option passed directly to the lifecycle.
	// If true, publishes Image directly to a registry.
	// Assumes Image contains a valid registry with credentials
	// provided by the docker client.
	Publish bool

	// Clear the build cache from previous builds.
	ClearCache bool

	// TrustBuilder when true optimizes builds by running
	// all lifecycle phases in a single container.
	// This places registry credentials on the builder's build image.
	// Only trust builders from reputable sources.
	TrustBuilder bool

	// List of buildpack images or archives to add to a builder.
	// These buildpacks may overwrite those on the builder if they
	// share both an ID and Version with a buildpack on the builder.
	Buildpacks []string

	// Additional image tags to push to, each will contain contents identical to Image
	AdditionalTags []string

	// Configure the proxy environment variables,
	// These variables will only be set in the build image
	// and will not be used if proxy env vars are already set.
	ProxyConfig *ProxyConfig

	// Configure network and volume mounts for the build containers.
	ContainerConfig ContainerConfig

	// Process type that will be used when setting container start command.
	DefaultProcessType string

	// Strategy for updating local images before a build.
	PullPolicy config.PullPolicy

	// ProjectDescriptorBaseDir is the base directory to find relative resources referenced by the ProjectDescriptor
	ProjectDescriptorBaseDir string

	// ProjectDescriptor describes the project and any configuration specific to the project
	ProjectDescriptor project.Descriptor

	// The lifecycle image that will be used for the analysis, restore and export phases
	// when using an untrusted builder.
	LifecycleImage string
}

// ProxyConfig specifies proxy setting to be set as environment variables in a container.
type ProxyConfig struct {
	HTTPProxy  string // Used to set HTTP_PROXY env var.
	HTTPSProxy string // Used to set HTTPS_PROXY env var.
	NoProxy    string // Used to set NO_PROXY env var.
}

// ContainerConfig is additional configuration of the docker container that all build steps
// occur within.
type ContainerConfig struct {
	// Configure network settings of the build containers.
	// The value of Network is handed directly to the docker client.
	// For valid values of this field see:
	// https://docs.docker.com/network/#network-drivers
	Network string

	// Volumes are accessible during both detect build phases
	// should have the form: /path/in/host:/path/in/container.
	// For more about volume mounts, and their permissions see:
	// https://docs.docker.com/storage/volumes/
	//
	// It is strongly recommended you do not override any of the
	// paths with volume mounts at the following locations:
	// - /cnb
	// - /layers
	// - anything below /cnb/**
	Volumes []string
}

// Build configures settings for the build container(s) and lifecycle.
// It then invokes the lifecycle to build an app image.
// If any configuration is deemed invalid, or if any lifecycle phases fail,
// an error will be returned and no image produced.
func (c *Client) Build(ctx context.Context, opts BuildOptions) error {
	imageRef, err := c.parseTagReference(opts.Image)
	if err != nil {
		return errors.Wrapf(err, "invalid image name '%s'", opts.Image)
	}

	appPath, err := c.processAppPath(opts.AppPath)
	if err != nil {
		return errors.Wrapf(err, "invalid app path '%s'", opts.AppPath)
	}

	proxyConfig := c.processProxyConfig(opts.ProxyConfig)

	builderRef, err := c.processBuilderName(opts.Builder)
	if err != nil {
		return errors.Wrapf(err, "invalid builder '%s'", opts.Builder)
	}

	rawBuilderImage, err := c.imageFetcher.Fetch(ctx, builderRef.Name(), true, opts.PullPolicy)
	if err != nil {
		return errors.Wrapf(err, "failed to fetch builder image '%s'", builderRef.Name())
	}

	bldr, err := c.getBuilder(rawBuilderImage)
	if err != nil {
		return errors.Wrapf(err, "invalid builder %s", style.Symbol(opts.Builder))
	}

	runImageName := c.resolveRunImage(opts.RunImage, imageRef.Context().RegistryStr(), builderRef.Context().RegistryStr(), bldr.Stack(), opts.AdditionalMirrors, opts.Publish)
	runImage, err := c.validateRunImage(ctx, runImageName, opts.PullPolicy, opts.Publish, bldr.StackID)
	if err != nil {
		return errors.Wrapf(err, "invalid run-image '%s'", runImageName)
	}

	var runMixins []string
	if _, err := dist.GetLabel(runImage, stack.MixinsLabel, &runMixins); err != nil {
		return err
	}

	fetchedBPs, order, err := c.processBuildpacks(ctx, bldr.Image(), bldr.Buildpacks(), bldr.Order(), opts)
	if err != nil {
		return err
	}

	if err := c.validateMixins(fetchedBPs, bldr, runImageName, runMixins); err != nil {
		return errors.Wrap(err, "validating stack mixins")
	}

	buildEnvs := map[string]string{}
	for _, envVar := range opts.ProjectDescriptor.Build.Env {
		buildEnvs[envVar.Name] = envVar.Value
	}

	for k, v := range opts.Env {
		buildEnvs[k] = v
	}

	ephemeralBuilder, err := c.createEphemeralBuilder(rawBuilderImage, buildEnvs, order, fetchedBPs)
	if err != nil {
		return err
	}
	defer c.docker.ImageRemove(context.Background(), ephemeralBuilder.Name(), types.ImageRemoveOptions{Force: true})

	builderPlatformAPIs := append(
		ephemeralBuilder.LifecycleDescriptor().APIs.Platform.Deprecated,
		ephemeralBuilder.LifecycleDescriptor().APIs.Platform.Supported...,
	)

	if !supportsPlatformAPI(builderPlatformAPIs) {
		c.logger.Debugf("pack %s supports Platform API(s): %s", Version, strings.Join(build.SupportedPlatformAPIVersions.AsStrings(), ", "))
		c.logger.Debugf("Builder %s supports Platform API(s): %s", style.Symbol(opts.Builder), strings.Join(builderPlatformAPIs.AsStrings(), ", "))
		return errors.Errorf("Builder %s is incompatible with this version of pack", style.Symbol(opts.Builder))
	}

	imgOS, err := rawBuilderImage.OS()
	if err != nil {
		return errors.Wrapf(err, "getting builder OS")
	}

	processedVolumes, warnings, err := processVolumes(imgOS, opts.ContainerConfig.Volumes)
	if err != nil {
		return err
	}

	for _, warning := range warnings {
		c.logger.Warn(warning)
	}

	fileFilter, err := getFileFilter(opts.ProjectDescriptor)
	if err != nil {
		return err
	}

	lifecycleOpts := build.LifecycleOptions{
		AppPath:            appPath,
		Image:              imageRef,
		Builder:            ephemeralBuilder,
		RunImage:           runImageName,
		ClearCache:         opts.ClearCache,
		Publish:            opts.Publish,
		DockerHost:         opts.DockerHost,
		UseCreator:         false,
		TrustBuilder:       opts.TrustBuilder,
		LifecycleImage:     ephemeralBuilder.Name(),
		HTTPProxy:          proxyConfig.HTTPProxy,
		HTTPSProxy:         proxyConfig.HTTPSProxy,
		NoProxy:            proxyConfig.NoProxy,
		Network:            opts.ContainerConfig.Network,
		AdditionalTags:     opts.AdditionalTags,
		Volumes:            processedVolumes,
		DefaultProcessType: opts.DefaultProcessType,
		FileFilter:         fileFilter,
		CacheImage:         opts.CacheImage,
	}

	lifecycleVersion := ephemeralBuilder.LifecycleDescriptor().Info.Version
	// Technically the creator is supported as of platform API version 0.3 (lifecycle version 0.7.0+) but earlier versions
	// have bugs that make using the creator problematic.
	lifecycleSupportsCreator := !lifecycleVersion.LessThan(semver.MustParse(minLifecycleVersionSupportingCreator))

	if lifecycleSupportsCreator && opts.TrustBuilder {
		lifecycleOpts.UseCreator = true
		// no need to fetch a lifecycle image, it won't be used
		if err := c.lifecycleExecutor.Execute(ctx, lifecycleOpts); err != nil {
			return errors.Wrap(err, "executing lifecycle")
		}

		return c.logImageNameAndSha(ctx, opts.Publish, imageRef)
	}

	if !opts.TrustBuilder {
		if lifecycleImageSupported(imgOS, lifecycleVersion) {
			lifecycleImageName := opts.LifecycleImage
			if lifecycleImageName == "" {
				lifecycleImageName = fmt.Sprintf("%s:%s", internalConfig.DefaultLifecycleImageRepo, lifecycleVersion.String())
			}
			lifecycleImage, err := c.imageFetcher.Fetch(
				ctx,
				lifecycleImageName,
				true,
				opts.PullPolicy,
			)
			if err != nil {
				return errors.Wrap(err, "fetching lifecycle image")
			}

			lifecycleOpts.LifecycleImage = lifecycleImage.Name()
		} else {
			return errors.Errorf("Lifecycle %s does not have an associated lifecycle image. Builder must be trusted.", lifecycleVersion.String())
		}
	}

	if err := c.lifecycleExecutor.Execute(ctx, lifecycleOpts); err != nil {
		return errors.Wrap(err, "executing lifecycle. This may be the result of using an untrusted builder")
	}

	return c.logImageNameAndSha(ctx, opts.Publish, imageRef)
}

func getFileFilter(descriptor project.Descriptor) (func(string) bool, error) {
	if len(descriptor.Build.Exclude) > 0 {
		excludes := ignore.CompileIgnoreLines(descriptor.Build.Exclude...)
		return func(fileName string) bool {
			return !excludes.MatchesPath(fileName)
		}, nil
	}
	if len(descriptor.Build.Include) > 0 {
		includes := ignore.CompileIgnoreLines(descriptor.Build.Include...)
		return includes.MatchesPath, nil
	}

	return nil, nil
}

func lifecycleImageSupported(builderOS string, lifecycleVersion *builder.Version) bool {
	return lifecycleVersion.Equal(builder.VersionMustParse(prevLifecycleVersionSupportingImage)) ||
		!lifecycleVersion.LessThan(semver.MustParse(minLifecycleVersionSupportingImage))
}

// supportsPlatformAPI determines whether pack can build using the builder based on the builder's supported Platform API versions.
func supportsPlatformAPI(builderPlatformAPIs builder.APISet) bool {
	for _, packSupportedAPI := range build.SupportedPlatformAPIVersions {
		for _, builderSupportedAPI := range builderPlatformAPIs {
			supportsPlatform := packSupportedAPI.Compare(builderSupportedAPI) == 0
			if supportsPlatform {
				return true
			}
		}
	}

	return false
}

func (c *Client) processBuilderName(builderName string) (name.Reference, error) {
	if builderName == "" {
		return nil, errors.New("builder is a required parameter if the client has no default builder")
	}
	return name.ParseReference(builderName, name.WeakValidation)
}

func (c *Client) getBuilder(img imgutil.Image) (*builder.Builder, error) {
	bldr, err := builder.FromImage(img)
	if err != nil {
		return nil, err
	}
	if bldr.Stack().RunImage.Image == "" {
		return nil, errors.New("builder metadata is missing run-image")
	}

	lifecycleDescriptor := bldr.LifecycleDescriptor()
	if lifecycleDescriptor.Info.Version == nil {
		return nil, errors.New("lifecycle version must be specified in builder")
	}
	if len(lifecycleDescriptor.APIs.Buildpack.Supported) == 0 {
		return nil, errors.New("supported Lifecycle Buildpack APIs not specified")
	}
	if len(lifecycleDescriptor.APIs.Platform.Supported) == 0 {
		return nil, errors.New("supported Lifecycle Platform APIs not specified")
	}

	return bldr, nil
}

func (c *Client) validateRunImage(context context.Context, name string, pullPolicy config.PullPolicy, publish bool, expectedStack string) (imgutil.Image, error) {
	if name == "" {
		return nil, errors.New("run image must be specified")
	}
	img, err := c.imageFetcher.Fetch(context, name, !publish, pullPolicy)
	if err != nil {
		return nil, err
	}
	stackID, err := img.Label("io.buildpacks.stack.id")
	if err != nil {
		return nil, err
	}
	if stackID != expectedStack {
		return nil, fmt.Errorf("run-image stack id '%s' does not match builder stack '%s'", stackID, expectedStack)
	}
	return img, nil
}

func (c *Client) validateMixins(additionalBuildpacks []dist.Buildpack, bldr *builder.Builder, runImageName string, runMixins []string) error {
	if err := stack.ValidateMixins(bldr.Image().Name(), bldr.Mixins(), runImageName, runMixins); err != nil {
		return err
	}

	bps, err := allBuildpacks(bldr.Image(), additionalBuildpacks)
	if err != nil {
		return err
	}
	mixins := assembleAvailableMixins(bldr.Mixins(), runMixins)

	for _, bp := range bps {
		if err := bp.EnsureStackSupport(bldr.StackID, mixins, true); err != nil {
			return err
		}
	}
	return nil
}

// assembleAvailableMixins returns the set of mixins that are common between the two provided sets, plus build-only mixins and run-only mixins.
func assembleAvailableMixins(buildMixins, runMixins []string) []string {
	// NOTE: We cannot simply union the two mixin sets, as this could introduce a mixin that is only present on one stack
	// image but not the other. A buildpack that happens to require the mixin would fail to run properly, even though validation
	// would pass.
	//
	// For example:
	//
	//  Incorrect:
	//    Run image mixins:   [A, B]
	//    Build image mixins: [A]
	//    Merged: [A, B]
	//    Buildpack requires: [A, B]
	//    Match? Yes
	//
	//  Correct:
	//    Run image mixins:   [A, B]
	//    Build image mixins: [A]
	//    Merged: [A]
	//    Buildpack requires: [A, B]
	//    Match? No

	buildOnly := stack.FindStageMixins(buildMixins, "build")
	runOnly := stack.FindStageMixins(runMixins, "run")
	_, _, common := stringset.Compare(buildMixins, runMixins)

	return append(common, append(buildOnly, runOnly...)...)
}

// allBuildpacks aggregates all buildpacks declared on the image with additional buildpacks passed in. They are sorted
// by ID then Version.
func allBuildpacks(builderImage imgutil.Image, additionalBuildpacks []dist.Buildpack) ([]dist.BuildpackDescriptor, error) {
	var all []dist.BuildpackDescriptor
	var bpLayers dist.BuildpackLayers
	if _, err := dist.GetLabel(builderImage, dist.BuildpackLayersLabel, &bpLayers); err != nil {
		return nil, err
	}
	for id, bps := range bpLayers {
		for ver, bp := range bps {
			desc := dist.BuildpackDescriptor{
				Info: dist.BuildpackInfo{
					ID:      id,
					Version: ver,
				},
				Stacks: bp.Stacks,
				Order:  bp.Order,
			}
			all = append(all, desc)
		}
	}
	for _, bp := range additionalBuildpacks {
		all = append(all, bp.Descriptor())
	}

	sort.Slice(all, func(i, j int) bool {
		if all[i].Info.ID != all[j].Info.ID {
			return all[i].Info.ID < all[j].Info.ID
		}
		return all[i].Info.Version < all[j].Info.Version
	})

	return all, nil
}

func (c *Client) processAppPath(appPath string) (string, error) {
	var (
		resolvedAppPath string
		err             error
	)

	if appPath == "" {
		if appPath, err = os.Getwd(); err != nil {
			return "", errors.Wrap(err, "get working dir")
		}
	}

	if resolvedAppPath, err = filepath.EvalSymlinks(appPath); err != nil {
		return "", errors.Wrap(err, "evaluate symlink")
	}

	if resolvedAppPath, err = filepath.Abs(resolvedAppPath); err != nil {
		return "", errors.Wrap(err, "resolve absolute path")
	}

	fi, err := os.Stat(resolvedAppPath)
	if err != nil {
		return "", errors.Wrap(err, "stat file")
	}

	if !fi.IsDir() {
		fh, err := os.Open(resolvedAppPath)
		if err != nil {
			return "", errors.Wrap(err, "read file")
		}
		defer fh.Close()

		isZip, err := archive.IsZip(fh)
		if err != nil {
			return "", errors.Wrap(err, "check zip")
		}

		if !isZip {
			return "", errors.New("app path must be a directory or zip")
		}
	}

	return resolvedAppPath, nil
}

func (c *Client) processProxyConfig(config *ProxyConfig) ProxyConfig {
	var (
		httpProxy, httpsProxy, noProxy string
		ok                             bool
	)
	if config != nil {
		return *config
	}
	if httpProxy, ok = os.LookupEnv("HTTP_PROXY"); !ok {
		httpProxy = os.Getenv("http_proxy")
	}
	if httpsProxy, ok = os.LookupEnv("HTTPS_PROXY"); !ok {
		httpsProxy = os.Getenv("https_proxy")
	}
	if noProxy, ok = os.LookupEnv("NO_PROXY"); !ok {
		noProxy = os.Getenv("no_proxy")
	}
	return ProxyConfig{
		HTTPProxy:  httpProxy,
		HTTPSProxy: httpsProxy,
		NoProxy:    noProxy,
	}
}

// processBuildpacks computes an order group based on the existing builder order and declared buildpacks. Additionally,
// it returns buildpacks that should be added to the builder.
//
// Visual examples:
//
// 	BUILDER ORDER
// 	----------
//  - group:
//		- A
//		- B
//  - group:
//		- A
//
//	WITH DECLARED: "from=builder", X
// 	----------
// 	- group:
//		- A
//		- B
//		- X
// 	 - group:
//		- A
//		- X
//
//	WITH DECLARED: X, "from=builder", Y
// 	----------
// 	- group:
//		- X
//		- A
//		- B
//      - Y
// 	- group:
//		- X
//		- A
//      - Y
//
//	WITH DECLARED: X
// 	----------
//	- group:
//		- X
//
//	WITH DECLARED: A
// 	----------
// 	- group:
//		- A
func (c *Client) processBuildpacks(ctx context.Context, builderImage imgutil.Image, builderBPs []dist.BuildpackInfo, builderOrder dist.Order, opts BuildOptions) (fetchedBPs []dist.Buildpack, order dist.Order, err error) {
	pullPolicy := opts.PullPolicy
	publish := opts.Publish
	registry := opts.Registry
	relativeBaseDir := opts.RelativeBaseDir
	declaredBPs := opts.Buildpacks

	// declare buildpacks provided by project descriptor when no buildpacks are declared
	if len(declaredBPs) == 0 && len(opts.ProjectDescriptor.Build.Buildpacks) != 0 {
		relativeBaseDir = opts.ProjectDescriptorBaseDir

		for _, bp := range opts.ProjectDescriptor.Build.Buildpacks {
			if bp.URI == "" {
				declaredBPs = append(declaredBPs, fmt.Sprintf("%s@%s", bp.ID, bp.Version))
			} else {
				declaredBPs = append(declaredBPs, bp.URI)
			}
		}
	}

	order = dist.Order{{Group: []dist.BuildpackRef{}}}
	for _, bp := range declaredBPs {
		locatorType, err := buildpack.GetLocatorType(bp, relativeBaseDir, builderBPs)
		if err != nil {
			return nil, nil, err
		}

		switch locatorType {
		case buildpack.FromBuilderLocator:
			switch {
			case len(order) == 0 || len(order[0].Group) == 0:
				order = builderOrder
			case len(order) > 1:
				// This should only ever be possible if they are using from=builder twice which we don't allow
				return nil, nil, errors.New("buildpacks from builder can only be defined once")
			default:
				newOrder := dist.Order{}
				groupToAdd := order[0].Group
				for _, bOrderEntry := range builderOrder {
					newEntry := dist.OrderEntry{Group: append(groupToAdd, bOrderEntry.Group...)}
					newOrder = append(newOrder, newEntry)
				}

				order = newOrder
			}
		case buildpack.IDLocator:
			id, version := buildpack.ParseIDLocator(bp)
			order = appendBuildpackToOrder(order, dist.BuildpackInfo{
				ID:      id,
				Version: version,
			})
		case buildpack.URILocator:
			bp, err = paths.FilePathToURI(bp, relativeBaseDir)
			if err != nil {
				return fetchedBPs, order, errors.Wrapf(err, "making absolute: %s", style.Symbol(bp))
			}

			c.logger.Debugf("Downloading buildpack from URI: %s", style.Symbol(bp))

			err := ensureBPSupport(bp)
			if err != nil {
				return fetchedBPs, order, errors.Wrapf(err, "checking support")
			}

			blob, err := c.downloader.Download(ctx, bp)
			if err != nil {
				return fetchedBPs, order, errors.Wrapf(err, "downloading buildpack from %s", style.Symbol(bp))
			}

			imageOS, err := builderImage.OS()
			if err != nil {
				return fetchedBPs, order, errors.Wrapf(err, "getting OS from %s", style.Symbol(builderImage.Name()))
			}

			mainBP, depBPs, err := decomposeBuildpack(blob, imageOS)
			if err != nil {
				return fetchedBPs, order, errors.Wrapf(err, "extracting from %s", style.Symbol(bp))
			}

			fetchedBPs = append(append(fetchedBPs, mainBP), depBPs...)
			order = appendBuildpackToOrder(order, mainBP.Descriptor().Info)
		case buildpack.PackageLocator:
			imageName := buildpack.ParsePackageLocator(bp)
			mainBP, depBPs, err := extractPackagedBuildpacks(ctx, imageName, c.imageFetcher, publish, pullPolicy)
			if err != nil {
				return fetchedBPs, order, errors.Wrapf(err, "creating from buildpackage %s", style.Symbol(bp))
			}

			fetchedBPs = append(append(fetchedBPs, mainBP), depBPs...)
			order = appendBuildpackToOrder(order, mainBP.Descriptor().Info)
		case buildpack.RegistryLocator:
			registryCache, err := c.getRegistry(c.logger, registry)
			if err != nil {
				return fetchedBPs, order, errors.Wrapf(err, "invalid registry '%s'", registry)
			}

			registryBp, err := registryCache.LocateBuildpack(bp)
			if err != nil {
				return fetchedBPs, order, errors.Wrapf(err, "locating in registry %s", style.Symbol(bp))
			}

			mainBP, depBPs, err := extractPackagedBuildpacks(ctx, registryBp.Address, c.imageFetcher, publish, pullPolicy)
			if err != nil {
				return fetchedBPs, order, errors.Wrapf(err, "extracting from registry %s", style.Symbol(bp))
			}

			fetchedBPs = append(append(fetchedBPs, mainBP), depBPs...)
			order = appendBuildpackToOrder(order, mainBP.Descriptor().Info)
		default:
			return nil, nil, fmt.Errorf("invalid buildpack string %s", style.Symbol(bp))
		}
	}

	return fetchedBPs, order, nil
}

func appendBuildpackToOrder(order dist.Order, bpInfo dist.BuildpackInfo) (newOrder dist.Order) {
	for _, orderEntry := range order {
		newEntry := orderEntry
		newEntry.Group = append(newEntry.Group, dist.BuildpackRef{
			BuildpackInfo: bpInfo,
			Optional:      false,
		})
		newOrder = append(newOrder, newEntry)
	}

	return newOrder
}

// decomposeBuildpack decomposes a buildpack blob into the main builder (order buildpack) and it's dependencies buildpacks.
func decomposeBuildpack(blob blob.Blob, imageOS string) (mainBP dist.Buildpack, depBPs []dist.Buildpack, err error) {
	isOCILayout, err := buildpackage.IsOCILayoutBlob(blob)
	if err != nil {
		return mainBP, depBPs, errors.Wrap(err, "inspecting buildpack blob")
	}

	if isOCILayout {
		mainBP, depBPs, err = buildpackage.BuildpacksFromOCILayoutBlob(blob)
		if err != nil {
			return mainBP, depBPs, errors.Wrap(err, "extracting buildpacks")
		}
	} else {
		layerWriterFactory, err := layer.NewWriterFactory(imageOS)
		if err != nil {
			return mainBP, depBPs, errors.Wrapf(err, "get tar writer factory for OS %s", style.Symbol(imageOS))
		}

		mainBP, err = dist.BuildpackFromRootBlob(blob, layerWriterFactory)
		if err != nil {
			return mainBP, depBPs, errors.Wrap(err, "reading buildpack")
		}
	}

	return mainBP, depBPs, nil
}

func ensureBPSupport(bpPath string) (err error) {
	p := bpPath
	if paths.IsURI(bpPath) {
		var u *url.URL
		u, err = url.Parse(bpPath)
		if err != nil {
			return err
		}

		if u.Scheme == "file" {
			p, err = paths.URIToFilePath(bpPath)
			if err != nil {
				return err
			}
		}
	}

	if runtime.GOOS == "windows" && !paths.IsURI(p) {
		isDir, err := paths.IsDir(p)
		if err != nil {
			return err
		}

		if isDir {
			return fmt.Errorf("buildpack %s: directory-based buildpacks are not currently supported on Windows", style.Symbol(bpPath))
		}
	}

	return nil
}

func (c *Client) createEphemeralBuilder(rawBuilderImage imgutil.Image, env map[string]string, order dist.Order, buildpacks []dist.Buildpack) (*builder.Builder, error) {
	origBuilderName := rawBuilderImage.Name()
	bldr, err := builder.New(rawBuilderImage, fmt.Sprintf("pack.local/builder/%x:latest", randString(10)))
	if err != nil {
		return nil, errors.Wrapf(err, "invalid builder %s", style.Symbol(origBuilderName))
	}

	bldr.SetEnv(env)
	for _, bp := range buildpacks {
		bpInfo := bp.Descriptor().Info
		c.logger.Debugf("Adding buildpack %s version %s to builder", style.Symbol(bpInfo.ID), style.Symbol(bpInfo.Version))
		bldr.AddBuildpack(bp)
	}
	if len(order) > 0 && len(order[0].Group) > 0 {
		c.logger.Debug("Setting custom order")
		bldr.SetOrder(order)
	}

	if err := bldr.Save(c.logger, builder.CreatorMetadata{Version: Version}); err != nil {
		return nil, err
	}
	return bldr, nil
}

func randString(n int) string {
	b := make([]byte, n)
	for i := range b {
		b[i] = 'a' + byte(rand.Intn(26))
	}
	return string(b)
}

func processVolumes(imgOS string, volumes []string) (processed []string, warnings []string, err error) {
	parserOS := mounts.OSLinux
	if imgOS == "windows" {
		parserOS = mounts.OSWindows
	}
	parser := mounts.NewParser(parserOS)
	for _, v := range volumes {
		volume, err := parser.ParseMountRaw(v, "")
		if err != nil {
			return nil, nil, errors.Wrapf(err, "platform volume %q has invalid format", v)
		}

		sensitiveDirs := []string{"/cnb", "/layers"}
		if imgOS == "windows" {
			sensitiveDirs = []string{`c:/cnb`, `c:\cnb`, `c:/layers`, `c:\layers`}
		}
		for _, p := range sensitiveDirs {
			if strings.HasPrefix(strings.ToLower(volume.Spec.Target), p) {
				warnings = append(warnings, fmt.Sprintf("Mounting to a sensitive directory %s", style.Symbol(volume.Spec.Target)))
			}
		}

		processed = append(processed, fmt.Sprintf("%s:%s:%s", volume.Spec.Source, volume.Spec.Target, processMode(volume.Mode)))
	}
	return processed, warnings, nil
}

func processMode(mode string) string {
	if mode == "" {
		return "ro"
	}

	return mode
}

func (c *Client) logImageNameAndSha(ctx context.Context, publish bool, imageRef name.Reference) error {
	// The image name and sha are printed in the lifecycle logs, and there is no need to print it again, unless output is suppressed.
	if !logging.IsQuiet(c.logger) {
		return nil
	}

	img, err := c.imageFetcher.Fetch(ctx, imageRef.Name(), !publish, config.PullNever)
	if err != nil {
		return errors.Wrap(err, "fetching built image")
	}

	id, err := img.Identifier()
	if err != nil {
		return errors.Wrap(err, "reading image sha")
	}

	// Remove tag, if it exists, from the image name
	imgName := strings.TrimSuffix(imageRef.String(), imageRef.Identifier())
	imgNameAndSha := fmt.Sprintf("%s@%s\n", imgName, parseDigestFromImageID(id))

	// Access the logger's Writer directly to bypass ReportSuccessfulQuietBuild mode
	_, err = c.logger.Writer().Write([]byte(imgNameAndSha))
	return err
}

func parseDigestFromImageID(id imgutil.Identifier) string {
	var digest string
	switch v := id.(type) {
	case local.IDIdentifier:
		digest = v.String()
	case remote.DigestIdentifier:
		digest = v.Digest.DigestStr()
	}

	digest = strings.TrimPrefix(digest, "sha256:")
	return fmt.Sprintf("sha256:%s", digest)
}
