package database

import (
	"time"

	"github.com/Xe/ln"
	"github.com/Xe/uuid"
	"github.com/asdine/storm"
	"github.com/brandur/simplebox"
	"github.com/pkg/errors"
	"golang.org/x/net/context"
)

// Database errors
var (
	ErrNotImplemented     = errors.New("database: not implemented")
	ErrInvalidKind        = errors.New("database: invalid route kind")
	ErrRouteAlreadyExists = errors.New("database: route already exists")
	ErrTokenAleradyExists = errors.New("database: token already exists")
	ErrNoSuchRoute        = errors.New("database: no such route")
	ErrNoSuchToken        = errors.New("database: no such token")
	ErrCantDecryptCert    = errors.New("database: can't decrypt cert")
	ErrUnknownCryptMethod = errors.New("database: unknown encryption method")
	ErrUnknown            = errors.New("database: unknown error")
)

// BoltDBStorage is a backend that uses https://github.com/boltdb/bolt to store
// route data.
type BoltDBStorage struct {
	db *storm.DB
	sb *simplebox.SimpleBox
}

// NewBoltStorage creates a new Storage instance backed by BoltDB + Storm.
func NewBoltStorage(path string, key *[32]byte) (Storage, error) {
	db, err := storm.Open(path)
	if err != nil {
		return nil, err
	}

	b := &BoltDBStorage{
		db: db,
		sb: simplebox.NewFromSecretKey(key),
	}

	return Storage(b), nil
}

// interface compliance
var (
	_ Storage = &BoltDBStorage{}
)

func (b *BoltDBStorage) GetRoute(ctx context.Context, id string) (Route, error) {
	return b.getRouteBy(ctx, "ID", id)
}

func (b *BoltDBStorage) GetRouteHost(ctx context.Context, id string) (Route, error) {
	return b.getRouteBy(ctx, "Hostname", id)
}

// getRouteBy gets a single route out of the database by a given field data.
func (b *BoltDBStorage) getRouteBy(ctx context.Context, match, val string) (Route, error) {
	r := Route{}
	err := b.db.One(match, val, &r)
	if err != nil {
		ln.Error(ctx, err, ln.Action("get route"), ln.F{"match": match, "val": val})

		switch err {
		case storm.ErrNotFound:
			return Route{}, errors.Wrapf(err, "%v", ErrNoSuchRoute)
		case storm.ErrAlreadyExists:
			return Route{}, errors.Wrapf(err, "%v", ErrRouteAlreadyExists)
		default:
			return Route{}, errors.Wrapf(err, "%v", ErrUnknown)
		}
	}

	return r, nil
}

// GetAllRoutes gets all routes out of the database for a given user by username.
func (b *BoltDBStorage) GetAllRoutes(ctx context.Context, user string) ([]Route, error) {
	rs := []Route{}
	err := b.db.All(&rs)
	return rs, err
}

// PutRoute creates a new route in the database.
func (b *BoltDBStorage) PutRoute(ctx context.Context, domain, creator string) (Route, error) {
	r := Route{
		ID:       uuid.New(),
		Creator:  creator,
		Hostname: domain,
	}

	err := b.db.Save(&r)
	if err != nil {
		return Route{}, err
	}
	defer b.db.Commit()

	ln.Log(ctx, r, ln.Action("new route created in database"))

	return r, err
}

// DeleteRoute removes a route from the database.
func (b *BoltDBStorage) DeleteRoute(ctx context.Context, id string) error {
	r := Route{}
	err := b.db.One("ID", id, &r)
	if err != nil {
		return err
	}
	defer b.db.Commit()

	ln.Log(ctx, r, ln.Action("route deleted from database"))

	return b.db.DeleteStruct(&r)
}

// GetToken fetches a token from the database. This is mainly used in validation
// of tokens.
func (b *BoltDBStorage) GetToken(ctx context.Context, token string) (Token, error) {
	t := Token{}
	err := b.db.One("Body", token, &t)
	return t, err
}

