package platform

import (
	"fmt"
	"os"
	"path/filepath"
	"slices"
	"strconv"
	"strings"
	"time"

	"github.com/buildpacks/imgutil/remote"
	"github.com/google/go-containerregistry/pkg/authn"

	"github.com/buildpacks/lifecycle/api"
	"github.com/buildpacks/lifecycle/internal/str"
	"github.com/buildpacks/lifecycle/log"
)

// LifecycleInputs holds the values of command-line flags and args i.e., platform inputs to the lifecycle.
// Fields are the cumulative total of inputs across all lifecycle phases and all supported Platform APIs.
type LifecycleInputs struct {
	PlatformAPI           *api.Version
	AnalyzedPath          string
	AppDir                string
	BuildConfigDir        string
	BuildImageRef         string
	BuildpacksDir         string
	CacheDir              string
	CacheImageRef         string
	DefaultProcessType    string
	DeprecatedRunImageRef string
	ExecEnv               string
	ExtendKind            string
	ExtendedDir           string
	ExtensionsDir         string
	GeneratedDir          string
	GroupPath             string
	KanikoDir             string
	LaunchCacheDir        string
	LauncherPath          string
	LauncherSBOMDir       string
	LayersDir             string
	LayoutDir             string
	LogLevel              string
	OrderPath             string
	OutputImageRef        string
	PlanPath              string
	PlatformDir           string
	PreviousImageRef      string
	ProjectMetadataPath   string
	ReportPath            string
	RunImageRef           string
	RunPath               string
	StackPath             string
	SystemPath            string
	UID                   int
	GID                   int
	ForceRebase           bool
	NoColor               bool
	ParallelExport        bool
	SkipLayers            bool
	UseDaemon             bool
	UseLayout             bool
	AdditionalTags        str.Slice // str.Slice satisfies the `Value` interface required by the `flag` package
	KanikoCacheTTL        time.Duration
	InsecureRegistries    str.Slice
}

const PlaceholderLayers = "<layers>"

