package object

import (
	"fmt"
	"strconv"
	"strings"

	internalclient "git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/client"
	"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/commonflags"
	"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/key"
	commonCmd "git.frostfs.info/TrueCloudLab/frostfs-node/cmd/internal/common"
	cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
	objectSDK "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object"
	oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id"
	"github.com/spf13/cobra"
)

const (
	newAttrsFlagName     = "new-attrs"
	replaceAttrsFlagName = "replace-attrs"
	rangeFlagName        = "range"
	payloadFlagName      = "payload"
)

var objectPatchCmd = &cobra.Command{
	Use:   "patch",
	Run:   patch,
	Short: "Patch FrostFS object",
	Long:  "Patch FrostFS object. Each range passed to the command requires to pass a corresponding patch payload.",
	Example: `
frostfs-cli -c config.yml -r 127.0.0.1:8080 object patch --cid <CID> --oid <OID> --new-attrs 'key1=val1,key2=val2' --replace-attrs
frostfs-cli -c config.yml -r 127.0.0.1:8080 object patch --cid <CID> --oid <OID> --range offX:lnX --payload /path/to/payloadX --range offY:lnY --payload /path/to/payloadY
frostfs-cli -c config.yml -r 127.0.0.1:8080 object patch --cid <CID> --oid <OID> --new-attrs 'key1=val1,key2=val2' --replace-attrs --range offX:lnX --payload /path/to/payload
`,
}

func initObjectPatchCmd() {
	commonflags.Init(objectPatchCmd)
	initFlagSession(objectPatchCmd, "PATCH")

	flags := objectPatchCmd.Flags()

	flags.String(commonflags.CIDFlag, "", commonflags.CIDFlagUsage)
	_ = objectRangeCmd.MarkFlagRequired(commonflags.CIDFlag)

	flags.String(commonflags.OIDFlag, "", commonflags.OIDFlagUsage)
	_ = objectRangeCmd.MarkFlagRequired(commonflags.OIDFlag)

	flags.String(newAttrsFlagName, "", "New object attributes in form of Key1=Value1,Key2=Value2")
	flags.Bool(replaceAttrsFlagName, false, "Replace object attributes by new ones.")
	flags.StringSlice(rangeFlagName, []string{}, "Range to which patch payload is applied. Format: offset:length")
	flags.StringSlice(payloadFlagName, []string{}, "Path to file with patch payload.")
}

func patch(cmd *cobra.Command, _ []string) {
	var cnr cid.ID
	var obj oid.ID

	objAddr := readObjectAddress(cmd, &cnr, &obj)

	ranges, err := getRangeSlice(cmd)
	commonCmd.ExitOnErr(cmd, "", err)

	payloads := patchPayloadPaths(cmd)

	if len(ranges) != len(payloads) {
		commonCmd.ExitOnErr(cmd, "", fmt.Errorf("the number of ranges and payloads are not equal: ranges = %d, payloads = %d", len(ranges), len(payloads)))
	}

	newAttrs, err := parseNewObjectAttrs(cmd)
	commonCmd.ExitOnErr(cmd, "can't parse new object attributes: %w", err)
	replaceAttrs, _ := cmd.Flags().GetBool(replaceAttrsFlagName)

	pk := key.GetOrGenerate(cmd)

	cli := internalclient.GetSDKClientByFlag(cmd, pk, commonflags.RPC)

	var prm internalclient.PatchObjectPrm
	prm.SetClient(cli)
	Prepare(cmd, &prm)
	ReadOrOpenSession(cmd, &prm, pk, cnr, nil)

	prm.SetAddress(objAddr)
	prm.NewAttributes = newAttrs
	prm.ReplaceAttribute = replaceAttrs

	for i := range ranges {
		prm.PayloadPatches = append(prm.PayloadPatches, internalclient.PayloadPatch{
			Range:       ranges[i],
			PayloadPath: payloads[i],
		})
	}

	res, err := internalclient.Patch(cmd.Context(), prm)
	if err != nil {
		commonCmd.ExitOnErr(cmd, "can't patch the object: %w", err)
	}
	cmd.Println("Patched object ID: ", res.OID.EncodeToString())
}

func parseNewObjectAttrs(cmd *cobra.Command) ([]objectSDK.Attribute, error) {
	var rawAttrs []string

	raw := cmd.Flag(newAttrsFlagName).Value.String()
	if len(raw) != 0 {
		rawAttrs = strings.Split(raw, ",")
	}

	attrs := make([]objectSDK.Attribute, len(rawAttrs), len(rawAttrs)+2) // name + timestamp attributes
	for i := range rawAttrs {
		k, v, found := strings.Cut(rawAttrs[i], "=")
		if !found {
			return nil, fmt.Errorf("invalid attribute format: %s", rawAttrs[i])
		}
		attrs[i].SetKey(k)
		attrs[i].SetValue(v)
	}
	return attrs, nil
}

func getRangeSlice(cmd *cobra.Command) ([]objectSDK.Range, error) {
	v, _ := cmd.Flags().GetStringSlice(rangeFlagName)
	if len(v) == 0 {
		return []objectSDK.Range{}, nil
	}
	rs := make([]objectSDK.Range, len(v))
	for i := range v {
		before, after, found := strings.Cut(v[i], rangeSep)
		if !found {
			return nil, fmt.Errorf("invalid range specifier: %s", v[i])
		}

		offset, err := strconv.ParseUint(before, 10, 64)
		if err != nil {
			return nil, fmt.Errorf("invalid '%s' range offset specifier: %w", v[i], err)
		}
		length, err := strconv.ParseUint(after, 10, 64)
		if err != nil {
			return nil, fmt.Errorf("invalid '%s' range length specifier: %w", v[i], err)
		}

		rs[i].SetOffset(offset)
		rs[i].SetLength(length)
	}
	return rs, nil
}

func patchPayloadPaths(cmd *cobra.Command) []string {
	v, _ := cmd.Flags().GetStringSlice(payloadFlagName)
	return v
}