package getsvc

import (
	"context"
	"fmt"

	"git.frostfs.info/TrueCloudLab/frostfs-node/internal/logs"
	"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/core/container"
	"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/core/policy"
	"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/util/logger"
	apistatus "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client/status"
	objectSDK "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object"
	"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/erasurecode"
	oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id"
	"go.uber.org/zap"
	"golang.org/x/sync/errgroup"
)

type assemblerec struct {
	addr      oid.Address
	ecInfo    *objectSDK.ECInfo
	rng       *objectSDK.Range
	objGetter objectGetter
	cs        container.Source
	log       *logger.Logger
}

func newAssemblerEC(
	addr oid.Address,
	ecInfo *objectSDK.ECInfo,
	rng *objectSDK.Range,
	objGetter objectGetter,
	cs container.Source,
	log *logger.Logger,
) *assemblerec {
	return &assemblerec{
		addr:      addr,
		rng:       rng,
		ecInfo:    ecInfo,
		objGetter: objGetter,
		cs:        cs,
		log:       log,
	}
}

// Assemble assembles erasure-coded object and writes it's content to ObjectWriter.
// It returns parent object.
func (a *assemblerec) Assemble(ctx context.Context, writer ObjectWriter, headOnly bool) (*objectSDK.Object, error) {
	if headOnly {
		return a.reconstructHeader(ctx, writer)
	} else if a.rng != nil {
		return a.reconstructRange(ctx, writer)
	}
	return a.reconstructObject(ctx, writer)
}

func (a *assemblerec) getConstructor() (*erasurecode.Constructor, error) {
	cnt, err := a.cs.Get(a.addr.Container())
	if err != nil {
		return nil, err
	}
	dataCount := policy.ECDataCount(cnt.Value.PlacementPolicy())
	parityCount := policy.ECParityCount(cnt.Value.PlacementPolicy())
	return erasurecode.NewConstructor(dataCount, parityCount)
}

func (a *assemblerec) reconstructHeader(ctx context.Context, writer ObjectWriter) (*objectSDK.Object, error) {
	parts := a.retrieveParts(ctx, true)
	c, err := a.getConstructor()
	if err != nil {
		return nil, err
	}
	obj, err := c.ReconstructHeader(parts)
	if err == nil {
		return obj, writer.WriteHeader(ctx, obj)
	}
	return nil, err
}

func (a *assemblerec) reconstructRange(ctx context.Context, writer ObjectWriter) (*objectSDK.Object, error) {
	parts := a.retrieveParts(ctx, false)
	c, err := a.getConstructor()
	if err != nil {
		return nil, err
	}
	obj, err := c.Reconstruct(parts)
	if err != nil {
		return nil, err
	}

	from := a.rng.GetOffset()
	to := from + a.rng.GetLength()
	if pLen := uint64(len(obj.Payload())); to < from || pLen < from || pLen < to {
		return nil, &apistatus.ObjectOutOfRange{}
	}
	err = writer.WriteChunk(ctx, obj.Payload()[from:to])
	if err != nil {
		return nil, err
	}
	return obj, err
}

func (a *assemblerec) reconstructObject(ctx context.Context, writer ObjectWriter) (*objectSDK.Object, error) {
	parts := a.retrieveParts(ctx, false)
	c, err := a.getConstructor()
	if err != nil {
		return nil, err
	}
	obj, err := c.Reconstruct(parts)
	if err == nil {
		err = writer.WriteHeader(ctx, obj.CutPayload())
		if err == nil {
			err = writer.WriteChunk(ctx, obj.Payload())
			if err != nil {
				return nil, err
			}
		}
	}
	return obj, err
}

func (a *assemblerec) retrieveParts(mainCtx context.Context, headOnly bool) []*objectSDK.Object {
	parts := make([]*objectSDK.Object, int(a.ecInfo.Chunks[0].Total))
	errGroup, ctx := errgroup.WithContext(mainCtx)

	for i := range a.ecInfo.Chunks {
		chunk := a.ecInfo.Chunks[i]
		errGroup.Go(func() error {
			objID := new(oid.ID)
			err := objID.ReadFromV2(chunk.ID)
			if err != nil {
				return fmt.Errorf("invalid object ID: %w", err)
			}
			var obj *objectSDK.Object
			if headOnly {
				obj, err = a.objGetter.HeadObject(ctx, *objID)
				if err != nil {
					a.log.Debug(logs.GetUnableToHeadPartECObject, zap.Stringer("part_id", objID), zap.Error(err))
					return nil
				}
			} else {
				sow := NewSimpleObjectWriter()
				obj, err = a.objGetter.GetObjectAndWritePayload(ctx, *objID, nil, sow)
				if err != nil {
					a.log.Debug(logs.GetUnableToGetPartECObject, zap.Stringer("part_id", objID), zap.Error(err))
					return nil
				}
				obj.SetPayload(sow.pld)
			}
			parts[chunk.Index] = obj
			return nil
		})
	}

	if err := errGroup.Wait(); err != nil {
		a.log.Debug(logs.GetUnableToGetAllPartsECObject, zap.Error(err))
	}
	return parts
}