package endpoint

import (
	"context"
)

var EmptyMetadata = &Metadata{}

// EndpointFunc represents a function endpoint.
type EndpointFunc func(ctx context.Context, request interface{}) (response interface{}, err error)

// Invoke implements endpoint invocation on EndpointFunc.
func (fn EndpointFunc) Invoke(ctx context.Context, request interface{}) (response interface{}, err error) {
	return fn(ctx, request)
}

// Metadata returns endpoint metadata, which is always nil for function endpoint.
func (fn EndpointFunc) Metadata() *Metadata {
	return EmptyMetadata
}

// Middleware represents a function that can intercept an endpoint.
type Middleware func(Endpoint) Endpoint

// EndpointOption is a function that can alter endpoint data.
type EndpointOption func(e *endpointImpl)

// Endpoint represents an abstraction of a transport-independent service endpoint.
type Endpoint interface {
	Invoke(ctx context.Context, request interface{}) (response interface{}, err error)
	Metadata() *Metadata
}

// Metadata is additional information about an endpoint.
type Metadata struct {
	Name string
}

// endpointImpl represents an implementation of Endpoint.
type endpointImpl struct {
	internal Endpoint
	metadata *Metadata
}

// NewEndpoint returns a new Endpoint implementation
func NewEndpoint(fn EndpointFunc, opts ...EndpointOption) Endpoint {
	e := &endpointImpl{internal: Endpoint(fn)}

	for _, opt := range opts {
		opt(e)
	}

	return e
}

// Invoke implements endpoint invocation.
func (e *endpointImpl) Invoke(ctx context.Context, request interface{}) (response interface{}, err error) {
	return e.internal.Invoke(ctx, request)
}

// Metadata returns endpoint metadata.
func (e *endpointImpl) Metadata() *Metadata {
	return e.metadata
}

// UseMiddleware creates a new Endpoint with given middleware injected.
func UseMiddleware(ep Endpoint, middlewares ...Middleware) Endpoint {
	metadata := ep.Metadata()

	for i := len(middlewares) - 1; i >= 0; i-- {
		ep = &endpointImpl{
			middlewares[i](ep),
			metadata,
		}
	}
	return ep
}

// WithMetadata sets the metadata of an endpoint.
func WithMetadata(metadata *Metadata) EndpointOption {
	return func(e *endpointImpl) {
		e.metadata = metadata
	}
}
