// Copyright 2016-2023, Pulumi Corporation.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// A small library for creating typed, consistent and documented environmental variable
// accesses.
//
// Declaring a variable is as simple as declaring a module level constant.
//
//	var Var = env.Bool("VAR", "A boolean variable")
//
// Typed values can be retrieved by calling `Var.Value()`.
package env

import (
	"fmt"
	"os"
	"sort"
	"strconv"
	"strings"
)

// Store holds a collection of key, value? pairs.
//
// Store acts like the environment that values are drawn from.
type Store interface {
	// Retrieve a raw value from the Store. If the value is not present, "", false should
	// be returned.
	Raw(key string) (string, bool)
}

// A strongly typed environment.
type Env interface {
	GetString(val StringValue) string
	GetBool(val BoolValue) bool
	GetInt(val IntValue) int
}

// Create a new strongly typed Env from an untyped Store.
func NewEnv(store Store) Env {
	return env{store}
}

type env struct{ s Store }

func (e env) GetString(val StringValue) string {
	return StringValue{val.withStore(e.s)}.Value()
}

func (e env) GetBool(val BoolValue) bool {
	return BoolValue{val.withStore(e.s)}.Value()
}

func (e env) GetInt(val IntValue) int {
	return IntValue{val.withStore(e.s)}.Value()
}

type envStore struct{}

func (envStore) Raw(key string) (string, bool) {
	return os.LookupEnv(key)
}

type MapStore map[string]string

func (m MapStore) Raw(key string) (string, bool) {
	v, ok := m[key]
	return v, ok
}

// The global store of values that Value.Value() uses.
//
// Setting this value is not thread safe, and should be restricted to testing.
var Global Store = envStore{}

// An environmental variable.
type Var struct {
	name        string
	Value       Value
	Description string
	options     options
}

// The default prefix for a environmental variable.
const Prefix = "PULUMI_"

// The name of an environmental variable.
//
// Name accounts for prefix if appropriate.
func (v Var) Name() string {
	return v.options.name(v.name)
}

// The alternative name of an environmental variable.
func (v Var) Alternative() string {
	if v.options.alternative == "" {
		return ""
	}
	return v.options.name(v.options.alternative)
}

// The list of variables that a must be truthy for `v` to be set.
func (v Var) Requires() []BoolValue { return v.options.prerequs }

var envVars []Var

// Variables is a list of variables declared.
func Variables() []Var {
	vars := envVars
	sort.SliceStable(vars, func(i, j int) bool {
		return vars[i].Name() < vars[j].Name()
	})
	return vars
}

// An Option to configure a environmental variable.
type Option func(*options)

type options struct {
	prerequs    []BoolValue
	noPrefix    bool
	secret      bool
	alternative string
}

func (o options) name(underlying string) string {
	if o.noPrefix {
		return underlying
	}
	return Prefix + underlying
}

// Needs indicates that a variable can only be set if `val` is truthy.
func Needs(val BoolValue) Option {
	return func(o *options) {
		o.prerequs = append(o.prerequs, val)
	}
}

// NoPrefix indicates that a variable should not have the default prefix applied.
func NoPrefix(opts *options) {
	opts.noPrefix = true
}

// Secret indicates that the value should not be displayed in plaintext.
func Secret(opts *options) {
	opts.secret = true
}

// Alternative indicates that the variable has an alternative name. This is generally used for backwards compatibility.
func Alternative(name string) Option {
	return func(opts *options) {
		opts.alternative = name
	}
}

// The value of a environmental variable.
//
// In general, `Value`s should only be used in collections. For specific values, used the
// typed version (StringValue, BoolValue).
//
// Every implementer of Value also includes a `Value() T` method that returns a typed
// representation of the value.
type Value interface {
	fmt.Stringer

	// Retrieve the underlying string value associated with the variable.
	//
	// If the variable was not set, ("", false) is returned.
	Underlying() (string, bool)
	// Retrieve the Var associated with this value.
	Var() Var

	Validate() ValidateError

	// set the associated variable for the value. This is necessary since Value and Var
	// are inherently cyclical.
	setVar(Var)
	// A type correct formatting for the value. This is used for display purposes and
	// should not be quoted.
	formattedValue() string
}

type ValidateError struct {
	Warning error
	Error   error
}

// An implementation helper for Value. New Values should be a typed wrapper around *value.
type value struct {
	variable Var
	store    Store
}

func (v value) withStore(store Store) *value {
	v.store = store // This is non-mutating since `v` is taken by value.
	return &v
}

func (v value) String() string {
	_, present := v.Underlying()
	if !present {
		return "unset"
	}
	if m := v.missingPrerequs(); m != "" {
		return fmt.Sprintf("need %s (%s)", m, v.Var().Value.formattedValue())
	}
	return v.Var().Value.formattedValue()
}

func (v *value) setVar(variable Var) {
	v.variable = variable
}

