package datastore

import (
	"context"
	"errors"
	"io"
	"iter"

	query "github.com/ipfs/go-datastore/query"
)

/*
Datastore represents storage for any key-value pair.

Datastores are general enough to be backed by all kinds of different storage:
in-memory caches, databases, a remote datastore, flat files on disk, etc.

The general idea is to wrap a more complicated storage facility in a simple,
uniform interface, keeping the freedom of using the right tools for the job.
In particular, a Datastore can aggregate other datastores in interesting ways,
like sharded (to distribute load) or tiered access (caches before databases).

While Datastores should be written general enough to accept all sorts of
values, some implementations will undoubtedly have to be specific (e.g. SQL
databases where fields should be decomposed into columns), particularly to
support queries efficiently. Moreover, certain datastores may enforce certain
types of values (e.g. requiring an io.Reader, a specific struct, etc) or
serialization formats (JSON, Protobufs, etc).

IMPORTANT: No Datastore should ever Panic! This is a cross-module interface,
and thus it should behave predictably and handle exceptional conditions with
proper error reporting. Thus, all Datastore calls may return errors, which
should be checked by callers.
*/
type Datastore interface {
	Read
	Write
	// Sync guarantees that any Put or Delete calls under prefix that returned
	// before Sync(prefix) was called will be observed after Sync(prefix)
	// returns, even if the program crashes. If Put/Delete operations already
	// satisfy these requirements then Sync may be a no-op.
	//
	// If the prefix fails to Sync this method returns an error.
	Sync(ctx context.Context, prefix Key) error
	io.Closer
}

// Write is the write-side of the Datastore interface.
type Write interface {
	// Put stores the object `value` named by `key`.
	//
	// The generalized Datastore interface does not impose a value type,
	// allowing various datastore middleware implementations (which do not
	// handle the values directly) to be composed together.
	//
	// Ultimately, the lowest-level datastore will need to do some value checking
	// or risk getting incorrect values. It may also be useful to expose a more
	// type-safe interface to your application, and do the checking up-front.
	Put(ctx context.Context, key Key, value []byte) error

	// Delete removes the value for given `key`. If the key is not in the
	// datastore, this method returns no error.
	Delete(ctx context.Context, key Key) error
}

// Read is the read-side of the Datastore interface.
type Read interface {
	// Get retrieves the object `value` named by `key`.
	// Get will return ErrNotFound if the key is not mapped to a value.
	Get(ctx context.Context, key Key) (value []byte, err error)

	// Has returns whether the `key` is mapped to a `value`.
	// In some contexts, it may be much cheaper only to check for existence of
	// a value, rather than retrieving the value itself. (e.g. HTTP HEAD).
	// The default implementation is found in `GetBackedHas`.
	Has(ctx context.Context, key Key) (exists bool, err error)

	// GetSize returns the size of the `value` named by `key`.
	// In some contexts, it may be much cheaper to only get the size of the
	// value rather than retrieving the value itself.
	GetSize(ctx context.Context, key Key) (size int, err error)

	// Query searches the datastore and returns a query result. This function
	// may return before the query actually runs. To wait for the query:
	//
	//   result, _ := ds.Query(q)
	//
	//   // use the channel interface; result may come in at different times
	//   for entry := range result.Next() { ... }
	//
	//   // or wait for the query to be completely done
	//   entries, _ := result.Rest()
	//   for entry := range entries { ... }
	//
	Query(ctx context.Context, q query.Query) (query.Results, error)
}

// QueryIter returns a go iterator that allows ranging over query results.
// The range yields two values, a result Entry and an error. If an error is
// returned then iteration stops.
//
// Example:
//
//	qry := query.Query{
//		Prefix: keyPrefix,
//	}
//	var foundVal []byte
//	for ent, err := range QueryIter(ctx, dstore, qry) {
//		if err != nil {
//			return err
//		}
//		if ent.Key == lookingFor {
//			foundVal = ent.Val
//			break
//		}
//	}
func QueryIter(ctx context.Context, ds Read, q query.Query) iter.Seq2[query.Entry, error] {
	return func(yield func(query.Entry, error) bool) {
		results, err := ds.Query(ctx, q)
		if err != nil {
			yield(query.Entry{}, err)
			return
		}
		defer results.Close()

		for result := range results.Next() {
			if ctx.Err() != nil {
				yield(query.Entry{}, ctx.Err())
				return
			}
			if result.Error != nil {
				yield(query.Entry{}, result.Error)
				return
			}
			if !yield(result.Entry, nil) {
				return
			}
		}
	}
}

