package implementations

import (
	"context"
	"io"
	"sync"
	"time"

	"github.com/multiformats/go-multiaddr"
	"github.com/nspcc-dev/neofs-api-go/hash"
	"github.com/nspcc-dev/neofs-api-go/object"
	"github.com/nspcc-dev/neofs-api-go/refs"
	"github.com/nspcc-dev/neofs-api-go/service"
	"github.com/nspcc-dev/neofs-node/internal"
	"github.com/nspcc-dev/neofs-node/lib/transport"
	"github.com/pkg/errors"
	"go.uber.org/zap"
)

/*
	File source code includes implementation of unified objects container handler.
	Implementation provides the opportunity to perform any logic over object container distributed in network.
	Implementation holds placement and object transport implementations in a black box.
	Any special logic could be tuned through passing handle parameters.
	NOTE: Although the implementation of the other interfaces via OCH is the same, they are still separated in order to avoid mess.
*/

type (
	// SelectiveContainerExecutor is an interface the tool that performs
	// object operations in container with preconditions.
	SelectiveContainerExecutor interface {
		Put(context.Context, *PutParams) error
		Get(context.Context, *GetParams) error
		Head(context.Context, *HeadParams) error
		Search(context.Context, *SearchParams) error
		RangeHash(context.Context, *RangeHashParams) error
	}

	// PutParams groups the parameters
	// of selective object Put.
	PutParams struct {
		SelectiveParams
		Object  *object.Object
		Handler func(multiaddr.Multiaddr, bool)

		CopiesNumber uint32
	}

	// GetParams groups the parameters
	// of selective object Get.
	GetParams struct {
		SelectiveParams
		Handler func(multiaddr.Multiaddr, *object.Object)
	}

	// HeadParams groups the parameters
	// of selective object Head.
	HeadParams struct {
		GetParams
		FullHeaders bool
	}

	// SearchParams groups the parameters
	// of selective object Search.
	SearchParams struct {
		SelectiveParams
		SearchCID   refs.CID
		SearchQuery []byte
		Handler     func(multiaddr.Multiaddr, []refs.Address)
	}

	// RangeHashParams groups the parameters
	// of selective object GetRangeHash.
	RangeHashParams struct {
		SelectiveParams
		Ranges  []object.Range
		Salt    []byte
		Handler func(multiaddr.Multiaddr, []hash.Hash)
	}

	// SelectiveParams groups the parameters of
	// the execution of selective container operation.
	SelectiveParams struct {
		/* Should be set to true only if service under object transport implementations is served on localhost. */
		ServeLocal bool

		/* Raw option of the request */
		Raw bool

		/* TTL for object transport. All transport operations inherit same value. */
		TTL uint32

		/* Required ID of processing container. If empty or not set, an error is returned. */
		CID

		/* List of nodes selected for processing. If not specified => nodes will be selected during. */
		Nodes []multiaddr.Multiaddr

		/*
			Next two parameters provide the opportunity to process selective objects in container.
			At least on of non-empty IDList or Query is required, an error is returned otherwise.
		*/

		/* List of objects to process (overlaps query). */
		IDList []refs.ObjectID
		/* If no objects is indicated, query is used for selection. */
		Query []byte

		/*
			If function provided, it is called after every successful operation.
			True result breaks operation performing.
		*/
		Breaker func(refs.Address) ProgressControlFlag

		/* Public session token */
		Token service.SessionToken

		/* Bearer token */
		Bearer service.BearerToken

		/* Extended headers */
		ExtendedHeaders []service.ExtendedHeader
	}

	// ProgressControlFlag is an enumeration of progress control flags.
	ProgressControlFlag int

	// ObjectContainerHandlerParams grops the parameters of SelectiveContainerExecutor constructor.
	ObjectContainerHandlerParams struct {
		NodeLister ContainerNodesLister
		Executor   ContainerTraverseExecutor
		*zap.Logger
	}

	simpleTraverser struct {
		*sync.Once
		list []multiaddr.Multiaddr
	}

	selectiveCnrExec struct {
		cnl      ContainerNodesLister
		Executor ContainerTraverseExecutor
		log      *zap.Logger
	}

	metaInfo struct {
		ttl uint32
		raw bool
		rt  object.RequestType

		token service.SessionToken

		bearer service.BearerToken

		extHdrs []service.ExtendedHeader
	}

	putInfo struct {
		metaInfo
		obj *object.Object
		cn  uint32
	}

	getInfo struct {
		metaInfo
		addr Address
		raw  bool
	}

	headInfo struct {
		getInfo
		fullHdr bool
	}

	searchInfo struct {
		metaInfo
		cid   CID
		query []byte
	}

	rangeHashInfo struct {
		metaInfo
		addr   Address
		ranges []object.Range
		salt   []byte
	}

	execItems struct {
		params          SelectiveParams
		metaConstructor func(addr Address) transport.MetaInfo
		handler         transport.ResultHandler
	}

	searchTarget struct {
		list []refs.Address
	}

	// ContainerTraverseExecutor is an interface of
	// object operation executor with container traversing.
	ContainerTraverseExecutor interface {
		Execute(context.Context, TraverseParams)
	}

	// TraverseParams groups the parameters of container traversing.
	TraverseParams struct {
		TransportInfo        transport.MetaInfo
		Handler              transport.ResultHandler
		Traverser            Traverser
		WorkerPool           WorkerPool
		ExecutionInterceptor func(context.Context, multiaddr.Multiaddr) bool
	}

	// WorkerPool is an interface of go-routine pool
	WorkerPool interface {
		Submit(func()) error
	}

	// Traverser is an interface of container traverser.
	Traverser interface {
		Next(context.Context) []multiaddr.Multiaddr
	}

	cnrTraverseExec struct {
		transport transport.ObjectTransport
	}

	singleRoutinePool struct{}

	emptyReader struct{}
)

