package object

import (
	"encoding/hex"
	"errors"
	"fmt"
	"os"

	"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/refs"
	internalclient "git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/client"
	"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/common"
	"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"
)

var objectHeadCmd = &cobra.Command{
	Use:   "head",
	Short: "Get object header",
	Long:  "Get object header",
	Run:   getObjectHeader,
}

func initObjectHeadCmd() {
	commonflags.Init(objectHeadCmd)
	initFlagSession(objectHeadCmd, "HEAD")

	flags := objectHeadCmd.Flags()

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

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

	flags.String(fileFlag, "", "File to write header to. Default: stdout.")
	flags.Bool("main-only", false, "Return only main fields")
	flags.Bool(commonflags.JSON, false, "Marshal output in JSON")
	flags.Bool("proto", false, "Marshal output in Protobuf")
	flags.Bool(rawFlag, false, rawFlagDesc)
}

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

	objAddr := readObjectAddress(cmd, &cnr, &obj)
	mainOnly, _ := cmd.Flags().GetBool("main-only")
	pk := key.GetOrGenerate(cmd)

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

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

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

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

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

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

	err = saveAndPrintHeader(cmd, res.Header(), cmd.Flag(fileFlag).Value.String())
	commonCmd.ExitOnErr(cmd, "", err)
}

func saveAndPrintHeader(cmd *cobra.Command, obj *objectSDK.Object, filename string) error {
	bs, err := marshalHeader(cmd, obj)
	if err != nil {
		return fmt.Errorf("could not marshal header: %w", err)
	}
	if len(bs) != 0 {
		if filename == "" {
			cmd.Println(string(bs))
			return nil
		}
		err = os.WriteFile(filename, bs, os.ModePerm)
		if err != nil {
			return fmt.Errorf("could not write header to file: %w", err)
		}
		cmd.Printf("[%s] Header successfully saved.", filename)
	}

	return printHeader(cmd, obj)
}

func marshalHeader(cmd *cobra.Command, hdr *objectSDK.Object) ([]byte, error) {
	toJSON, _ := cmd.Flags().GetBool(commonflags.JSON)
	toProto, _ := cmd.Flags().GetBool("proto")
	switch {
	case toJSON && toProto:
		return nil, errors.New("'--json' and '--proto' flags are mutually exclusive")
	case toJSON:
		return hdr.MarshalJSON()
	case toProto:
		return hdr.Marshal()
	default:
		return nil, nil
	}
}

func printObjectID(cmd *cobra.Command, recv func() (oid.ID, bool)) {
	var strID string

	id, ok := recv()
	if ok {
		strID = id.String()
	} else {
		strID = "<empty>"
	}

	cmd.Printf("ID: %s\n", strID)
}

func printContainerID(cmd *cobra.Command, recv func() (cid.ID, bool)) {
	var strID string

	id, ok := recv()
	if ok {
		strID = id.String()
	} else {
		strID = "<empty>"
	}

	cmd.Printf("CID: %s\n", strID)
}

func printHeader(cmd *cobra.Command, obj *objectSDK.Object) error {
	printObjectID(cmd, obj.ID)
	printContainerID(cmd, obj.ContainerID)
	cmd.Printf("Owner: %s\n", obj.OwnerID())
	cmd.Printf("CreatedAt: %d\n", obj.CreationEpoch())
	cmd.Printf("Size: %d\n", obj.PayloadSize())
	common.PrintChecksum(cmd, "HomoHash", obj.PayloadHomomorphicHash)
	common.PrintChecksum(cmd, "Checksum", obj.PayloadChecksum)
	cmd.Printf("Type: %s\n", obj.Type())

	cmd.Println("Attributes:")
	for _, attr := range obj.Attributes() {
		if attr.Key() == objectSDK.AttributeTimestamp {
			cmd.Printf("  %s=%s (%s)\n",
				attr.Key(),
				attr.Value(),
				common.PrettyPrintUnixTime(attr.Value()))
			continue
		}
		cmd.Printf("  %s=%s\n", attr.Key(), attr.Value())
	}

	if signature := obj.Signature(); signature != nil {
		cmd.Print("ID signature:\n")

		// TODO(@carpawell): #468 implement and use another approach to avoid conversion
		var sigV2 refs.Signature
		signature.WriteToV2(&sigV2)

		cmd.Printf("  public key: %s\n", hex.EncodeToString(sigV2.GetKey()))
		cmd.Printf("  signature: %s\n", hex.EncodeToString(sigV2.GetSign()))
	}

	if ecHeader := obj.ECHeader(); ecHeader != nil {
		cmd.Print("EC header:\n")

		cmd.Printf("  parent object ID: %s\n", ecHeader.Parent().EncodeToString())
		cmd.Printf("  index: %d\n", ecHeader.Index())
		cmd.Printf("  total: %d\n", ecHeader.Total())
		cmd.Printf("  header length: %d\n", ecHeader.HeaderLength())
	}

	return printSplitHeader(cmd, obj)
}

func printSplitHeader(cmd *cobra.Command, obj *objectSDK.Object) error {
	if splitID := obj.SplitID(); splitID != nil {
		cmd.Printf("Split ID: %s\n", splitID)
	}

	if oid, ok := obj.ParentID(); ok {
		cmd.Printf("Split ParentID: %s\n", oid)
	}

	if prev, ok := obj.PreviousID(); ok {
		cmd.Printf("Split PreviousID: %s\n", prev)
	}

	for _, child := range obj.Children() {
		cmd.Printf("Split ChildID: %s\n", child.String())
	}

	parent := obj.Parent()
	if parent != nil {
		cmd.Print("\nSplit Parent Header:\n")

		return printHeader(cmd, parent)
	}

	return nil
}