// Copyright 2017 Pilosa Corp.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package gossip

import (
	"bytes"
	"context"
	"fmt"
	"io"
	"io/ioutil"
	"log"
	"net"
	"os"
	"strconv"
	"strings"
	"sync"
	"time"

	"github.com/hashicorp/memberlist"
	"github.com/pilosa/pilosa/v2"
	"github.com/pilosa/pilosa/v2/logger"
	"github.com/pilosa/pilosa/v2/roaring"
	"github.com/pilosa/pilosa/v2/toml"
	"github.com/pkg/errors"
)

// Ensure GossipMemberSet implements interfaces.
var _ memberlist.Delegate = &memberSet{}

// memberSet represents a gossip implementation of MemberSet using memberlist.
type memberSet struct {
	mu         sync.RWMutex
	memberlist *memberlist.Memberlist

	broadcasts *memberlist.TransmitLimitedQueue

	papi   *pilosa.API
	config *config

	Logger logger.Logger

	// stdLogger is only used when passed into memberlist library things that take a std library logger rather than an interface.
	stdLogger *log.Logger
	// logOutput is similar to stdLogger in that it's passed to memberlist things which can't take a pilosa Logger.
	logOutput io.Writer

	transport *Transport

	eventReceiver *eventReceiver
}

// Open implements the MemberSet interface to start network activity.
func (g *memberSet) Open() (err error) {
	g.mu.Lock()
	g.memberlist, err = memberlist.Create(g.config.memberlistConfig)
	g.mu.Unlock()
	if err != nil {
		return errors.Wrap(err, "creating memberlist")
	}

	g.broadcasts = &memberlist.TransmitLimitedQueue{
		NumNodes: func() int {
			g.mu.RLock()
			defer g.mu.RUnlock()
			return g.memberlist.NumMembers()
		},
		RetransmitMult: 3,
	}

	var uris = make([]*pilosa.URI, len(g.config.gossipSeeds))
	for i, addr := range g.config.gossipSeeds {
		uris[i], err = pilosa.NewURIFromAddress(addr)
		if err != nil {
			return fmt.Errorf("new uri from address: %s", err)
		}
	}

	var nodes = make([]*pilosa.Node, len(uris))
	for i, uri := range uris {
		nodes[i] = &pilosa.Node{URI: *uri}
	}

	g.mu.RLock()
	err = g.joinWithRetry(pilosa.URIs(pilosa.Nodes(nodes).URIs()).HostPortStrings())
	g.mu.RUnlock()
	if err != nil {
		return errors.Wrap(err, "joinWithRetry")
	}
	return nil
}

// Close attempts to gracefully leave the cluster, and finally calls shutdown
// after (at most) a timeout period.
func (g *memberSet) Close() error {
	g.eventReceiver.Close()
	leaveErr := g.memberlist.Leave(5 * time.Second)
	shutdownErr := g.memberlist.Shutdown()
	if leaveErr != nil || shutdownErr != nil {
		return fmt.Errorf("leaving: '%v', shutting down: '%v'", leaveErr, shutdownErr)
	}
	return nil
}

// joinWithRetry wraps the standard memberlist Join function in a retry.
func (g *memberSet) joinWithRetry(hosts []string) error {
	err := retry(60, 2*time.Second, func() error {
		_, err := g.memberlist.Join(hosts)
		return err
	})
	return err
}

// retry periodically retries function fn a specified number of attempts.
func retry(attempts int, sleep time.Duration, fn func() error) (err error) { // nolint: unparam
	for i := 0; ; i++ {
		err = fn()
		if err == nil {
			return
		}
		if i >= (attempts - 1) {
			break
		}
		time.Sleep(sleep)
		log.Println("retrying after error:", err)
	}
	return fmt.Errorf("after %d attempts, last error: %s", attempts, err)
}

////////////////////////////////////////////////////////////////

type config struct {
	gossipSeeds      []string
	memberlistConfig *memberlist.Config
}

// memberSetOption describes a functional option for GossipMemberSet.
type memberSetOption func(*memberSet) error

// WithTransport is a functional option for providing a transport to NewMemberSet.
func WithTransport(transport *Transport) memberSetOption {
	return func(g *memberSet) error {
		g.transport = transport
		return nil
	}
}

