package main

import (
	"context"
	"flag"
	"fmt"
	"regexp"
	"time"

	log "github.com/golang/glog"
	vapi "github.com/hashicorp/vault/api"
)

var (
	vaultTokenTTL       = flag.String("vault_token_ttl", "20m", "TTL at which token must be renewed.")
	vaultTokenMaxTTL    = flag.String("vault_token_max_ttl", "1d", "TTL at which token must be reissued.")
	vaultTokenRenewable = flag.Bool("vault_token_renewable", true, "Whether the tokens are renewable or reissued every time.")
)

type vaultTokenInteractor struct {
	v *vapi.Client
}

var _ tokenInteractor = ((*vaultTokenInteractor)(nil))

func (i *vaultTokenInteractor) Revoke(ctx context.Context, tokenSecret *TokenSecret) error {
	// Use the token, since we have it, to revoke itself.
	c, err := i.v.Clone()
	if err != nil {
		return fmt.Errorf("generating a new Vault client: %w", err)
	}

	token, err := tokenSecret.TokenID()
	if err != nil {
		return fmt.Errorf("getting token from secret: %w", err)
	}

	c.SetToken(token)

	// Since we have the token, we could potentially use /revoke, or even /revoke-self.
	if err := c.Auth().Token().RevokeSelf(""); err != nil {
		return fmt.Errorf("revoking token: %w", err)
	}
	return nil
}

func computePolicies(selfPolicies []string, username string, isPlainUser bool) []string {
	appMatchRE := regexp.MustCompile(fmt.Sprintf(`^(server/[^/]+/)?app/%s$`, regexp.QuoteMeta(username)))
	userMatchRE := regexp.MustCompile(fmt.Sprintf(`^(server/[^/]+/user|server-user)(/%s)?$`, regexp.QuoteMeta(username)))

	var outPolicies []string
	for _, p := range selfPolicies {
		if p == "default" || appMatchRE.MatchString(p) || (isPlainUser && userMatchRE.MatchString(p)) {
			outPolicies = append(outPolicies, p)
		}
	}
	return outPolicies
}

func policiesForToken(ts *TokenSecret) ([]string, error) {
	var ps []string
	psIntf, ok := ts.Data["policies"].([]interface{})
	if !ok {
		return nil, fmt.Errorf("policies not present or not expected []interface{} type (was %T)", ts.Data["policies"])
	}
	for _, p := range psIntf {
		ps = append(ps, p.(string))
	}
	return ps, nil
}

func (i *vaultTokenInteractor) Issue(ctx context.Context, username string, isPlainUser bool) (*TokenSecret, error) {
	// Look up our own token to work out what policies we might want to apply.
	// TODO: maybe consider caching this so we don't need to do it every time?
	self, err := i.v.Auth().Token().LookupSelf()
	if err != nil {
		return nil, fmt.Errorf("looking up server-wide token: %w", err)
	}
	selfPolicies, err := policiesForToken(self)
	if err != nil {
		return nil, fmt.Errorf("retrieving self policies: %w", err)
	}

	wantPolicies := computePolicies(selfPolicies, username, isPlainUser)
	log.Infof("policies for %v: %v", username, wantPolicies)

	return i.v.Auth().Token().Create(&vapi.TokenCreateRequest{
		// TODO: extend rather than replace metadata?
		Metadata: map[string]string{
			"app": username,
		},
		TTL:            *vaultTokenTTL,
		ExplicitMaxTTL: *vaultTokenMaxTTL,
		Renewable:      vaultTokenRenewable,
		Policies:       wantPolicies,
	})
}

func (i *vaultTokenInteractor) Renew(ctx context.Context, tokenSecret *TokenSecret) (*TokenSecret, error) {
	token, err := tokenSecret.TokenID()
	if err != nil {
		return nil, fmt.Errorf("getting token from secret: %w", err)
	}

	return i.v.Auth().Token().RenewTokenAsSelf(token, int((20 * time.Minute).Seconds()))
}