// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.

package config

import (
	"bytes"
	"encoding/base64"
	"fmt"
	"io"
	"io/ioutil"
	"os"
	"path/filepath"

	"github.com/talos-systems/crypto/x509"
	yaml "gopkg.in/yaml.v3"
)

// Config represents the client configuration file (talosconfig).
type Config struct {
	Context  string              `yaml:"context"`
	Contexts map[string]*Context `yaml:"contexts"`
}

// NewConfig returns the client configuration file with a single context.
func NewConfig(contextName string, endpoints []string, caCrt []byte, client *x509.PEMEncodedCertificateAndKey) *Config {
	return &Config{
		Context: contextName,
		Contexts: map[string]*Context{
			contextName: {
				Endpoints: endpoints,
				CA:        base64.StdEncoding.EncodeToString(caCrt),
				Crt:       base64.StdEncoding.EncodeToString(client.Crt),
				Key:       base64.StdEncoding.EncodeToString(client.Key),
			},
		},
	}
}

func (c *Config) upgrade() {
	for _, ctx := range c.Contexts {
		ctx.upgrade()
	}
}

// Context represents the set of credentials required to talk to a target.
type Context struct {
	DeprecatedTarget string   `yaml:"target,omitempty"` // Field deprecated in favor of Endpoints
	Endpoints        []string `yaml:"endpoints"`
	Nodes            []string `yaml:"nodes,omitempty"`
	CA               string   `yaml:"ca"`
	Crt              string   `yaml:"crt"`
	Key              string   `yaml:"key"`
}

func (c *Context) upgrade() {
	if c.DeprecatedTarget != "" {
		c.Endpoints = append(c.Endpoints, c.DeprecatedTarget)
		c.DeprecatedTarget = ""
	}
}

// Open reads the config and initializes a Config struct.
func Open(p string) (c *Config, err error) {
	if err = ensure(p); err != nil {
		return nil, err
	}

	var f *os.File

	f, err = os.Open(p)
	if err != nil {
		return
	}

	defer f.Close() //nolint:errcheck

	return ReadFrom(f)
}

// FromString returns a config from a string.
func FromString(p string) (c *Config, err error) {
	return ReadFrom(bytes.NewReader([]byte(p)))
}

// FromBytes returns a config from []byte.
func FromBytes(b []byte) (c *Config, err error) {
	return ReadFrom(bytes.NewReader(b))
}

// ReadFrom reads a config from io.Reader.
func ReadFrom(r io.Reader) (c *Config, err error) {
	c = &Config{}

	if err = yaml.NewDecoder(r).Decode(c); err != nil {
		return
	}

	c.upgrade()

	return
}

// Save writes the config to disk.
func (c *Config) Save(p string) (err error) {
	configBytes, err := c.Bytes()
	if err != nil {
		return
	}

	if err = os.MkdirAll(filepath.Dir(p), 0o700); err != nil {
		return err
	}

	if err = ioutil.WriteFile(p, configBytes, 0o600); err != nil {
		return
	}

	return nil
}

// Bytes gets yaml encoded config data.
func (c *Config) Bytes() ([]byte, error) {
	return yaml.Marshal(c)
}

// Rename describes context rename during merge.
type Rename struct {
	From string
	To   string
}

// String converts to "from" -> "to".
func (r *Rename) String() string {
	return fmt.Sprintf("%q -> %q", r.From, r.To)
}

// Merge in additional contexts from another Config.
//
// Current context is overridden from passed in config.
func (c *Config) Merge(cfg *Config) []Rename {
	if c.Contexts == nil {
		c.Contexts = map[string]*Context{}
	}

	mappedContexts := map[string]string{}
	renames := []Rename{}

	for name, ctx := range cfg.Contexts {
		mergedName := name

		if _, exists := c.Contexts[mergedName]; exists {
			for i := 1; ; i++ {
				mergedName = fmt.Sprintf("%s-%d", name, i)

				if _, exists := c.Contexts[mergedName]; !exists {
					break
				}
			}
		}

		mappedContexts[name] = mergedName

		if name != mergedName {
			renames = append(renames, Rename{name, mergedName})
		}

		c.Contexts[mergedName] = ctx
	}

	if cfg.Context != "" {
		c.Context = mappedContexts[cfg.Context]
	}

	return renames
}

func ensure(filename string) (err error) {
	if _, err := os.Stat(filename); os.IsNotExist(err) {
		config := &Config{
			Context:  "",
			Contexts: map[string]*Context{},
		}

		return config.Save(filename)
	}

	return nil
}