const (
	_ ProgressControlFlag = iota

	// NextAddress is a ProgressControlFlag of to go to the next address of the object.
	NextAddress

	// NextNode is a ProgressControlFlag of to go to the next node.
	NextNode

	// BreakProgress is a ProgressControlFlag to interrupt the execution.
	BreakProgress
)

const (
	instanceFailMsg          = "could not create  container objects collector"
	errEmptyLogger           = internal.Error("empty logger")
	errEmptyNodeLister       = internal.Error("empty container node lister")
	errEmptyTraverseExecutor = internal.Error("empty container traverse executor")

	errSelectiveParams = internal.Error("neither ID list nor query provided")
)

var errNilObjectTransport = errors.New("object transport is nil")

func (s *selectiveCnrExec) Put(ctx context.Context, p *PutParams) error {
	meta := &putInfo{
		metaInfo: metaInfo{
			ttl: p.TTL,
			rt:  object.RequestPut,
			raw: p.Raw,

			token: p.Token,

			bearer: p.Bearer,

			extHdrs: p.ExtendedHeaders,
		},
		obj: p.Object,
		cn:  p.CopiesNumber,
	}

	return s.exec(ctx, &execItems{
		params:          p.SelectiveParams,
		metaConstructor: func(Address) transport.MetaInfo { return meta },
		handler:         p,
	})
}

func (s *selectiveCnrExec) Get(ctx context.Context, p *GetParams) error {
	return s.exec(ctx, &execItems{
		params: p.SelectiveParams,
		metaConstructor: func(addr Address) transport.MetaInfo {
			return &getInfo{
				metaInfo: metaInfo{
					ttl: p.TTL,
					rt:  object.RequestGet,
					raw: p.Raw,

					token: p.Token,

					bearer: p.Bearer,

					extHdrs: p.ExtendedHeaders,
				},
				addr: addr,
				raw:  p.Raw,
			}
		},
		handler: p,
	})
}

func (s *selectiveCnrExec) Head(ctx context.Context, p *HeadParams) error {
	return s.exec(ctx, &execItems{
		params: p.SelectiveParams,
		metaConstructor: func(addr Address) transport.MetaInfo {
			return &headInfo{
				getInfo: getInfo{
					metaInfo: metaInfo{
						ttl: p.TTL,
						rt:  object.RequestHead,
						raw: p.Raw,

						token: p.Token,

						bearer: p.Bearer,

						extHdrs: p.ExtendedHeaders,
					},
					addr: addr,
					raw:  p.Raw,
				},
				fullHdr: p.FullHeaders,
			}
		},
		handler: p,
	})
}

func (s *selectiveCnrExec) Search(ctx context.Context, p *SearchParams) error {
	return s.exec(ctx, &execItems{
		params: p.SelectiveParams,
		metaConstructor: func(Address) transport.MetaInfo {
			return &searchInfo{
				metaInfo: metaInfo{
					ttl: p.TTL,
					rt:  object.RequestSearch,
					raw: p.Raw,

					token: p.Token,

					bearer: p.Bearer,

					extHdrs: p.ExtendedHeaders,
				},
				cid:   p.SearchCID,
				query: p.SearchQuery,
			}
		},
		handler: p,
	})
}

