package bolt

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

	bolt "github.com/coreos/bbolt"
	influxdb "github.com/influxdata/influxdb"
)

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

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

func (c *Client) initializeSecretService(ctx context.Context, tx *bolt.Tx) error {
	if _, err := tx.CreateBucketIfNotExists([]byte(secretBucket)); err != nil {
		return err
	}
	return nil
}

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

		v = val
		return nil
	})

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

	return v, nil
}

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

	val := tx.Bucket(secretBucket).Get(key)
	if len(val) == 0 {
		return "", &influxdb.Error{
			Code: influxdb.ENotFound,
			Msg:  influxdb.ErrSecretNotFound,
		}
	}

	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 (c *Client) GetSecretKeys(ctx context.Context, orgID influxdb.ID) ([]string, error) {
	var vs []string
	err := c.db.View(func(tx *bolt.Tx) error {
		vals, err := c.getSecretKeys(ctx, tx, orgID)
		if err != nil {
			return err
		}

		vs = vals
		return nil
	})

	if err != nil {
		return nil, err
	}

	return vs, nil
}

func (c *Client) getSecretKeys(ctx context.Context, tx *bolt.Tx, orgID influxdb.ID) ([]string, error) {
	cur := tx.Bucket(secretBucket).Cursor()
	prefix, err := orgID.Encode()
	if err != nil {
		return nil, err
	}
	k, _ := cur.Seek(prefix)

	if len(k) == 0 {
		return []string{}, nil
	}

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

	if id != orgID {
		return nil, fmt.Errorf("organization has no secret keys")
	}

	keys := []string{key}

	for {
		k, _ = cur.Next()

		if len(k) == 0 {
			// We've reached the end of the keys so we're done
			break
		}

		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 (c *Client) PutSecret(ctx context.Context, orgID influxdb.ID, k, v string) error {
	return c.db.Update(func(tx *bolt.Tx) error {
		return c.putSecret(ctx, tx, orgID, k, v)
	})
}

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

	val := encodeSecretValue(v)

	if err := tx.Bucket(secretBucket).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 := make([]byte, base64.StdEncoding.DecodedLen(len(val)))
	if _, err := base64.StdEncoding.Decode(v, val); 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 (c *Client) PutSecrets(ctx context.Context, orgID influxdb.ID, m map[string]string) error {
	return c.db.Update(func(tx *bolt.Tx) error {
		keys, err := c.getSecretKeys(ctx, tx, orgID)
		if err != nil {
			return err
		}
		for k, v := range m {
			if err := c.putSecret(ctx, tx, orgID, k, v); err != nil {
				return err
			}
		}
		for _, k := range keys {
			if _, ok := m[k]; !ok {
				if err := c.deleteSecret(ctx, tx, orgID, k); err != nil {
					return err
				}
			}
		}
		return nil
	})
}

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

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

func (c *Client) deleteSecret(ctx context.Context, tx *bolt.Tx, orgID influxdb.ID, k string) error {
	key, err := encodeSecretKey(orgID, k)
	if err != nil {
		return err
	}
	return tx.Bucket(secretBucket).Delete(key)
}