// WithLogger is a functional option for providing a Go logger to NewMemberSet.
// If the memberSet's transport is nil, this logger will be used when creating
// one. If WithLogOutput is not used, this logger will be passed to memberlist
// for it to use internally. This logger is not used for logging by code in this
// (gossip) package - for that, use the WithPilosaLogger option.
func WithLogger(logger *log.Logger) memberSetOption {
	return func(g *memberSet) error {
		g.stdLogger = logger
		return nil
	}
}

// WithLogOutput allows one to pass a Writer which will in turn be passed to
// memberlist for use in logging.
func WithLogOutput(o io.Writer) memberSetOption {
	return func(g *memberSet) error {
		g.logOutput = o
		return nil
	}
}

// WithPilosaLogger allows one to configure a memberSet with a logger of their
// choice which satisfies the pilosa logger interface.
func WithPilosaLogger(l logger.Logger) memberSetOption {
	return func(g *memberSet) error {
		g.Logger = l
		return nil
	}
}

// NewMemberSet returns a new instance of GossipMemberSet based on options. The
// logging options which can be passed to NewMemberSet are complicated for
// historical reasons - please pass WithPilosaLogger, and either WithLogOutput
// or WithLogger. If you pass WithLogOutput, be sure to also pass in a Transport
// using WithTransport.
func NewMemberSet(cfg Config, api *pilosa.API, options ...memberSetOption) (*memberSet, error) {
	host := api.Node().URI.Host
	g := &memberSet{
		papi:   api,
		Logger: logger.NopLogger,
	}

	// options
	for _, opt := range options {
		if err := opt(g); err != nil {
			return nil, errors.Wrap(err, "executing option")
		}
	}

	ger := newEventReceiver(g.Logger, api)
	g.eventReceiver = ger

	if g.transport == nil {
		port, err := strconv.Atoi(cfg.Port)
		if err != nil {
			return nil, fmt.Errorf("convert port: %s", err)
		}

		if g.stdLogger == nil {
			if g.logOutput != nil {
				g.stdLogger = logger.NewStandardLogger(g.logOutput).Logger()
			} else {
				g.stdLogger = log.New(os.Stderr, "", log.LstdFlags)
			}
		}

		// Set up the transport.
		transport, err := NewTransport(host, port, g.stdLogger)
		if err != nil {
			return nil, fmt.Errorf("new tranport: %s", err)
		}

		g.transport = transport
	}

	port := g.transport.net.GetAutoBindPort()

	var gossipKey []byte
	var err error
	if cfg.Key != "" {
		gossipKey, err = ioutil.ReadFile(cfg.Key)
		if err != nil {
			return nil, fmt.Errorf("reading gossip key: %s", err)
		}
	}

	////////////////////
	// memberlist config
	conf := memberlist.DefaultWANConfig()
	conf.Transport = g.transport.net
	conf.Name = api.Node().ID
	conf.BindAddr = api.Node().URI.Host
	conf.BindPort = port
	// AdvertisePort
	if cfg.AdvertisePort != "" {
		if p, err := strconv.Atoi(cfg.Port); err != nil {
			return nil, fmt.Errorf("convert advertise port: %s", err)
		} else {
			conf.AdvertisePort = p
		}
	} else {
		conf.AdvertisePort = port
	}
	// AdvertiseHost
	if cfg.AdvertiseHost != "" {
		conf.AdvertiseAddr = cfg.AdvertiseHost
	} else {
		conf.AdvertiseAddr = hostToIP(api.Node().URI.Host)
	}
	//
	conf.TCPTimeout = time.Duration(cfg.StreamTimeout)
	conf.SuspicionMult = cfg.SuspicionMult
	conf.PushPullInterval = time.Duration(cfg.PushPullInterval)
	conf.ProbeTimeout = time.Duration(cfg.ProbeTimeout)
	conf.ProbeInterval = time.Duration(cfg.ProbeInterval)
	conf.GossipNodes = cfg.Nodes
	conf.GossipInterval = time.Duration(cfg.Interval)
	conf.GossipToTheDeadTime = time.Duration(cfg.ToTheDeadTime)
	//
	conf.Delegate = g
	conf.SecretKey = gossipKey
	conf.Events = ger
	if g.logOutput != nil {
		conf.LogOutput = g.logOutput
	} else {
		conf.Logger = g.stdLogger
	}

	g.config = &config{
		memberlistConfig: conf,
		gossipSeeds:      cfg.Seeds,
	}

	return g, nil
}

