package wallet

import (
	"encoding/hex"
	"errors"
	"fmt"
	"math/big"

	"github.com/nspcc-dev/neo-go/cli/flags"
	"github.com/nspcc-dev/neo-go/cli/input"
	"github.com/nspcc-dev/neo-go/cli/options"
	"github.com/nspcc-dev/neo-go/cli/paramcontext"
	"github.com/nspcc-dev/neo-go/pkg/core/transaction"
	"github.com/nspcc-dev/neo-go/pkg/encoding/address"
	"github.com/nspcc-dev/neo-go/pkg/encoding/fixedn"
	"github.com/nspcc-dev/neo-go/pkg/rpc/client"
	"github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest"
	"github.com/nspcc-dev/neo-go/pkg/util"
	"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
	"github.com/nspcc-dev/neo-go/pkg/wallet"
	"github.com/urfave/cli"
)

func newNEP11Commands() []cli.Command {
	tokenAddressFlag := flags.AddressFlag{
		Name:  "token",
		Usage: "Token contract address or hash in LE",
	}
	ownerAddressFlag := flags.AddressFlag{
		Name:  "address",
		Usage: "NFT owner address or hash in LE",
	}
	tokenID := cli.StringFlag{
		Name:  "id",
		Usage: "Hex-encoded token ID",
	}

	balanceFlags := make([]cli.Flag, len(baseBalanceFlags))
	copy(balanceFlags, baseBalanceFlags)
	balanceFlags = append(balanceFlags, tokenID)
	balanceFlags = append(balanceFlags, options.RPC...)
	transferFlags := make([]cli.Flag, len(baseTransferFlags))
	copy(transferFlags, baseTransferFlags)
	transferFlags = append(transferFlags, tokenID)
	transferFlags = append(transferFlags, options.RPC...)
	return []cli.Command{
		{
			Name:      "balance",
			Usage:     "get address balance",
			UsageText: "balance --wallet <path> --rpc-endpoint <node> [--timeout <time>] [--address <address>] --token <hash-or-name> [--id <token-id>]",
			Action:    getNEP11Balance,
			Flags:     balanceFlags,
		},
		{
			Name:      "import",
			Usage:     "import NEP-11 token to a wallet",
			UsageText: "import --wallet <path> --rpc-endpoint <node> --timeout <time> --token <hash>",
			Action:    importNEP11Token,
			Flags:     importFlags,
		},
		{
			Name:      "info",
			Usage:     "print imported NEP-11 token info",
			UsageText: "print --wallet <path> [--token <hash-or-name>]",
			Action:    printNEP11Info,
			Flags: []cli.Flag{
				walletPathFlag,
				tokenFlag,
			},
		},
		{
			Name:      "remove",
			Usage:     "remove NEP-11 token from the wallet",
			UsageText: "remove --wallet <path> --token <hash-or-name>",
			Action:    removeNEP11Token,
			Flags: []cli.Flag{
				walletPathFlag,
				tokenFlag,
				forceFlag,
			},
		},
		{
			Name:      "transfer",
			Usage:     "transfer NEP-11 tokens",
			UsageText: "transfer --wallet <path> --rpc-endpoint <node> --timeout <time> --from <addr> --to <addr> --token <hash-or-name> --id <token-id> [--amount string] [data] [-- <cosigner1:Scope> [<cosigner2> [...]]]",
			Action:    transferNEP11,
			Flags:     transferFlags,
			Description: `Transfers specified NEP-11 token with optional cosigners list attached to
   the transfer. Amount should be specified for divisible NEP-11
   tokens and omitted for non-divisible NEP-11 tokens. See
   'contract testinvokefunction' documentation for the details
   about cosigners syntax. If no cosigners are given then the
   sender with CalledByEntry scope will be used as the only
   signer.
`,
		},
		{
			Name:      "properties",
			Usage:     "print properties of NEP-11 token",
			UsageText: "properties --rpc-endpoint <node> --timeout <time> --token <hash> --id <token-id>",
			Action:    printNEP11Properties,
			Flags: append([]cli.Flag{
				tokenAddressFlag,
				tokenID,
			}, options.RPC...),
		},
		{
			Name:      "ownerOf",
			Usage:     "print owner of non-divisible NEP-11 token with the specified ID",
			UsageText: "ownerOf --rpc-endpoint <node> --timeout <time> --token <hash> --id <token-id>",
			Action:    printNEP11NDOwner,
			Flags: append([]cli.Flag{
				tokenAddressFlag,
				tokenID,
			}, options.RPC...),
		},
		{
			Name:      "ownerOfD",
			Usage:     "print set of owners of divisible NEP-11 token with the specified ID",
			UsageText: "ownerOfD --rpc-endpoint <node> --timeout <time> --token <hash> --id <token-id>",
			Action:    printNEP11DOwner,
			Flags: append([]cli.Flag{
				tokenAddressFlag,
				tokenID,
			}, options.RPC...),
		},
		{
			Name:      "tokensOf",
			Usage:     "print list of tokens IDs for the specified NFT owner",
			UsageText: "tokensOf --rpc-endpoint <node> --timeout <time> --token <hash> --address <addr>",
			Action:    printNEP11TokensOf,
			Flags: append([]cli.Flag{
				tokenAddressFlag,
				ownerAddressFlag,
			}, options.RPC...),
		},
		{
			Name:      "tokens",
			Usage:     "print list of tokens IDs minted by the specified NFT (optional method)",
			UsageText: "tokens --rpc-endpoint <node> --timeout <time> --token <hash>",
			Action:    printNEP11Tokens,
			Flags: append([]cli.Flag{
				tokenAddressFlag,
			}, options.RPC...),
		},
	}
}

