package sink

import (
	"context"
	"errors"
	"fmt"
	"io/ioutil"
	"math/rand"
	"os"
	"sync/atomic"
	"time"

	hclog "github.com/hashicorp/go-hclog"
	"github.com/hashicorp/vault/api"
	"github.com/hashicorp/vault/helper/dhutil"
	"github.com/hashicorp/vault/sdk/helper/jsonutil"
)

type Sink interface {
	WriteToken(string) error
}

type SinkReader interface {
	Token() string
}

type SinkConfig struct {
	Sink
	Logger             hclog.Logger
	Config             map[string]interface{}
	Client             *api.Client
	WrapTTL            time.Duration
	DHType             string
	DHPath             string
	DeriveKey          bool
	AAD                string
	cachedRemotePubKey []byte
	cachedPubKey       []byte
	cachedPriKey       []byte
}

type SinkServerConfig struct {
	Logger        hclog.Logger
	Client        *api.Client
	Context       context.Context
	ExitAfterAuth bool
}

// SinkServer is responsible for pushing tokens to sinks
type SinkServer struct {
	logger        hclog.Logger
	client        *api.Client
	random        *rand.Rand
	exitAfterAuth bool
	remaining     *int32
}

func NewSinkServer(conf *SinkServerConfig) *SinkServer {
	ss := &SinkServer{
		logger:        conf.Logger,
		client:        conf.Client,
		random:        rand.New(rand.NewSource(int64(time.Now().Nanosecond()))),
		exitAfterAuth: conf.ExitAfterAuth,
		remaining:     new(int32),
	}

	return ss
}

// Run executes the server's run loop, which is responsible for reading
// in new tokens and pushing them out to the various sinks.
func (ss *SinkServer) Run(ctx context.Context, incoming chan string, sinks []*SinkConfig) error {
	latestToken := new(string)
	writeSink := func(currSink *SinkConfig, currToken string) error {
		if currToken != *latestToken {
			return nil
		}
		var err error

		if currSink.WrapTTL != 0 {
			if currToken, err = currSink.wrapToken(ss.client, currSink.WrapTTL, currToken); err != nil {
				return err
			}
		}

		if currSink.DHType != "" {
			if currToken, err = currSink.encryptToken(currToken); err != nil {
				return err
			}
		}

		return currSink.WriteToken(currToken)
	}

	if incoming == nil {
		return errors.New("sink server: incoming channel is nil")
	}

	ss.logger.Info("starting sink server")
	defer func() {
		ss.logger.Info("sink server stopped")
	}()

	type sinkToken struct {
		sink  *SinkConfig
		token string
	}
	sinkCh := make(chan sinkToken, len(sinks))
	for {
		select {
		case <-ctx.Done():
			return nil

		case token := <-incoming:
			if len(sinks) > 0 {
				if token != *latestToken {

					// Drain the existing funcs
				drainLoop:
					for {
						select {
						case <-sinkCh:
							atomic.AddInt32(ss.remaining, -1)
						default:
							break drainLoop
						}
					}

					*latestToken = token

					for _, s := range sinks {
						atomic.AddInt32(ss.remaining, 1)
						sinkCh <- sinkToken{s, token}
					}
				}
			} else {
				ss.logger.Trace("no sinks, ignoring new token")
				if ss.exitAfterAuth {
					ss.logger.Trace("no sinks, exitAfterAuth, bye")
					return nil
				}
			}
		case st := <-sinkCh:
			atomic.AddInt32(ss.remaining, -1)
			select {
			case <-ctx.Done():
				return nil
			default:
			}

			if err := writeSink(st.sink, st.token); err != nil {
				backoff := 2*time.Second + time.Duration(ss.random.Int63()%int64(time.Second*2)-int64(time.Second))
				ss.logger.Error("error returned by sink function, retrying", "error", err, "backoff", backoff.String())
				select {
				case <-ctx.Done():
					return nil
				case <-time.After(backoff):
					atomic.AddInt32(ss.remaining, 1)
					sinkCh <- st
				}
			} else {
				if atomic.LoadInt32(ss.remaining) == 0 && ss.exitAfterAuth {
					return nil
				}
			}
		}
	}
}

