package writecache

import (
	"bytes"
	"context"
	"errors"
	"fmt"
	"io/fs"
	"os"
	"path/filepath"
	"time"

	objectSDK "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object"
	oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id"
	"go.etcd.io/bbolt"
)

const dbName = "small.bolt"

var defaultBucket = []byte{0}

func (c *cache) flushAndDropBBoltDB(ctx context.Context) error {
	_, err := os.Stat(filepath.Join(c.path, dbName))
	if err != nil && os.IsNotExist(err) {
		return nil
	}
	if err != nil {
		return fmt.Errorf("could not check write-cache database existence: %w", err)
	}
	db, err := OpenDB(c.path, true, os.OpenFile)
	if err != nil {
		return fmt.Errorf("could not open write-cache database: %w", err)
	}
	defer func() {
		_ = db.Close()
	}()

	var last string
	for {
		batch, err := c.readNextDBBatch(db, last)
		if err != nil {
			return err
		}
		if len(batch) == 0 {
			break
		}
		for _, item := range batch {
			var obj objectSDK.Object
			if err := obj.Unmarshal(item.data); err != nil {
				return fmt.Errorf("unmarshal object from database: %w", err)
			}
			if err := c.flushObject(ctx, &obj, item.data, StorageTypeDB); err != nil {
				return fmt.Errorf("flush object from database: %w", err)
			}
		}
		last = batch[len(batch)-1].address
	}
	if err := db.Close(); err != nil {
		return fmt.Errorf("close write-cache database: %w", err)
	}
	if err := os.Remove(filepath.Join(c.path, dbName)); err != nil {
		return fmt.Errorf("remove write-cache database: %w", err)
	}
	return nil
}

type batchItem struct {
	data    []byte
	address string
}

func (c *cache) readNextDBBatch(db *bbolt.DB, last string) ([]batchItem, error) {
	const batchSize = 100
	var batch []batchItem
	err := db.View(func(tx *bbolt.Tx) error {
		var addr oid.Address

		b := tx.Bucket(defaultBucket)
		cs := b.Cursor()
		for k, data := cs.Seek([]byte(last)); k != nil; k, data = cs.Next() {
			sa := string(k)
			if sa == last {
				continue
			}
			if err := addr.DecodeString(sa); err != nil {
				return fmt.Errorf("decode address from database: %w", err)
			}

			batch = append(batch, batchItem{data: bytes.Clone(data), address: sa})
			if len(batch) == batchSize {
				return errIterationCompleted
			}
		}
		return nil
	})
	if err == nil || errors.Is(err, errIterationCompleted) {
		return batch, nil
	}
	return nil, err
}

// OpenDB opens BoltDB instance for write-cache. Opens in read-only mode if ro is true.
func OpenDB(p string, ro bool, openFile func(string, int, fs.FileMode) (*os.File, error)) (*bbolt.DB, error) {
	return bbolt.Open(filepath.Join(p, dbName), os.ModePerm, &bbolt.Options{
		NoFreelistSync: true,
		ReadOnly:       ro,
		Timeout:        100 * time.Millisecond,
		OpenFile:       openFile,
	})
}