func (s *selectiveCnrExec) RangeHash(ctx context.Context, p *RangeHashParams) error {
	return s.exec(ctx, &execItems{
		params: p.SelectiveParams,
		metaConstructor: func(addr Address) transport.MetaInfo {
			return &rangeHashInfo{
				metaInfo: metaInfo{
					ttl: p.TTL,
					rt:  object.RequestRangeHash,
					raw: p.Raw,

					token: p.Token,

					bearer: p.Bearer,

					extHdrs: p.ExtendedHeaders,
				},
				addr:   addr,
				ranges: p.Ranges,
				salt:   p.Salt,
			}
		},
		handler: p,
	})
}

func (s *selectiveCnrExec) exec(ctx context.Context, p *execItems) error {
	if err := p.params.validate(); err != nil {
		return err
	}

	nodes, err := s.prepareNodes(ctx, &p.params)
	if err != nil {
		return err
	}

loop:
	for i := range nodes {
		addrList := s.prepareAddrList(ctx, &p.params, nodes[i])
		if len(addrList) == 0 {
			continue
		}

		for j := range addrList {
			if p.params.Breaker != nil {
				switch cFlag := p.params.Breaker(addrList[j]); cFlag {
				case NextAddress:
					continue
				case NextNode:
					continue loop
				case BreakProgress:
					break loop
				}
			}

			s.Executor.Execute(ctx, TraverseParams{
				TransportInfo: p.metaConstructor(addrList[j]),
				Handler:       p.handler,
				Traverser:     newSimpleTraverser(nodes[i]),
			})
		}
	}

	return nil
}

func (s *SelectiveParams) validate() error {
	switch {
	case len(s.IDList) == 0 && len(s.Query) == 0:
		return errSelectiveParams
	default:
		return nil
	}
}

func (s *selectiveCnrExec) prepareNodes(ctx context.Context, p *SelectiveParams) ([]multiaddr.Multiaddr, error) {
	if len(p.Nodes) > 0 {
		return p.Nodes, nil
	}

	// If node serves Object transport service on localhost => pass single empty node
	if p.ServeLocal {
		// all transport implementations will use localhost by default
		return []multiaddr.Multiaddr{nil}, nil
	}

	// Otherwise use container nodes
	return s.cnl.ContainerNodes(ctx, p.CID)
}

func (s *selectiveCnrExec) prepareAddrList(ctx context.Context, p *SelectiveParams, node multiaddr.Multiaddr) []refs.Address {
	var (
		addrList []Address
		l        = len(p.IDList)
	)

	if l > 0 {
		addrList = make([]Address, 0, l)
		for i := range p.IDList {
			addrList = append(addrList, Address{CID: p.CID, ObjectID: p.IDList[i]})
		}

		return addrList
	}

	handler := new(searchTarget)

	s.Executor.Execute(ctx, TraverseParams{
		TransportInfo: &searchInfo{
			metaInfo: metaInfo{
				ttl: p.TTL,
				rt:  object.RequestSearch,
				raw: p.Raw,

				token: p.Token,

				bearer: p.Bearer,

				extHdrs: p.ExtendedHeaders,
			},
			cid:   p.CID,
			query: p.Query,
		},
		Handler:   handler,
		Traverser: newSimpleTraverser(node),
	})

	return handler.list
}

func newSimpleTraverser(list ...multiaddr.Multiaddr) Traverser {
	return &simpleTraverser{
		Once: new(sync.Once),
		list: list,
	}
}

func (s *simpleTraverser) Next(context.Context) (res []multiaddr.Multiaddr) {
	s.Do(func() {
		res = s.list
	})

	return
}

func (s metaInfo) GetTTL() uint32 { return s.ttl }

func (s metaInfo) GetTimeout() time.Duration { return 0 }

func (s metaInfo) GetRaw() bool { return s.raw }

func (s metaInfo) Type() object.RequestType { return s.rt }

func (s metaInfo) GetSessionToken() service.SessionToken { return s.token }

func (s metaInfo) GetBearerToken() service.BearerToken { return s.bearer }

func (s metaInfo) ExtendedHeaders() []service.ExtendedHeader { return s.extHdrs }

func (s *putInfo) GetHead() *object.Object { return s.obj }