// NodeMeta implementation of the memberlist.Delegate interface.
func (g *memberSet) NodeMeta(limit int) []byte {
	buf, err := g.papi.Serializer.Marshal(g.papi.Node())
	if err != nil {
		g.Logger.Printf("marshal message error: %s", err)
		return []byte{}
	}
	return buf
}

// NotifyMsg implementation of the memberlist.Delegate interface
// called when a user-data message is received.
func (g *memberSet) NotifyMsg(b []byte) {
	err := g.papi.ClusterMessage(context.Background(), bytes.NewBuffer(b))
	if err != nil {
		g.Logger.Printf("cluster message error: %s", err)
	}
}

// GetBroadcasts implementation of the memberlist.Delegate interface
// called when user data messages can be broadcast.
func (g *memberSet) GetBroadcasts(overhead, limit int) [][]byte {
	return g.broadcasts.GetBroadcasts(overhead, limit)
}

// LocalState implementation of the memberlist.Delegate interface
// sends this Node's state data.
func (g *memberSet) LocalState(join bool) []byte {
	m := &pilosa.NodeStatus{
		Node:   g.papi.Node(),
		Schema: &pilosa.Schema{Indexes: g.papi.Schema(context.Background())},
	}
	for _, idx := range m.Schema.Indexes {
		is := &pilosa.IndexStatus{Name: idx.Name, CreatedAt: idx.CreatedAt}

		for _, f := range idx.Fields {
			availableShards := roaring.NewBitmap()
			if field, _ := g.papi.Field(context.Background(), idx.Name, f.Name); field != nil {
				availableShards = field.AvailableShards(false)
			}

			fs := &pilosa.FieldStatus{
				Name:            f.Name,
				CreatedAt:       f.CreatedAt,
				AvailableShards: availableShards,
			}
			is.Fields = append(is.Fields, fs)
		}
		m.Indexes = append(m.Indexes, is)
	}

	// Marshal nodestate data to bytes.
	buf, err := pilosa.MarshalInternalMessage(m, g.papi.Serializer)
	if err != nil {
		g.Logger.Printf("error marshalling nodestate data, err=%s", err)
		return []byte{}
	}
	return buf
}

// MergeRemoteState implementation of the memberlist.Delegate interface
// receive and process the remote side's LocalState.
func (g *memberSet) MergeRemoteState(buf []byte, join bool) {
	err := g.papi.ClusterMessage(context.Background(), bytes.NewBuffer(buf))
	if err != nil {
		g.Logger.Printf("merge state error: %s", err)
	}
}

// eventReceiver is used to enable an application to receive
// events about joins and leaves over a channel.
//
// Care must be taken that events are processed in a timely manner from
// the channel, since this delegate will block until an event can be sent.
type eventReceiver struct {
	ch     chan memberlist.NodeEvent
	closed chan struct{}
	papi   *pilosa.API

	logger logger.Logger
}

// newEventReceiver returns a new instance of GossipEventReceiver.
func newEventReceiver(logger logger.Logger, papi *pilosa.API) *eventReceiver {
	ger := &eventReceiver{
		ch:     make(chan memberlist.NodeEvent, 1),
		closed: make(chan struct{}),
		logger: logger,
		papi:   papi,
	}
	go ger.listen()
	return ger
}

func (g *eventReceiver) NotifyJoin(n *memberlist.Node) {
	// copy node to avoid data race
	n2 := *n
	n2.Meta = make([]byte, len(n.Meta))
	copy(n2.Meta, n.Meta)

	select {
	case g.ch <- memberlist.NodeEvent{Event: memberlist.NodeJoin, Node: &n2}:
	case <-g.closed:
	}
}

