package server

import (
	"fmt"
	"io"
	"io/ioutil"
	"os"
	"path/filepath"
	"strings"

	"github.com/hashicorp/hcl"
	hclobj "github.com/hashicorp/hcl/hcl"
)

// Config is the configuration for the vault server.
type Config struct {
	Listeners []*Listener `hcl:"-"`
	Backend   *Backend    `hcl:"-"`

	StatsiteAddr string `hcl:"statsite_addr"`
	StatsdAddr   string `hcl:"statsd_addr"`
}

// DevConfig is a Config that is used for dev mode of Vault.
func DevConfig() *Config {
	return &Config{
		Backend: &Backend{
			Type: "inmem",
		},

		Listeners: []*Listener{
			&Listener{
				Type: "tcp",
				Config: map[string]string{
					"tls_disable": "1",
				},
			},
		},
	}
}

// Listener is the listener configuration for the server.
type Listener struct {
	Type   string
	Config map[string]string
}

func (l *Listener) GoString() string {
	return fmt.Sprintf("*%#v", *l)
}

// Backend is the backend configuration for the server.
type Backend struct {
	Type          string
	AdvertiseAddr string
	Config        map[string]string
}

func (b *Backend) GoString() string {
	return fmt.Sprintf("*%#v", *b)
}

// Merge merges two configurations.
func (c *Config) Merge(c2 *Config) *Config {
	result := new(Config)
	for _, l := range c.Listeners {
		result.Listeners = append(result.Listeners, l)
	}
	for _, l := range c2.Listeners {
		result.Listeners = append(result.Listeners, l)
	}

	result.Backend = c.Backend
	if c2.Backend != nil {
		result.Backend = c2.Backend
	}

	if c2.StatsiteAddr != "" {
		result.StatsiteAddr = c2.StatsiteAddr
	}
	if c2.StatsdAddr != "" {
		result.StatsdAddr = c2.StatsdAddr
	}

	return result
}

// LoadConfig loads the configuration at the given path, regardless if
// its a file or directory.
func LoadConfig(path string) (*Config, error) {
	fi, err := os.Stat(path)
	if err != nil {
		return nil, err
	}

	if fi.IsDir() {
		return LoadConfigDir(path)
	} else {
		return LoadConfigFile(path)
	}
}

// LoadConfigFile loads the configuration from the given file.
func LoadConfigFile(path string) (*Config, error) {
	// Read the file
	d, err := ioutil.ReadFile(path)
	if err != nil {
		return nil, err
	}

	// Parse!
	obj, err := hcl.Parse(string(d))
	if err != nil {
		return nil, err
	}

	// Start building the result
	var result Config
	if err := hcl.DecodeObject(&result, obj); err != nil {
		return nil, err
	}

	if objs := obj.Get("listener", false); objs != nil {
		result.Listeners, err = loadListeners(objs)
		if err != nil {
			return nil, err
		}
	}
	if objs := obj.Get("backend", false); objs != nil {
		result.Backend, err = loadBackend(objs)
		if err != nil {
			return nil, err
		}
	}

	return &result, nil
}

// LoadConfigDir loads all the configurations in the given directory
// in alphabetical order.
func LoadConfigDir(dir string) (*Config, error) {
	f, err := os.Open(dir)
	if err != nil {
		return nil, err
	}
	defer f.Close()

	fi, err := f.Stat()
	if err != nil {
		return nil, err
	}
	if !fi.IsDir() {
		return nil, fmt.Errorf(
			"configuration path must be a directory: %s",
			dir)
	}

	var files []string
	err = nil
	for err != io.EOF {
		var fis []os.FileInfo
		fis, err = f.Readdir(128)
		if err != nil && err != io.EOF {
			return nil, err
		}

		for _, fi := range fis {
			// Ignore directories
			if fi.IsDir() {
				continue
			}

			// Only care about files that are valid to load.
			name := fi.Name()
			skip := true
			if strings.HasSuffix(name, ".hcl") {
				skip = false
			} else if strings.HasSuffix(name, ".json") {
				skip = false
			}
			if skip || isTemporaryFile(name) {
				continue
			}

			path := filepath.Join(dir, name)
			files = append(files, path)
		}
	}

	var result *Config
	for _, f := range files {
		config, err := LoadConfigFile(f)
		if err != nil {
			return nil, fmt.Errorf("Error loading %s: %s", f, err)
		}

		if result == nil {
			result = config
		} else {
			result = result.Merge(config)
		}
	}

	return result, nil
}

// isTemporaryFile returns true or false depending on whether the
// provided file name is a temporary file for the following editors:
// emacs or vim.
func isTemporaryFile(name string) bool {
	return strings.HasSuffix(name, "~") || // vim
		strings.HasPrefix(name, ".#") || // emacs
		(strings.HasPrefix(name, "#") && strings.HasSuffix(name, "#")) // emacs
}

func loadListeners(os *hclobj.Object) ([]*Listener, error) {
	var allNames []*hclobj.Object

	// Really confusing iteration. The key is the false/true parameter
	// of whether we're expanding or not. We first iterate over all
	// the "listeners"
	for _, o1 := range os.Elem(false) {
		// Iterate expand to get the list of types
		for _, o2 := range o1.Elem(true) {
			switch o2.Type {
			case hclobj.ValueTypeList:
				// This switch is for JSON, to allow them to do this:
				//
				// "tcp": [{ ... }, { ... }]
				//
				// To configure multiple listeners of the same type.
				for _, o3 := range o2.Elem(true) {
					o3.Key = o2.Key
					allNames = append(allNames, o3)
				}
			case hclobj.ValueTypeObject:
				// This is for the standard `listener "tcp" { ... }` syntax
				allNames = append(allNames, o2)
			}
		}
	}

	if len(allNames) == 0 {
		return nil, nil
	}

	// Now go over all the types and their children in order to get
	// all of the actual resources.
	result := make([]*Listener, 0, len(allNames))
	for _, obj := range allNames {
		k := obj.Key

		var config map[string]string
		if err := hcl.DecodeObject(&config, obj); err != nil {
			return nil, fmt.Errorf(
				"Error reading config for %s: %s",
				k,
				err)
		}

		result = append(result, &Listener{
			Type:   k,
			Config: config,
		})
	}

	return result, nil
}

func loadBackend(os *hclobj.Object) (*Backend, error) {
	var allNames []*hclobj.Object

	// See loadListeners
	for _, o1 := range os.Elem(false) {
		// Iterate expand to get the list of types
		for _, o2 := range o1.Elem(true) {
			// Iterate non-expand to get the full list of types
			for _, o3 := range o2.Elem(false) {
				allNames = append(allNames, o3)
			}
		}
	}

	if len(allNames) == 0 {
		return nil, nil
	}
	if len(allNames) > 1 {
		keys := make([]string, 0, len(allNames))
		for _, o := range allNames {
			keys = append(keys, o.Key)
		}

		return nil, fmt.Errorf(
			"Multiple backends declared. Only one is allowed: %v", keys)
	}

	// Now go over all the types and their children in order to get
	// all of the actual resources.
	var result Backend
	obj := allNames[0]
	result.Type = obj.Key

	var config map[string]string
	if err := hcl.DecodeObject(&config, obj); err != nil {
		return nil, fmt.Errorf(
			"Error reading config for backend %s: %s",
			result.Type,
			err)
	}

	if v, ok := config["advertise_addr"]; ok {
		result.AdvertiseAddr = v
		delete(config, "advertise_addr")
	}

	result.Config = config
	return &result, nil
}
