package store

import (
	"context"

	"github.com/cockroachdb/pebble"
	ds "github.com/ipfs/go-datastore"
	dsq "github.com/ipfs/go-datastore/query"
	"github.com/pkg/errors"
)

// shim structs for go-datastore
type batch struct {
	b  *transaction
	db KVDB
}

type transaction struct {
	tx Transaction
}

type PeerstoreDatastore struct {
	db KVDB
}

const (
	PEERSTORE = 0x06
)

type Peerstore interface {
	ds.TxnDatastore
	ds.PersistentDatastore
	ds.Batching
}

var _ ds.Datastore = (*PeerstoreDatastore)(nil)
var _ ds.TxnDatastore = (*PeerstoreDatastore)(nil)
var _ ds.Txn = (*transaction)(nil)
var _ ds.PersistentDatastore = (*PeerstoreDatastore)(nil)
var _ ds.Batching = (*PeerstoreDatastore)(nil)
var _ ds.Batch = (*batch)(nil)
var _ Peerstore = (*PeerstoreDatastore)(nil)

func NewPeerstoreDatastore(db KVDB) (*PeerstoreDatastore, error) {
	ds := PeerstoreDatastore{
		db: db,
	}
	return &ds, nil
}

func (d *PeerstoreDatastore) Put(
	ctx context.Context,
	key ds.Key,
	value []byte,
) (err error) {
	return d.db.Set(
		append([]byte{PEERSTORE}, key.Bytes()...),
		value,
	)
}

func (d *PeerstoreDatastore) Sync(ctx context.Context, prefix ds.Key) error {
	return nil
}

func (d *PeerstoreDatastore) Get(
	ctx context.Context,
	key ds.Key,
) (value []byte, err error) {
	val, closer, err := d.db.Get(append([]byte{PEERSTORE}, key.Bytes()...))
	if err != nil {
		if err == pebble.ErrNotFound {
			return nil, ds.ErrNotFound
		}
		return nil, err
	}

	out := make([]byte, len(val))
	copy(out[:], val[:])
	closer.Close()

	return val, nil
}

func (d *PeerstoreDatastore) Has(
	ctx context.Context,
	key ds.Key,
) (exists bool, err error) {
	if _, err := d.Get(ctx, key); err != nil {
		if err == ds.ErrNotFound {
			return false, nil
		}
		return false, errors.Wrap(err, "has")
	}

	return true, nil
}

func (d *PeerstoreDatastore) GetSize(
	ctx context.Context,
	key ds.Key,
) (size int, err error) {
	return ds.GetBackedSize(ctx, d, key)
}

func (d *PeerstoreDatastore) Delete(
	ctx context.Context,
	key ds.Key,
) (err error) {
	return d.db.Delete(append([]byte{PEERSTORE}, key.Bytes()...))
}

func (d *PeerstoreDatastore) Query(ctx context.Context, q dsq.Query) (
	dsq.Results,
	error,
) {
	rnge := []byte{PEERSTORE}

	qNaive := q
	prefix := ds.NewKey(q.Prefix).String()
	if prefix != "/" {
		rnge = append(rnge, []byte(prefix+"/")...)
		qNaive.Prefix = ""
	}

	i, err := d.db.NewIter(rnge, nil)
	if err != nil {
		return nil, errors.Wrap(err, "query")
	}

	next := i.Next
	if len(q.Orders) > 0 {
		switch q.Orders[0].(type) {
		case dsq.OrderByKey, *dsq.OrderByKey:
			qNaive.Orders = nil
			i.First()
		case dsq.OrderByKeyDescending, *dsq.OrderByKeyDescending:
			next = func() bool {
				next = i.Prev
				return i.Last()
			}
			qNaive.Orders = nil
		default:
			i.First()
		}
	} else {
		i.First()
	}
	r := dsq.ResultsFromIterator(q, dsq.Iterator{
		Next: func() (dsq.Result, bool) {
			if !next() {
				return dsq.Result{}, false
			}
			k := string(i.Key()[1:])
			e := dsq.Entry{Key: k, Size: len(i.Value())}

			if !q.KeysOnly {
				buf := make([]byte, len(i.Value()))
				copy(buf, i.Value())
				e.Value = buf
			}
			return dsq.Result{Entry: e}, true
		},
		Close: func() error {
			return i.Close()
		},
	})
	return dsq.NaiveQueryApply(qNaive, r), nil
}

