package contract

import (
	"context"
	"fmt"
	"math/big"

	policycontract "git.frostfs.info/TrueCloudLab/frostfs-contract/policy"
	policyclient "git.frostfs.info/TrueCloudLab/frostfs-contract/rpcclient/policy"
	"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/frostfs/policy"
	frostfsutil "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/frostfs/util"
	"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/rpcclient"
	"github.com/nspcc-dev/neo-go/pkg/rpcclient/actor"
	"github.com/nspcc-dev/neo-go/pkg/rpcclient/notary"
	"github.com/nspcc-dev/neo-go/pkg/util"
	"github.com/nspcc-dev/neo-go/pkg/wallet"
)

type Client struct {
	actor          *actor.Actor
	policyContract *policyclient.Contract
}

type Config struct {
	// RPCAddress is an endpoint to connect to neo rpc.
	RPCAddress string

	// Contract is hash of contract or its name in NNS.
	Contract string

	// ProxyContract is hash of proxy contract or its name in NNS to interact with policy.
	ProxyContract string

	// Key is used to interact with policy contract.
	// If this is nil than random key will be generated.
	Key *keys.PrivateKey
}

var _ policy.Contract = (*Client)(nil)

// New creates new Policy contract wrapper.
func New(ctx context.Context, cfg Config) (*Client, error) {
	contractHash, err := frostfsutil.ResolveContractHash(cfg.Contract, cfg.RPCAddress)
	if err != nil {
		return nil, fmt.Errorf("resolve frostfs contract hash: %w", err)
	}

	key := cfg.Key
	if key == nil {
		if key, err = keys.NewPrivateKey(); err != nil {
			return nil, fmt.Errorf("generate anon private key for policy: %w", err)
		}
	}

	rpcCli, err := rpcclient.New(ctx, cfg.RPCAddress, rpcclient.Options{})
	if err != nil {
		return nil, fmt.Errorf("create policy rpc client: %w", err)
	}

	proxyContractHash, err := frostfsutil.ResolveContractHash(cfg.ProxyContract, cfg.RPCAddress)
	if err != nil {
		return nil, fmt.Errorf("resolve frostfs contract hash: %w", err)
	}

	act, err := actor.New(rpcCli, getSigners(key, proxyContractHash, contractHash))
	if err != nil {
		return nil, fmt.Errorf("create new actor: %w", err)
	}

	return &Client{
		actor:          act,
		policyContract: policyclient.New(act, contractHash),
	}, nil
}

func getSigners(key *keys.PrivateKey, proxyHash, contractHash util.Uint160) []actor.SignerAccount {
	acc := wallet.NewAccountFromPrivateKey(key)

	return []actor.SignerAccount{
		{
			Signer: transaction.Signer{
				Account:          proxyHash,
				Scopes:           transaction.CustomContracts,
				AllowedContracts: []util.Uint160{contractHash},
			},
			Account: notary.FakeContractAccount(proxyHash),
		},
		{
			Signer: transaction.Signer{
				Account: acc.Contract.ScriptHash(),
				Scopes:  transaction.CalledByEntry,
			},
			Account: acc,
		},
	}
}

func (c *Client) AddChain(kind policycontract.Kind, entity string, name []byte, chain []byte) (util.Uint256, uint32, error) {
	return c.policyContract.AddChain(big.NewInt(int64(kind)), entity, name, chain)
}

func (c *Client) GetChain(kind policycontract.Kind, entity string, name []byte) ([]byte, error) {
	return c.policyContract.GetChain(big.NewInt(int64(kind)), entity, name)
}

func (c *Client) RemoveChain(kind policycontract.Kind, entity string, name []byte) (util.Uint256, uint32, error) {
	return c.policyContract.RemoveChain(big.NewInt(int64(kind)), entity, name)
}

func (c *Client) ListChains(kind policycontract.Kind, entity string, name []byte) ([][]byte, error) {
	items, err := c.policyContract.ListChainsByPrefix(big.NewInt(int64(kind)), entity, name)
	if err != nil {
		return nil, err
	}

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

	return res, nil
}

func (c *Client) Wait(tx util.Uint256, vub uint32, err error) error {
	_, err = c.actor.Wait(tx, vub, err)
	return err
}