package env

import (
	"os"
	"errors"
	"fmt"
	"time"
	"net/url"
	"strconv"
)

// OS is a default client backed by `os.Getenv`.
var OS = NewClient(os.Getenv)

var _ Client = OS

var (
	// ErrEmptyValue defines the error when a key has an empty value.
	ErrEmptyValue = errors.New("environment variable has empty key")
	// ErrUnparsableValue defines the error when a key's value can't be parsed.
	ErrUnparsableValue = errors.New("environment variable has unparsable value")
)

// Error for the package env
type Error struct {
	Err error
	Key string
}

// Error implements the Error interface.
func (e Error) Error() string {
	return fmt.Sprintf("%v, key: %s", e.Err,  e.Key)
}

// GetFunc defines a function that given a key looks up a value.
type GetFunc func(k string) string

// Client defines the environment client interface.
type Client interface {
	Get(key string) (string, error)
	GetOr(key string, fallback string) string

	Bool(key string) (bool, error)
	BoolOr(key string, fallback bool) bool
	Duration(key string) (time.Duration, error)
	DurationOr(key string, fallback time.Duration) time.Duration
	Int(key string) (int, error)
	IntOr(key string, fallback int) int
	Time(key string) (time.Time, error)
	TimeOr(key string, fallback time.Time) time.Time
	URL(key string) (*url.URL, error)
	URLOr(key string, fallback *url.URL) *url.URL
}

type client struct {
	get GetFunc
}

// NewClient creates a new Client backed by provided func(string) string.
func NewClient(f GetFunc) Client {
	return &client{f}
}

// FromMap looks up a value for a given key using a map.
func FromMap(m map[string]string) GetFunc {
	return func(k string) string {
		return m[k]
	}
}

// Get looks up an environment variable. It is an error if the value is empty.
func (c *client) Get(key string) (string, error) {
	v := c.get(key)
	if v == "" {
		return "", Error{ErrEmptyValue, key}
	}
	return v, nil
}

// GetOr looks up an environment variable. If the value is empty, the fallback
// is returned.
func (c *client) GetOr(key string, fallback string) string {
	v, err := c.Get(key)
	if err != nil {
		return fallback
	}
	return v
}

// Bool looks up an environment variable and tries to parse its value as a bool.
// It is an error if the value is empty or can't be parsed.
func (c *client) Bool(key string) (bool, error) {
	v := c.get(key)
	if v == "" {
		return false, Error{ErrEmptyValue, key}
	}
	b, err := strconv.ParseBool(v)
	if err != nil {
		return false, Error{ErrUnparsableValue, key}
	}
	return b, nil
}

// BoolOr looks up an environment variable and tries to parse its value as a
// bool. If the value is empty or can't be parsed, the fallback is returned.
func (c *client) BoolOr(key string, fallback bool) bool {
	v, err := c.Bool(key)
	if err != nil {
		return fallback
	}
	return v
}

// Duration looks up an environment variable and tries to parse its value as a
// duration. It is an error if the value is empty or can't be parsed.
func (c *client) Duration(key string) (time.Duration, error) {
	v := c.get(key)
	if v == "" {
		return 0, Error{ErrEmptyValue, key}
	}
	d, err := time.ParseDuration(v)
	if err != nil {
		return 0, Error{ErrUnparsableValue, key}
	}
	return d, nil
}

// DurationOr looks up an environment variable and tries to parse its value as a
// duration. If the value is empty or can't be parsed, the fallback is returned.
func (c *client) DurationOr(key string, fallback time.Duration) time.Duration {
	v, err := c.Duration(key)
	if err != nil {
		return fallback
	}
	return v
}

// Int looks up an environment variable and tries to parse its value as an
// integer. It is an error if the value is empty or can't be parsed.
func (c *client) Int(key string) (int, error) {
	v := c.get(key)
	if v == "" {
		return 0, Error{ErrEmptyValue, key}
	}
	i, err := strconv.Atoi(v)
	if err != nil {
		return 0, Error{ErrUnparsableValue, key}
	}
	return i, nil
}

// IntOr looks up an environment variable and tries to parse its value as an
// integer. If the value is empty or can't be parsed, the fallback is returned.
func (c *client) IntOr(key string, fallback int) int {
	v, err := c.Int(key)
	if err != nil {
		return fallback
	}
	return v
}

// Time looks up an environment variable and tries to parse its value as an RFC
// 3339 formatted instant in time. It is an error if the value is empty or can't
// be parsed.
func (c *client) Time(key string) (time.Time, error) {
	v := c.get(key)
	if v == "" {
		return time.Time{}, Error{ErrEmptyValue, key}
	}
	t, err := time.Parse(time.RFC3339Nano, v)
	if err != nil {
		return time.Time{}, Error{ErrUnparsableValue, key}
	}
	return t, nil
}

// TimeOr looks up an environment variable and tries to parse its value as an
// RFC 3339 formatted instant in time. If the value is empty or can't be parsed,
// the fallback is returned.
func (c *client) TimeOr(key string, fallback time.Time) time.Time {
	v, err := c.Time(key)
	if err != nil {
		return fallback
	}
	return v
}

// URL looks up an environment variable and tries to parse its value as an URL.
// It is an error if the value is empty or can't be parsed.
func (c *client) URL(key string) (*url.URL, error) {
	v := c.get(key)
	if v == "" {
		return nil, Error{ErrEmptyValue, key}
	}
	u, err := url.Parse(v)
	if err != nil {
		return nil, Error{ErrUnparsableValue, key}
	}
	return u, nil
}

// URLOr looks up an environment variable and tries to parse its value as an
// URL. If the value is empty or can't be parsed, the fallback is returned.
func (c *client) URLOr(key string, fallback *url.URL) *url.URL {
	v, err := c.URL(key)
	if err != nil {
		return fallback
	}
	return v
}
