package session

import (
	"context"
	"fmt"
	"os"

	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"
	"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/network"
	"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client"
	frostfsecdsa "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/crypto/ecdsa"
	"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/session"
	"github.com/google/uuid"
	"github.com/spf13/cobra"
	"github.com/spf13/viper"
)

const (
	outFlag  = "out"
	jsonFlag = commonflags.JSON
)

const defaultLifetime = 10

var createCmd = &cobra.Command{
	Use:   "create",
	Short: "Create session token",
	Run:   createSession,
	PersistentPreRun: func(cmd *cobra.Command, args []string) {
		_ = viper.BindPFlag(commonflags.WalletPath, cmd.Flags().Lookup(commonflags.WalletPath))
		_ = viper.BindPFlag(commonflags.Account, cmd.Flags().Lookup(commonflags.Account))
		common.StartClientCommandSpan(cmd)
	},
	PersistentPostRun: common.StopClientCommandSpan,
}

func init() {
	createCmd.Flags().Uint64P(commonflags.Lifetime, "l", defaultLifetime, "Number of epochs for token to stay valid")
	createCmd.Flags().StringP(commonflags.WalletPath, commonflags.WalletPathShorthand, commonflags.WalletPathDefault, commonflags.WalletPathUsage)
	createCmd.Flags().StringP(commonflags.Account, commonflags.AccountShorthand, commonflags.AccountDefault, commonflags.AccountUsage)
	createCmd.Flags().String(outFlag, "", "File to write session token to")
	createCmd.Flags().Bool(jsonFlag, false, "Output token in JSON")
	createCmd.Flags().StringP(commonflags.RPC, commonflags.RPCShorthand, commonflags.RPCDefault, commonflags.RPCUsage)

	_ = cobra.MarkFlagRequired(createCmd.Flags(), commonflags.WalletPath)
	_ = cobra.MarkFlagRequired(createCmd.Flags(), outFlag)
	_ = cobra.MarkFlagRequired(createCmd.Flags(), commonflags.RPC)
}

func createSession(cmd *cobra.Command, _ []string) {
	privKey := key.Get(cmd)

	var netAddr network.Address
	addrStr, _ := cmd.Flags().GetString(commonflags.RPC)
	commonCmd.ExitOnErr(cmd, "can't parse endpoint: %w", netAddr.FromString(addrStr))

	c, err := internalclient.GetSDKClient(cmd.Context(), cmd, privKey, netAddr)
	commonCmd.ExitOnErr(cmd, "can't create client: %w", err)

	lifetime := uint64(defaultLifetime)
	if lfArg, _ := cmd.Flags().GetUint64(commonflags.Lifetime); lfArg != 0 {
		lifetime = lfArg
	}

	var tok session.Object

	err = CreateSession(cmd.Context(), &tok, c, lifetime)
	commonCmd.ExitOnErr(cmd, "can't create session: %w", err)

	var data []byte

	if toJSON, _ := cmd.Flags().GetBool(jsonFlag); toJSON {
		data, err = tok.MarshalJSON()
		commonCmd.ExitOnErr(cmd, "can't decode session token JSON: %w", err)
	} else {
		data = tok.Marshal()
	}

	filename, _ := cmd.Flags().GetString(outFlag)
	err = os.WriteFile(filename, data, 0644)
	commonCmd.ExitOnErr(cmd, "can't write token to file: %w", err)
}

// CreateSession opens a new communication with FrostFS storage node using client connection.
// The session is expected to be maintained by the storage node during the given
// number of epochs.
//
// Fills ID, lifetime and session key.
func CreateSession(ctx context.Context, dst *session.Object, c *client.Client, lifetime uint64) error {
	netInfoPrm := internalclient.NetworkInfoPrm{
		Client: c,
	}

	ni, err := internalclient.NetworkInfo(ctx, netInfoPrm)
	if err != nil {
		return fmt.Errorf("can't fetch network info: %w", err)
	}

	cur := ni.NetworkInfo().CurrentEpoch()
	exp := cur + lifetime

	var sessionPrm internalclient.CreateSessionPrm
	sessionPrm.SetClient(c)
	sessionPrm.Expiration = exp

	sessionRes, err := internalclient.CreateSession(ctx, sessionPrm)
	if err != nil {
		return fmt.Errorf("can't open session: %w", err)
	}

	binIDSession := sessionRes.ID()

	var keySession frostfsecdsa.PublicKey

	err = keySession.Decode(sessionRes.SessionKey())
	if err != nil {
		return fmt.Errorf("decode public session key: %w", err)
	}

	var idSession uuid.UUID

	err = idSession.UnmarshalBinary(binIDSession)
	if err != nil {
		return fmt.Errorf("decode session ID: %w", err)
	}

	dst.SetID(idSession)
	dst.SetNbf(cur)
	dst.SetIat(cur)
	dst.SetExp(exp)
	dst.SetAuthKey(&keySession)

	return nil
}