package kv

import (
	"context"
	"encoding/base64"
	"errors"

	"github.com/influxdata/influxdb/v2"
)

var (
	secretBucket = []byte("secretsv1")
)

var _ influxdb.SecretService = (*Service)(nil)

func (s *Service) initializeSecrets(ctx context.Context, tx Tx) error {
	if _, err := tx.Bucket(secretBucket); err != nil {
		return err
	}
	return nil
}

// LoadSecret retrieves the secret value v found at key k for organization orgID.
func (s *Service) LoadSecret(ctx context.Context, orgID influxdb.ID, k string) (string, error) {
	var v string
	err := s.kv.View(ctx, func(tx Tx) error {
		val, err := s.loadSecret(ctx, tx, orgID, k)
		if err != nil {
			return err
		}

		v = val
		return nil
	})

	if err != nil {
		return "", err
	}

	return v, nil
}

func (s *Service) loadSecret(ctx context.Context, tx Tx, orgID influxdb.ID, k string) (string, error) {
	key, err := encodeSecretKey(orgID, k)
	if err != nil {
		return "", err
	}

	b, err := tx.Bucket(secretBucket)
	if err != nil {
		return "", err
	}

	val, err := b.Get(key)
	if IsNotFound(err) {
		return "", &influxdb.Error{
			Code: influxdb.ENotFound,
			Msg:  influxdb.ErrSecretNotFound,
		}
	}

	if err != nil {
		return "", err
	}

	v, err := decodeSecretValue(val)
	if err != nil {
		return "", err
	}

	return v, nil
}

// GetSecretKeys retrieves all secret keys that are stored for the organization orgID.
func (s *Service) GetSecretKeys(ctx context.Context, orgID influxdb.ID) ([]string, error) {
	var vs []string
	err := s.kv.View(ctx, func(tx Tx) error {
		vals, err := s.getSecretKeys(ctx, tx, orgID)
		if err != nil {
			return err
		}

		vs = vals
		return nil
	})

	if err != nil {
		return nil, err
	}

	return vs, nil
}

func (s *Service) getSecretKeys(ctx context.Context, tx Tx, orgID influxdb.ID) ([]string, error) {
	b, err := tx.Bucket(secretBucket)
	if err != nil {
		return nil, err
	}

	prefix, err := orgID.Encode()
	if err != nil {
		return nil, err
	}

	cur, err := b.ForwardCursor(prefix, WithCursorPrefix(prefix))
	if err != nil {
		return nil, err
	}

	keys := []string{}

	for k, _ := cur.Next(); k != nil; k, _ = cur.Next() {

		id, key, err := decodeSecretKey(k)
		if err != nil {
			return nil, err
		}

		if id != orgID {
			// We've reached the end of the keyspace for the provided orgID
			break
		}

		keys = append(keys, key)
	}

	return keys, nil
}

// PutSecret stores the secret pair (k,v) for the organization orgID.
func (s *Service) PutSecret(ctx context.Context, orgID influxdb.ID, k, v string) error {
	return s.kv.Update(ctx, func(tx Tx) error {
		return s.putSecret(ctx, tx, orgID, k, v)
	})
}

func (s *Service) putSecret(ctx context.Context, tx Tx, orgID influxdb.ID, k, v string) error {
	key, err := encodeSecretKey(orgID, k)
	if err != nil {
		return err
	}

	val := encodeSecretValue(v)

	b, err := tx.Bucket(secretBucket)
	if err != nil {
		return err
	}

	if err := b.Put(key, val); err != nil {
		return err
	}

	return nil
}

func encodeSecretKey(orgID influxdb.ID, k string) ([]byte, error) {
	buf, err := orgID.Encode()
	if err != nil {
		return nil, err
	}

	key := make([]byte, 0, influxdb.IDLength+len(k))
	key = append(key, buf...)
	key = append(key, k...)

	return key, nil
}

func decodeSecretKey(key []byte) (influxdb.ID, string, error) {
	if len(key) < influxdb.IDLength {
		// This should not happen.
		return influxdb.InvalidID(), "", errors.New("provided key is too short to contain an ID (please report this error)")
	}

	var id influxdb.ID
	if err := id.Decode(key[:influxdb.IDLength]); err != nil {
		return influxdb.InvalidID(), "", err
	}

	k := string(key[influxdb.IDLength:])

	return id, k, nil
}

func decodeSecretValue(val []byte) (string, error) {
	// store the secret value base64 encoded so that it's marginally better than plaintext
	v, err := base64.StdEncoding.DecodeString(string(val))
	if err != nil {
		return "", err
	}

	return string(v), nil
}

func encodeSecretValue(v string) []byte {
	val := make([]byte, base64.StdEncoding.EncodedLen(len(v)))
	base64.StdEncoding.Encode(val, []byte(v))
	return val
}

// PutSecrets puts all provided secrets and overwrites any previous values.
func (s *Service) PutSecrets(ctx context.Context, orgID influxdb.ID, m map[string]string) error {
	return s.kv.Update(ctx, func(tx Tx) error {
		keys, err := s.getSecretKeys(ctx, tx, orgID)
		if err != nil {
			return err
		}
		for k, v := range m {
			if err := s.putSecret(ctx, tx, orgID, k, v); err != nil {
				return err
			}
		}
		for _, k := range keys {
			if _, ok := m[k]; !ok {
				if err := s.deleteSecret(ctx, tx, orgID, k); err != nil {
					return err
				}
			}
		}
		return nil
	})
}

// PatchSecrets patches all provided secrets and updates any previous values.
func (s *Service) PatchSecrets(ctx context.Context, orgID influxdb.ID, m map[string]string) error {
	return s.kv.Update(ctx, func(tx Tx) error {
		for k, v := range m {
			if err := s.putSecret(ctx, tx, orgID, k, v); err != nil {
				return err
			}
		}
		return nil
	})
}

// DeleteSecret removes secrets from the secret store.
func (s *Service) DeleteSecret(ctx context.Context, orgID influxdb.ID, ks ...string) error {
	return s.kv.Update(ctx, func(tx Tx) error {
		for _, k := range ks {
			if err := s.deleteSecret(ctx, tx, orgID, k); err != nil {
				return err
			}
		}
		return nil
	})
}

func (s *Service) deleteSecret(ctx context.Context, tx Tx, orgID influxdb.ID, k string) error {
	key, err := encodeSecretKey(orgID, k)
	if err != nil {
		return err
	}

	b, err := tx.Bucket(secretBucket)
	if err != nil {
		return err
	}

	return b.Delete(key)
}
