package enterprise

import (
	"bytes"
	"encoding/json"
	"errors"
	"fmt"
	"strings"

	"github.com/aergoio/aergo/v2/consensus"
	"github.com/aergoio/aergo/v2/internal/enc/base64"
	"github.com/aergoio/aergo/v2/state"
	"github.com/aergoio/aergo/v2/state/statedb"
	"github.com/aergoio/aergo/v2/types"
)

const SetConf = "setConf"
const AppendConf = "appendConf"
const RemoveConf = "removeConf"
const AppendAdmin = "appendAdmin"
const RemoveAdmin = "removeAdmin"
const EnableConf = "enableConf"
const DisableConf = "disableConf"
const ChangeCluster = "changeCluster"

var ErrTxEnterpriseAdminIsNotSet = errors.New("admin is not set")

func ValidateEnterpriseTx(tx *types.TxBody, sender *state.AccountState,
	scs *statedb.ContractState, blockNo types.BlockNo) (*EnterpriseContext, error) {
	var ci types.CallInfo
	if err := json.Unmarshal(tx.Payload, &ci); err != nil {
		return nil, err
	}
	context := &EnterpriseContext{Call: &ci}
	switch ci.Name {
	case AppendAdmin, RemoveAdmin:
		if len(ci.Args) != 1 { //args[0] : encoded admin address
			return nil, fmt.Errorf("invalid arguments in payload for SetAdmin: %s", ci.Args)
		}
		arg := ci.Args[0].(string)
		context.Args = append(context.Args, arg)

		address := types.ToAddress(arg)
		if len(address) == 0 {
			return nil, fmt.Errorf("invalid arguments[0]: %s", ci.Args[0])
		}
		admins, err := checkAdmin(scs, sender.ID())
		if err != nil &&
			err != ErrTxEnterpriseAdminIsNotSet {
			return nil, err
		}
		context.Admins = admins
		if ci.Name == AppendAdmin && context.IsAdminExist(address) {
			return nil, fmt.Errorf("already exist admin: %s", ci.Args[0])
		} else if ci.Name == RemoveAdmin {
			if !context.IsAdminExist(address) {
				return nil, fmt.Errorf("admins is not exist : %s", ci.Args[0])
			}
			conf, err := getConf(scs, []byte(AccountWhite))
			if err != nil {
				return nil, err
			}
			if conf != nil && conf.On {
				for _, v := range conf.Values {
					if arg == v {
						return nil, fmt.Errorf("admin is in the account whitelist: %s", ci.Args[0])
					}
				}
			}
		}

	case SetConf:
		if len(ci.Args) <= 1 { //args[0] : key, args[1:] : values
			return nil, fmt.Errorf("invalid arguments in payload for setConf: %s", ci.Args)
		}
		if err := checkArgs(context, &ci); err != nil {
			return nil, err
		}
		key := []byte(context.Args[0])
		admins, err := checkAdmin(scs, sender.ID())
		if err != nil {
			return nil, err
		}
		context.Admins = admins
		if context.Conf, err = setConfValues(scs, key, context.Args[1:]); err != nil {
			return nil, err
		}

		if conf, err := getConf(scs, []byte(context.Args[0])); err == nil && conf != nil {
			if err := conf.Validate(key, context); err != nil {
				return nil, err
			}
		}

	case AppendConf, RemoveConf:
		if len(ci.Args) != 2 { //args[0] : key, args[1] : a value
			return nil, fmt.Errorf("invalid arguments in payload for %s : %s", ci.Name, ci.Args)
		}
		if err := checkArgs(context, &ci); err != nil {
			return nil, err
		}
		admins, err := checkAdmin(scs, sender.ID())
		if err != nil {
			return nil, err
		}
		conf, err := getConf(scs, []byte(context.Args[0]))
		if err != nil {
			return nil, err
		}
		if conf == nil {
			conf = &Conf{On: false}
		}
		context.Conf = conf
		context.Admins = admins

		key := []byte(context.Args[0])
		if ci.Name == AppendConf {
			if context.HasConfValue(context.Args[1]) {
				return nil, fmt.Errorf("already included config value : %v", context.Args)
			}
			conf.AppendValue(context.Args[1])
		} else if ci.Name == RemoveConf {
			if !context.HasConfValue(context.Args[1]) {
				return nil, fmt.Errorf("value not exist : %v", context.Args)
			}
			conf.RemoveValue(context.Args[1])
		}

		if err := conf.Validate(key, context); err != nil {
			return nil, err
		}

	case EnableConf:
		if len(ci.Args) != 2 { //args[0] : key, args[1] : true/false
			return nil, fmt.Errorf("invalid arguments in payload for enableConf: %s", ci.Args)
		}
		arg0, ok := ci.Args[0].(string)
		if !ok {
			return nil, fmt.Errorf("not string in payload for enableConf : %s", ci.Args)
		}
		if _, ok := enterpriseKeyDict[strings.ToUpper(ci.Args[0].(string))]; !ok {
			return nil, fmt.Errorf("not allowed key : %s", ci.Args[0])
		}
		context.Args = append(context.Args, arg0)
		key := []byte(arg0)
		value, ok := ci.Args[1].(bool)
		if !ok {
			return nil, fmt.Errorf("not bool in payload for enableConf : %s", ci.Args)
		}
		admins, err := checkAdmin(scs, sender.ID())
		if err != nil {
			return nil, err
		}
		conf, err := enableConf(scs, key, value)
		if err != nil {
			return nil, err
		}

		context.Admins = admins
		context.Conf = conf

		if err := conf.Validate(key, context); err != nil {
			return nil, err
		}

	case ChangeCluster:
		if !consensus.UseRaft() {
			return nil, ErrNotSupportedMethod
		}

		cc, err := ValidateChangeCluster(ci, blockNo)
		if err != nil {
			return nil, err
		}

		context.ArgsAny = append(context.ArgsAny, cc)

		admins, err := checkAdmin(scs, sender.ID())
		if err != nil {
			return nil, err
		}
		context.Admins = admins
	default:
		return nil, fmt.Errorf("unsupported call %s", ci.Name)
	}
	return context, nil
}

