// Copyright 2018 Drone.IO Inc
// Use of this software is governed by the Drone Enterpise License
// that can be found in the LICENSE file.

package vault

import (
	"errors"
	"path"
	"strings"
	"time"

	"github.com/Sirupsen/logrus"
	"github.com/drone/drone/model"
	"github.com/drone/drone/plugins/secrets"

	"github.com/hashicorp/vault/api"
	"gopkg.in/yaml.v2"
)

// yaml configuration representation
//
//    secrets:
//      docker_username:
//        file: path/to/docker/username
//      docker_password:
//        file: path/to/docker/password
//
type vaultConfig struct {
	Secrets map[string]struct {
		Driver     string
		DriverOpts struct {
			Path string
			Key  string
		} `yaml:"driver_opts"`

		// deprecated. do not use.
		Vault string
		Path  string
		File  string
	}
}

type vault struct {
	store    model.ConfigStore
	client   *api.Client
	ttl      time.Duration
	renew    time.Duration
	auth     string
	kubeAuth kubeAuth
	done     chan struct{}
}

type kubeAuth struct {
	addr, role, mount string
}

// New returns a new store with secrets loaded from vault.
func New(store model.ConfigStore, opts ...Opts) (secrets.Plugin, error) {
	client, err := api.NewClient(nil)
	if err != nil {
		return nil, err
	}
	v := &vault{
		store:  store,
		client: client,
	}
	for _, opt := range opts {
		opt(v)
	}
	if v.auth == "kubernetes" {
		err = v.initKubernetes()
		if err != nil {
			return nil, err
		}
	}
	v.start() // start the refresh process.
	return v, nil
}

func (v *vault) initKubernetes() error {
	if v.renew == 0 {
		return errors.New("vault: token renewal not configured")
	}
	token, ttl, err := getKubernetesToken(
		v.kubeAuth.addr,
		v.kubeAuth.role,
		v.kubeAuth.mount,
		"/var/run/secrets/kubernetes.io/serviceaccount/token",
	)
	if err != nil {
		logrus.Debugf("vault: failed to obtain token via kubernetes-auth backend: %s", err)
		return err
	}

	v.client.SetToken(token)
	v.ttl = ttl
	return nil
}

func (v *vault) SecretListBuild(repo *model.Repo, build *model.Build) ([]*model.Secret, error) {
	return v.list(repo, build)
}

func (v *vault) list(repo *model.Repo, build *model.Build) ([]*model.Secret, error) {
	conf, err := v.store.ConfigLoad(build.ConfigID)
	if err != nil {
		return nil, err
	}
	var (
		in  = []byte(conf.Data)
		out = new(vaultConfig)

		secrets []*model.Secret
	)
	err = yaml.Unmarshal(in, out)
	if err != nil {
		return nil, err
	}
	for key, val := range out.Secrets {
		var path, field string
		switch {
		case val.Path != "":
			path = val.Path
		case val.File != "":
			path = val.File
		case val.Vault != "":
			path = val.Vault
		case val.DriverOpts.Path != "":
			path = val.DriverOpts.Path
			field = val.DriverOpts.Key
		}
		if field == "" {
			field = "value"
		}

		if path == "" {
			continue
		}

		logrus.Debugf("vault: read secret: %s", path)

		vaultSecret, err := v.get(path, field)
		if err != nil {
			logrus.Debugf("vault: read secret failed: %s: %s", path, err)
			return nil, err
		}
		if vaultSecret == nil {
			logrus.Debugf("vault: read secret failed: %s: not found or empty value", path)
			continue
		}
		if !vaultSecret.Match(repo.FullName) {
			logrus.Debugf("vault: read secret: %s: restricted: %s", path, repo.FullName)
			continue
		}

		logrus.Debugf("vault: read secret success: %s", err)

		secrets = append(secrets, &model.Secret{
			Name:   key,
			Value:  vaultSecret.Value,
			Events: vaultSecret.Event,
			Images: vaultSecret.Image,
		})
	}
	return secrets, nil
}

func (v *vault) get(path, key string) (*vaultSecret, error) {
	secret, err := v.client.Logical().Read(path)
	if err != nil {
		return nil, err
	}
	if secret == nil || secret.Data == nil {
		return nil, nil
	}
	return parseVaultSecret(secret.Data, key), nil
}

// start starts the renewal loop.
func (v *vault) start() {
	if v.renew == 0 || v.ttl == 0 {
		logrus.Debugf("vault: token renewal disabled")
		return
	}
	if v.done != nil {
		close(v.done)
	}
	logrus.Infof("vault: token renewal enabled: renew every %v", v.renew)
	v.done = make(chan struct{})
	if v.renew != 0 {
		go v.renewLoop()
	}
}

// stop stops the renewal loop.
func (v *vault) stop() {
	close(v.done)
}

func (v *vault) renewLoop() {
	for {
		select {
		case <-time.After(v.renew):
			incr := int(v.ttl / time.Second)

			logrus.Debugf("vault: refreshing token: increment %v", v.ttl)
			_, err := v.client.Auth().Token().RenewSelf(incr)
			if err != nil {
				logrus.Errorf("vault: refreshing token failed: %s", err)
			} else {
				logrus.Debugf("vault: refreshing token succeeded")
			}
		case <-v.done:
			return
		}
	}
}

type vaultSecret struct {
	Value string
	Image []string
	Event []string
	Repo  []string
}

func parseVaultSecret(data map[string]interface{}, key string) *vaultSecret {
	secret := new(vaultSecret)

	if vvalue, ok := data[key]; ok {
		if svalue, ok := vvalue.(string); ok {
			secret.Value = svalue
		}
	}
	if vimage, ok := data["image"]; ok {
		if simage, ok := vimage.(string); ok {
			secret.Image = strings.Split(simage, ",")
		}
	}
	if vevent, ok := data["event"]; ok {
		if sevent, ok := vevent.(string); ok {
			secret.Event = strings.Split(sevent, ",")
		}
	}
	if vrepo, ok := data["repo"]; ok {
		if srepo, ok := vrepo.(string); ok {
			secret.Repo = strings.Split(srepo, ",")
		}
	}
	if secret.Event == nil {
		secret.Event = []string{}
	}
	if secret.Image == nil {
		secret.Image = []string{}
	}
	if secret.Repo == nil {
		secret.Repo = []string{}
	}
	return secret
}

func (v *vaultSecret) Match(name string) bool {
	if len(v.Repo) == 0 {
		return true
	}
	for _, pattern := range v.Repo {
		if ok, _ := path.Match(pattern, name); ok {
			return true
		}
	}
	return false
}