func importNEP11Token(ctx *cli.Context) error {
	return importNEPToken(ctx, manifest.NEP11StandardName)
}

func printNEP11Info(ctx *cli.Context) error {
	return printNEPInfo(ctx, manifest.NEP11StandardName)
}

func removeNEP11Token(ctx *cli.Context) error {
	return removeNEPToken(ctx, manifest.NEP11StandardName)
}

func getNEP11Balance(ctx *cli.Context) error {
	var accounts []*wallet.Account

	wall, err := readWallet(ctx.String("wallet"))
	if err != nil {
		return cli.NewExitError(fmt.Errorf("bad wallet: %w", err), 1)
	}
	defer wall.Close()

	addrFlag := ctx.Generic("address").(*flags.Address)
	if addrFlag.IsSet {
		addrHash := addrFlag.Uint160()
		acc := wall.GetAccount(addrHash)
		if acc == nil {
			return cli.NewExitError(fmt.Errorf("can't find account for the address: %s", address.Uint160ToString(addrHash)), 1)
		}
		accounts = append(accounts, acc)
	} else {
		if len(wall.Accounts) == 0 {
			return cli.NewExitError(errors.New("no accounts in the wallet"), 1)
		}
		accounts = wall.Accounts
	}

	gctx, cancel := options.GetTimeoutContext(ctx)
	defer cancel()

	c, err := options.GetRPCClient(gctx, ctx)
	if err != nil {
		return cli.NewExitError(err, 1)
	}

	name := ctx.String("token")
	if name == "" {
		return cli.NewExitError("token hash or name should be specified", 1)
	}
	token, err := getMatchingToken(ctx, wall, name, manifest.NEP11StandardName)
	if err != nil {
		tokenHash, err := flags.ParseAddress(name)
		if err != nil {
			return cli.NewExitError(fmt.Errorf("can't fetch matching token from RPC-node: %w", err), 1)
		}
		token, err = c.NEP11TokenInfo(tokenHash)
		if err != nil {
			return cli.NewExitError(err.Error(), 1)
		}
	}

	tokenID := ctx.String("id")
	tokenIDBytes, err := hex.DecodeString(tokenID)
	if err != nil {
		return cli.NewExitError(fmt.Errorf("invalid tokenID bytes: %w", err), 1)
	}
	for k, acc := range accounts {
		addrHash, err := address.StringToUint160(acc.Address)
		if err != nil {
			return cli.NewExitError(fmt.Errorf("invalid account address: %w", err), 1)
		}

		if k != 0 {
			fmt.Fprintln(ctx.App.Writer)
		}
		fmt.Fprintf(ctx.App.Writer, "Account %s\n", acc.Address)

		var amount int64
		if len(tokenIDBytes) == 0 {
			amount, err = c.NEP11BalanceOf(token.Hash, addrHash)
		} else {
			amount, err = c.NEP11DBalanceOf(token.Hash, addrHash, tokenIDBytes)
		}
		if err != nil {
			continue
		}
		amountStr := fixedn.ToString(big.NewInt(amount), int(token.Decimals))

		format := "%s: %s (%s)\n"
		formatArgs := []interface{}{token.Symbol, token.Name, token.Hash.StringLE()}
		if len(tokenIDBytes) != 0 {
			format = "%s: %s (%s, %s)\n"
			formatArgs = append(formatArgs, tokenID)
		}
		fmt.Fprintf(ctx.App.Writer, format, formatArgs...)
		fmt.Fprintf(ctx.App.Writer, "\tAmount : %s\n", amountStr)
	}
	return nil
}

