package env

import (
	"context"
	"errors"
	"os"
	"runtime"
	"strings"
)

var envContextKey int

func copyMap(m map[string]string) map[string]string {
	out := make(map[string]string, len(m))
	for k, v := range m {
		out[k] = v
	}
	return out
}

func getMap(ctx context.Context) map[string]string {
	if ctx == nil {
		return nil
	}
	m, ok := ctx.Value(&envContextKey).(map[string]string)
	if !ok {
		return nil
	}
	return m
}

func setMap(ctx context.Context, m map[string]string) context.Context {
	return context.WithValue(ctx, &envContextKey, m)
}

// Lookup key in the context or the the environment.
// Context has precedence.
func Lookup(ctx context.Context, key string) (string, bool) {
	m := getMap(ctx)

	// Return if the key is set in the context.
	v, ok := m[key]
	if ok {
		return v, true
	}

	// Fall back to the environment.
	return os.LookupEnv(key)
}

// Get key from the context or the environment.
// Context has precedence.
func Get(ctx context.Context, key string) string {
	v, _ := Lookup(ctx, key)
	return v
}

// Set key on the context.
//
// Note: this does NOT mutate the processes' actual environment variables.
// It is only visible to other code that uses this package.
func Set(ctx context.Context, key, value string) context.Context {
	m := copyMap(getMap(ctx))
	m[key] = value
	return setMap(ctx, m)
}

func homeEnvVar() string {
	if runtime.GOOS == "windows" {
		return "USERPROFILE"
	}
	return "HOME"
}

func WithUserHomeDir(ctx context.Context, value string) context.Context {
	return Set(ctx, homeEnvVar(), value)
}

// ErrNoHomeEnv indicates the absence of $HOME env variable
var ErrNoHomeEnv = errors.New("$HOME is not set")

func UserHomeDir(ctx context.Context) (string, error) {
	home := Get(ctx, homeEnvVar())
	if home == "" {
		return "", ErrNoHomeEnv
	}
	return home, nil
}

// All returns environment variables that are defined in both os.Environ
// and this package. `env.Set(ctx, x, y)` will override x from os.Environ.
func All(ctx context.Context) map[string]string {
	m := map[string]string{}
	for _, line := range os.Environ() {
		split := strings.SplitN(line, "=", 2)
		if len(split) != 2 {
			continue
		}
		m[split[0]] = split[1]
	}
	// override existing environment variables with the ones we set
	for k, v := range getMap(ctx) {
		m[k] = v
	}
	return m
}