package badgerdb

import (
	"context"
	"fmt"
	"path/filepath"
	"sync"

	"github.com/ark-network/ark/internal/core/domain"
	dbtypes "github.com/ark-network/ark/internal/infrastructure/db/types"
	"github.com/dgraph-io/badger/v4"
	"github.com/timshannon/badgerhold/v4"
)

const eventStoreDir = "round-events"

type eventsDTO struct {
	Events [][]byte
}

type eventRepository struct {
	store     *badgerhold.Store
	lock      *sync.RWMutex
	chUpdates chan *domain.Round
	handler   func(round *domain.Round)
}

func NewRoundEventRepository(config ...interface{}) (dbtypes.EventStore, error) {
	if len(config) != 2 {
		return nil, fmt.Errorf("invalid config")
	}
	baseDir, ok := config[0].(string)
	if !ok {
		return nil, fmt.Errorf("invalid base directory")
	}

	var logger badger.Logger
	if config[1] != nil {
		logger, ok = config[1].(badger.Logger)
		if !ok {
			return nil, fmt.Errorf("invalid logger")
		}
	}

	var dir string
	if len(baseDir) > 0 {
		dir = filepath.Join(baseDir, eventStoreDir)
	}
	store, err := createDB(dir, logger)
	if err != nil {
		return nil, fmt.Errorf("failed to open round events store: %s", err)
	}
	chEvents := make(chan *domain.Round)
	lock := &sync.RWMutex{}
	repo := &eventRepository{store, lock, chEvents, nil}
	go repo.listen()
	return repo, nil
}

func (r *eventRepository) Save(
	ctx context.Context, id string, events ...domain.RoundEvent,
) error {
	allEvents, err := r.get(ctx, id)
	if err != nil {
		return err
	}

	allEvents = append(allEvents, events...)
	if err := r.upsert(ctx, id, allEvents); err != nil {
		return err
	}
	go r.publishEvents(allEvents)
	return nil
}
func (r *eventRepository) Load(
	ctx context.Context, id string,
) (*domain.Round, error) {
	events, err := r.get(ctx, id)
	if err != nil {
		return nil, err
	}
	return domain.NewRoundFromEvents(events), nil
}

func (r *eventRepository) RegisterEventsHandler(
	handler func(round *domain.Round),
) {
	r.lock.Lock()
	defer r.lock.Unlock()

	r.handler = handler
}

func (r *eventRepository) Close() {
	close(r.chUpdates)
	r.store.Close()
}

func (r *eventRepository) get(
	ctx context.Context, id string,
) ([]domain.RoundEvent, error) {
	dto := eventsDTO{}
	var err error
	if ctx.Value("tx") != nil {
		tx := ctx.Value("tx").(*badger.Txn)
		err = r.store.TxGet(tx, id, &dto)
	} else {
		err = r.store.Get(id, &dto)
	}
	if err != nil {
		if err == badgerhold.ErrNotFound {
			return nil, nil
		}
		return nil, fmt.Errorf("failed to get events with id %s: %s", id, err)
	}

	return deserializeEvents(dto.Events)
}

func (r *eventRepository) upsert(
	ctx context.Context, id string, events []domain.RoundEvent,
) error {
	buf, err := serializeEvents(events)
	if err != nil {
		return err
	}
	if ctx.Value("tx") != nil {
		tx := ctx.Value("tx").(*badger.Txn)
		err = r.store.TxUpsert(tx, id, buf)
	} else {
		err = r.store.Upsert(id, buf)
	}
	if err != nil {
		return fmt.Errorf("failed to upsert events with id %s: %s", id, err)
	}
	return nil
}

func (r *eventRepository) listen() {
	for updatedRound := range r.chUpdates {
		r.lock.RLock()
		if r.handler != nil {
			r.handler(updatedRound)
		}
		r.lock.RUnlock()
	}
}

func (r *eventRepository) publishEvents(events []domain.RoundEvent) {
	r.lock.Lock()
	defer r.lock.Unlock()
	round := domain.NewRoundFromEvents(events)
	r.chUpdates <- round
}
