// Note: this file is used as a proof of concept for having a generic
// keyvalue store backed by specific implementations of kv.Store.
package kv

import (
	"context"
	"encoding/json"
	"fmt"

	"github.com/influxdata/platform"
)

var (
	exampleBucket = []byte("examplesv1")
	exampleIndex  = []byte("exampleindexv1")
)

// ExampleService is an example user like service built on a generic kv store.
type ExampleService struct {
	kv          Store
	idGenerator platform.IDGenerator
}

// NewExampleService creates an instance of an example service.
func NewExampleService(kv Store, idGen platform.IDGenerator) *ExampleService {
	return &ExampleService{
		kv:          kv,
		idGenerator: idGen,
	}
}

// Initialize creates the buckets for the example service
func (c *ExampleService) Initialize() error {
	return c.kv.Update(func(tx Tx) error {
		if _, err := tx.Bucket([]byte(exampleBucket)); err != nil {
			return err
		}
		if _, err := tx.Bucket([]byte(exampleIndex)); err != nil {
			return err
		}
		return nil
	})
}

// FindUserByID retrieves a example by id.
func (c *ExampleService) FindUserByID(ctx context.Context, id platform.ID) (*platform.User, error) {
	var u *platform.User

	err := c.kv.View(func(tx Tx) error {
		usr, err := c.findUserByID(ctx, tx, id)
		if err != nil {
			return err
		}
		u = usr
		return nil
	})

	if err != nil {
		return nil, &platform.Error{
			Op:  "kv/" + platform.OpFindUserByID,
			Err: err,
		}
	}

	return u, nil
}

func (c *ExampleService) findUserByID(ctx context.Context, tx Tx, id platform.ID) (*platform.User, error) {
	encodedID, err := id.Encode()
	if err != nil {
		return nil, err
	}

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

	v, err := b.Get(encodedID)
	if err == ErrKeyNotFound {
		return nil, &platform.Error{
			Code: platform.ENotFound,
			Msg:  "user not found",
		}
	}
	if err != nil {
		return nil, err
	}

	var u platform.User
	if err := json.Unmarshal(v, &u); err != nil {
		return nil, err
	}

	return &u, nil
}

// FindUserByName returns a example by name for a particular example.
func (c *ExampleService) FindUserByName(ctx context.Context, n string) (*platform.User, error) {
	var u *platform.User

	err := c.kv.View(func(tx Tx) error {
		usr, err := c.findUserByName(ctx, tx, n)
		if err != nil {
			return err
		}
		u = usr
		return nil
	})

	return u, err
}

func (c *ExampleService) findUserByName(ctx context.Context, tx Tx, n string) (*platform.User, error) {
	b, err := tx.Bucket(exampleIndex)
	if err != nil {
		return nil, err
	}
	uid, err := b.Get(exampleIndexKey(n))
	if err == ErrKeyNotFound {
		return nil, &platform.Error{
			Code: platform.ENotFound,
			Msg:  "user not found",
			Op:   "kv/" + platform.OpFindUser,
		}
	}
	if err != nil {
		return nil, err
	}

	var id platform.ID
	if err := id.Decode(uid); err != nil {
		return nil, err
	}
	return c.findUserByID(ctx, tx, id)
}

// FindUser retrives a example using an arbitrary example filter.
// Filters using ID, or Name should be efficient.
// Other filters will do a linear scan across examples until it finds a match.
func (c *ExampleService) FindUser(ctx context.Context, filter platform.UserFilter) (*platform.User, error) {
	if filter.ID != nil {
		u, err := c.FindUserByID(ctx, *filter.ID)
		if err != nil {
			return nil, &platform.Error{
				Op:  "kv/" + platform.OpFindUser,
				Err: err,
			}
		}
		return u, nil
	}

	if filter.Name != nil {
		return c.FindUserByName(ctx, *filter.Name)
	}

	filterFn := filterExamplesFn(filter)

	var u *platform.User
	err := c.kv.View(func(tx Tx) error {
		return forEachExample(ctx, tx, func(usr *platform.User) bool {
			if filterFn(usr) {
				u = usr
				return false
			}
			return true
		})
	})

	if err != nil {
		return nil, err
	}

	if u == nil {
		return nil, &platform.Error{
			Code: platform.ENotFound,
			Msg:  "user not found",
		}
	}

	return u, nil
}

func filterExamplesFn(filter platform.UserFilter) func(u *platform.User) bool {
	if filter.ID != nil {
		return func(u *platform.User) bool {
			return u.ID.Valid() && u.ID == *filter.ID
		}
	}

	if filter.Name != nil {
		return func(u *platform.User) bool {
			return u.Name == *filter.Name
		}
	}

	return func(u *platform.User) bool { return true }
}

// FindUsers retrives all examples that match an arbitrary example filter.
// Filters using ID, or Name should be efficient.
// Other filters will do a linear scan across all examples searching for a match.
func (c *ExampleService) FindUsers(ctx context.Context, filter platform.UserFilter, opt ...platform.FindOptions) ([]*platform.User, int, error) {
	op := platform.OpFindUsers
	if filter.ID != nil {
		u, err := c.FindUserByID(ctx, *filter.ID)
		if err != nil {
			return nil, 0, &platform.Error{
				Err: err,
				Op:  "kv/" + op,
			}
		}

		return []*platform.User{u}, 1, nil
	}

	if filter.Name != nil {
		u, err := c.FindUserByName(ctx, *filter.Name)
		if err != nil {
			return nil, 0, &platform.Error{
				Err: err,
				Op:  "kv/" + op,
			}
		}

		return []*platform.User{u}, 1, nil
	}

	us := []*platform.User{}
	filterFn := filterExamplesFn(filter)
	err := c.kv.View(func(tx Tx) error {
		return forEachExample(ctx, tx, func(u *platform.User) bool {
			if filterFn(u) {
				us = append(us, u)
			}
			return true
		})
	})

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

	return us, len(us), nil
}