func transferNEP11(ctx *cli.Context) error {
	return transferNEP(ctx, manifest.NEP11StandardName)
}

func signAndSendNEP11Transfer(ctx *cli.Context, c *client.Client, acc *wallet.Account, token, to util.Uint160, tokenID []byte, amount *big.Int, data interface{}, cosigners []client.SignerAccount) error {
	gas := flags.Fixed8FromContext(ctx, "gas")
	sysgas := flags.Fixed8FromContext(ctx, "sysgas")

	var (
		tx  *transaction.Transaction
		err error
	)
	if amount != nil {
		var from util.Uint160

		from, err = address.StringToUint160(acc.Address)
		if err != nil {
			return cli.NewExitError(fmt.Errorf("bad account address: %w", err), 1)
		}
		tx, err = c.CreateNEP11TransferTx(acc, token, int64(gas), cosigners, from, to, amount, tokenID, data)
	} else {
		tx, err = c.CreateNEP11TransferTx(acc, token, int64(gas), cosigners, to, tokenID, data)
	}
	if err != nil {
		return cli.NewExitError(err, 1)
	}
	tx.SystemFee += int64(sysgas)

	if outFile := ctx.String("out"); outFile != "" {
		m, err := c.GetNetwork()
		if err != nil {
			return cli.NewExitError(fmt.Errorf("failed to save tx: %w", err), 1)
		}
		if err := paramcontext.InitAndSave(m, tx, acc, outFile); err != nil {
			return cli.NewExitError(err, 1)
		}
	} else {
		if !ctx.Bool("force") {
			err := input.ConfirmTx(ctx.App.Writer, tx)
			if err != nil {
				return cli.NewExitError(err, 1)
			}
		}
		_, err := c.SignAndPushTx(tx, acc, cosigners)
		if err != nil {
			return cli.NewExitError(err, 1)
		}
	}

	fmt.Fprintln(ctx.App.Writer, tx.Hash().StringLE())
	return nil
}

func printNEP11NDOwner(ctx *cli.Context) error {
	return printNEP11Owner(ctx, false)
}

func printNEP11DOwner(ctx *cli.Context) error {
	return printNEP11Owner(ctx, true)
}

