package lmdb

import (
	"os"

	"github.com/PowerDNS/lmdb-go/lmdb"
	"github.com/nbd-wtf/go-nostr/sdk/kvstore"
)

var _ kvstore.KVStore = (*Store)(nil)

type Store struct {
	env *lmdb.Env
	dbi lmdb.DBI
}

func NewStore(path string) (*Store, error) {
	// create directory if it doesn't exist
	if err := os.MkdirAll(path, 0o755); err != nil {
		return nil, err
	}

	// initialize environment
	env, err := lmdb.NewEnv()
	if err != nil {
		return nil, err
	}

	// set max DBs and map size
	env.SetMaxDBs(1)
	env.SetMapSize(1 << 30) // 1GB

	// open the environment
	if err := env.Open(path, lmdb.NoTLS|lmdb.WriteMap, 0o644); err != nil {
		return nil, err
	}

	store := &Store{env: env}

	// open the database
	if err := env.Update(func(txn *lmdb.Txn) error {
		dbi, err := txn.OpenDBI("store", lmdb.Create)
		if err != nil {
			return err
		}
		store.dbi = dbi
		return nil
	}); err != nil {
		env.Close()
		return nil, err
	}

	return store, nil
}

func (s *Store) Get(key []byte) ([]byte, error) {
	var value []byte
	err := s.env.View(func(txn *lmdb.Txn) error {
		v, err := txn.Get(s.dbi, key)
		if lmdb.IsNotFound(err) {
			return nil
		}
		if err != nil {
			return err
		}
		// make a copy since v is only valid during the transaction
		value = make([]byte, len(v))
		copy(value, v)
		return nil
	})
	if err != nil {
		return nil, err
	}
	return value, nil
}

func (s *Store) Set(key []byte, value []byte) error {
	return s.env.Update(func(txn *lmdb.Txn) error {
		return txn.Put(s.dbi, key, value, 0)
	})
}

func (s *Store) Delete(key []byte) error {
	return s.env.Update(func(txn *lmdb.Txn) error {
		return txn.Del(s.dbi, key, nil)
	})
}

func (s *Store) Close() error {
	s.env.Close()
	return nil
}

func (s *Store) Update(key []byte, f func([]byte) ([]byte, error)) error {
	return s.env.Update(func(txn *lmdb.Txn) error {
		var val []byte
		v, err := txn.Get(s.dbi, key)
		if err == nil {
			// make a copy since v is only valid during the transaction
			val = make([]byte, len(v))
			copy(val, v)
		} else if !lmdb.IsNotFound(err) {
			return err
		}

		newVal, err := f(val)
		if err == kvstore.NoOp {
			return nil
		} else if err != nil {
			return err
		}

		if newVal == nil {
			return txn.Del(s.dbi, key, nil)
		}
		return txn.Put(s.dbi, key, newVal, 0)
	})
}