// CreateUser creates a platform example and sets b.ID.
func (c *ExampleService) CreateUser(ctx context.Context, u *platform.User) error {
	err := c.kv.Update(func(tx Tx) error {
		unique := c.uniqueExampleName(ctx, tx, u)

		if !unique {
			// TODO: make standard error
			return &platform.Error{
				Code: platform.EConflict,
				Msg:  fmt.Sprintf("user with name %s already exists", u.Name),
			}
		}

		u.ID = c.idGenerator.ID()

		return c.putUser(ctx, tx, u)
	})

	if err != nil {
		return &platform.Error{
			Err: err,
			Op:  "kv/" + platform.OpCreateUser,
		}
	}

	return nil
}

// PutUser will put a example without setting an ID.
func (c *ExampleService) PutUser(ctx context.Context, u *platform.User) error {
	return c.kv.Update(func(tx Tx) error {
		return c.putUser(ctx, tx, u)
	})
}

func (c *ExampleService) putUser(ctx context.Context, tx Tx, u *platform.User) error {
	v, err := json.Marshal(u)
	if err != nil {
		return err
	}
	encodedID, err := u.ID.Encode()
	if err != nil {
		return err
	}

	idx, err := tx.Bucket(exampleIndex)
	if err != nil {
		return err
	}

	if err := idx.Put(exampleIndexKey(u.Name), encodedID); err != nil {
		return err
	}

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

	return b.Put(encodedID, v)
}

func exampleIndexKey(n string) []byte {
	return []byte(n)
}

// forEachExample will iterate through all examples while fn returns true.
func forEachExample(ctx context.Context, tx Tx, fn func(*platform.User) bool) error {
	b, err := tx.Bucket(exampleBucket)
	if err != nil {
		return err
	}

	cur, err := b.Cursor()
	if err != nil {
		return err
	}

	for k, v := cur.First(); k != nil; k, v = cur.Next() {
		u := &platform.User{}
		if err := json.Unmarshal(v, u); err != nil {
			return err
		}
		if !fn(u) {
			break
		}
	}

	return nil
}

func (c *ExampleService) uniqueExampleName(ctx context.Context, tx Tx, u *platform.User) bool {
	idx, err := tx.Bucket(exampleIndex)
	if err != nil {
		return false
	}

	if _, err := idx.Get(exampleIndexKey(u.Name)); err == ErrKeyNotFound {
		return true
	}
	return false
}

// UpdateUser updates a example according the parameters set on upd.
func (c *ExampleService) UpdateUser(ctx context.Context, id platform.ID, upd platform.UserUpdate) (*platform.User, error) {
	var u *platform.User
	err := c.kv.Update(func(tx Tx) error {
		usr, err := c.updateUser(ctx, tx, id, upd)
		if err != nil {
			return err
		}
		u = usr
		return nil
	})

	if err != nil {
		return nil, &platform.Error{
			Err: err,
			Op:  "kv/" + platform.OpUpdateUser,
		}
	}

	return u, nil
}

func (c *ExampleService) updateUser(ctx context.Context, tx Tx, id platform.ID, upd platform.UserUpdate) (*platform.User, error) {
	u, err := c.findUserByID(ctx, tx, id)
	if err != nil {
		return nil, err
	}

	if upd.Name != nil {
		// Examples are indexed by name and so the example index must be pruned
		// when name is modified.
		idx, err := tx.Bucket(exampleIndex)
		if err != nil {
			return nil, err
		}

		if err := idx.Delete(exampleIndexKey(u.Name)); err != nil {
			return nil, err
		}
		u.Name = *upd.Name
	}

	if err := c.putUser(ctx, tx, u); err != nil {
		return nil, err
	}

	return u, nil
}

// DeleteUser deletes a example and prunes it from the index.
func (c *ExampleService) DeleteUser(ctx context.Context, id platform.ID) error {
	err := c.kv.Update(func(tx Tx) error {
		return c.deleteUser(ctx, tx, id)
	})

	if err != nil {
		return &platform.Error{
			Op:  "kv/" + platform.OpDeleteUser,
			Err: err,
		}
	}

	return nil
}

func (c *ExampleService) deleteUser(ctx context.Context, tx Tx, id platform.ID) error {
	u, err := c.findUserByID(ctx, tx, id)
	if err != nil {
		return err
	}
	encodedID, err := id.Encode()
	if err != nil {
		return err
	}

	idx, err := tx.Bucket(exampleIndex)
	if err != nil {
		return err
	}

	if err := idx.Delete(exampleIndexKey(u.Name)); err != nil {
		return err
	}

	b, err := tx.Bucket(exampleBucket)
	if err != nil {
		return err
	}
	if err := b.Delete(encodedID); err != nil {
		return err
	}

	return nil
}