// TODO: get disk usage of peerstore later
func (d *PeerstoreDatastore) DiskUsage(ctx context.Context) (uint64, error) {
	return 0, nil
}

// Closing is not done here:
func (d *PeerstoreDatastore) Close() (err error) {
	return nil
}

func (d *PeerstoreDatastore) Batch(ctx context.Context) (ds.Batch, error) {
	return &batch{
		b:  &transaction{tx: d.db.NewBatch()},
		db: d.db,
	}, nil
}

func (d *PeerstoreDatastore) NewTransaction(
	ctx context.Context,
	readOnly bool,
) (ds.Txn, error) {
	tx := d.db.NewBatch()
	return &transaction{tx}, nil
}

func (b *batch) Put(ctx context.Context, key ds.Key, value []byte) error {
	b.b.Put(ctx, key, value)
	return nil
}

func (b *batch) Commit(ctx context.Context) error {
	return b.b.Commit(ctx)
}

func (b *batch) Delete(ctx context.Context, key ds.Key) error {
	b.b.Delete(ctx, key)
	return nil
}

func (t *transaction) Commit(ctx context.Context) error {
	return t.tx.Commit()
}

func (t *transaction) Discard(ctx context.Context) {
	t.tx.Abort()
}

func (t *transaction) Get(
	ctx context.Context,
	key ds.Key,
) (value []byte, err error) {
	b, closer, err := t.tx.Get(append([]byte{PEERSTORE}, key.Bytes()...))
	if err != nil {
		if err == pebble.ErrNotFound {
			return nil, ds.ErrNotFound
		}
		return nil, errors.Wrap(err, "get")
	}

	out := make([]byte, len(b))
	copy(out[:], b[:])
	closer.Close()

	return b, nil
}

func (t *transaction) Put(ctx context.Context, key ds.Key, value []byte) error {
	return t.tx.Set(append([]byte{PEERSTORE}, key.Bytes()...), value)
}

func (t *transaction) Has(ctx context.Context, key ds.Key) (
	exists bool,
	err error,
) {
	if _, err := t.Get(ctx, key); err != nil {
		if errors.Is(err, ErrNotFound) {
			return false, nil
		}

		return false, errors.Wrap(err, "has")
	}

	return true, nil
}

func (t *transaction) GetSize(
	ctx context.Context,
	key ds.Key,
) (size int, err error) {
	return ds.GetBackedSize(ctx, t, key)
}

func (t *transaction) Delete(ctx context.Context, key ds.Key) (err error) {
	return t.tx.Delete(append([]byte{PEERSTORE}, key.Bytes()...))
}

func (t *transaction) Query(ctx context.Context, q dsq.Query) (
	dsq.Results,
	error,
) {
	rnge := []byte{PEERSTORE}
	qNaive := q
	prefix := ds.NewKey(q.Prefix).String()
	if prefix != "/" {
		rnge = append(rnge, []byte(prefix+"/")...)
		qNaive.Prefix = ""
	}

	i, err := t.tx.NewIter(rnge, nil)
	if err != nil {
		return nil, errors.Wrap(err, "query")
	}

	next := i.Next
	if len(q.Orders) > 0 {
		switch q.Orders[0].(type) {
		case dsq.OrderByKey, *dsq.OrderByKey:
			qNaive.Orders = nil
		case dsq.OrderByKeyDescending, *dsq.OrderByKeyDescending:
			next = func() bool {
				next = i.Prev
				return i.Last()
			}
			qNaive.Orders = nil
		default:
		}
	}
	r := dsq.ResultsFromIterator(q, dsq.Iterator{
		Next: func() (dsq.Result, bool) {
			if !next() {
				return dsq.Result{}, false
			}
			k := string(i.Key()[1:])
			e := dsq.Entry{Key: k, Size: len(i.Value())}

			if !q.KeysOnly {
				buf := make([]byte, len(i.Value()))
				copy(buf, i.Value())
				e.Value = buf
			}
			return dsq.Result{Entry: e}, true
		},
		Close: func() error {
			return i.Close()
		},
	})
	return dsq.NaiveQueryApply(qNaive, r), nil
}
