package launch

import (
	"fmt"

	"github.com/pkg/errors"

	"github.com/buildpacks/lifecycle/api"
)

// ProcessFor creates a process from container cmd
//
//	If the Platform API if 0.4 or greater and DefaultProcess is set:
//	  * The default process is returned with `cmd` appended to the process args
//	If the Platform API is less than 0.4
//	  * If there is exactly one argument and it matches a process type, it returns that process.
//	  * If cmd is empty, it returns the default process
//	Else
//	  * it constructs a new process from cmd
//	  * If the first element in cmd is `cmd` the process shall be direct
func (l *Launcher) ProcessFor(cmd []string) (Process, error) {
	if l.DefaultProcessType == "" {
		process, err := l.userProvidedProcess(cmd)
		if err != nil {
			return Process{}, err
		}
		return process, nil
	}

	process, ok := l.findProcessType(l.DefaultProcessType)
	if !ok {
		return Process{}, fmt.Errorf("process type %s was not found", l.DefaultProcessType)
	}

	if l.PlatformAPI.LessThan("0.10") {
		return l.handleUserArgsPlatformLessThan010(process, cmd)
	}
	return l.handleUserArgs(process, cmd)
}

func (l *Launcher) handleUserArgsPlatformLessThan010(process Process, userArgs []string) (Process, error) {
	process.Args = append(process.Args, userArgs...)
	return process, nil
}

func (l *Launcher) handleUserArgs(process Process, userArgs []string) (Process, error) {
	switch {
	case len(process.Command.Entries) > 1: // definitely newer buildpack
		overridableArgs := process.Args
		process.Args = process.Command.Entries[1:]                     // set always-provided args
		process.Command.Entries = []string{process.Command.Entries[0]} // when exec'ing the process we always expect Command to have just one entry
		if len(userArgs) > 0 {
			process.Args = append(process.Args, userArgs...)
		} else {
			process.Args = append(process.Args, overridableArgs...)
		}
	case len(userArgs) == 0:
		// nothing to do, we just provide whatever the original process args were
	default:
		// we have user-provided args, and we need to check the buildpack API to know how to handle them
		bp, err := l.buildpackForProcess(process)
		if err != nil {
			return Process{}, err
		}
		if api.MustParse(bp.API).LessThan("0.9") {
			process.Args = append(process.Args, userArgs...) // user-provided args are appended to process args
		} else {
			process.Args = userArgs // user-provided args replace process args
		}
	}

	return process, nil
}

func (l *Launcher) buildpackForProcess(process Process) (Buildpack, error) {
	for _, bp := range l.Buildpacks {
		if bp.ID == process.BuildpackID {
			return bp, nil
		}
	}
	return Buildpack{}, fmt.Errorf("failed to find buildpack for process %s with buildpack ID %s", process.Type, process.BuildpackID)
}

func (l *Launcher) processForLegacy(cmd []string) (Process, error) {
	if len(cmd) == 0 {
		if process, ok := l.findProcessType(l.DefaultProcessType); ok {
			return process, nil
		}

		return Process{}, fmt.Errorf("process type %s was not found", l.DefaultProcessType)
	}

	if len(cmd) == 1 {
		if process, ok := l.findProcessType(cmd[0]); ok {
			return process, nil
		}
	}

	return l.userProvidedProcess(cmd)
}

func (l *Launcher) findProcessType(pType string) (Process, bool) {
	for _, p := range l.Processes {
		if p.Type == pType && l.isProcessEligibleForExecEnv(p) {
			return p, true
		}
	}

	return Process{}, false
}

// isProcessEligibleForExecEnv checks if a process is eligible for the current execution environment
// According to the spec:
// - A process is eligible if it has no exec-env specified, OR
// - Its exec-env includes the current execution environment, OR
// - Its exec-env includes the special value "*" which indicates compatibility with all environments
func (l *Launcher) isProcessEligibleForExecEnv(p Process) bool {
	// Note: This is gated by Platform API 0.15, not Buildpack API
	// The platform controls the Platform API version and determines when this is available
	if l.PlatformAPI == nil || !l.PlatformAPI.AtLeast("0.15") {
		return true // Skip execution environment filtering for older Platform APIs
	}

	// If no exec-env specified, process applies to all execution environments
	if len(p.ExecEnv) == 0 {
		return true
	}

	// Check if process supports all execution environments
	for _, env := range p.ExecEnv {
		if env == "*" {
			return true
		}
	}

	// Check if process supports the current execution environment
	for _, env := range p.ExecEnv {
		if env == l.ExecEnv {
			return true
		}
	}

	return false
}

func (l *Launcher) userProvidedProcess(cmd []string) (Process, error) {
	if len(cmd) == 0 {
		return Process{}, errors.New("when there is no default process a command is required")
	}
	if len(cmd) > 1 && cmd[0] == "--" {
		return Process{Command: RawCommand{Entries: []string{cmd[1]}}, Args: cmd[2:], Direct: true}, nil
	}

	return Process{Command: RawCommand{Entries: []string{cmd[0]}}, Args: cmd[1:]}, nil
}

func getProcessWorkingDirectory(process Process, appDir string) string {
	if process.WorkingDirectory == "" {
		return appDir
	}
	return process.WorkingDirectory
}
