package object

import (
	"bytes"
	"fmt"
	"io"
	"os"

	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/cheggaaa/pb"
	"github.com/spf13/cobra"
)

var objectGetCmd = &cobra.Command{
	Use:   "get",
	Short: "Get object from FrostFS",
	Long:  "Get object from FrostFS",
	Run:   getObject,
}

func initObjectGetCmd() {
	commonflags.Init(objectGetCmd)
	initFlagSession(objectGetCmd, "GET")

	flags := objectGetCmd.Flags()

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

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

	flags.String(fileFlag, "", "File to write object payload to(with -b together with signature and header). Default: stdout.")
	flags.Bool(rawFlag, false, rawFlagDesc)
	flags.Bool(noProgressFlag, false, "Do not show progress bar")
	flags.Bool(binaryFlag, false, "Serialize whole object structure into given file(id + signature + header + payload).")
}

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

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

	filename := cmd.Flag(fileFlag).Value.String()
	out, closer := createOutWriter(cmd, filename)
	defer closer()

	pk := key.GetOrGenerate(cmd)

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

	var prm internalclient.GetObjectPrm
	prm.SetClient(cli)
	Prepare(cmd, &prm)
	readSession(cmd, &prm, pk, cnr, obj)

	raw, _ := cmd.Flags().GetBool(rawFlag)
	prm.SetRawFlag(raw)
	prm.SetAddress(objAddr)

	var p *pb.ProgressBar
	noProgress, _ := cmd.Flags().GetBool(noProgressFlag)

	var payloadWriter io.Writer
	var payloadBuffer *bytes.Buffer
	binary, _ := cmd.Flags().GetBool(binaryFlag)
	if binary {
		payloadBuffer = new(bytes.Buffer)
		payloadWriter = payloadBuffer
	} else {
		payloadWriter = out
	}

	if filename == "" || noProgress {
		prm.SetPayloadWriter(payloadWriter)
	} else {
		p = pb.New64(0)
		p.Output = cmd.OutOrStdout()
		prm.SetPayloadWriter(p.NewProxyWriter(payloadWriter))
		prm.SetHeaderCallback(func(o *objectSDK.Object) {
			p.SetTotal64(int64(o.PayloadSize()))
			p.Start()
		})
	}

	res, err := internalclient.GetObject(cmd.Context(), prm)
	if p != nil {
		p.Finish()
	}
	if err != nil {
		if ok := printSplitInfoErr(cmd, err); ok {
			return
		}

		if ok := printECInfoErr(cmd, err); ok {
			return
		}

		commonCmd.ExitOnErr(cmd, "rpc error: %w", err)
	}

	processResult(cmd, res, binary, payloadBuffer, out, filename)
}

func processResult(cmd *cobra.Command, res *internalclient.GetObjectRes, binary bool, payloadBuffer *bytes.Buffer, out io.Writer, filename string) {
	if binary {
		objToStore := res.Header()
		// TODO(@acid-ant): #1932 Use streams to marshal/unmarshal payload
		objToStore.SetPayload(payloadBuffer.Bytes())
		objBytes, err := objToStore.Marshal()
		commonCmd.ExitOnErr(cmd, "", err)
		_, err = out.Write(objBytes)
		commonCmd.ExitOnErr(cmd, "unable to write binary object in out: %w ", err)
	}

	if filename != "" && !strictOutput(cmd) {
		cmd.Printf("[%s] Object successfully saved\n", filename)
	}

	// Print header only if file is not streamed to stdout.
	if filename != "" {
		err := printHeader(cmd, res.Header())
		commonCmd.ExitOnErr(cmd, "", err)
	}
}

func createOutWriter(cmd *cobra.Command, filename string) (out io.Writer, closer func()) {
	if filename == "" {
		out = os.Stdout
		closer = func() {}
	} else {
		f, err := os.OpenFile(filename, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o644)
		if err != nil {
			commonCmd.ExitOnErr(cmd, "", fmt.Errorf("can't open file '%s': %w", filename, err))
		}

		out = f
		closer = func() {
			f.Close()
		}
	}
	return
}

func strictOutput(cmd *cobra.Command) bool {
	toJSON, _ := cmd.Flags().GetBool(commonflags.JSON)
	toProto, _ := cmd.Flags().GetBool("proto")
	return toJSON || toProto
}