package cmd

import (
	"bytes"
	"encoding/json"
	"errors"
	"fmt"
	"io/ioutil"
	"strconv"
	"time"

	"github.com/nspcc-dev/neofs-api-go/pkg"
	"github.com/nspcc-dev/neofs-api-go/pkg/token"
	v2ACL "github.com/nspcc-dev/neofs-api-go/v2/acl"
	"github.com/spf13/cobra"
)

var (
	utilCmd = &cobra.Command{
		Use:   "util",
		Short: "Utility operations",
	}

	signCmd = &cobra.Command{
		Use:   "sign",
		Short: "sign NeoFS structure",
	}

	signBearerCmd = &cobra.Command{
		Use:   "bearer-token",
		Short: "sign bearer token to use it in requests",
		RunE:  signBearerToken,
	}

	convertCmd = &cobra.Command{
		Use:   "convert",
		Short: "convert representation of NeoFS structures",
	}

	convertEACLCmd = &cobra.Command{
		Use:   "eacl",
		Short: "convert representation of extended ACL table",
		RunE:  convertEACLTable,
	}
)

func init() {
	rootCmd.AddCommand(utilCmd)

	utilCmd.AddCommand(signCmd)
	utilCmd.AddCommand(convertCmd)

	signCmd.AddCommand(signBearerCmd)
	signBearerCmd.Flags().String("from", "", "File with JSON or binary encoded bearer token to sign")
	_ = signBearerCmd.MarkFlagFilename("from")
	_ = signBearerCmd.MarkFlagRequired("from")
	signBearerCmd.Flags().String("to", "", "File to dump signed bearer token (default: binary encoded)")
	signBearerCmd.Flags().Bool("json", false, "Dump bearer token in JSON encoding")

	convertCmd.AddCommand(convertEACLCmd)
	convertEACLCmd.Flags().String("from", "", "File with JSON or binary encoded extended ACL table")
	_ = convertEACLCmd.MarkFlagFilename("from")
	_ = convertEACLCmd.MarkFlagRequired("from")
	convertEACLCmd.Flags().String("to", "", "File to dump extended ACL table (default: binary encoded)")
	convertEACLCmd.Flags().Bool("json", false, "Dump extended ACL table in JSON encoding")
}

func signBearerToken(cmd *cobra.Command, _ []string) error {
	btok, err := getBearerToken(cmd, "from")
	if err != nil {
		return err
	}

	key, err := getKey()
	if err != nil {
		return err
	}

	err = completeBearerToken(btok)
	if err != nil {
		return err
	}

	err = btok.SignToken(key)
	if err != nil {
		return err
	}

	to := cmd.Flag("to").Value.String()
	jsonFlag, _ := cmd.Flags().GetBool("json")

	var data []byte
	if jsonFlag || len(to) == 0 {
		data, err = v2ACL.BearerTokenToJSON(btok.ToV2())
		if err != nil {
			return fmt.Errorf("can't JSON encode bearer token: %w", err)
		}
	} else {
		data, err = btok.ToV2().StableMarshal(nil)
		if err != nil {
			return fmt.Errorf("can't binary encode bearer token: %w", err)
		}
	}

	if len(to) == 0 {
		prettyPrintJSON(cmd, data)

		return nil
	}

	err = ioutil.WriteFile(to, data, 0644)
	if err != nil {
		return fmt.Errorf("can't write signed bearer token to file: %w", err)
	}

	cmd.Printf("signed bearer token was successfully dumped to %s\n", to)

	return nil
}

func convertEACLTable(cmd *cobra.Command, _ []string) error {
	pathFrom := cmd.Flag("from").Value.String()
	to := cmd.Flag("to").Value.String()
	jsonFlag, _ := cmd.Flags().GetBool("json")

	table, err := parseEACL(pathFrom)
	if err != nil {
		return err
	}

	var data []byte
	if jsonFlag || len(to) == 0 {
		data, err = v2ACL.TableToJSON(table.ToV2())
		if err != nil {
			return fmt.Errorf("can't JSON encode extended ACL table: %w", err)
		}
	} else {
		data, err = table.ToV2().StableMarshal(nil)
		if err != nil {
			return fmt.Errorf("can't binary encode extended ACL table: %w", err)
		}
	}

	if len(to) == 0 {
		prettyPrintJSON(cmd, data)

		return nil
	}

	err = ioutil.WriteFile(to, data, 0644)
	if err != nil {
		return fmt.Errorf("can't write exteded ACL table to file: %w", err)
	}

	cmd.Printf("extended ACL table was successfully dumped to %s\n", to)

	return nil
}

func completeBearerToken(btok *token.BearerToken) error {
	if v2 := btok.ToV2(); v2 != nil {
		// set eACL table version, because it usually omitted
		table := v2.GetBody().GetEACL()
		table.SetVersion(pkg.SDKVersion().ToV2())

		// back to SDK token
		btok = token.NewBearerTokenFromV2(v2)
	} else {
		return errors.New("unsupported bearer token version")
	}

	return nil
}

func prettyPrintJSON(cmd *cobra.Command, data []byte) {
	buf := new(bytes.Buffer)
	if err := json.Indent(buf, data, "", "  "); err != nil {
		printVerbose("Can't pretty print json: %w", err)
	}

	cmd.Println(buf)
}

func prettyPrintUnixTime(s string) string {
	unixTime, err := strconv.ParseInt(s, 10, 64)
	if err != nil {
		return "malformed"
	}

	timestamp := time.Unix(unixTime, 0)

	return timestamp.String()
}