package resolver

import (
	"fmt"

	nns "github.com/nspcc-dev/neo-go/examples/nft-nd-nns"
	"github.com/nspcc-dev/neo-go/pkg/rpc/client"
	"github.com/nspcc-dev/neo-go/pkg/rpc/response/result"
	"github.com/nspcc-dev/neo-go/pkg/smartcontract"
	"github.com/nspcc-dev/neo-go/pkg/util"
	"github.com/nspcc-dev/neo-go/pkg/vm"
	"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
	cid "github.com/nspcc-dev/neofs-sdk-go/container/id"
)

type NNSResolver interface {
	// ResolveContainerName get record for domain name+'.container' from NNS contract.
	ResolveContainerName(name string) (*cid.ID, error)
}

type nnsResolver struct {
	rpc     *client.Client
	nnsHash util.Uint160
}

const (
	resolve         = "resolve"
	containerDomain = ".container"
)

// NewNNSResolver creates resolver that can get records from NNS contract.
func NewNNSResolver(rpc *client.Client) (NNSResolver, error) {
	nnsContract, err := rpc.GetContractStateByID(1)
	if err != nil {
		return nil, err
	}

	return &nnsResolver{
		rpc:     rpc,
		nnsHash: nnsContract.Hash,
	}, nil
}

func (n *nnsResolver) ResolveContainerName(name string) (*cid.ID, error) {
	res, err := n.rpc.InvokeFunction(n.nnsHash, resolve, []smartcontract.Parameter{
		{
			Type:  smartcontract.StringType,
			Value: name + containerDomain,
		},
		{
			Type:  smartcontract.IntegerType,
			Value: int64(nns.TXT),
		},
	}, nil)
	if err != nil {
		return nil, err
	}
	if err = getInvocationError(res); err != nil {
		return nil, err
	}

	arr, err := getArrString(res.Stack)
	if err != nil {
		return nil, err
	}

	cnrID := cid.New()
	for _, rec := range arr {
		if err = cnrID.Parse(rec); err != nil {
			continue
		}
		return cnrID, nil
	}

	return nil, fmt.Errorf("not found")
}

func getArrString(st []stackitem.Item) ([]string, error) {
	array, err := getArray(st)
	if err != nil {
		return nil, err
	}

	res := make([]string, len(array))
	for i, item := range array {
		bs, err := item.TryBytes()
		if err != nil {
			return nil, err
		}
		res[i] = string(bs)
	}

	return res, nil
}

func getArray(st []stackitem.Item) ([]stackitem.Item, error) {
	index := len(st) - 1 // top stack element is last in the array
	arr, err := st[index].Convert(stackitem.ArrayT)
	if err != nil {
		return nil, err
	}
	if _, ok := arr.(stackitem.Null); ok {
		return nil, nil
	}

	iterator, ok := arr.Value().([]stackitem.Item)
	if !ok {
		return nil, fmt.Errorf("bad conversion")
	}
	return iterator, nil
}

func getInvocationError(result *result.Invoke) error {
	if result.State != vm.HaltState.String() {
		return fmt.Errorf("invocation failed: %s", result.FaultException)
	}
	if len(result.Stack) == 0 {
		return fmt.Errorf("result stack is empty")
	}
	return nil
}