func (v value) Var() Var {
	return v.variable
}

func (v value) Underlying() (string, bool) {
	s := v.store
	if s == nil {
		s = Global
	}
	raw, has := s.Raw(v.Var().Name())
	if has {
		return raw, true
	}

	alt := v.Var().Alternative()
	if alt != "" {
		return s.Raw(alt)
	}

	return "", false
}

func (v value) missingPrerequs() string {
	for _, p := range v.variable.options.prerequs {
		if !p.Value() {
			return p.Var().Name()
		}
	}
	return ""
}

// A string retrieved from the environment.
type StringValue struct{ *value }

func (StringValue) Type() string { return "string" }

func (s StringValue) formattedValue() string {
	if s.variable.options.secret {
		return "[secret]"
	}
	return fmt.Sprintf("%#v", s.Value())
}

func (StringValue) Validate() ValidateError { return ValidateError{} }

// The string value of the variable.
//
// If the variable is unset, "" is returned.
func (s StringValue) Value() string {
	if s.missingPrerequs() != "" {
		return ""
	}
	v, ok := s.Underlying()
	if !ok {
		return ""
	}
	return v
}

// A boolean retrieved from the environment.
type BoolValue struct{ *value }

func (BoolValue) Type() string { return "bool" }

func (b BoolValue) formattedValue() string {
	return fmt.Sprintf("%#v", b.Value())
}

func (b BoolValue) Validate() ValidateError {
	v, ok := b.Underlying()
	if !ok || b.Value() || v == "0" || strings.EqualFold(v, "false") {
		return ValidateError{}
	}
	return ValidateError{
		Warning: fmt.Errorf("%#v is falsy, but doesn't look like a boolean", v),
	}
}

// The boolean value of the variable.
//
// If the variable is unset, false is returned.
func (b BoolValue) Value() bool {
	if b.missingPrerequs() != "" {
		return false
	}
	v, ok := b.Underlying()
	if !ok {
		return false
	}
	return v == "1" || strings.EqualFold(v, "true")
}

// An integer retrieved from the environment.
type IntValue struct{ *value }

func (IntValue) Type() string { return "int" }

func (i IntValue) Validate() ValidateError {
	v, ok := i.Underlying()
	if !ok {
		return ValidateError{}
	}
	_, err := strconv.ParseInt(v, 10, 64)
	return ValidateError{
		Error: err,
	}
}

// The integer value of the variable.
//
// If the variable is unset or not parsable, 0 is returned.
func (i IntValue) Value() int {
	if i.missingPrerequs() != "" {
		return 0
	}
	v, ok := i.Underlying()
	if !ok {
		return 0
	}
	parsed, err := strconv.ParseInt(v, 10, 64)
	if err != nil {
		return 0
	}
	return int(parsed)
}

func (i IntValue) formattedValue() string {
	return fmt.Sprintf("%#v", i.Value())
}

func setVar(val Value, variable Var) Value {
	variable.Value = val
	val.setVar(variable)
	envVars = append(envVars, variable)
	return val
}

// Declare a new environmental value.
//
// `name` is the runtime name of the variable. Unless `NoPrefix` is passed, name is
// pre-appended with `Prefix`. For example, a variable named "FOO" would be set by
// declaring "PULUMI_FOO=val" in the enclosing environment.
//
// `description` is the string description of what the variable does.
func String(name, description string, opts ...Option) StringValue {
	var options options
	for _, opt := range opts {
		opt(&options)
	}
	val := StringValue{&value{}}
	variable := Var{
		name:        name,
		Description: description,
		options:     options,
	}
	return setVar(val, variable).(StringValue)
}

// Declare a new environmental value of type bool.
//
// `name` is the runtime name of the variable. Unless `NoPrefix` is passed, name is
// pre-appended with `Prefix`. For example, a variable named "FOO" would be set by
// declaring "PULUMI_FOO=1" in the enclosing environment.
//
// `description` is the string description of what the variable does.
func Bool(name, description string, opts ...Option) BoolValue {
	var options options
	for _, opt := range opts {
		opt(&options)
	}
	val := BoolValue{&value{}}
	variable := Var{
		name:        name,
		Description: description,
		options:     options,
	}
	return setVar(val, variable).(BoolValue)
}

// Declare a new environmental value of type integer.
//
// `name` is the runtime name of the variable. Unless `NoPrefix` is passed, name is
// pre-appended with `Prefix`. For example, a variable named "FOO" would be set by
// declaring "PULUMI_FOO=1" in the enclosing environment.
//
// `description` is the string description of what the variable does.
func Int(name, description string, opts ...Option) IntValue {
	var options options
	for _, opt := range opts {
		opt(&options)
	}
	val := IntValue{&value{}}
	variable := Var{
		name:        name,
		Description: description,
		options:     options,
	}
	return setVar(val, variable).(IntValue)
}