// GetTokenID fetches a token by a given token ID.
func (b *BoltDBStorage) GetTokenID(ctx context.Context, id string) (Token, error) {
	t := Token{}
	err := b.db.One("ID", id, &t)
	if err != nil {
		switch err {
		case storm.ErrNotFound:
			return Token{}, ErrNoSuchToken
		default:
			return Token{}, err
		}
	}

	return t, nil
}

// GetTokensForOwner fetches all of the tokens owned by a given owner.
func (b *BoltDBStorage) GetTokensForOwner(ctx context.Context, owner string) ([]Token, error) {
	ts := []Token{}
	err := b.db.Find("Owner", owner, &ts)
	return ts, err
}

// PutToken adds a new token to the database.
func (b *BoltDBStorage) PutToken(ctx context.Context, token, owner string, scopes []string) (Token, error) {
	t := Token{
		ID:     uuid.New(),
		Body:   token,
		Owner:  owner,
		Scopes: scopes,

		CreatedAt: time.Now(),
		Active:    true,
	}

	err := b.db.Save(&t)
	if err != nil {
		return Token{}, err
	}
	defer b.db.Commit()

	ln.Log(ctx, t, ln.Action("new token put into database"))

	return t, nil
}

// DeleteToken removes a token from the database.
func (b *BoltDBStorage) DeleteToken(ctx context.Context, id string) error {
	t := Token{}
	err := b.db.One("ID", id, &t)
	if err != nil {
		return err
	}

	ln.Log(ctx, t, ln.Action("token deleted from database"))

	return b.db.DeleteStruct(&t)
}

// DeactivateToken de-activates a token in the database. This should be used
// instead of deletion in many cases.
func (b *BoltDBStorage) DeactivateToken(ctx context.Context, id string) error {
	t := Token{}
	err := b.db.One("ID", id, &t)
	if err != nil {
		return err
	}
	defer b.db.Commit()

	t.Active = false

	ln.Log(ctx, t, ln.Action("token deactivated"))

	return b.db.Save(&t)
}

// GetCert fetches a TLS certificate from the database.
func (b *BoltDBStorage) GetCert(ctx context.Context, key string) ([]byte, error) {
	cc := CachedCert{}
	err := b.db.One("Key", key, &cc)
	if err != nil {
		return nil, err
	}

	var body []byte

	switch cc.CryptoLevel {
	case CryptoLevelNone:
		body = cc.Body
	case CryptoLevelSecretbox:
		if b.sb == nil {
			return nil, ErrCantDecryptCert
		}

		body, err = b.sb.Decrypt(cc.Body)
		if err != nil {
			return nil, err
		}
	}

	return body, nil
}

// PutCert adds a new TLS certificate to the database.
func (b *BoltDBStorage) PutCert(ctx context.Context, key string, data []byte) error {
	cc := CachedCert{
		Key:         key,
		CryptoLevel: CryptoLevelNone,
		Body:        data,
	}

	if b.sb != nil {
		cc.CryptoLevel = CryptoLevelSecretbox
		cc.Body = b.sb.Encrypt(data)
	}

	defer b.db.Commit()

	ln.Log(ctx, ln.Action("certificate saved to database"), ln.F{"domain": key})

	return b.db.Save(&cc)
}

// DeleteCert removes a certificate from the database.
func (b *BoltDBStorage) DeleteCert(ctx context.Context, key string) error {
	cc := CachedCert{}
	err := b.db.One("Key", key, &cc)
	if err != nil {
		return err
	}

	defer b.db.Commit()

	ln.Log(ctx, ln.F{"domain": key}, ln.Action("certificate deleted from database"))

	return b.db.DeleteStruct(&cc)
}

// Close ...
func (b *BoltDBStorage) Close() error {
	return b.db.Close()
}
