package cmdargs

import (
	"errors"
	"fmt"
	"strings"

	"github.com/nspcc-dev/neo-go/cli/flags"
	"github.com/nspcc-dev/neo-go/pkg/core/transaction"
	"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
	"github.com/nspcc-dev/neo-go/pkg/encoding/address"
	"github.com/nspcc-dev/neo-go/pkg/rpc/client"
	"github.com/nspcc-dev/neo-go/pkg/smartcontract"
	"github.com/nspcc-dev/neo-go/pkg/wallet"
	"github.com/urfave/cli"
)

const (
	// CosignersSeparator marks the start of cosigners cli args.
	CosignersSeparator = "--"
	// ArrayStartSeparator marks the start of array cli arg.
	ArrayStartSeparator = "["
	// ArrayEndSeparator marks the end of array cli arg.
	ArrayEndSeparator = "]"
)

// GetSignersFromContext returns signers parsed from context args starting
// from the specified offset.
func GetSignersFromContext(ctx *cli.Context, offset int) ([]transaction.Signer, *cli.ExitError) {
	args := ctx.Args()
	var signers []transaction.Signer
	if args.Present() && len(args) > offset {
		for i, c := range args[offset:] {
			cosigner, err := parseCosigner(c)
			if err != nil {
				return nil, cli.NewExitError(fmt.Errorf("failed to parse signer #%d: %w", i, err), 1)
			}
			signers = append(signers, cosigner)
		}
	}
	return signers, nil
}

func parseCosigner(c string) (transaction.Signer, error) {
	var (
		err error
		res = transaction.Signer{
			Scopes: transaction.CalledByEntry,
		}
	)
	data := strings.SplitN(c, ":", 2)
	s := data[0]
	res.Account, err = flags.ParseAddress(s)
	if err != nil {
		return res, err
	}

	if len(data) == 1 {
		return res, nil
	}

	res.Scopes = 0
	scopes := strings.Split(data[1], ",")
	for _, s := range scopes {
		sub := strings.Split(s, ":")
		scope, err := transaction.ScopesFromString(sub[0])
		if err != nil {
			return transaction.Signer{}, err
		}
		if scope == transaction.Global && res.Scopes&^transaction.Global != 0 ||
			scope != transaction.Global && res.Scopes&transaction.Global != 0 {
			return transaction.Signer{}, errors.New("Global scope can not be combined with other scopes")
		}

		res.Scopes |= scope

		switch scope {
		case transaction.CustomContracts:
			if len(sub) == 1 {
				return transaction.Signer{}, errors.New("CustomContracts scope must refer to at least one contract")
			}
			for _, s := range sub[1:] {
				addr, err := flags.ParseAddress(s)
				if err != nil {
					return transaction.Signer{}, err
				}

				res.AllowedContracts = append(res.AllowedContracts, addr)
			}
		case transaction.CustomGroups:
			if len(sub) == 1 {
				return transaction.Signer{}, errors.New("CustomGroups scope must refer to at least one group")
			}
			for _, s := range sub[1:] {
				pub, err := keys.NewPublicKeyFromString(s)
				if err != nil {
					return transaction.Signer{}, err
				}

				res.AllowedGroups = append(res.AllowedGroups, pub)
			}
		}
	}
	return res, nil
}

// GetDataFromContext returns data parameter from context args.
func GetDataFromContext(ctx *cli.Context) (int, interface{}, *cli.ExitError) {
	var (
		data   interface{}
		offset int
		params []smartcontract.Parameter
		err    error
	)
	args := ctx.Args()
	if args.Present() {
		offset, params, err = ParseParams(args, true)
		if err != nil {
			return offset, nil, cli.NewExitError(fmt.Errorf("unable to parse 'data' parameter: %w", err), 1)
		}
		if len(params) > 1 {
			return offset, nil, cli.NewExitError("'data' should be represented as a single parameter", 1)
		}
		if len(params) != 0 {
			data, err = smartcontract.ExpandParameterToEmitable(params[0])
			if err != nil {
				return offset, nil, cli.NewExitError(fmt.Sprintf("failed to convert 'data' to emitable type: %s", err.Error()), 1)
			}
		}
	}
	return offset, data, nil
}

// ParseParams extracts array of smartcontract.Parameter from the given args and
// returns the number of handled words, the array itself and an error.
// `calledFromMain` denotes whether the method was called from the outside or
// recursively and used to check if CosignersSeparator and ArrayEndSeparator are
// allowed to be in `args` sequence.
func ParseParams(args []string, calledFromMain bool) (int, []smartcontract.Parameter, error) {
	res := []smartcontract.Parameter{}
	for k := 0; k < len(args); {
		s := args[k]
		switch s {
		case CosignersSeparator:
			if calledFromMain {
				return k + 1, res, nil // `1` to convert index to numWordsRead
			}
			return 0, []smartcontract.Parameter{}, errors.New("invalid array syntax: missing closing bracket")
		case ArrayStartSeparator:
			numWordsRead, array, err := ParseParams(args[k+1:], false)
			if err != nil {
				return 0, nil, fmt.Errorf("failed to parse array: %w", err)
			}
			res = append(res, smartcontract.Parameter{
				Type:  smartcontract.ArrayType,
				Value: array,
			})
			k += 1 + numWordsRead // `1` for opening bracket
		case ArrayEndSeparator:
			if calledFromMain {
				return 0, nil, errors.New("invalid array syntax: missing opening bracket")
			}
			return k + 1, res, nil // `1`to convert index to numWordsRead
		default:
			param, err := smartcontract.NewParameterFromString(s)
			if err != nil {
				// '--' argument is skipped by urfave/cli library, which leads
				// to [--, addr:scope] being transformed to [addr:scope] and
				// interpreted as a parameter if other positional arguments are not present.
				// Here we fallback to parsing cosigners in this specific case to
				// create a better user experience ('-- addr:scope' vs '-- -- addr:scope').
				if k == 0 {
					if _, err := parseCosigner(s); err == nil {
						return 0, nil, nil
					}
				}
				return 0, nil, fmt.Errorf("failed to parse argument #%d: %w", k+1, err)
			}
			res = append(res, *param)
			k++
		}
	}
	if calledFromMain {
		return len(args), res, nil
	}
	return 0, []smartcontract.Parameter{}, errors.New("invalid array syntax: missing closing bracket")
}

// GetSignersAccounts returns the list of signers combined with the corresponding
// accounts from the provided wallet.
func GetSignersAccounts(wall *wallet.Wallet, signers []transaction.Signer) ([]client.SignerAccount, error) {
	signersAccounts := make([]client.SignerAccount, len(signers))
	for i := range signers {
		signerAcc := wall.GetAccount(signers[i].Account)
		if signerAcc == nil {
			return nil, fmt.Errorf("no account was found in the wallet for signer #%d (%s)", i, address.Uint160ToString(signers[i].Account))
		}
		signersAccounts[i] = client.SignerAccount{
			Signer:  signers[i],
			Account: signerAcc,
		}
	}
	return signersAccounts, nil
}