package layer

import (
	"bytes"
	"context"
	"encoding/xml"
	"errors"
	"fmt"

	"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/data"
	apierr "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/errors"
	"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/layer/frostfs"
	"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/layer/tree"
	"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/logs"
	oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id"
	"go.uber.org/zap"
)

type PutBucketLifecycleParams struct {
	BktInfo       *data.BucketInfo
	LifecycleCfg  *data.LifecycleConfiguration
	CopiesNumbers []uint32
}

func (n *Layer) PutBucketLifecycleConfiguration(ctx context.Context, p *PutBucketLifecycleParams) error {
	cfgBytes, err := xml.Marshal(p.LifecycleCfg)
	if err != nil {
		return fmt.Errorf("marshal lifecycle configuration: %w", err)
	}

	prm := frostfs.PrmObjectCreate{
		Payload:      bytes.NewReader(cfgBytes),
		Filepath:     p.BktInfo.LifecycleConfigurationObjectName(),
		CreationTime: TimeNow(ctx),
	}

	var lifecycleBkt *data.BucketInfo
	if n.lifecycleCnrInfo == nil {
		lifecycleBkt = p.BktInfo
		prm.CopiesNumber = p.CopiesNumbers
	} else {
		lifecycleBkt = n.lifecycleCnrInfo
		prm.PrmAuth.PrivateKey = &n.gateKey.PrivateKey
	}

	prm.Container = lifecycleBkt.CID

	createdObj, err := n.objectPutAndHash(ctx, prm, lifecycleBkt)
	if err != nil {
		return fmt.Errorf("put lifecycle object: %w", err)
	}

	objsToDelete, err := n.treeService.PutBucketLifecycleConfiguration(ctx, p.BktInfo, newAddress(lifecycleBkt.CID, createdObj.ID))
	objsToDeleteNotFound := errors.Is(err, tree.ErrNoNodeToRemove)
	if err != nil && !objsToDeleteNotFound {
		return err
	}

	if !objsToDeleteNotFound {
		for _, addr := range objsToDelete {
			n.deleteLifecycleObject(ctx, p.BktInfo, addr)
		}
	}

	n.cache.PutLifecycleConfiguration(n.BearerOwner(ctx), p.BktInfo, p.LifecycleCfg)

	return nil
}

// deleteLifecycleObject removes object and logs in case of error.
func (n *Layer) deleteLifecycleObject(ctx context.Context, bktInfo *data.BucketInfo, addr oid.Address) {
	var prmAuth frostfs.PrmAuth
	lifecycleBkt := bktInfo
	if !addr.Container().Equals(bktInfo.CID) {
		lifecycleBkt = &data.BucketInfo{CID: addr.Container()}
		prmAuth.PrivateKey = &n.gateKey.PrivateKey
	}

	if err := n.objectDeleteWithAuth(ctx, lifecycleBkt, addr.Object(), prmAuth); err != nil {
		n.reqLogger(ctx).Error(logs.CouldntDeleteLifecycleObject, zap.Error(err),
			zap.String("cid", lifecycleBkt.CID.EncodeToString()),
			zap.String("oid", addr.Object().EncodeToString()))
	}
}

func (n *Layer) GetBucketLifecycleConfiguration(ctx context.Context, bktInfo *data.BucketInfo) (*data.LifecycleConfiguration, error) {
	owner := n.BearerOwner(ctx)
	if cfg := n.cache.GetLifecycleConfiguration(owner, bktInfo); cfg != nil {
		return cfg, nil
	}

	addr, err := n.treeService.GetBucketLifecycleConfiguration(ctx, bktInfo)
	objNotFound := errors.Is(err, tree.ErrNodeNotFound)
	if err != nil && !objNotFound {
		return nil, err
	}

	if objNotFound {
		return nil, fmt.Errorf("%w: %s", apierr.GetAPIError(apierr.ErrNoSuchLifecycleConfiguration), err.Error())
	}

	var prmAuth frostfs.PrmAuth
	lifecycleBkt := bktInfo
	if !addr.Container().Equals(bktInfo.CID) {
		lifecycleBkt = &data.BucketInfo{CID: addr.Container()}
		prmAuth.PrivateKey = &n.gateKey.PrivateKey
	}

	obj, err := n.objectGetWithAuth(ctx, lifecycleBkt, addr.Object(), prmAuth)
	if err != nil {
		return nil, fmt.Errorf("get lifecycle object: %w", err)
	}

	lifecycleCfg := &data.LifecycleConfiguration{}

	if err = xml.NewDecoder(obj.Payload).Decode(&lifecycleCfg); err != nil {
		return nil, fmt.Errorf("unmarshal lifecycle configuration: %w", err)
	}

	n.cache.PutLifecycleConfiguration(owner, bktInfo, lifecycleCfg)

	for i := range lifecycleCfg.Rules {
		if lifecycleCfg.Rules[i].Expiration != nil {
			lifecycleCfg.Rules[i].Expiration.Epoch = nil
		}
	}

	return lifecycleCfg, nil
}

func (n *Layer) DeleteBucketLifecycleConfiguration(ctx context.Context, bktInfo *data.BucketInfo) error {
	objs, err := n.treeService.DeleteBucketLifecycleConfiguration(ctx, bktInfo)
	objsNotFound := errors.Is(err, tree.ErrNoNodeToRemove)
	if err != nil && !objsNotFound {
		return err
	}
	if !objsNotFound {
		for _, addr := range objs {
			n.deleteLifecycleObject(ctx, bktInfo, addr)
		}
	}

	n.cache.DeleteLifecycleConfiguration(bktInfo)

	return nil
}