package env

import (
	"os"
	"path/filepath"
	"strings"

	"github.com/pkg/errors"

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

// Env is used to modify and return environment variables
type Env struct {
	// RootDirMap maps directories in a posix root filesystem to a slice of environment variables that
	RootDirMap map[string][]string
	Vars       *Vars
}

// AddRootDir modifies the environment given a root dir. If the root dir contains a directory that matches a key in
// the Env RooDirMap, the absolute path to the keyed directory will be prepended to all the associated environment variables
// using the OS path list separator as a delimiter.
func (p *Env) AddRootDir(dir string) error {
	absDir, err := filepath.Abs(dir)
	if err != nil {
		return err
	}
	for dir, vars := range p.RootDirMap {
		childDir := filepath.Join(absDir, dir)
		if _, err := os.Stat(childDir); os.IsNotExist(err) {
			continue
		} else if err != nil {
			return err
		}
		for _, key := range vars {
			p.Vars.Set(key, childDir+prefix(p.Vars.Get(key), os.PathListSeparator))
		}
	}
	return nil
}

func (p *Env) isRootEnv(name string) bool {
	for _, m := range p.RootDirMap {
		for _, k := range m {
			if k == name {
				return true
			}
		}
	}
	return false
}

type ActionType string

const (
	ActionTypePrepend     ActionType = "prepend"
	ActionTypeAppend      ActionType = "append"
	ActionTypeOverride    ActionType = "override"
	ActionTypeDefault     ActionType = "default"
	ActionTypePrependPath ActionType = ""
)

// DefaultActionType returns the default action to perform for an unsuffixed env file as specified for the given
// buildpack API
func DefaultActionType(_ *api.Version) ActionType {
	return ActionTypeOverride
}

// AddEnvDir modified the Env given a directory containing env files. For each file in the envDir, if the file has
// a period delimited suffix, the action matching the given suffix will be performed. If the file has no suffix,
// the default action will be performed. If the suffix does not match a known type, AddEnvDir will ignore the file.
func (p *Env) AddEnvDir(envDir string, defaultAction ActionType) error {
	return addEnvDir(p.Vars, envDir, defaultAction)
}

// Set sets the environment variable with the given name to the given value.
func (p *Env) Set(name, v string) {
	p.Vars.Set(name, v)
}

// WithOverrides returns the environment after applying modifications from the given platform dir and build config
// If platformDir is non-empty, for each file in the platformDir, if the name of the file does not match an environment variable name in the
// RootDirMap, the given variable will be set to the contents of the file. If the name does match an environment
// variable name in the RootDirMap, the contents of the file will be prepended to the environment variable value
// using the OS path list separator as a delimiter.
// If baseConfigDir is non-empty, for each file in the envDir, if the file has
// a period delimited suffix, the action matching the given suffix will be performed. If the file has no suffix,
// the default action will be performed. If the suffix does not match a known type, AddEnvDir will ignore the file.
func (p *Env) WithOverrides(platformDir string, baseConfigDir string) (output []string, err error) {
	vars := NewVars(p.Vars.vals, p.Vars.ignoreCase)

	if platformDir != "" {
		if err := eachEnvFile(filepath.Join(platformDir, "env"), func(k, v string) error {
			if p.isRootEnv(k) {
				vars.Set(k, v+prefix(vars.Get(k), os.PathListSeparator))
				return nil
			}
			vars.Set(k, v)
			return nil
		}); err != nil {
			return nil, err
		}
	}

	if baseConfigDir != "" {
		if err := addEnvDir(vars, filepath.Join(baseConfigDir, "env"), ActionTypeDefault); err != nil {
			return nil, err
		}
	}

	return vars.List(), nil
}

func addEnvDir(vars *Vars, envDir string, defaultAction ActionType) error {
	if err := eachEnvFile(envDir, func(k, v string) error {
		parts := strings.SplitN(k, ".", 2)
		name := parts[0]
		var action ActionType
		if len(parts) > 1 {
			action = ActionType(parts[1])
		} else {
			action = defaultAction
		}
		switch action {
		case ActionTypePrepend:
			vars.Set(name, v+prefix(vars.Get(name), delim(envDir, name)...))
		case ActionTypeAppend:
			vars.Set(name, suffix(vars.Get(name), delim(envDir, name)...)+v)
		case ActionTypeOverride:
			vars.Set(name, v)
		case ActionTypeDefault:
			if vars.Get(name) != "" {
				return nil
			}
			vars.Set(name, v)
		case ActionTypePrependPath:
			vars.Set(name, v+prefix(vars.Get(name), delim(envDir, name, os.PathListSeparator)...))
		}
		return nil
	}); err != nil {
		return errors.Wrapf(err, "apply env files from dir '%s'", envDir)
	}
	return nil
}

func prefix(s string, prefix ...byte) string {
	if s == "" {
		return ""
	}
	return string(prefix) + s
}

func suffix(s string, suffix ...byte) string {
	if s == "" {
		return ""
	}
	return s + string(suffix)
}

func delim(dir, name string, def ...byte) []byte {
	value, err := os.ReadFile(filepath.Join(dir, name+".delim"))
	if err != nil {
		return def
	}
	return value
}

func eachEnvFile(dir string, fn func(k, v string) error) error {
	files, err := os.ReadDir(dir)
	if os.IsNotExist(err) {
		return nil
	} else if err != nil {
		return err
	}
	for _, f := range files {
		if f.IsDir() {
			continue
		}
		if f.Type()&os.ModeSymlink != 0 {
			lnFile, err := os.Stat(filepath.Join(dir, f.Name()))
			if err != nil {
				return err
			}
			if lnFile.IsDir() {
				continue
			}
		}
		value, err := os.ReadFile(filepath.Join(dir, f.Name()))
		if err != nil {
			return err
		}
		if err := fn(f.Name(), string(value)); err != nil {
			return err
		}
	}
	return nil
}

// List returns the environment
func (p *Env) List() []string {
	return p.Vars.List()
}

// Get returns the value for the given key
func (p *Env) Get(k string) string {
	return p.Vars.Get(k)
}
