package object

import (
	"crypto/ecdsa"
	"errors"
	"fmt"
	"strings"

	internal "github.com/nspcc-dev/neofs-node/cmd/neofs-cli/internal/client"
	"github.com/nspcc-dev/neofs-node/cmd/neofs-cli/internal/common"
	"github.com/nspcc-dev/neofs-node/cmd/neofs-cli/internal/commonflags"
	"github.com/nspcc-dev/neofs-sdk-go/bearer"
	cid "github.com/nspcc-dev/neofs-sdk-go/container/id"
	neofsecdsa "github.com/nspcc-dev/neofs-sdk-go/crypto/ecdsa"
	oid "github.com/nspcc-dev/neofs-sdk-go/object/id"
	"github.com/nspcc-dev/neofs-sdk-go/session"
	"github.com/spf13/cobra"
	"github.com/spf13/viper"
)

const (
	bearerTokenFlag = "bearer"

	rawFlag     = "raw"
	rawFlagDesc = "Set raw request option"
)

type RPCParameters interface {
	SetBearerToken(prm *bearer.Token)
	SetTTL(uint32)
	SetXHeaders([]string)
}

// InitBearer adds bearer token flag to a command.
func InitBearer(cmd *cobra.Command) {
	flags := cmd.Flags()
	flags.String(bearerTokenFlag, "", "File with signed JSON or binary encoded bearer token")
}

// Prepare prepares object-related parameters for a command.
func Prepare(cmd *cobra.Command, prms ...RPCParameters) {
	ttl := viper.GetUint32(commonflags.TTL)
	common.PrintVerbose("TTL: %d", ttl)

	for i := range prms {
		btok := common.ReadBearerToken(cmd, bearerTokenFlag)

		prms[i].SetBearerToken(btok)
		prms[i].SetTTL(ttl)
		prms[i].SetXHeaders(parseXHeaders(cmd))
	}
}

func parseXHeaders(cmd *cobra.Command) []string {
	xHeaders, _ := cmd.Flags().GetStringSlice(commonflags.XHeadersKey)
	xs := make([]string, 0, 2*len(xHeaders))

	for i := range xHeaders {
		kv := strings.SplitN(xHeaders[i], "=", 2)
		if len(kv) != 2 {
			panic(fmt.Errorf("invalid X-Header format: %s", xHeaders[i]))
		}

		xs = append(xs, kv[0], kv[1])
	}

	return xs
}

func readObjectAddress(cmd *cobra.Command, cnr *cid.ID, obj *oid.ID) oid.Address {
	readCID(cmd, cnr)
	readOID(cmd, obj)

	var addr oid.Address
	addr.SetContainer(*cnr)
	addr.SetObject(*obj)
	return addr
}

func readCID(cmd *cobra.Command, id *cid.ID) {
	err := id.DecodeString(cmd.Flag("cid").Value.String())
	common.ExitOnErr(cmd, "decode container ID string: %w", err)
}

func readOID(cmd *cobra.Command, id *oid.ID) {
	err := id.DecodeString(cmd.Flag("oid").Value.String())
	common.ExitOnErr(cmd, "decode object ID string: %w", err)
}

// common interface of object operation's input which supports sessions.
// Implemented on types like internal.PutObjectPrm.
type sessionPrm interface {
	SetSessionToken(*session.Object)
}

// forwards all parameters to _readSession and object as nil.
func readSessionGlobal(cmd *cobra.Command, dst sessionPrm, key *ecdsa.PrivateKey, cnr cid.ID) {
	_readSession(cmd, dst, key, cnr, nil)
}

// forwards all parameters to _readSession.
func readSession(cmd *cobra.Command, dst sessionPrm, key *ecdsa.PrivateKey, cnr cid.ID, obj oid.ID) {
	_readSession(cmd, dst, key, cnr, &obj)
}

// decodes object session from JSON file from "session" command flag if it is provided,
// and writes resulting session into the provided sessionPrm. Checks:
//
//   - if session verb corresponds to given sessionPrm according to its type
//   - relation to the given container
//   - relation to the given object if non-nil
//   - relation to the given private key used withing the command
//   - session signature
//
// sessionPrm MUST be one of:
//
//	*internal.PutObjectPrm
//	*internal.DeleteObjectPrm
//	*internal.GetObjectPrm
//	*internal.HeadObjectPrm
//	*internal.SearchObjectsPrm
//	*internal.PayloadRangePrm
//	*internal.HashPayloadRangesPrm
func _readSession(cmd *cobra.Command, dst sessionPrm, key *ecdsa.PrivateKey, cnr cid.ID, obj *oid.ID) {
	var cmdVerb session.ObjectVerb

	switch dst.(type) {
	default:
		panic(fmt.Sprintf("unsupported op parameters %T", dst))
	case *internal.PutObjectPrm:
		cmdVerb = session.VerbObjectPut
	case *internal.DeleteObjectPrm:
		cmdVerb = session.VerbObjectDelete
	case *internal.GetObjectPrm:
		cmdVerb = session.VerbObjectGet
	case *internal.HeadObjectPrm:
		cmdVerb = session.VerbObjectHead
	case *internal.SearchObjectsPrm:
		cmdVerb = session.VerbObjectSearch
	case *internal.PayloadRangePrm:
		cmdVerb = session.VerbObjectRange
	case *internal.HashPayloadRangesPrm:
		cmdVerb = session.VerbObjectRangeHash
	}

	sessionTokenPath, _ := cmd.Flags().GetString(commonflags.SessionToken)
	if sessionTokenPath != "" {
		common.PrintVerbose("Reading object session from the JSON file [%s]...", sessionTokenPath)

		var tok session.Object
		common.ReadSessionToken(cmd, &tok, sessionTokenPath)

		common.PrintVerbose("Checking session correctness...")

		switch false {
		case tok.AssertContainer(cnr):
			common.ExitOnErr(cmd, "", errors.New("unrelated container in the session"))
		case obj == nil || tok.AssertObject(*obj):
			common.ExitOnErr(cmd, "", errors.New("unrelated object in the session"))
		case tok.AssertVerb(cmdVerb):
			common.ExitOnErr(cmd, "", errors.New("wrong verb of the session"))
		case tok.AssertAuthKey((*neofsecdsa.PublicKey)(&key.PublicKey)):
			common.ExitOnErr(cmd, "", errors.New("unrelated key in the session"))
		case tok.VerifySignature():
			common.ExitOnErr(cmd, "", errors.New("invalid signature of the session data"))
		}

		common.PrintVerbose("Session is correct.")

		dst.SetSessionToken(&tok)
	} else {
		common.PrintVerbose("Session is not provided.")
	}
}