package acmeserver

import (
	"errors"
	"fmt"
	"io/ioutil"
	"log"
	"path/filepath"

	"gopkg.in/yaml.v2"

	"git.autistici.org/ai3/go-common/clientutil"
)

// Config holds the configuration for an acmeserver instance.
//
// The reason for supporting multiple config_dirs is to allow
// integration with third-party automation systems: in some cases,
// automation tools need control of an entire directory in order to
// safely delete entries that no longer exist.
//
// nolint: maligned
type Config struct {
	Testing              bool   `yaml:"testing"`
	DirectoryURL         string `yaml:"directory_url"`
	DefaultChallengeType string `yaml:"default_challenge"`
	UseRSA               bool   `yaml:"use_rsa"`
	RenewalDays          int    `yaml:"renewal_days"`

	AccountKeyPath string `yaml:"account_key_path"`
	Email          string `yaml:"email"`

	Dirs []string `yaml:"config_dirs"`

	Output struct {
		Path   string                    `yaml:"path"`
		ReplDS *clientutil.BackendConfig `yaml:"replds"`
	} `yaml:"output"`

	HTTP struct {
		Enabled bool `yaml:"enabled"`
	} `yaml:"http"`

	DNS struct {
		Enabled       bool     `yaml:"enabled"`
		Nameservers   []string `yaml:"nameservers"`
		TSIGKeyName   string   `yaml:"tsig_key_name"`
		TSIGKeyAlgo   string   `yaml:"tsig_key_algo"`
		TSIGKeySecret string   `yaml:"tsig_key_secret"`
	} `yaml:"dns"`
}

// This is all the configuration we need to generate a certificate.
type certConfig struct {
	// List of names for this certificate. The first one will be
	// the certificate CN, all of them will be subjectAltNames.
	Names []string `yaml:"names"`

	// Challenge type to use for this domain. If empty, the
	// defaults will be applied.
	ChallengeType string `yaml:"challenge,omitempty"`
}

func (c *certConfig) check() error {
	if len(c.Names) == 0 {
		return errors.New("empty names list")
	}
	// We can't check here if the challenge type is actually
	// configured or not, but at least we can report unknown /
	// unsupported types as syntax errors.
	switch c.ChallengeType {
	case "", dns01Challenge, http01Challenge:
	default:
		return fmt.Errorf("unkown or unsupported challenge type '%s'", c.ChallengeType)
	}
	return nil
}

func readCertConfigs(path string) ([]*certConfig, error) {
	data, err := ioutil.ReadFile(path) // nolint: gosec
	if err != nil {
		return nil, err
	}
	var cc []*certConfig
	if err := yaml.Unmarshal(data, &cc); err != nil {
		return nil, err
	}
	return cc, nil
}

func readCertConfigsFromDir(dir string, certs []*certConfig) ([]*certConfig, error) {
	files, err := filepath.Glob(filepath.Join(dir, "*.yml"))
	if err != nil {
		return nil, err
	}
	for _, f := range files {
		cc, err := readCertConfigs(f)
		if err != nil {
			log.Printf("error reading %s: %v", f, err)
			continue
		}
		// Validate the cert configs, skip ones with errors.
		for _, c := range cc {
			if err := c.check(); err != nil {
				log.Printf("configuration error in %s: %v", f, err)
				continue
			}
			certs = append(certs, c)
		}
	}
	return certs, nil
}

func readCertConfigsFromDirs(dirs []string) ([]*certConfig, error) {
	var out []*certConfig
	for _, dir := range dirs {
		certs, err := readCertConfigsFromDir(dir, out)
		if err != nil {
			return nil, err
		}
		out = certs
	}
	return out, nil
}