package macaroons

import (
	"crypto/rand"
	"fmt"
	"io"

	"github.com/boltdb/bolt"
)

const (
	// RootKeyLen is the length of a root key.
	RootKeyLen = 32
)

var (
	// rootKeyBucketName is the name of the root key store bucket.
	rootKeyBucketName = []byte("macrootkeys")

	// defaultRootKeyID is the ID of the default root key. The first is
	// just 0, to emulate the memory storage that comes with bakery.
	//
	// TODO(aakselrod): Add support for key rotation.
	defaultRootKeyID = "0"

	// macaroonBucketName is the name of the macaroon store bucket.
	macaroonBucketName = []byte("macaroons")
)

// RootKeyStorage implements the bakery.RootKeyStorage interface.
type RootKeyStorage struct {
	*bolt.DB
}

// NewRootKeyStorage creates a RootKeyStorage instance.
// TODO(aakselrod): Add support for encryption of data with passphrase.
func NewRootKeyStorage(db *bolt.DB) (*RootKeyStorage, error) {
	// If the store's bucket doesn't exist, create it.
	err := db.Update(func(tx *bolt.Tx) error {
		_, err := tx.CreateBucketIfNotExists(rootKeyBucketName)
		return err
	})
	if err != nil {
		return nil, err
	}

	// Return the DB wrapped in a RootKeyStorage object.
	return &RootKeyStorage{db}, nil
}

// Get implements the Get method for the bakery.RootKeyStorage interface.
func (r *RootKeyStorage) Get(id string) ([]byte, error) {
	var rootKey []byte
	err := r.View(func(tx *bolt.Tx) error {
		dbKey := tx.Bucket(rootKeyBucketName).Get([]byte(id))
		if len(dbKey) == 0 {
			return fmt.Errorf("root key with id %s doesn't exist",
				id)
		}

		rootKey = make([]byte, len(dbKey))
		copy(rootKey[:], dbKey)
		return nil
	})
	if err != nil {
		return nil, err
	}

	return rootKey, nil
}

// RootKey implements the RootKey method for the bakery.RootKeyStorage
// interface.
// TODO(aakselrod): Add support for key rotation.
func (r *RootKeyStorage) RootKey() ([]byte, string, error) {
	var rootKey []byte
	id := defaultRootKeyID
	err := r.Update(func(tx *bolt.Tx) error {
		ns := tx.Bucket(rootKeyBucketName)
		rootKey = ns.Get([]byte(id))

		// If there's no root key stored in the bucket yet, create one.
		if len(rootKey) != 0 {
			return nil
		}

		// Create a RootKeyLen-byte root key.
		rootKey = make([]byte, RootKeyLen)
		if _, err := io.ReadFull(rand.Reader, rootKey[:]); err != nil {
			return err
		}
		return ns.Put([]byte(id), rootKey)
	})
	if err != nil {
		return nil, "", err
	}

	return rootKey, id, nil
}

// Storage implements the bakery.Storage interface.
type Storage struct {
	*bolt.DB
}

// NewStorage creates a Storage instance.
//
// TODO(aakselrod): Add support for encryption of data with passphrase.
func NewStorage(db *bolt.DB) (*Storage, error) {
	// If the store's bucket doesn't exist, create it.
	err := db.Update(func(tx *bolt.Tx) error {
		_, err := tx.CreateBucketIfNotExists(macaroonBucketName)
		return err
	})
	if err != nil {
		return nil, err
	}

	// Return the DB wrapped in a Storage object.
	return &Storage{db}, nil
}

// Put implements the Put method for the bakery.Storage interface.
func (s *Storage) Put(location string, item string) error {
	return s.Update(func(tx *bolt.Tx) error {
		return tx.Bucket(macaroonBucketName).Put([]byte(location),
			[]byte(item))
	})
}

// Get implements the Get method for the bakery.Storage interface.
func (s *Storage) Get(location string) (string, error) {
	var item []byte
	err := s.View(func(tx *bolt.Tx) error {
		itemBytes := tx.Bucket(macaroonBucketName).Get([]byte(location))
		if len(itemBytes) == 0 {
			return fmt.Errorf("couldn't get item for location %s",
				location)
		}

		item = make([]byte, len(itemBytes))
		copy(item, itemBytes)
		return nil
	})
	if err != nil {
		return "", err
	}

	return string(item), nil
}

// Del implements the Del method for the bakery.Storage interface.
func (s *Storage) Del(location string) error {
	return s.Update(func(tx *bolt.Tx) error {
		return tx.Bucket(macaroonBucketName).Delete([]byte(location))
	})
}
