package layer

import (
	"context"
	"errors"
	"fmt"
	"io"

	"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/data"
	oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id"
)

type partObj struct {
	OID  oid.ID
	Size uint64
}

type readerInitiator interface {
	initFrostFSObjectPayloadReader(ctx context.Context, p getFrostFSParams) (io.Reader, error)
}

// implements io.Reader of payloads of the object list stored in the FrostFS network.
type multiObjectReader struct {
	ctx context.Context

	layer readerInitiator

	startPartOffset uint64
	endPartLength   uint64

	prm getFrostFSParams

	curIndex  int
	curReader io.Reader

	parts []partObj
}

type multiObjectReaderConfig struct {
	layer readerInitiator

	// the offset of complete object and total size to read
	off, ln uint64

	bktInfo *data.BucketInfo
	parts   []partObj
}

var (
	errOffsetIsOutOfRange = errors.New("offset is out of payload range")
	errLengthIsOutOfRange = errors.New("length is out of payload range")
	errEmptyPartsList     = errors.New("empty parts list")
	errorZeroRangeLength  = errors.New("zero range length")
)

func newMultiObjectReader(ctx context.Context, cfg multiObjectReaderConfig) (*multiObjectReader, error) {
	if len(cfg.parts) == 0 {
		return nil, errEmptyPartsList
	}

	r := &multiObjectReader{
		ctx:   ctx,
		layer: cfg.layer,
		prm: getFrostFSParams{
			bktInfo: cfg.bktInfo,
		},
		parts: cfg.parts,
	}

	if cfg.off+cfg.ln == 0 {
		return r, nil
	}

	if cfg.off > 0 && cfg.ln == 0 {
		return nil, errorZeroRangeLength
	}

	startPartIndex, startPartOffset := findStartPart(cfg)
	if startPartIndex == -1 {
		return nil, errOffsetIsOutOfRange
	}
	r.startPartOffset = startPartOffset

	endPartIndex, endPartLength := findEndPart(cfg)
	if endPartIndex == -1 {
		return nil, errLengthIsOutOfRange
	}
	r.endPartLength = endPartLength

	r.parts = cfg.parts[startPartIndex : endPartIndex+1]

	return r, nil
}

func findStartPart(cfg multiObjectReaderConfig) (index int, offset uint64) {
	position := cfg.off
	for i, part := range cfg.parts {
		// Strict inequality when searching for start position to avoid reading zero length part.
		if position < part.Size {
			return i, position
		}
		position -= part.Size
	}

	return -1, 0
}

func findEndPart(cfg multiObjectReaderConfig) (index int, length uint64) {
	position := cfg.off + cfg.ln
	for i, part := range cfg.parts {
		// Non-strict inequality when searching for end position to avoid out of payload range error.
		if position <= part.Size {
			return i, position
		}
		position -= part.Size
	}

	return -1, 0
}

func (x *multiObjectReader) Read(p []byte) (n int, err error) {
	if x.curReader != nil {
		n, err = x.curReader.Read(p)
		if !errors.Is(err, io.EOF) {
			return n, err
		}
		x.curIndex++
	}

	if x.curIndex == len(x.parts) {
		return n, io.EOF
	}

	x.prm.oid = x.parts[x.curIndex].OID

	if x.curIndex == 0 {
		x.prm.off = x.startPartOffset
		x.prm.ln = x.parts[x.curIndex].Size - x.startPartOffset
	}

	if x.curIndex == len(x.parts)-1 {
		x.prm.ln = x.endPartLength - x.prm.off
	}

	x.curReader, err = x.layer.initFrostFSObjectPayloadReader(x.ctx, x.prm)
	if err != nil {
		return n, fmt.Errorf("init payload reader for the next part: %w", err)
	}

	x.prm.off = 0
	x.prm.ln = 0

	next, err := x.Read(p[n:])

	return n + next, err
}