package client

import (
	"errors"
	"fmt"
	"math/big"
	"strconv"
	"time"

	"git.frostfs.info/TrueCloudLab/frostfs-contract/nns"
	nnsClient "git.frostfs.info/TrueCloudLab/frostfs-contract/rpcclient/nns"
	"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/util"
	"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
)

const (
	nnsContractID = 1 // NNS contract must be deployed first in the sidechain

	// NNSBalanceContractName is a name of the balance contract in NNS.
	NNSBalanceContractName = "balance.frostfs"
	// NNSContainerContractName is a name of the container contract in NNS.
	NNSContainerContractName = "container.frostfs"
	// NNSFrostFSIDContractName is a name of the frostfsid contract in NNS.
	NNSFrostFSIDContractName = "frostfsid.frostfs"
	// NNSNetmapContractName is a name of the netmap contract in NNS.
	NNSNetmapContractName = "netmap.frostfs"
	// NNSProxyContractName is a name of the proxy contract in NNS.
	NNSProxyContractName = "proxy.frostfs"
	// NNSGroupKeyName is a name for the FrostFS group key record in NNS.
	NNSGroupKeyName = "group.frostfs"
	// NNSPolicyContractName is a name of the policy contract in NNS.
	NNSPolicyContractName = "policy.frostfs"
)

// ErrNNSRecordNotFound means that there is no such record in NNS contract.
var ErrNNSRecordNotFound = errors.New("record has not been found in NNS contract")

// NNSAlphabetContractName returns contract name of the alphabet contract in NNS
// based on alphabet index.
func NNSAlphabetContractName(index int) string {
	return "alphabet" + strconv.Itoa(index) + ".frostfs"
}

// NNSContractAddress returns contract address script hash based on its name
// in NNS contract.
// If script hash has not been found, returns ErrNNSRecordNotFound.
func (c *Client) NNSContractAddress(name string) (sh util.Uint160, err error) {
	c.switchLock.RLock()
	defer c.switchLock.RUnlock()

	if c.inactive {
		return util.Uint160{}, ErrConnectionLost
	}

	sh, err = nnsResolve(c.nnsReader, name)
	if err != nil {
		return sh, fmt.Errorf("NNS.resolve: %w", err)
	}
	return sh, nil
}

func nnsResolveItem(r *nnsClient.ContractReader, domain string) ([]stackitem.Item, error) {
	available, err := r.IsAvailable(domain)
	if err != nil {
		return nil, fmt.Errorf("check presence in NNS contract for %s: %w", domain, err)
	}

	if available {
		return nil, ErrNNSRecordNotFound
	}

	return r.Resolve(domain, big.NewInt(int64(nns.TXT)))
}

func nnsResolve(r *nnsClient.ContractReader, domain string) (util.Uint160, error) {
	arr, err := nnsResolveItem(r, domain)
	if err != nil {
		return util.Uint160{}, err
	}

	if len(arr) == 0 {
		return util.Uint160{}, errors.New("NNS record is missing")
	}
	bs, err := arr[0].TryBytes()
	if err != nil {
		return util.Uint160{}, fmt.Errorf("malformed response: %w", err)
	}

	// We support several formats for hash encoding, this logic should be maintained in sync
	// with parseNNSResolveResult from cmd/frostfs-adm/internal/modules/morph/initialize_nns.go
	h, err := util.Uint160DecodeStringLE(string(bs))
	if err == nil {
		return h, nil
	}

	h, err = address.StringToUint160(string(bs))
	if err == nil {
		return h, nil
	}

	return util.Uint160{}, errors.New("no valid hashes are found")
}

// SetGroupSignerScope makes the default signer scope include all FrostFS contracts.
// Should be called for side-chain client only.
func (c *Client) SetGroupSignerScope() error {
	c.switchLock.RLock()
	defer c.switchLock.RUnlock()

	if c.inactive {
		return ErrConnectionLost
	}

	pub, err := c.contractGroupKey()
	if err != nil {
		return err
	}

	// Don't change c before everything is OK.
	cfg := c.cfg
	cfg.signer = &transaction.Signer{
		Scopes:        transaction.CustomGroups | transaction.CalledByEntry,
		AllowedGroups: []*keys.PublicKey{pub},
	}
	rpcActor, err := newActor(c.client, c.acc, cfg)
	if err != nil {
		return err
	}
	c.cfg = cfg
	c.setActor(rpcActor)
	return nil
}

// contractGroupKey returns public key designating FrostFS contract group.
func (c *Client) contractGroupKey() (*keys.PublicKey, error) {
	success := false
	startedAt := time.Now()
	defer func() {
		c.cache.metrics.AddMethodDuration("GroupKey", success, time.Since(startedAt))
	}()

	if gKey := c.cache.groupKey(); gKey != nil {
		success = true
		return gKey, nil
	}

	arr, err := nnsResolveItem(c.nnsReader, NNSGroupKeyName)
	if err != nil {
		return nil, err
	}

	if len(arr) == 0 {
		return nil, errors.New("NNS record is missing")
	}

	bs, err := arr[0].TryBytes()
	if err != nil {
		return nil, err
	}

	pub, err := keys.NewPublicKeyFromString(string(bs))
	if err != nil {
		return nil, err
	}

	c.cache.setGroupKey(pub)

	success = true
	return pub, nil
}