func (g *eventReceiver) NotifyLeave(n *memberlist.Node) {
	// copy node to avoid data race
	n2 := *n
	n2.Meta = make([]byte, len(n.Meta))
	copy(n2.Meta, n.Meta)

	select {
	case g.ch <- memberlist.NodeEvent{Event: memberlist.NodeLeave, Node: &n2}:
	case <-g.closed:
	}
}

func (g *eventReceiver) NotifyUpdate(n *memberlist.Node) {
	// copy node to avoid data race
	n2 := *n
	n2.Meta = make([]byte, len(n.Meta))
	copy(n2.Meta, n.Meta)

	select {
	case g.ch <- memberlist.NodeEvent{Event: memberlist.NodeUpdate, Node: &n2}:
	case <-g.closed:
	}
}

func (g *eventReceiver) Close() {
	close(g.closed)
}

func (g *eventReceiver) listen() {
	var nodeEventType pilosa.NodeEventType
	for {
		var e memberlist.NodeEvent
		select {
		case <-g.closed:
			return
		case e = <-g.ch:
		}
		switch e.Event {
		case memberlist.NodeJoin:
			nodeEventType = pilosa.NodeJoin
		case memberlist.NodeLeave:
			nodeEventType = pilosa.NodeLeave
		case memberlist.NodeUpdate:
			nodeEventType = pilosa.NodeUpdate
		default:
			continue
		}

		// Get the node from the event.Node meta data.
		var n pilosa.Node
		if err := g.papi.Serializer.Unmarshal(e.Node.Meta, &n); err != nil {
			panic("failed to unmarshal event node meta into node")
		}

		ne := &pilosa.NodeEvent{
			Event: nodeEventType,
			Node:  &n,
		}
		buf, err := pilosa.MarshalInternalMessage(ne, g.papi.Serializer)
		if err != nil {
			panic(err)
		}
		if err := g.papi.ClusterMessage(context.Background(), bytes.NewBuffer(buf)); err != nil {
			g.logger.Printf("receive event error: %s", err)
		}
	}
}

// Transport is a gossip transport for binding to a port.
type Transport struct {
	//memberlist.Transport
	net *memberlist.NetTransport
	URI *pilosa.URI
}

// NewTransport returns a NetTransport based on the given host and port.
// It will dynamically bind to a port if port is 0.
// This is useful for test cases where specifying a port is not reasonable.
//func NewTransport(host string, port int) (*memberlist.NetTransport, error) {
func NewTransport(host string, port int, logger *log.Logger) (*Transport, error) {
	// memberlist config
	conf := memberlist.DefaultWANConfig()
	conf.BindAddr = host
	conf.BindPort = port
	conf.AdvertisePort = port
	conf.Logger = logger

	net, err := newTransport(conf)
	if err != nil {
		return nil, fmt.Errorf("new transport: %s", err)
	}

	uri, err := pilosa.NewURIFromHostPort(host, uint16(net.GetAutoBindPort()))
	if err != nil {
		return nil, fmt.Errorf("new uri from host port: %s", err)
	}

	return &Transport{
		net: net,
		URI: uri,
	}, nil
}

// newTransport returns a NetTransport based on the memberlist configuration.
// It will dynamically bind to a port if conf.BindPort is 0.
func newTransport(conf *memberlist.Config) (*memberlist.NetTransport, error) {
	nc := &memberlist.NetTransportConfig{
		BindAddrs: []string{conf.BindAddr},
		BindPort:  conf.BindPort,
		Logger:    conf.Logger,
	}

	// See comment below for details about the retry in here.
	makeNetRetry := func(limit int) (*memberlist.NetTransport, error) {
		var err error
		for try := 0; try < limit; try++ {
			var nt *memberlist.NetTransport
			if nt, err = memberlist.NewNetTransport(nc); err == nil {
				return nt, nil
			}
			if strings.Contains(err.Error(), "address already in use") {
				conf.Logger.Printf("[DEBUG] Got bind error: %v", err)
				continue
			}
		}

		return nil, fmt.Errorf("failed to obtain an address: %v", err)
	}

	// The dynamic bind port operation is inherently racy because
	// even though we are using the kernel to find a port for us, we
	// are attempting to bind multiple protocols (and potentially
	// multiple addresses) with the same port number. We build in a
	// few retries here since this often gets transient errors in
	// busy unit tests.
	limit := 1
	if conf.BindPort == 0 {
		limit = 10
	}

	nt, err := makeNetRetry(limit)
	if err != nil {
		return nil, errors.Wrap(err, "could not set up network transport")
	}

	return nt, nil
}

