package contractstorage

import (
	"git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain"
	"git.frostfs.info/TrueCloudLab/policy-engine/pkg/engine"
	policy_morph "git.frostfs.info/TrueCloudLab/policy-engine/pkg/morph/policy"
	"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/actor"
	"github.com/nspcc-dev/neo-go/pkg/rpcclient/invoker"
	"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 ProxyAdaptedContractStorage interface {
	AddMorphRuleChain(name chain.Name, target engine.Target, c *chain.Chain) (util.Uint256, uint32, error)

	RemoveMorphRuleChain(name chain.Name, target engine.Target, chainID chain.ID) (util.Uint256, uint32, error)

	ListMorphRuleChains(name chain.Name, target engine.Target) ([]*chain.Chain, error)
}

var _ ProxyAdaptedContractStorage = (engine.MorphRuleChainStorage)(nil)

type RPCActorProvider interface {
	GetRPCActor() actor.RPCActor
}

// ProxyVerificationContractStorage uses decorated MorphRuleChainStorage with actor where cosigner is a proxy contract.
type ProxyVerificationContractStorage struct {
	rpcActorProvider RPCActorProvider

	cosigners []actor.SignerAccount

	policyScriptHash util.Uint160
}

var _ ProxyAdaptedContractStorage = (*ProxyVerificationContractStorage)(nil)

func NewProxyVerificationContractStorage(rpcActorProvider RPCActorProvider, key *keys.PrivateKey, proxyScriptHash, policyScriptHash util.Uint160) *ProxyVerificationContractStorage {
	acc := wallet.NewAccountFromPrivateKey(key)
	return &ProxyVerificationContractStorage{
		rpcActorProvider: rpcActorProvider,

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

		policyScriptHash: policyScriptHash,
	}
}

// contractStorageActorAdapter adapats *actor.Actor to policy_morph.ContractStorageActor interface.
type contractStorageActorAdapter struct {
	*actor.Actor
	rpcActor invoker.RPCInvoke
}

func (n *contractStorageActorAdapter) GetRPCInvoker() invoker.RPCInvoke {
	return n.rpcActor
}

func (contractStorage *ProxyVerificationContractStorage) newContractStorageActor() (policy_morph.ContractStorageActor, error) {
	rpcActor := contractStorage.rpcActorProvider.GetRPCActor()
	act, err := actor.New(rpcActor, contractStorage.cosigners)
	if err != nil {
		return nil, err
	}
	return &contractStorageActorAdapter{
		Actor:    act,
		rpcActor: rpcActor,
	}, nil
}

// AddMorphRuleChain add morph rule chain to Policy contract using both Proxy contract and storage account as consigners.
func (contractStorage *ProxyVerificationContractStorage) AddMorphRuleChain(name chain.Name, target engine.Target, c *chain.Chain) (util.Uint256, uint32, error) {
	// contractStorageActor is reconstructed per each method invocation because RPCActor's (that is, basically, WSClient) connection may get invalidated, but
	// ProxyVerificationContractStorage does not manage reconnections.
	contractStorageActor, err := contractStorage.newContractStorageActor()
	if err != nil {
		return util.Uint256{}, 0, err
	}
	return policy_morph.NewContractStorage(contractStorageActor, contractStorage.policyScriptHash).AddMorphRuleChain(name, target, c)
}

// RemoveMorphRuleChain removes morph rule chain from Policy contract using both Proxy contract and storage account as consigners.
func (contractStorage *ProxyVerificationContractStorage) RemoveMorphRuleChain(name chain.Name, target engine.Target, chainID chain.ID) (util.Uint256, uint32, error) {
	// contractStorageActor is reconstructed per each method invocation because RPCActor's (that is, basically, WSClient) connection may get invalidated, but
	// ProxyVerificationContractStorage does not manage reconnections.
	contractStorageActor, err := contractStorage.newContractStorageActor()
	if err != nil {
		return util.Uint256{}, 0, err
	}
	return policy_morph.NewContractStorage(contractStorageActor, contractStorage.policyScriptHash).RemoveMorphRuleChain(name, target, chainID)
}

// ListMorphRuleChains lists morph rule chains from Policy contract using both Proxy contract and storage account as consigners.
func (contractStorage *ProxyVerificationContractStorage) ListMorphRuleChains(name chain.Name, target engine.Target) ([]*chain.Chain, error) {
	rpcActor := contractStorage.rpcActorProvider.GetRPCActor()
	inv := &invokerAdapter{Invoker: invoker.New(rpcActor, nil), rpcInvoker: rpcActor}
	return policy_morph.NewContractStorageReader(inv, contractStorage.policyScriptHash).ListMorphRuleChains(name, target)
}

type invokerAdapter struct {
	*invoker.Invoker
	rpcInvoker invoker.RPCInvoke
}

func (n *invokerAdapter) GetRPCInvoker() invoker.RPCInvoke {
	return n.rpcInvoker
}