func checkAdmin(scs *statedb.ContractState, address []byte) ([][]byte, error) {
	admins, err := getAdmins(scs)
	if err != nil {
		return nil, fmt.Errorf("could not get admin in enterprise contract")
	}
	if admins == nil {
		return nil, ErrTxEnterpriseAdminIsNotSet
	}
	if i := bytes.Index(bytes.Join(admins, []byte("")), address); i == -1 && i%types.AddressLength != 0 {
		return nil, fmt.Errorf("admin address not matched")
	}
	return admins, nil
}

type checkArgsFunc func(string) error

func checkArgs(context *EnterpriseContext, ci *types.CallInfo) error {
	key := strings.ToUpper(ci.Args[0].(string))
	if _, ok := enterpriseKeyDict[key]; !ok {
		return fmt.Errorf("not allowed key : %s", ci.Args[0])
	}

	unique := map[string]int{}
	var op checkArgsFunc
	switch key {
	case P2PWhite, P2PBlack:
		op = checkP2PBlackWhite
	case AccountWhite:
		op = checkAccountWhite
	case RPCPermissions:
		op = checkRPCPermissions
	default:
		op = checkNone
	}

	for i, v := range ci.Args {
		arg, ok := v.(string)
		if !ok {
			return fmt.Errorf("not string in payload for setting conf : %s", ci.Args)
		}
		if strings.Contains(arg, "\\") {
			return fmt.Errorf("not allowed charactor in %s", arg)
		}
		if unique[arg] != 0 {
			return fmt.Errorf("the request has duplicate arguments %s", arg)
		}
		unique[arg]++
		context.Args = append(context.Args, arg)
		if i == 0 { //it's key
			continue
		}
		if err := op(arg); err != nil {
			return err
		}
	}

	return nil
}

func checkP2PBlackWhite(v string) error {
	// v must be json object. e.g. {"peerid":"16Uiu2HAmPZE7gT1hF2bjpg1UVH65xyNUbBVRf3mBFBJpz3tgLGGt", "address":"", "cidr":"172.21.3.35/24" } , which address and cidr cannot be set in same time.
	if _, err := types.ParseListEntry(v); err != nil {
		return fmt.Errorf("invalid p2p whitelist %s", v)
	}
	return nil
}

func checkAccountWhite(v string) error {
	if _, err := types.DecodeAddress(v); err != nil {
		return fmt.Errorf("invalid account %s", v)
	}
	return nil
}

func checkRPCPermissions(v string) error {
	values := strings.Split(v, ":")
	if len(values) != 2 {
		return fmt.Errorf("invalid RPC permission %s", v)
	}

	if _, err := base64.Decode(values[0]); err != nil {
		return fmt.Errorf("invalid RPC cert %s", v)
	}

	return nil
}

func checkNone(v string) error {
	return nil
}