func (s *SinkConfig) encryptToken(token string) (string, error) {
	var aesKey []byte
	var err error
	resp := new(dhutil.Envelope)
	switch s.DHType {
	case "curve25519":
		if len(s.cachedRemotePubKey) == 0 {
			_, err = os.Lstat(s.DHPath)
			if err != nil {
				if !os.IsNotExist(err) {
					return "", fmt.Errorf("error stat-ing dh parameters file: %w", err)
				}
				return "", errors.New("no dh parameters file found, and no cached pub key")
			}
			fileBytes, err := ioutil.ReadFile(s.DHPath)
			if err != nil {
				return "", fmt.Errorf("error reading file for dh parameters: %w", err)
			}
			theirPubKey := new(dhutil.PublicKeyInfo)
			if err := jsonutil.DecodeJSON(fileBytes, theirPubKey); err != nil {
				return "", fmt.Errorf("error decoding public key: %w", err)
			}
			if len(theirPubKey.Curve25519PublicKey) == 0 {
				return "", errors.New("public key is nil")
			}
			s.cachedRemotePubKey = theirPubKey.Curve25519PublicKey
		}
		if len(s.cachedPubKey) == 0 {
			s.cachedPubKey, s.cachedPriKey, err = dhutil.GeneratePublicPrivateKey()
			if err != nil {
				return "", fmt.Errorf("error generating pub/pri curve25519 keys: %w", err)
			}
		}
		resp.Curve25519PublicKey = s.cachedPubKey
	}

	secret, err := dhutil.GenerateSharedSecret(s.cachedPriKey, s.cachedRemotePubKey)
	if err != nil {
		return "", fmt.Errorf("error calculating shared key: %w", err)
	}
	if s.DeriveKey {
		aesKey, err = dhutil.DeriveSharedKey(secret, s.cachedPubKey, s.cachedRemotePubKey)
	} else {
		aesKey = secret
	}

	if err != nil {
		return "", fmt.Errorf("error deriving shared key: %w", err)
	}
	if len(aesKey) == 0 {
		return "", errors.New("derived AES key is empty")
	}

	resp.EncryptedPayload, resp.Nonce, err = dhutil.EncryptAES(aesKey, []byte(token), []byte(s.AAD))
	if err != nil {
		return "", fmt.Errorf("error encrypting with shared key: %w", err)
	}
	m, err := jsonutil.EncodeJSON(resp)
	if err != nil {
		return "", fmt.Errorf("error encoding encrypted payload: %w", err)
	}

	return string(m), nil
}

func (s *SinkConfig) wrapToken(client *api.Client, wrapTTL time.Duration, token string) (string, error) {
	wrapClient, err := client.CloneWithHeaders()
	if err != nil {
		return "", fmt.Errorf("error deriving client for wrapping, not writing out to sink: %w)", err)
	}

	wrapClient.SetToken(token)
	wrapClient.SetWrappingLookupFunc(func(string, string) string {
		return wrapTTL.String()
	})

	secret, err := wrapClient.Logical().Write("sys/wrapping/wrap", map[string]interface{}{
		"token": token,
	})
	if err != nil {
		return "", fmt.Errorf("error wrapping token, not writing out to sink: %w)", err)
	}
	if secret == nil {
		return "", errors.New("nil secret returned, not writing out to sink")
	}
	if secret.WrapInfo == nil {
		return "", errors.New("nil wrap info returned, not writing out to sink")
	}

	m, err := jsonutil.EncodeJSON(secret.WrapInfo)
	if err != nil {
		return "", fmt.Errorf("error marshaling token, not writing out to sink: %w)", err)
	}

	return string(m), nil
}