func (s *putInfo) Payload() io.Reader { return new(emptyReader) }

func (*emptyReader) Read(p []byte) (int, error) { return 0, io.EOF }

func (s *putInfo) CopiesNumber() uint32 {
	return s.cn
}

func (s *getInfo) GetAddress() refs.Address { return s.addr }

func (s *getInfo) Raw() bool { return s.raw }

func (s *headInfo) GetFullHeaders() bool { return s.fullHdr }

func (s *searchInfo) GetCID() refs.CID { return s.cid }

func (s *searchInfo) GetQuery() []byte { return s.query }

func (s *rangeHashInfo) GetAddress() refs.Address { return s.addr }

func (s *rangeHashInfo) GetRanges() []object.Range { return s.ranges }

func (s *rangeHashInfo) GetSalt() []byte { return s.salt }

func (s *searchTarget) HandleResult(_ context.Context, _ multiaddr.Multiaddr, r interface{}, e error) {
	if e == nil {
		s.list = append(s.list, r.([]refs.Address)...)
	}
}

// HandleResult calls Handler with:
//  - Multiaddr with argument value;
//  - error equality to nil.
func (s *PutParams) HandleResult(_ context.Context, node multiaddr.Multiaddr, _ interface{}, e error) {
	s.Handler(node, e == nil)
}

// HandleResult calls Handler if error argument is nil with:
//  - Multiaddr with argument value;
//  - result casted to an Object pointer.
func (s *GetParams) HandleResult(_ context.Context, node multiaddr.Multiaddr, r interface{}, e error) {
	if e == nil {
		s.Handler(node, r.(*object.Object))
	}
}

// HandleResult calls Handler if error argument is nil with:
//  - Multiaddr with argument value;
//  - result casted to Address slice.
func (s *SearchParams) HandleResult(_ context.Context, node multiaddr.Multiaddr, r interface{}, e error) {
	if e == nil {
		s.Handler(node, r.([]refs.Address))
	}
}

// HandleResult calls Handler if error argument is nil with:
//  - Multiaddr with argument value;
//  - result casted to Hash slice.
func (s *RangeHashParams) HandleResult(_ context.Context, node multiaddr.Multiaddr, r interface{}, e error) {
	if e == nil {
		s.Handler(node, r.([]hash.Hash))
	}
}

func (s *cnrTraverseExec) Execute(ctx context.Context, p TraverseParams) {
	if p.WorkerPool == nil {
		p.WorkerPool = new(singleRoutinePool)
	}

	ctx, cancel := context.WithCancel(ctx)
	defer cancel()

	wg := new(sync.WaitGroup)

	for {
		select {
		case <-ctx.Done():
			return
		default:
		}

		nodes := p.Traverser.Next(ctx)
		if len(nodes) == 0 {
			break
		}

		for i := range nodes {
			node := nodes[i]

			wg.Add(1)

			if err := p.WorkerPool.Submit(func() {
				defer wg.Done()

				if p.ExecutionInterceptor != nil && p.ExecutionInterceptor(ctx, node) {
					return
				}

				s.transport.Transport(ctx, transport.ObjectTransportParams{
					TransportInfo: p.TransportInfo,
					TargetNode:    node,
					ResultHandler: p.Handler,
				})
			}); err != nil {
				wg.Done()
			}
		}

		wg.Wait()
	}
}

func (*singleRoutinePool) Submit(fn func()) error {
	fn()
	return nil
}

// NewObjectContainerHandler is a SelectiveContainerExecutor constructor.
func NewObjectContainerHandler(p ObjectContainerHandlerParams) (SelectiveContainerExecutor, error) {
	switch {
	case p.Executor == nil:
		return nil, errors.Wrap(errEmptyTraverseExecutor, instanceFailMsg)
	case p.Logger == nil:
		return nil, errors.Wrap(errEmptyLogger, instanceFailMsg)
	case p.NodeLister == nil:
		return nil, errors.Wrap(errEmptyNodeLister, instanceFailMsg)
	}

	return &selectiveCnrExec{
		cnl:      p.NodeLister,
		Executor: p.Executor,
		log:      p.Logger,
	}, nil
}

// NewContainerTraverseExecutor is a ContainerTraverseExecutor executor.
func NewContainerTraverseExecutor(t transport.ObjectTransport) (ContainerTraverseExecutor, error) {
	if t == nil {
		return nil, errNilObjectTransport
	}

	return &cnrTraverseExec{transport: t}, nil
}