package shard

import (
	"encoding/binary"
	"errors"
	"io"
	"os"

	"github.com/nspcc-dev/neofs-node/pkg/local_object_storage/blobstor"
	"github.com/nspcc-dev/neofs-node/pkg/local_object_storage/writecache"
)

var dumpMagic = []byte("NEOF")

// DumpPrm groups the parameters of Dump operation.
type DumpPrm struct {
	path         string
	stream       io.Writer
	ignoreErrors bool
}

// WithPath is an Dump option to set the destination path.
func (p *DumpPrm) WithPath(path string) *DumpPrm {
	p.path = path
	return p
}

// WithStream is an Dump option to set the destination stream.
// It takes priority over `path` option.
func (p *DumpPrm) WithStream(r io.Writer) *DumpPrm {
	p.stream = r
	return p
}

// WithIgnoreErrors is an Dump option to allow ignore all errors during iteration.
// This includes invalid blobovniczas as well as corrupted objects.
func (p *DumpPrm) WithIgnoreErrors(ignore bool) *DumpPrm {
	p.ignoreErrors = ignore
	return p
}

// DumpRes groups the result fields of Dump operation.
type DumpRes struct {
	count int
}

// Count return amount of object written.
func (r *DumpRes) Count() int {
	return r.count
}

var ErrMustBeReadOnly = errors.New("shard must be in read-only mode")

// Dump dumps all objects from the shard to a file or stream.
//
// Returns any error encountered.
func (s *Shard) Dump(prm *DumpPrm) (*DumpRes, error) {
	s.m.RLock()
	defer s.m.RUnlock()

	if s.info.Mode != ModeReadOnly {
		return nil, ErrMustBeReadOnly
	}

	w := prm.stream
	if w == nil {
		f, err := os.OpenFile(prm.path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0640)
		if err != nil {
			return nil, err
		}
		defer f.Close()

		w = f
	}

	_, err := w.Write(dumpMagic)
	if err != nil {
		return nil, err
	}

	var count int

	if s.hasWriteCache() {
		err := s.writeCache.Iterate(new(writecache.IterationPrm).WithHandler(func(data []byte) error {
			var size [4]byte
			binary.LittleEndian.PutUint32(size[:], uint32(len(data)))
			if _, err := w.Write(size[:]); err != nil {
				return err
			}

			if _, err := w.Write(data); err != nil {
				return err
			}

			count++
			return nil
		}).WithIgnoreErrors(prm.ignoreErrors))
		if err != nil {
			return nil, err
		}
	}

	var pi blobstor.IteratePrm

	if prm.ignoreErrors {
		pi.IgnoreErrors()
	}
	pi.SetIterationHandler(func(elem blobstor.IterationElement) error {
		data := elem.ObjectData()

		var size [4]byte
		binary.LittleEndian.PutUint32(size[:], uint32(len(data)))
		if _, err := w.Write(size[:]); err != nil {
			return err
		}

		if _, err := w.Write(data); err != nil {
			return err
		}

		count++
		return nil
	})

	if _, err := s.blobStor.Iterate(pi); err != nil {
		return nil, err
	}

	return &DumpRes{count: count}, nil
}