package main

import (
	"context"
	"os/user"
	"sync"
	"time"

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

type TokenSecret = vapi.Secret

type ttledSecret struct {
	*TokenSecret

	expiration time.Time

	isRenewable    bool
	renewThreshold time.Time
}

func wrapToken(s *TokenSecret) ttledSecret {
	now := time.Now()

	ttl := s.Auth.LeaseDuration
	renewable := s.Auth.Renewable

	var ttlBuffer time.Duration
	if ttl < 300 {
		// Give ourselves an extra two minute buffer for renewal.
		ttlBuffer = 2 * time.Minute
	}

	// DEBUG: debugging some annoying renewal(?) issues with tokend
	accessorData, _ := s.TokenAccessor()
	ttlData, _ := s.TokenTTL()
	log.Infof("wrapping token accessor %v with token TTL %v", accessorData, ttlData)

	return ttledSecret{
		TokenSecret: s,

		expiration: now.Add(time.Duration(ttl) * time.Second),

		isRenewable:    renewable,
		renewThreshold: now.Add(time.Duration(ttl/2) * time.Second).Add(-ttlBuffer),
	}
}

func (s ttledSecret) Expired() bool {
	// We use !After rather than Before so that if it's _exactly_ now then we still return true.
	return !s.expiration.After(time.Now())
}

func (s ttledSecret) ShouldRenew() bool {
	if !s.isRenewable {
		return false
	}

	return time.Now().After(s.renewThreshold)
}

type tokenInteractor interface {
	Revoke(ctx context.Context, tokenSecret *TokenSecret) error
	Issue(ctx context.Context, username string, isPlainUser bool) (*TokenSecret, error)
	Renew(ctx context.Context, tokenSecret *TokenSecret) (*TokenSecret, error)
}

type tokenUserCache struct {
	l sync.RWMutex
	m map[string]ttledSecret
	i tokenInteractor
}

func newCache(i tokenInteractor) *tokenUserCache {
	return &tokenUserCache{
		m: make(map[string]ttledSecret),
		i: i,
	}
}

func (c *tokenUserCache) expire() (revoke map[string]*TokenSecret) {
	c.l.Lock()
	defer c.l.Unlock()

	var remove []string
	revoke = make(map[string]*TokenSecret)
	for username, s := range c.m {
		// If the token has expired, remove it.
		if s.Expired() {
			remove = append(remove, username)
			continue
		}

		// If the user no longer exists on the system, revoke it.
		if _, err := user.Lookup(username); err != nil {
			log.Infof("token for %v will be revoked because user lookup returned error: %v", username, err)
			remove = append(remove, username)
			revoke[username] = s.TokenSecret
			continue
		}

		// Otherwise, leave it alone.
	}

	for _, username := range remove {
		delete(c.m, username)
	}

	return revoke
}

func (c *tokenUserCache) renew() {
	ctx := context.Background()

	toRenew := make(map[string]*TokenSecret)
	c.l.Lock()
	for username, s := range c.m {
		if s.ShouldRenew() {
			toRenew[username] = s.TokenSecret
		}
	}
	c.l.Unlock()

	for username, s := range toRenew {
		log.Infof("renewing token for %v", username)
		newS, err := c.i.Renew(ctx, s)
		if err != nil {
			log.Errorf("renewing token for %v: %w (discarding cached token)", username, err)

			// Discard the token in cache.
			c.l.Lock()
			delete(c.m, username)
			c.l.Unlock()
			continue
		}

		c.l.Lock()
		if oldS, ok := c.m[username]; ok && oldS.TokenSecret == s {
			c.m[username] = wrapToken(newS)
		} else if ok {
			log.Warningf("after renewing token for %v discovered that the token in cache had changed in the meantime; dropping refreshed token")
		}
		c.l.Unlock()
	}
}

func (c *tokenUserCache) tick(ctx context.Context) error {
	log.Info("token cache is ticking")
	revoke := c.expire()
	for username, tokenSecret := range revoke {
		if err := c.i.Revoke(ctx, tokenSecret); err != nil {
			log.Errorf("unable to revoke token for %v: %w", username, err)
		}
	}

	c.renew()

	return nil
}

func (c *tokenUserCache) Get(ctx context.Context, username string, isPlainUser bool) (*TokenSecret, error) {
	c.l.RLock()
	token, ok := c.m[username]
	c.l.RUnlock()

	// If we got a token, but it's expired, delete it and pretend it didn't exist.
	if ok && token.Expired() {
		c.l.Lock()
		delete(c.m, username)
		c.l.Unlock()

		ok = false
	}

	if ok {
		// We have a non-expired pre-existing token!
		return token.TokenSecret, nil
	}

	// Issue a new token.
	issuedToken, err := c.i.Issue(ctx, username, isPlainUser)
	if err != nil {
		return nil, err
	}

	// OK, now we check if someone has issued a token for us in the
	// meantime...  We could (but don't) do any more complex coordination
	// than this, so we might actually issue two tokens for the same user
	// concurrently. Even if we do this though, one of the tokens will
	// "win" and we'll revoke the other one.
	c.l.Lock()
	defer c.l.Unlock()

	if bgToken, ok := c.m[username]; ok && !bgToken.Expired() {
		// Just use this one, I guess.
		go c.i.Revoke(context.Background(), issuedToken)
		return bgToken.TokenSecret, nil
	}

	token = wrapToken(issuedToken)
	c.m[username] = token

	return token.TokenSecret, nil
}

func (c *tokenUserCache) Purge(ctx context.Context, username string) {
	c.l.Lock()
	token, ok := c.m[username]
	delete(c.m, username)
	c.l.Unlock()

	if !ok {
		return
	}

	// Revoke the token as well, for good measure, in case the parent hasn't already done that.
	c.i.Revoke(ctx, token.TokenSecret)
}