// Config holds toml-friendly memberlist configuration.
type Config struct {
	// Port indicates the port to which pilosa should bind for internal state sharing.
	Port string `toml:"port"`

	// AdvertiseHost is the hostname or IP other nodes should use to connect to
	// this host. If left blank, the value for Host will be used. This is useful
	// in some proxy and NAT scenarios.
	AdvertiseHost string `toml:"advertise-host"`
	// AdvertisePort is the port other nodes will use to connect to this one.
	// Behaves like AdvertiseHost.
	AdvertisePort string `toml:"advertise-port"`

	Seeds []string `toml:"seeds"`
	Key   string   `toml:"key"`
	// StreamTimeout is the timeout for establishing a stream connection with
	// a remote node for a full state sync, and for stream read and write
	// operations. Maps to memberlist TCPTimeout.
	StreamTimeout toml.Duration `toml:"stream-timeout"`
	// SuspicionMult is the multiplier for determining the time an
	// inaccessible node is considered suspect before declaring it dead.
	// The actual timeout is calculated using the formula:
	//
	//   SuspicionTimeout = SuspicionMult * log(N+1) * ProbeInterval
	//
	// This allows the timeout to scale properly with expected propagation
	// delay with a larger cluster size. The higher the multiplier, the longer
	// an inaccessible node is considered part of the cluster before declaring
	// it dead, giving that suspect node more time to refute if it is indeed
	// still alive.
	SuspicionMult int `toml:"suspicion-mult"`
	// PushPullInterval is the interval between complete state syncs.
	// Complete state syncs are done with a single node over TCP and are
	// quite expensive relative to standard gossiped messages. Setting this
	// to zero will disable state push/pull syncs completely.
	//
	// Setting this interval lower (more frequent) will increase convergence
	// speeds across larger clusters at the expense of increased bandwidth
	// usage.
	PushPullInterval toml.Duration `toml:"push-pull-interval"`
	// ProbeInterval and ProbeTimeout are used to configure probing behavior
	// for memberlist.
	//
	// ProbeInterval is the interval between random node probes. Setting
	// this lower (more frequent) will cause the memberlist cluster to detect
	// failed nodes more quickly at the expense of increased bandwidth usage.
	//
	// ProbeTimeout is the timeout to wait for an ack from a probed node
	// before assuming it is unhealthy. This should be set to 99-percentile
	// of RTT (round-trip time) on your network.
	ProbeInterval toml.Duration `toml:"probe-interval"`
	ProbeTimeout  toml.Duration `toml:"probe-timeout"`

	// Interval and Nodes are used to configure the gossip
	// behavior of memberlist.
	//
	// Interval is the interval between sending messages that need
	// to be gossiped that haven't been able to piggyback on probing messages.
	// If this is set to zero, non-piggyback gossip is disabled. By lowering
	// this value (more frequent) gossip messages are propagated across
	// the cluster more quickly at the expense of increased bandwidth.
	//
	// Nodes is the number of random nodes to send gossip messages to
	// per Interval. Increasing this number causes the gossip messages
	// to propagate across the cluster more quickly at the expense of
	// increased bandwidth.
	//
	// ToTheDeadTime is the interval after which a node has died that
	// we will still try to gossip to it. This gives it a chance to refute.
	Interval      toml.Duration `toml:"interval"`
	Nodes         int           `toml:"nodes"`
	ToTheDeadTime toml.Duration `toml:"to-the-dead-time"`
}

// hostToIP converts host to an IP4 address based on net.LookupIP().
func hostToIP(host string) string {
	// if host is not an IP addr, check net.LookupIP()
	if net.ParseIP(host) == nil {
		hosts, err := net.LookupIP(host)
		if err != nil {
			return host
		}
		for _, h := range hosts {
			// this restricts pilosa to IP4
			if h.To4() != nil {
				return h.String()
			}
		}
	}
	return host
}
