package tenant

import (
	"context"
	"time"

	"github.com/influxdata/influxdb/v2/kit/platform"
	"github.com/influxdata/influxdb/v2/kit/platform/errors"
	"github.com/influxdata/influxdb/v2/kit/tracing"
	"github.com/influxdata/influxdb/v2/kv"
	"github.com/influxdata/influxdb/v2/rand"
	"github.com/influxdata/influxdb/v2/snowflake"
	"github.com/influxdata/influxdb/v2/tenant/index"
)

const MaxIDGenerationN = 100

type Store struct {
	kvStore     kv.Store
	IDGen       platform.IDGenerator
	OrgIDGen    platform.IDGenerator
	BucketIDGen platform.IDGenerator

	now func() time.Time

	urmByUserIndex *kv.Index
}

type StoreOption func(*Store)

func NewStore(kvStore kv.Store, opts ...StoreOption) *Store {
	store := &Store{
		kvStore:     kvStore,
		IDGen:       snowflake.NewDefaultIDGenerator(),
		OrgIDGen:    rand.NewOrgBucketID(time.Now().UnixNano()),
		BucketIDGen: rand.NewOrgBucketID(time.Now().UnixNano()),
		now: func() time.Time {
			return time.Now().UTC()
		},
		urmByUserIndex: kv.NewIndex(index.URMByUserIndexMapping, kv.WithIndexReadPathEnabled),
	}

	for _, opt := range opts {
		opt(store)
	}

	return store
}

func (s *Store) RLock() {
	s.kvStore.RLock()
}

func (s *Store) RUnlock() {
	s.kvStore.RUnlock()
}

// View opens up a transaction that will not write to any data. Implementing interfaces
// should take care to ensure that all view transactions do not mutate any data.
func (s *Store) View(ctx context.Context, fn func(kv.Tx) error) error {
	return s.kvStore.View(ctx, fn)
}

// Update opens up a transaction that will mutate data.
func (s *Store) Update(ctx context.Context, fn func(kv.Tx) error) error {
	return s.kvStore.Update(ctx, fn)
}

// generateSafeID attempts to create ids for buckets
// and orgs that are without backslash, commas, and spaces, BUT ALSO do not already exist.
func (s *Store) generateSafeID(ctx context.Context, tx kv.Tx, bucket []byte, gen platform.IDGenerator) (platform.ID, error) {
	for i := 0; i < MaxIDGenerationN; i++ {
		id := gen.ID()

		err := s.uniqueID(ctx, tx, bucket, id)
		if err == nil {
			return id, nil
		}

		if err == ErrIDNotUnique {
			continue
		}

		return platform.InvalidID(), err
	}

	return platform.InvalidID(), ErrFailureGeneratingID
}

func (s *Store) uniqueID(ctx context.Context, tx kv.Tx, bucket []byte, id platform.ID) error {
	span, _ := tracing.StartSpanFromContext(ctx)
	defer span.Finish()

	encodedID, err := id.Encode()
	if err != nil {
		return &errors.Error{
			Code: errors.EInvalid,
			Err:  err,
		}
	}

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

	_, err = b.Get(encodedID)
	if kv.IsNotFound(err) {
		return nil
	}

	return ErrIDNotUnique
}
