package implementations

import (
	"context"

	sc "github.com/nspcc-dev/neo-go/pkg/smartcontract"
	libacl "github.com/nspcc-dev/neofs-api-go/acl"
	"github.com/nspcc-dev/neofs-node/internal"
	"github.com/nspcc-dev/neofs-node/lib/acl"
	"github.com/nspcc-dev/neofs-node/lib/blockchain/goclient"
	"github.com/nspcc-dev/neofs-node/lib/container"

	"github.com/nspcc-dev/neo-go/pkg/util"
	"github.com/nspcc-dev/neofs-api-go/refs"
	"github.com/pkg/errors"
)

// Consider moving ACLHelper implementation to the ACL library.

type (
	// ACLHelper is an interface, that provides useful functions
	// for ACL object pre-processor.
	ACLHelper interface {
		BasicACLGetter
		ContainerOwnerChecker
	}

	// BasicACLGetter helper provides function to return basic ACL value.
	BasicACLGetter interface {
		GetBasicACL(context.Context, CID) (uint32, error)
	}

	// ContainerOwnerChecker checks owner of the container.
	ContainerOwnerChecker interface {
		IsContainerOwner(context.Context, CID, refs.OwnerID) (bool, error)
	}

	aclHelper struct {
		cnr container.Storage
	}
)

type binaryEACLSource struct {
	binaryStore acl.BinaryExtendedACLSource
}

// StaticContractClient is a wrapper over Neo:Morph client
// that invokes single smart contract methods with fixed fee.
type StaticContractClient struct {
	// neo-go client instance
	client *goclient.Client

	// contract script-hash
	scScriptHash util.Uint160

	// invocation fee
	fee util.Fixed8
}

// MorphContainerContract is a wrapper over StaticContractClient
// for Container contract calls.
type MorphContainerContract struct {
	// NeoFS Container smart-contract
	containerContract StaticContractClient

	// set EACL method name of container contract
	eaclSetMethodName string

	// get EACL method name of container contract
	eaclGetMethodName string

	// get container method name of container contract
	cnrGetMethodName string

	// put container method name of container contract
	cnrPutMethodName string

	// delete container method name of container contract
	cnrDelMethodName string

	// list containers method name of container contract
	cnrListMethodName string
}

const (
	errNewACLHelper = internal.Error("cannot create ACLHelper instance")
)

// GetBasicACL returns basic ACL of the container.
func (h aclHelper) GetBasicACL(ctx context.Context, cid CID) (uint32, error) {
	gp := container.GetParams{}
	gp.SetContext(ctx)
	gp.SetCID(cid)

	gResp, err := h.cnr.GetContainer(gp)
	if err != nil {
		return 0, err
	}

	return gResp.Container().BasicACL, nil
}

// IsContainerOwner returns true if provided id is an owner container.
func (h aclHelper) IsContainerOwner(ctx context.Context, cid CID, id refs.OwnerID) (bool, error) {
	gp := container.GetParams{}
	gp.SetContext(ctx)
	gp.SetCID(cid)

	gResp, err := h.cnr.GetContainer(gp)
	if err != nil {
		return false, err
	}

	return gResp.Container().OwnerID.Equal(id), nil
}

// NewACLHelper returns implementation of the ACLHelper interface.
func NewACLHelper(cnr container.Storage) (ACLHelper, error) {
	if cnr == nil {
		return nil, errNewACLHelper
	}

	return aclHelper{cnr}, nil
}

// ExtendedACLSourceFromBinary wraps BinaryExtendedACLSource and returns ExtendedACLSource.
//
// If passed BinaryExtendedACLSource is nil, acl.ErrNilBinaryExtendedACLStore returns.
func ExtendedACLSourceFromBinary(v acl.BinaryExtendedACLSource) (acl.ExtendedACLSource, error) {
	if v == nil {
		return nil, acl.ErrNilBinaryExtendedACLStore
	}

	return &binaryEACLSource{
		binaryStore: v,
	}, nil
}