func printNEP11Owner(ctx *cli.Context, divisible bool) error {
	var err error
	tokenHash := ctx.Generic("token").(*flags.Address)
	if !tokenHash.IsSet {
		return cli.NewExitError("token contract hash was not set", 1)
	}

	tokenID := ctx.String("id")
	if tokenID == "" {
		return cli.NewExitError(errors.New("token ID should be specified"), 1)
	}
	tokenIDBytes, err := hex.DecodeString(tokenID)
	if err != nil {
		return cli.NewExitError(fmt.Errorf("invalid tokenID bytes: %w", err), 1)
	}

	gctx, cancel := options.GetTimeoutContext(ctx)
	defer cancel()

	c, err := options.GetRPCClient(gctx, ctx)
	if err != nil {
		return cli.NewExitError(err, 1)
	}

	if divisible {
		result, err := c.NEP11DOwnerOf(tokenHash.Uint160(), tokenIDBytes)
		if err != nil {
			return cli.NewExitError(fmt.Sprintf("failed to call NEP-11 divisible `ownerOf` method: %s", err.Error()), 1)
		}
		for _, h := range result {
			fmt.Fprintln(ctx.App.Writer, address.Uint160ToString(h))
		}
	} else {
		result, err := c.NEP11NDOwnerOf(tokenHash.Uint160(), tokenIDBytes)
		if err != nil {
			return cli.NewExitError(fmt.Sprintf("failed to call NEP-11 non-divisible `ownerOf` method: %s", err.Error()), 1)
		}
		fmt.Fprintln(ctx.App.Writer, address.Uint160ToString(result))
	}

	return nil
}

func printNEP11TokensOf(ctx *cli.Context) error {
	var err error
	tokenHash := ctx.Generic("token").(*flags.Address)
	if !tokenHash.IsSet {
		return cli.NewExitError("token contract hash was not set", 1)
	}

	acc := ctx.Generic("address").(*flags.Address)
	if !acc.IsSet {
		return cli.NewExitError("owner address flag was not set", 1)
	}

	gctx, cancel := options.GetTimeoutContext(ctx)
	defer cancel()

	c, err := options.GetRPCClient(gctx, ctx)
	if err != nil {
		return cli.NewExitError(err, 1)
	}

	result, err := c.NEP11TokensOf(tokenHash.Uint160(), acc.Uint160())
	if err != nil {
		return cli.NewExitError(fmt.Sprintf("failed to call NEP-11 `tokensOf` method: %s", err.Error()), 1)
	}

	for i := range result {
		fmt.Fprintln(ctx.App.Writer, hex.EncodeToString(result[i]))
	}
	return nil
}

func printNEP11Tokens(ctx *cli.Context) error {
	var err error
	tokenHash := ctx.Generic("token").(*flags.Address)
	if !tokenHash.IsSet {
		return cli.NewExitError("token contract hash was not set", 1)
	}

	gctx, cancel := options.GetTimeoutContext(ctx)
	defer cancel()

	c, err := options.GetRPCClient(gctx, ctx)
	if err != nil {
		return cli.NewExitError(err, 1)
	}

	result, err := c.NEP11Tokens(tokenHash.Uint160())
	if err != nil {
		return cli.NewExitError(fmt.Sprintf("failed to call optional NEP-11 `tokens` method: %s", err.Error()), 1)
	}

	for i := range result {
		fmt.Fprintln(ctx.App.Writer, hex.EncodeToString(result[i]))
	}
	return nil
}

func printNEP11Properties(ctx *cli.Context) error {
	var err error
	tokenHash := ctx.Generic("token").(*flags.Address)
	if !tokenHash.IsSet {
		return cli.NewExitError("token contract hash was not set", 1)
	}

	tokenID := ctx.String("id")
	if tokenID == "" {
		return cli.NewExitError(errors.New("token ID should be specified"), 1)
	}
	tokenIDBytes, err := hex.DecodeString(tokenID)
	if err != nil {
		return cli.NewExitError(fmt.Errorf("invalid tokenID bytes: %w", err), 1)
	}

	gctx, cancel := options.GetTimeoutContext(ctx)
	defer cancel()

	c, err := options.GetRPCClient(gctx, ctx)
	if err != nil {
		return cli.NewExitError(err, 1)
	}

	result, err := c.NEP11Properties(tokenHash.Uint160(), tokenIDBytes)
	if err != nil {
		return cli.NewExitError(fmt.Sprintf("failed to call NEP-11 `properties` method: %s", err.Error()), 1)
	}

	bytes, err := stackitem.ToJSON(result)
	if err != nil {
		return cli.NewExitError(fmt.Sprintf("failed to convert result to JSON: %s", err), 1)
	}
	fmt.Fprintln(ctx.App.Writer, string(bytes))
	return nil
}