package cacheleveldown

import (
	"context"
	"fmt"
	"os"
	"path"
	"sync"

	"berty.tech/go-orbit-db/address"
	"berty.tech/go-orbit-db/cache"
	datastore "github.com/ipfs/go-datastore"
	"github.com/ipfs/go-datastore/query"
	leveldb "github.com/ipfs/go-ds-leveldb"
	"go.uber.org/zap"
)

var InMemoryDirectory = ":memory:"

type levelDownCache struct {
	muCaches sync.Mutex
	caches   map[string]*wrappedCache
	logger   *zap.Logger
}

type wrappedCache struct {
	wrappedCache datastore.Datastore
	manager      *levelDownCache
	id           string
	closed       bool
}

func (w *wrappedCache) Get(ctx context.Context, key datastore.Key) (value []byte, err error) {
	return w.wrappedCache.Get(ctx, key)
}

func (w *wrappedCache) Has(ctx context.Context, key datastore.Key) (exists bool, err error) {
	return w.wrappedCache.Has(ctx, key)
}

func (w *wrappedCache) GetSize(ctx context.Context, key datastore.Key) (size int, err error) {
	return w.wrappedCache.GetSize(ctx, key)
}

func (w *wrappedCache) Query(ctx context.Context, q query.Query) (query.Results, error) {
	return w.wrappedCache.Query(ctx, q)
}

func (w *wrappedCache) Put(ctx context.Context, key datastore.Key, value []byte) error {
	return w.wrappedCache.Put(ctx, key, value)
}

func (w *wrappedCache) Delete(ctx context.Context, key datastore.Key) error {
	return w.wrappedCache.Delete(ctx, key)
}

func (w *wrappedCache) Sync(ctx context.Context, key datastore.Key) error {
	return w.wrappedCache.Sync(ctx, key)
}

func (w *wrappedCache) Close() error {
	if w.closed {
		return nil
	}

	w.manager.muCaches.Lock()

	w.closed = true
	err := w.wrappedCache.Close()
	delete(w.manager.caches, w.id)

	w.manager.muCaches.Unlock()

	return err
}

func (l *levelDownCache) Load(directory string, dbAddress address.Address) (ds datastore.Datastore, err error) {
	keyPath := datastoreKey(directory, dbAddress)

	l.muCaches.Lock()
	defer l.muCaches.Unlock()

	var ok bool
	if ds, ok = l.caches[keyPath]; ok {
		return
	}

	l.logger.Debug("opening cache db", zap.String("path", keyPath))

	if directory == InMemoryDirectory {
		ds, err = leveldb.NewDatastore("", nil)
	} else {
		ds, err = leveldb.NewDatastore(keyPath, nil)
	}

	if err != nil {
		err = fmt.Errorf("unable to init leveldb datastore: %w", err)
		return
	}

	l.caches[keyPath] = &wrappedCache{wrappedCache: ds, id: keyPath, manager: l}
	return
}

func (l *levelDownCache) Close() error {
	for _, c := range l.caches {
		_ = c.Close()
	}

	return nil
}

func datastoreKey(directory string, dbAddress address.Address) string {
	dbPath := path.Join(dbAddress.GetRoot().String(), dbAddress.GetPath())
	keyPath := path.Join(directory, dbPath)

	return keyPath
}

func (l *levelDownCache) Destroy(directory string, dbAddress address.Address) error {
	keyPath := datastoreKey(directory, dbAddress)
	l.muCaches.Lock()
	defer l.muCaches.Unlock()

	if wc, ok := l.caches[keyPath]; ok {
		wc.Close()
	}

	if directory != InMemoryDirectory {
		if err := os.RemoveAll(keyPath); err != nil {
			return fmt.Errorf("unable to delete datastore: %w", err)
		}
	}

	return nil
}

// New Creates a new leveldb data store
func New(opts *cache.Options) cache.Interface {
	if opts == nil {
		opts = &cache.Options{}
	}

	logger := opts.Logger
	if logger == nil {
		logger = zap.NewNop()
	}

	return &levelDownCache{
		caches: map[string]*wrappedCache{},
		logger: logger,
	}
}

var _ cache.Interface = &levelDownCache{}
var _ datastore.Datastore = &wrappedCache{}
