package ns

import (
	"context"
	"errors"
	"fmt"
	"net/url"

	"github.com/nspcc-dev/neo-go/pkg/core/state"
	neoclient "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"
	"github.com/nspcc-dev/neofs-contract/nns"
	cid "github.com/nspcc-dev/neofs-sdk-go/container/id"
)

// NNS looks up NeoFS names using Neo Name Service.
//
// Instances are created with a variable declaration. Before work, the connection
// to the NNS server MUST be established using Dial method.
type NNS struct {
	nnsContract util.Uint160

	neoClient neoClient
}

// represents virtual connection to Neo network used by NNS.Dial.
type neoClient interface {
	// calls specified method of the Neo smart contract with provided parameters.
	call(contract util.Uint160, method string, prm []smartcontract.Parameter) (*result.Invoke, error)
}

// implements neoClient using Neo HTTP client.
//
// note: see NNS.Dial to realize why this isn't defined as type wrapper like neoWebSocket.
type neoHTTP struct {
	*neoclient.Client
}

func (x *neoHTTP) call(contract util.Uint160, method string, prm []smartcontract.Parameter) (*result.Invoke, error) {
	return x.Client.InvokeFunction(contract, method, prm, nil)
}

// implements neoClient using Neo WebSocket client.
type neoWebSocket neoclient.WSClient

func (x *neoWebSocket) call(contract util.Uint160, method string, prm []smartcontract.Parameter) (*result.Invoke, error) {
	return (*neoclient.WSClient)(x).InvokeFunction(contract, method, prm, nil)
}

// Dial connects to the address of the NNS server. If fails, the instance
// MUST NOT be used.
//
// If URL address scheme is 'ws' or 'wss', then WebSocket protocol is used,
// otherwise HTTP.
func (n *NNS) Dial(address string) error {
	// multiSchemeClient unites neoClient and common interface of
	// neoclient.Client and neoclient.WSClient. Interface is anonymous
	// according to assumption that common interface of these client types
	// is not required by design and may diverge with changes.
	var multiSchemeClient interface {
		neoClient
		// Init turns client to "ready-to-work" state.
		Init() error
		// GetContractStateByID returns state of the NNS contract on 1 input.
		GetContractStateByID(int32) (*state.Contract, error)
	}

	uri, err := url.Parse(address)
	if err == nil && (uri.Scheme == "ws" || uri.Scheme == "wss") {
		cWebSocket, err := neoclient.NewWS(context.Background(), address, neoclient.Options{})
		if err != nil {
			return fmt.Errorf("create Neo WebSocket client: %w", err)
		}

		multiSchemeClient = (*neoWebSocket)(cWebSocket)
	} else {
		cHTTP, err := neoclient.New(context.Background(), address, neoclient.Options{})
		if err != nil {
			return fmt.Errorf("create Neo HTTP client: %w", err)
		}

		// if neoHTTP is defined as type wrapper
		//   type neoHTTP neoclient.Client
		// then next assignment causes compilation error
		//   multiSchemeClient = (*neoHTTP)(cHTTP)
		multiSchemeClient = &neoHTTP{
			Client: cHTTP,
		}
	}

	if err = multiSchemeClient.Init(); err != nil {
		return fmt.Errorf("initialize Neo client: %w", err)
	}

	nnsContract, err := multiSchemeClient.GetContractStateByID(1)
	if err != nil {
		return fmt.Errorf("get NNS contract state: %w", err)
	}

	n.neoClient = multiSchemeClient
	n.nnsContract = nnsContract.Hash

	return nil
}

// ResolveContainerName looks up for NNS TXT records for the given container name
// by calling `resolve` method of NNS contract. Returns the first record which represents
// valid container ID in a string format. Otherwise, returns an error.
//
// ResolveContainerName MUST NOT be called before successful Dial.
//
// See also https://docs.neo.org/docs/en-us/reference/nns.html.
func (n *NNS) ResolveContainerName(name string) (cid.ID, error) {
	res, err := n.neoClient.call(n.nnsContract, "resolve", []smartcontract.Parameter{
		{
			Type:  smartcontract.StringType,
			Value: name + ".container",
		},
		{
			Type:  smartcontract.IntegerType,
			Value: int64(nns.TXT),
		},
	})
	if err != nil {
		return cid.ID{}, fmt.Errorf("invoke NNS contract: %w", err)
	}

	if res.State != vm.HaltState.String() {
		return cid.ID{}, fmt.Errorf("NNS contract fault exception: %s", res.FaultException)
	} else if len(res.Stack) == 0 {
		return cid.ID{}, errors.New("empty stack in invocation result")
	}

	itemArr, err := res.Stack[len(res.Stack)-1].Convert(stackitem.ArrayT) // top stack element is last in the array
	if err != nil {
		return cid.ID{}, fmt.Errorf("convert stack item to %s", stackitem.ArrayT)
	}

	if _, ok := itemArr.(stackitem.Null); !ok {
		arr, ok := itemArr.Value().([]stackitem.Item)
		if !ok {
			// unexpected for types from stackitem package
			return cid.ID{}, errors.New("invalid cast to stack item slice")
		}

		var id cid.ID

		for i := range arr {
			bs, err := arr[i].TryBytes()
			if err != nil {
				return cid.ID{}, fmt.Errorf("convert array item to byte slice: %w", err)
			}

			err = id.DecodeString(string(bs))
			if err == nil {
				return id, nil
			}
		}
	}

	return cid.ID{}, errNotFound
}