package pilorama

import (
	"cmp"
	"encoding/binary"
	"slices"
	"sync"
	"time"

	cidSDK "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
	"go.etcd.io/bbolt"
)

type batch struct {
	forest *boltForest
	timer  *time.Timer
	// mtx protects timer and operations fields.
	// Because mtx can be taken inside a transaction,
	// transactions MUST NOT be executed with the mutex taken to avoid a deadlock.
	mtx        sync.Mutex
	start      sync.Once
	cid        cidSDK.ID
	treeID     string
	results    []chan<- error
	operations []*Move
}

func (b *batch) trigger() {
	b.mtx.Lock()
	if b.timer != nil {
		b.timer.Stop()
	}
	b.mtx.Unlock()
	b.start.Do(b.run)
}

func (b *batch) run() {
	fullID := bucketName(b.cid, b.treeID)
	err := b.forest.db.Update(func(tx *bbolt.Tx) error {
		bLog, bTree, err := b.forest.getTreeBuckets(tx, fullID)
		if err != nil {
			return err
		}

		b.mtx.Lock()
		b.timer = nil
		b.mtx.Unlock()

		// Sorting without a mutex is ok, because we append to this slice only if timer is non-nil.
		// See (*boltForest).addBatch for details.
		slices.SortFunc(b.operations, func(mi, mj *Move) int {
			return cmp.Compare(mi.Time, mj.Time)
		})
		b.operations = slices.CompactFunc(b.operations, func(x, y *Move) bool { return x.Time == y.Time })

		// Our main use-case is addition of new items. In this case,
		// we do not need to perform undo()/redo(), just do().
		// https://github.com/trvedata/move-op/blob/6c23447c12a7862ff31b7fc2205f6c90fbdb9dc0/proof/Move_Create.thy#L259
		//
		// For this optimization to work we need to ensure three things:
		// 1. The node itself is not yet in tree.
		// 2. The node is not a parent. This case is not mentioned in the article, because
		//    they consider a "static order" (perform all CREATE operations before MOVE).
		//    We need this because if node _is_ a parent, we could violate (3) for some late operation.
		//    See TestForest_ApplySameOperation for details.
		// 3. Parent of each operation is already in tree.
		var parents map[uint64]struct{}
		var cKey [maxKeySize]byte
		var slow bool
		for i := range b.operations {
			_, _, _, inTree := b.forest.getState(bTree, stateKey(cKey[:], b.operations[i].Child))
			if inTree {
				slow = true
				break
			}

			key := childrenKey(cKey[:], b.operations[i].Child, 0)
			k, _ := bTree.Cursor().Seek(key)
			if len(k) == childrenKeySize && binary.LittleEndian.Uint64(k[1:]) == b.operations[i].Child {
				slow = true
				break
			}

			if b.operations[i].Parent == RootID {
				continue
			} else if parents == nil {
				// Attaching key only to root is done frequently,
				// no allocations are performed unless necessary.
				parents = make(map[uint64]struct{})
			} else if _, ok := parents[b.operations[i].Parent]; ok {
				continue
			}

			p := b.operations[i].Parent
			_, ts, _, inTree := b.forest.getState(bTree, stateKey(cKey[:], p))
			if !inTree || b.operations[0].Time < ts {
				slow = true
				break
			}
			parents[b.operations[i].Parent] = struct{}{}
		}

		if slow {
			var lm Move
			return b.forest.applyOperation(bLog, bTree, b.operations, &lm)
		}

		for i := range b.operations {
			if err := b.forest.do(bLog, bTree, cKey[:], b.operations[i]); err != nil {
				return err
			}
		}
		return nil
	})
	for i := range b.results {
		b.results[i] <- err
	}
}