package config

import (
	"os"
	"strconv"
	"strings"

	"github.com/hashicorp/go-multierror"
	"github.com/homemade/scl"
	"github.com/nsheridan/cashier/server/helpers/vault"
	"github.com/pkg/errors"
)

// Config holds the final server configuration.
type Config struct {
	Server *Server `hcl:"server"`
	Auth   *Auth   `hcl:"auth"`
	SSH    *SSH    `hcl:"ssh"`
	AWS    *AWS    `hcl:"aws"`
	Vault  *Vault  `hcl:"vault"`
}

// Database holds database configuration.
type Database map[string]string

// Server holds the configuration specific to the web server and sessions.
type Server struct {
	UseTLS                bool     `hcl:"use_tls"`
	TLSKey                string   `hcl:"tls_key"`
	TLSCert               string   `hcl:"tls_cert"`
	LetsEncryptServername string   `hcl:"letsencrypt_servername"`
	LetsEncryptCache      string   `hcl:"letsencrypt_cachedir"`
	Addr                  string   `hcl:"address"`
	Port                  int      `hcl:"port"`
	User                  string   `hcl:"user"`
	CookieSecret          string   `hcl:"cookie_secret"`
	CSRFSecret            string   `hcl:"csrf_secret"`
	HTTPLogFile           string   `hcl:"http_logfile"`
	Database              Database `hcl:"database"`
	RequireReason         bool     `hcl:"require_reason"`
}

// Auth holds the configuration specific to the OAuth provider.
type Auth struct {
	OauthClientID     string            `hcl:"oauth_client_id"`
	OauthClientSecret string            `hcl:"oauth_client_secret"`
	OauthCallbackURL  string            `hcl:"oauth_callback_url"`
	Provider          string            `hcl:"provider"`
	ProviderOpts      map[string]string `hcl:"provider_opts"`
	UsersWhitelist    []string          `hcl:"users_whitelist"`
}

// SSH holds the configuration specific to signing ssh keys.
type SSH struct {
	SigningKey           string   `hcl:"signing_key"`
	AdditionalPrincipals []string `hcl:"additional_principals"`
	MaxAge               string   `hcl:"max_age"`
	Permissions          []string `hcl:"permissions"`
}

// AWS holds Amazon AWS configuration.
// AWS can also be configured using SDK methods.
type AWS struct {
	Region    string `hcl:"region"`
	AccessKey string `hcl:"access_key"`
	SecretKey string `hcl:"secret_key"`
}

// Vault holds Hashicorp Vault configuration.
type Vault struct {
	Address string `hcl:"address"`
	Token   string `hcl:"token"`
}

func verifyConfig(c *Config) error {
	var err error
	if c.SSH == nil {
		err = multierror.Append(err, errors.New("missing ssh config section"))
	}
	if c.Auth == nil {
		err = multierror.Append(err, errors.New("missing auth config section"))
	}
	if c.Server == nil {
		err = multierror.Append(err, errors.New("missing server config section"))
	}
	return err
}

func setFromEnvironment(c *Config) {
	port, err := strconv.Atoi(os.Getenv("PORT"))
	if err == nil {
		c.Server.Port = port
	}
	if os.Getenv("OAUTH_CLIENT_ID") != "" {
		c.Auth.OauthClientID = os.Getenv("OAUTH_CLIENT_ID")
	}
	if os.Getenv("OAUTH_CLIENT_SECRET") != "" {
		c.Auth.OauthClientSecret = os.Getenv("OAUTH_CLIENT_SECRET")
	}
	if os.Getenv("CSRF_SECRET") != "" {
		c.Server.CSRFSecret = os.Getenv("CSRF_SECRET")
	}
	if os.Getenv("COOKIE_SECRET") != "" {
		c.Server.CookieSecret = os.Getenv("COOKIE_SECRET")
	}
}

func setFromVault(c *Config) error {
	if c.Vault == nil || c.Vault.Token == "" || c.Vault.Address == "" {
		return nil
	}
	v, err := vault.NewClient(c.Vault.Address, c.Vault.Token)
	if err != nil {
		return errors.Wrap(err, "vault error")
	}
	var errs error
	get := func(value string) string {
		if strings.HasPrefix(value, "/vault/") {
			s, err := v.Read(value)
			if err != nil {
				errs = multierror.Append(errs, err)
			}
			return s
		}
		return value
	}
	c.Auth.OauthClientID = get(c.Auth.OauthClientID)
	c.Auth.OauthClientSecret = get(c.Auth.OauthClientSecret)
	c.Server.CSRFSecret = get(c.Server.CSRFSecret)
	c.Server.CookieSecret = get(c.Server.CookieSecret)
	if len(c.Server.Database) != 0 {
		c.Server.Database["password"] = get(c.Server.Database["password"])
	}
	if c.AWS != nil {
		c.AWS.AccessKey = get(c.AWS.AccessKey)
		c.AWS.SecretKey = get(c.AWS.SecretKey)
	}
	return errors.Wrap(errs, "errors reading from vault")
}

// ReadConfig parses a hcl configuration file into a Config struct.
func ReadConfig(f string) (*Config, error) {
	config := &Config{}
	if err := scl.DecodeFile(config, f); err != nil {
		return nil, errors.Wrapf(err, "unable to load config from file %s", f)
	}
	if err := setFromVault(config); err != nil {
		return nil, err
	}
	setFromEnvironment(config)
	if err := verifyConfig(config); err != nil {
		return nil, errors.Wrap(err, "unable to verify config")
	}
	return config, nil
}
