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

	cs *boltCertificateStorage
	rs *boltRouteStorage
	ts *boltTokenStorage
}

type boltCertificateStorage struct {
	*BoltDBStorage
}

type boltRouteStorage struct {
	*BoltDBStorage
}

type boltTokenStorage struct {
	*BoltDBStorage
}

// 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),
	}

	b.cs = &boltCertificateStorage{b}
	b.rs = &boltRouteStorage{b}
	b.ts = &boltTokenStorage{b}

	return b, nil
}

// Certs gets the certificate storage interface.
func (b *BoltDBStorage) Certs() Certs { return b.cs }

// Routes gets the route storage interface.
func (b *BoltDBStorage) Routes() Routes { return b.rs }

// Tokens gets the token storage interface.
func (b *BoltDBStorage) Tokens() Tokens { return b.ts }

// Close cleans up resources for this Storage.
func (b *BoltDBStorage) Close() error { return b.db.Close() }

// interface compliance
var (
	_ Storage = &BoltDBStorage{}
	_ Certs   = &boltCertificateStorage{}
	_ Routes  = &boltRouteStorage{}
	_ Tokens  = &boltTokenStorage{}
)

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

func (b *boltRouteStorage) GetHost(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 *boltRouteStorage) 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
}

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

// Put creates a new route in the database.
func (b *boltRouteStorage) Put(ctx context.Context, r Route) (Route, error) {
	if r.ID == "" {
		r.ID = uuid.New()
	}

	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
}

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

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

	return r, b.db.DeleteStruct(&r)
}

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

// Get fetches a token by a given token ID.
func (b *boltTokenStorage) Get(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
}

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

// Put adds a new token to the database.
func (b *boltTokenStorage) Put(ctx context.Context, t Token) (Token, error) {
	if t.ID == "" {
		t.ID = uuid.New()
		t.CreatedAt = time.Now()
		t.Active = true
		t.Body = uuid.New()
	}

	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
}

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

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

	return t, b.db.DeleteStruct(&t)
}

// DeleteExpired deletes all expired tokens.
func (b *boltTokenStorage) DeleteExpired(ctx context.Context) error {
	return errors.New("not implemented")
}

// GetAll fetches all certificates and returns their DECRYPTED BODIES to the caller.
// This is intended for usage in migration tools only.
func (b *boltCertificateStorage) GetAll(ctx context.Context) ([]CachedCert, error) {
	var cc []CachedCert
	err := b.db.All(&cc)
	if err != nil {
		return nil, err
	}

	var result []CachedCert

	for _, c := range cc {
		r := CachedCert{
			Key: c.Key,
		}

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

		result = append(result, r)
	}

	return result, nil
}

// Get fetches a TLS certificate from the database.
func (b *boltCertificateStorage) Get(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

	if b.sb == nil {
		return nil, ErrCantDecryptCert
	}

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

	return body, nil
}

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

	if b.sb != nil {
		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)
}

// Delete removes a certificate from the database.
func (b *boltCertificateStorage) Delete(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)
}