// Batching datastores support deferred, grouped updates to the database.
// `Batch`es do NOT have transactional semantics: updates to the underlying
// datastore are not guaranteed to occur in the same iota of time. Similarly,
// batched updates will not be flushed to the underlying datastore until
// `Commit` has been called. `Txn`s from a `TxnDatastore` have all the
// capabilities of a `Batch`, but the reverse is NOT true.
type Batching interface {
	Datastore
	BatchingFeature
}

// ErrBatchUnsupported is returned if the by Batch if the Datastore doesn't
// actually support batching.
var ErrBatchUnsupported = errors.New("this datastore does not support batching")

// CheckedDatastore is an interface that should be implemented by datastores
// which may need checking on-disk data integrity.
type CheckedDatastore interface {
	Datastore
	CheckedFeature
}

// ScrubbedDatastore is an interface that should be implemented by datastores
// which want to provide a mechanism to check data integrity and/or
// error correction.
type ScrubbedDatastore interface {
	Datastore
	ScrubbedFeature
}

// GCDatastore is an interface that should be implemented by datastores which
// don't free disk space by just removing data from them.
type GCDatastore interface {
	Datastore
	GCFeature
}

// PersistentDatastore is an interface that should be implemented by datastores
// which can report disk usage.
type PersistentDatastore interface {
	Datastore
	PersistentFeature
}

// DiskUsage checks if a Datastore is a
// PersistentDatastore and returns its DiskUsage(),
// otherwise returns 0.
func DiskUsage(ctx context.Context, d Datastore) (uint64, error) {
	persDs, ok := d.(PersistentDatastore)
	if !ok {
		return 0, nil
	}
	return persDs.DiskUsage(ctx)
}

// TTLDatastore is an interface that should be implemented by datastores that
// support expiring entries.
type TTLDatastore interface {
	Datastore
	TTL
}

// Txn extends the Datastore type. Txns allow users to batch queries and
// mutations to the Datastore into atomic groups, or transactions. Actions
// performed on a transaction will not take hold until a successful call to
// Commit has been made. Likewise, transactions can be aborted by calling
// Discard before a successful Commit has been made.
type Txn interface {
	Read
	Write

	// Commit finalizes a transaction, attempting to commit it to the Datastore.
	// May return an error if the transaction has gone stale. The presence of an
	// error is an indication that the data was not committed to the Datastore.
	Commit(ctx context.Context) error
	// Discard throws away changes recorded in a transaction without committing
	// them to the underlying Datastore. Any calls made to Discard after Commit
	// has been successfully called will have no effect on the transaction and
	// state of the Datastore, making it safe to defer.
	Discard(ctx context.Context)
}

// TxnDatastore is an interface that should be implemented by datastores that
// support transactions.
type TxnDatastore interface {
	Datastore
	TxnFeature
}

// Errors

type dsError struct {
	error
	isNotFound bool
}

func (e *dsError) NotFound() bool {
	return e.isNotFound
}

// ErrNotFound is returned by Get and GetSize when a datastore does not map the
// given key to a value.
var ErrNotFound error = &dsError{error: errors.New("datastore: key not found"), isNotFound: true}

// GetBackedHas provides a default Datastore.Has implementation.
// It exists so Datastore.Has implementations can use it, like so:
//
//	func (*d SomeDatastore) Has(key Key) (exists bool, err error) {
//	  return GetBackedHas(d, key)
//	}
func GetBackedHas(ctx context.Context, ds Read, key Key) (bool, error) {
	_, err := ds.Get(ctx, key)
	if err != nil {
		if err == ErrNotFound {
			return false, nil
		}
		return false, err
	}
	return true, nil
}

// GetBackedSize provides a default Datastore.GetSize implementation.
// It exists so Datastore.GetSize implementations can use it, like so:
//
//	func (*d SomeDatastore) GetSize(key Key) (size int, err error) {
//	  return GetBackedSize(d, key)
//	}
func GetBackedSize(ctx context.Context, ds Read, key Key) (int, error) {
	value, err := ds.Get(ctx, key)
	if err != nil {
		return -1, err
	}
	return len(value), nil
}

type Batch interface {
	Write

	// Commit applies the write operations to the datastore.
	//
	// A Batch should not be used after calling Commit. A new Batch must be
	// created to perform additional batched operations.
	Commit(ctx context.Context) error
}
