package buntdb

import (
	"context"
	"time"

	"gopkg.in/DataDog/dd-trace-go.v1/ddtrace"
	"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/ext"
	"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer"

	"github.com/tidwall/buntdb"
)

// A DB wraps a buntdb.DB, automatically tracing any transactions.
type DB struct {
	*buntdb.DB
	opts []Option
	ctx  context.Context
}

// Open calls buntdb.Open and wraps the result.
func Open(path string, opts ...Option) (*DB, error) {
	db, err := buntdb.Open(path)
	if err != nil {
		return nil, err
	}
	return WrapDB(db, opts...), nil
}

// WrapDB wraps a buntdb.DB so it can be traced.
func WrapDB(db *buntdb.DB, opts ...Option) *DB {
	return &DB{
		DB:   db,
		opts: opts,
		ctx:  context.Background(),
	}
}

// Begin calls the underlying DB.Begin and traces the transaction.
func (db *DB) Begin(writable bool) (*Tx, error) {
	tx, err := db.DB.Begin(writable)
	if err != nil {
		return nil, err
	}
	return WrapTx(tx, db.opts...), nil
}

// Update calls the underlying DB.Update and traces the transaction.
func (db *DB) Update(fn func(tx *Tx) error) error {
	return db.DB.Update(func(tx *buntdb.Tx) error {
		return fn(WrapTx(tx, db.opts...))
	})
}

// View calls the underlying DB.View and traces the transaction.
func (db *DB) View(fn func(tx *Tx) error) error {
	return db.DB.View(func(tx *buntdb.Tx) error {
		return fn(WrapTx(tx, db.opts...))
	})
}

// WithContext sets the context for the DB.
func (db *DB) WithContext(ctx context.Context) *DB {
	newdb := *db
	newdb.opts = append(newdb.opts[:len(newdb.opts):len(newdb.opts)], WithContext(ctx))
	return &newdb
}

// A Tx wraps a buntdb.Tx, automatically tracing any queries.
type Tx struct {
	*buntdb.Tx
	cfg *config
}

// WrapTx wraps a buntdb.Tx so it can be traced.
func WrapTx(tx *buntdb.Tx, opts ...Option) *Tx {
	cfg := new(config)
	defaults(cfg)
	for _, opt := range opts {
		opt(cfg)
	}
	return &Tx{
		Tx:  tx,
		cfg: cfg,
	}
}

func (tx *Tx) startSpan(name string) ddtrace.Span {
	span, _ := tracer.StartSpanFromContext(tx.cfg.ctx, "buntdb.query",
		tracer.SpanType(ext.AppTypeDB),
		tracer.ServiceName(tx.cfg.serviceName),
		tracer.ResourceName(name),
	)
	return span
}

// WithContext sets the context for the Tx.
func (tx *Tx) WithContext(ctx context.Context) *Tx {
	newcfg := *tx.cfg
	newcfg.ctx = ctx
	return &Tx{
		Tx:  tx.Tx,
		cfg: &newcfg,
	}
}

// Ascend calls the underlying Tx.Ascend and traces the query.
func (tx *Tx) Ascend(index string, iterator func(key, value string) bool) error {
	span := tx.startSpan("Ascend")
	err := tx.Tx.Ascend(index, iterator)
	span.Finish(tracer.WithError(err))
	return err
}

// AscendEqual calls the underlying Tx.AscendEqual and traces the query.
func (tx *Tx) AscendEqual(index, pivot string, iterator func(key, value string) bool) error {
	span := tx.startSpan("AscendEqual")
	err := tx.Tx.AscendEqual(index, pivot, iterator)
	span.Finish(tracer.WithError(err))
	return err
}

// AscendGreaterOrEqual calls the underlying Tx.AscendGreaterOrEqual and traces the query.
func (tx *Tx) AscendGreaterOrEqual(index, pivot string, iterator func(key, value string) bool) error {
	span := tx.startSpan("AscendGreaterOrEqual")
	err := tx.Tx.AscendGreaterOrEqual(index, pivot, iterator)
	span.Finish(tracer.WithError(err))
	return err
}