// NewLifecycleInputs constructs new lifecycle inputs for the provided Platform API version.
// Inputs can be specified by the platform (in order of precedence) through:
//   - command-line flags
//   - environment variables
//   - falling back to the default value
//
// NewLifecycleInputs provides, for each input, the value from the environment if specified, falling back to the default.
// As the final value of the layers directory (if provided via the command-line) is not known,
// inputs that default to a child of the layers directory are provided with PlaceholderLayers as the layers directory.
// To be valid, inputs obtained from calling NewLifecycleInputs MUST be updated using UpdatePlaceholderPaths
// once the final value of the layers directory is known.
func NewLifecycleInputs(platformAPI *api.Version) *LifecycleInputs {
	// FIXME: api compatibility should be validated here

	var skipLayers bool
	if boolEnv(EnvSkipLayers) || boolEnv(EnvSkipRestore) {
		skipLayers = true
	}

	inputs := &LifecycleInputs{
		// Operator config

		LogLevel:           envOrDefault(EnvLogLevel, DefaultLogLevel),
		NoColor:            boolEnv(EnvNoColor),
		PlatformAPI:        platformAPI,
		ExtendKind:         envOrDefault(EnvExtendKind, DefaultExtendKind),
		UseDaemon:          boolEnv(EnvUseDaemon),
		InsecureRegistries: sliceEnv(EnvInsecureRegistries),
		UseLayout:          boolEnv(EnvUseLayout),

		// Provided by the base image

		UID: intEnv(EnvUID),
		GID: intEnv(EnvGID),

		// Provided by the builder image

		BuildConfigDir: envOrDefault(EnvBuildConfigDir, DefaultBuildConfigDir),
		BuildpacksDir:  envOrDefault(EnvBuildpacksDir, DefaultBuildpacksDir),
		ExtensionsDir:  envOrDefault(EnvExtensionsDir, DefaultExtensionsDir),
		RunPath:        envOrDefault(EnvRunPath, DefaultRunPath),
		StackPath:      envOrDefault(EnvStackPath, DefaultStackPath),
		SystemPath:     envOrDefault(EnvSystemPath, CNBSystemPath),

		// Provided at build time

		AppDir:      envOrDefault(EnvAppDir, DefaultAppDir),
		ExecEnv:     envOrDefault(EnvExecEnv, DefaultExecEnv),
		LayersDir:   envOrDefault(EnvLayersDir, DefaultLayersDir),
		LayoutDir:   os.Getenv(EnvLayoutDir),
		OrderPath:   envOrDefault(EnvOrderPath, filepath.Join(PlaceholderLayers, DefaultOrderFile)),
		PlatformDir: envOrDefault(EnvPlatformDir, DefaultPlatformDir),

		// The following instruct the lifecycle where to write files and data during the build

		AnalyzedPath: envOrDefault(EnvAnalyzedPath, filepath.Join(PlaceholderLayers, DefaultAnalyzedFile)),
		ExtendedDir:  envOrDefault(EnvExtendedDir, filepath.Join(PlaceholderLayers, DefaultExtendedDir)),
		GeneratedDir: envOrDefault(EnvGeneratedDir, filepath.Join(PlaceholderLayers, DefaultGeneratedDir)),
		GroupPath:    envOrDefault(EnvGroupPath, filepath.Join(PlaceholderLayers, DefaultGroupFile)),
		PlanPath:     envOrDefault(EnvPlanPath, filepath.Join(PlaceholderLayers, DefaultPlanFile)),
		ReportPath:   envOrDefault(EnvReportPath, filepath.Join(PlaceholderLayers, DefaultReportFile)),

		// Configuration options with respect to caching

		CacheDir:       os.Getenv(EnvCacheDir),
		CacheImageRef:  os.Getenv(EnvCacheImage),
		KanikoCacheTTL: timeEnvOrDefault(EnvKanikoCacheTTL, DefaultKanikoCacheTTL),
		KanikoDir:      "/kaniko",
		LaunchCacheDir: os.Getenv(EnvLaunchCacheDir),
		SkipLayers:     skipLayers,
		ParallelExport: boolEnv(EnvParallelExport),

		// Images used by the lifecycle during the build

		AdditionalTags:        nil, // no default
		BuildImageRef:         os.Getenv(EnvBuildImage),
		DeprecatedRunImageRef: "", // no default
		OutputImageRef:        "", // no default
		PreviousImageRef:      os.Getenv(EnvPreviousImage),
		RunImageRef:           os.Getenv(EnvRunImage),

		// Configuration options for the output application image

		DefaultProcessType:  os.Getenv(EnvProcessType),
		LauncherPath:        DefaultLauncherPath,
		LauncherSBOMDir:     DefaultBuildpacksioSBOMDir,
		ProjectMetadataPath: envOrDefault(EnvProjectMetadataPath, filepath.Join(PlaceholderLayers, DefaultProjectMetadataFile)),

		// Configuration options for rebasing
		ForceRebase: boolEnv(EnvForceRebase),
	}

	return inputs
}

func (i *LifecycleInputs) AccessChecker() CheckReadAccess {
	if i.UseDaemon || i.UseLayout {
		// nop checker
		return func(_ string, _ authn.Keychain) (bool, error) {
			return true, nil
		}
	}
	// remote access checker
	return func(repo string, keychain authn.Keychain) (bool, error) {
		img, err := remote.NewImage(repo, keychain)
		if err != nil {
			return false, fmt.Errorf("failed to get remote image: %w", err)
		}
		return img.CheckReadAccess()
	}
}

type CheckReadAccess func(repo string, keychain authn.Keychain) (bool, error)

func (i *LifecycleInputs) DestinationImages() []string {
	var ret []string
	ret = appendOnce(ret, i.OutputImageRef)
	ret = appendOnce(ret, i.AdditionalTags...)
	return ret
}