// GetExtendedACLTable receives eACL table in a binary representation from storage,
// unmarshals it and returns ExtendedACLTable interface.
func (s binaryEACLSource) GetExtendedACLTable(ctx context.Context, cid refs.CID) (libacl.ExtendedACLTable, error) {
	key := acl.BinaryEACLKey{}
	key.SetCID(cid)

	val, err := s.binaryStore.GetBinaryEACL(ctx, key)
	if err != nil {
		return nil, err
	}

	eacl := val.EACL()

	// TODO: verify signature

	res := libacl.WrapEACLTable(nil)

	return res, res.UnmarshalBinary(eacl)
}

// NewStaticContractClient initializes a new StaticContractClient.
//
// If passed Client is nil, goclient.ErrNilClient returns.
func NewStaticContractClient(client *goclient.Client, scHash util.Uint160, fee util.Fixed8) (StaticContractClient, error) {
	res := StaticContractClient{
		client:       client,
		scScriptHash: scHash,
		fee:          fee,
	}

	var err error
	if client == nil {
		err = goclient.ErrNilClient
	}

	return res, err
}

// Invoke calls Invoke method of goclient with predefined script hash and fee.
// Supported args types are the same as in goclient.
//
// If Client is not initialized, goclient.ErrNilClient returns.
func (s StaticContractClient) Invoke(method string, args ...interface{}) error {
	if s.client == nil {
		return goclient.ErrNilClient
	}

	return s.client.Invoke(
		s.scScriptHash,
		s.fee,
		method,
		args...,
	)
}

// TestInvoke calls TestInvoke method of goclient with predefined script hash.
//
// If Client is not initialized, goclient.ErrNilClient returns.
func (s StaticContractClient) TestInvoke(method string, args ...interface{}) ([]sc.Parameter, error) {
	if s.client == nil {
		return nil, goclient.ErrNilClient
	}

	return s.client.TestInvoke(
		s.scScriptHash,
		method,
		args...,
	)
}

// SetContainerContractClient is a container contract client setter.
func (s *MorphContainerContract) SetContainerContractClient(v StaticContractClient) {
	s.containerContract = v
}

// SetEACLGetMethodName is a container contract Get EACL method name setter.
func (s *MorphContainerContract) SetEACLGetMethodName(v string) {
	s.eaclGetMethodName = v
}

// SetEACLSetMethodName is a container contract Set EACL method name setter.
func (s *MorphContainerContract) SetEACLSetMethodName(v string) {
	s.eaclSetMethodName = v
}

// SetContainerGetMethodName is a container contract Get method name setter.
func (s *MorphContainerContract) SetContainerGetMethodName(v string) {
	s.cnrGetMethodName = v
}

// SetContainerPutMethodName is a container contract Put method name setter.
func (s *MorphContainerContract) SetContainerPutMethodName(v string) {
	s.cnrPutMethodName = v
}

// SetContainerDeleteMethodName is a container contract Delete method name setter.
func (s *MorphContainerContract) SetContainerDeleteMethodName(v string) {
	s.cnrDelMethodName = v
}

// SetContainerListMethodName is a container contract List method name setter.
func (s *MorphContainerContract) SetContainerListMethodName(v string) {
	s.cnrListMethodName = v
}

// GetBinaryEACL performs the test invocation call of GetEACL method of NeoFS Container contract.
func (s *MorphContainerContract) GetBinaryEACL(_ context.Context, key acl.BinaryEACLKey) (acl.BinaryEACLValue, error) {
	res := acl.BinaryEACLValue{}

	prms, err := s.containerContract.TestInvoke(
		s.eaclGetMethodName,
		key.CID().Bytes(),
	)
	if err != nil {
		return res, err
	} else if ln := len(prms); ln != 1 {
		return res, errors.Errorf("unexpected stack parameter count: %d", ln)
	}

	eacl, err := goclient.BytesFromStackParameter(prms[0])
	if err == nil {
		res.SetEACL(eacl)
	}

	return res, err
}