// AscendKeys calls the underlying Tx.AscendKeys and traces the query.
func (tx *Tx) AscendKeys(pattern string, iterator func(key, value string) bool) error {
	span := tx.startSpan("AscendKeys")
	err := tx.Tx.AscendKeys(pattern, iterator)
	span.Finish(tracer.WithError(err))
	return err
}

// AscendLessThan calls the underlying Tx.AscendLessThan and traces the query.
func (tx *Tx) AscendLessThan(index, pivot string, iterator func(key, value string) bool) error {
	span := tx.startSpan("AscendLessThan")
	err := tx.Tx.AscendLessThan(index, pivot, iterator)
	span.Finish(tracer.WithError(err))
	return err
}

// AscendRange calls the underlying Tx.AscendRange and traces the query.
func (tx *Tx) AscendRange(index, greaterOrEqual, lessThan string, iterator func(key, value string) bool) error {
	span := tx.startSpan("AscendRange")
	err := tx.Tx.AscendRange(index, greaterOrEqual, lessThan, iterator)
	span.Finish(tracer.WithError(err))
	return err
}

// CreateIndex calls the underlying Tx.CreateIndex and traces the query.
func (tx *Tx) CreateIndex(name, pattern string, less ...func(a, b string) bool) error {
	span := tx.startSpan("CreateIndex")
	err := tx.Tx.CreateIndex(name, pattern, less...)
	span.Finish(tracer.WithError(err))
	return err
}

// CreateIndexOptions calls the underlying Tx.CreateIndexOptions and traces the query.
func (tx *Tx) CreateIndexOptions(name, pattern string, opts *buntdb.IndexOptions, less ...func(a, b string) bool) error {
	span := tx.startSpan("CreateIndexOptions")
	err := tx.Tx.CreateIndexOptions(name, pattern, opts, less...)
	span.Finish(tracer.WithError(err))
	return err
}

// CreateSpatialIndex calls the underlying Tx.CreateSpatialIndex and traces the query.
func (tx *Tx) CreateSpatialIndex(name, pattern string, rect func(item string) (min, max []float64)) error {
	span := tx.startSpan("CreateSpatialIndex")
	err := tx.Tx.CreateSpatialIndex(name, pattern, rect)
	span.Finish(tracer.WithError(err))
	return err
}

// CreateSpatialIndexOptions calls the underlying Tx.CreateSpatialIndexOptions and traces the query.
func (tx *Tx) CreateSpatialIndexOptions(name, pattern string, opts *buntdb.IndexOptions, rect func(item string) (min, max []float64)) error {
	span := tx.startSpan("CreateSpatialIndexOptions")
	err := tx.Tx.CreateSpatialIndexOptions(name, pattern, opts, rect)
	span.Finish(tracer.WithError(err))
	return err
}

// Delete calls the underlying Tx.Delete and traces the query.
func (tx *Tx) Delete(key string) (val string, err error) {
	span := tx.startSpan("Delete")
	val, err = tx.Tx.Delete(key)
	span.Finish(tracer.WithError(err))
	return val, err
}

// DeleteAll calls the underlying Tx.DeleteAll and traces the query.
func (tx *Tx) DeleteAll() error {
	span := tx.startSpan("DeleteAll")
	err := tx.Tx.DeleteAll()
	span.Finish(tracer.WithError(err))
	return err
}

// Descend calls the underlying Tx.Descend and traces the query.
func (tx *Tx) Descend(index string, iterator func(key, value string) bool) error {
	span := tx.startSpan("Descend")
	err := tx.Tx.Descend(index, iterator)
	span.Finish(tracer.WithError(err))
	return err
}

// DescendEqual calls the underlying Tx.DescendEqual and traces the query.
func (tx *Tx) DescendEqual(index, pivot string, iterator func(key, value string) bool) error {
	span := tx.startSpan("DescendEqual")
	err := tx.Tx.DescendEqual(index, pivot, iterator)
	span.Finish(tracer.WithError(err))
	return err
}