func (i *LifecycleInputs) Images() []string {
	var ret []string
	ret = appendOnce(ret, i.DestinationImages()...)
	ret = appendOnce(ret, i.PreviousImageRef, i.BuildImageRef, i.RunImageRef, i.DeprecatedRunImageRef, i.CacheImageRef)
	return ret
}

func (i *LifecycleInputs) RegistryImages() []string {
	var ret []string
	ret = appendOnce(ret, i.CacheImageRef)
	if i.UseDaemon {
		return ret
	}
	ret = appendOnce(ret, i.Images()...)
	return ret
}

func appendOnce(list []string, els ...string) []string {
	for _, el := range els {
		if el == "" {
			continue
		}
		if notIn(list, el) {
			list = append(list, el)
		}
	}
	return list
}

func notIn(list []string, str string) bool {
	return !slices.Contains(list, str)
}

// shared helpers

func boolEnv(k string) bool {
	v := os.Getenv(k)
	b, err := strconv.ParseBool(v)
	if err != nil {
		return false
	}
	return b
}

func envOrDefault(key string, defaultVal string) string {
	if envVal := os.Getenv(key); envVal != "" {
		return envVal
	}
	return defaultVal
}

func sliceEnv(k string) str.Slice {
	envVal := os.Getenv(k)
	if envVal != "" {
		return strings.Split(envVal, ",")
	}
	return str.Slice(nil)
}

func intEnv(k string) int {
	v := os.Getenv(k)
	d, err := strconv.Atoi(v)
	if err != nil {
		return 0
	}
	return d
}

func timeEnvOrDefault(key string, defaultVal time.Duration) time.Duration {
	envTTL := os.Getenv(key)
	if envTTL == "" {
		return defaultVal
	}
	ttl, err := time.ParseDuration(envTTL)
	if err != nil {
		return defaultVal
	}
	return ttl
}

// operations

func UpdatePlaceholderPaths(i *LifecycleInputs, _ log.Logger) error {
	toUpdate := i.placeholderPaths()
	for _, path := range toUpdate {
		if *path == "" {
			continue
		}
		if !isPlaceholder(*path) {
			continue
		}
		oldPath := *path
		toReplace := PlaceholderLayers
		if i.LayersDir == "" { // layers is unset when this call comes from the rebaser
			toReplace = PlaceholderLayers + string(filepath.Separator)
		}
		newPath := strings.Replace(*path, toReplace, i.LayersDir, 1)
		*path = newPath
		if isPlaceholderOrder(oldPath) {
			if _, err := os.Stat(newPath); err != nil {
				i.OrderPath = CNBOrderPath
			}
		}
	}
	return nil
}

func isPlaceholder(s string) bool {
	return strings.Contains(s, PlaceholderLayers)
}

func isPlaceholderOrder(s string) bool {
	return s == filepath.Join(PlaceholderLayers, DefaultOrderFile)
}

func (i *LifecycleInputs) placeholderPaths() []*string {
	return []*string{
		&i.AnalyzedPath,
		&i.ExtendedDir,
		&i.GeneratedDir,
		&i.GroupPath,
		&i.OrderPath,
		&i.PlanPath,
		&i.ProjectMetadataPath,
		&i.ReportPath,
	}
}

func ResolveAbsoluteDirPaths(i *LifecycleInputs, _ log.Logger) error {
	toUpdate := i.directoryPaths()
	for _, dir := range toUpdate {
		if *dir == "" {
			continue
		}
		abs, err := filepath.Abs(*dir)
		if err != nil {
			return err
		}
		*dir = abs
	}
	return nil
}

func (i *LifecycleInputs) directoryPaths() []*string {
	return []*string{
		&i.AppDir,
		&i.BuildConfigDir,
		&i.BuildpacksDir,
		&i.CacheDir,
		&i.ExtensionsDir,
		&i.GeneratedDir,
		&i.KanikoDir,
		&i.LaunchCacheDir,
		&i.LayersDir,
		&i.PlatformDir,
	}
}