// PutBinaryEACL invokes the call of SetEACL method of NeoFS Container contract.
func (s *MorphContainerContract) PutBinaryEACL(_ context.Context, key acl.BinaryEACLKey, val acl.BinaryEACLValue) error {
	return s.containerContract.Invoke(
		s.eaclSetMethodName,
		key.CID().Bytes(),
		val.EACL(),
		val.Signature(),
	)
}

// GetContainer performs the test invocation call of Get method of NeoFS Container contract.
func (s *MorphContainerContract) GetContainer(p container.GetParams) (*container.GetResult, error) {
	prms, err := s.containerContract.TestInvoke(
		s.cnrGetMethodName,
		p.CID().Bytes(),
	)
	if err != nil {
		return nil, errors.Wrap(err, "could not perform test invocation")
	} else if ln := len(prms); ln != 1 {
		return nil, errors.Errorf("unexpected stack item count: %d", ln)
	}

	cnrBytes, err := goclient.BytesFromStackParameter(prms[0])
	if err != nil {
		return nil, errors.Wrap(err, "could not get byte array from stack item")
	}

	cnr := new(container.Container)
	if err := cnr.Unmarshal(cnrBytes); err != nil {
		return nil, errors.Wrap(err, "could not unmarshal container from bytes")
	}

	res := new(container.GetResult)
	res.SetContainer(cnr)

	return res, nil
}

// PutContainer invokes the call of Put method of NeoFS Container contract.
func (s *MorphContainerContract) PutContainer(p container.PutParams) (*container.PutResult, error) {
	cnr := p.Container()

	cid, err := cnr.ID()
	if err != nil {
		return nil, errors.Wrap(err, "could not calculate container ID")
	}

	cnrBytes, err := cnr.Marshal()
	if err != nil {
		return nil, errors.Wrap(err, "could not marshal container")
	}

	if err := s.containerContract.Invoke(
		s.cnrPutMethodName,
		cnr.OwnerID.Bytes(),
		cnrBytes,
		[]byte{},
	); err != nil {
		return nil, errors.Wrap(err, "could not invoke contract method")
	}

	res := new(container.PutResult)
	res.SetCID(cid)

	return res, nil
}

// DeleteContainer invokes the call of Delete method of NeoFS Container contract.
func (s *MorphContainerContract) DeleteContainer(p container.DeleteParams) (*container.DeleteResult, error) {
	if err := s.containerContract.Invoke(
		s.cnrDelMethodName,
		p.CID().Bytes(),
		p.OwnerID().Bytes(),
		[]byte{},
	); err != nil {
		return nil, errors.Wrap(err, "could not invoke contract method")
	}

	return new(container.DeleteResult), nil
}

// ListContainers performs the test invocation call of Get method of NeoFS Container contract.
//
// If owner ID list in parameters is non-empty, bytes of first owner are attached to call.
func (s *MorphContainerContract) ListContainers(p container.ListParams) (*container.ListResult, error) {
	args := make([]interface{}, 0, 1)

	if ownerIDList := p.OwnerIDList(); len(ownerIDList) > 0 {
		args = append(args, ownerIDList[0].Bytes())
	}

	prms, err := s.containerContract.TestInvoke(
		s.cnrListMethodName,
		args...,
	)
	if err != nil {
		return nil, errors.Wrap(err, "could not perform test invocation")
	} else if ln := len(prms); ln != 1 {
		return nil, errors.Errorf("unexpected stack item count: %d", ln)
	}

	prms, err = goclient.ArrayFromStackParameter(prms[0])
	if err != nil {
		return nil, errors.Wrap(err, "could not get stack item array from stack item")
	}

	cidList := make([]CID, 0, len(prms))

	for i := range prms {
		cidBytes, err := goclient.BytesFromStackParameter(prms[i])
		if err != nil {
			return nil, errors.Wrap(err, "could not get byte array from stack item")
		}

		cid, err := refs.CIDFromBytes(cidBytes)
		if err != nil {
			return nil, errors.Wrap(err, "could not get container ID from bytes")
		}

		cidList = append(cidList, cid)
	}

	res := new(container.ListResult)
	res.SetCIDList(cidList)

	return res, nil
}