// DescendGreaterThan calls the underlying Tx.DescendGreaterThan and traces the query.
func (tx *Tx) DescendGreaterThan(index, pivot string, iterator func(key, value string) bool) error {
	span := tx.startSpan("DescendGreaterThan")
	err := tx.Tx.DescendGreaterThan(index, pivot, iterator)
	span.Finish(tracer.WithError(err))
	return err
}

// DescendKeys calls the underlying Tx.DescendKeys and traces the query.
func (tx *Tx) DescendKeys(pattern string, iterator func(key, value string) bool) error {
	span := tx.startSpan("DescendKeys")
	err := tx.Tx.DescendKeys(pattern, iterator)
	span.Finish(tracer.WithError(err))
	return err
}

// DescendLessOrEqual calls the underlying Tx.DescendLessOrEqual and traces the query.
func (tx *Tx) DescendLessOrEqual(index, pivot string, iterator func(key, value string) bool) error {
	span := tx.startSpan("DescendLessOrEqual")
	err := tx.Tx.DescendLessOrEqual(index, pivot, iterator)
	span.Finish(tracer.WithError(err))
	return err
}

// DescendRange calls the underlying Tx.DescendRange and traces the query.
func (tx *Tx) DescendRange(index, lessOrEqual, greaterThan string, iterator func(key, value string) bool) error {
	span := tx.startSpan("DescendRange")
	err := tx.Tx.DescendRange(index, lessOrEqual, greaterThan, iterator)
	span.Finish(tracer.WithError(err))
	return err
}

// DropIndex calls the underlying Tx.DropIndex and traces the query.
func (tx *Tx) DropIndex(name string) error {
	span := tx.startSpan("DropIndex")
	err := tx.Tx.DropIndex(name)
	span.Finish(tracer.WithError(err))
	return err
}

// Get calls the underlying Tx.Get and traces the query.
func (tx *Tx) Get(key string, ignoreExpired ...bool) (val string, err error) {
	span := tx.startSpan("Get")
	val, err = tx.Tx.Get(key, ignoreExpired...)
	span.Finish(tracer.WithError(err))
	return val, err
}

// Indexes calls the underlying Tx.Indexes and traces the query.
func (tx *Tx) Indexes() ([]string, error) {
	span := tx.startSpan("Indexes")
	indexes, err := tx.Tx.Indexes()
	span.Finish(tracer.WithError(err))
	return indexes, err
}

// Intersects calls the underlying Tx.Intersects and traces the query.
func (tx *Tx) Intersects(index, bounds string, iterator func(key, value string) bool) error {
	span := tx.startSpan("Intersects")
	err := tx.Tx.Intersects(index, bounds, iterator)
	span.Finish(tracer.WithError(err))
	return err
}

// Len calls the underlying Tx.Len and traces the query.
func (tx *Tx) Len() (int, error) {
	span := tx.startSpan("Len")
	n, err := tx.Tx.Len()
	span.Finish(tracer.WithError(err))
	return n, err
}

// Nearby calls the underlying Tx.Nearby and traces the query.
func (tx *Tx) Nearby(index, bounds string, iterator func(key, value string, dist float64) bool) error {
	span := tx.startSpan("Nearby")
	err := tx.Tx.Nearby(index, bounds, iterator)
	span.Finish(tracer.WithError(err))
	return err
}

// Set calls the underlying Tx.Set and traces the query.
func (tx *Tx) Set(key, value string, opts *buntdb.SetOptions) (previousValue string, replaced bool, err error) {
	span := tx.startSpan("Set")
	previousValue, replaced, err = tx.Tx.Set(key, value, opts)
	span.Finish(tracer.WithError(err))
	return previousValue, replaced, err
}

// TTL calls the underlying Tx.TTL and traces the query.
func (tx *Tx) TTL(key string) (time.Duration, error) {
	span := tx.startSpan("TTL")
	duration, err := tx.Tx.TTL(key)
	span.Finish(tracer.WithError(err))
	return duration, err
}
