// Copyright (C) 2017-2021  Nexedi SA and Contributors.
//                          Kirill Smelkov <kirr@nexedi.com>
//
// This program is free software: you can Use, Study, Modify and Redistribute
// it under the terms of the GNU General Public License version 3, or (at your
// option) any later version, as published by the Free Software Foundation.
//
// You can also Link and Combine this program with other software covered by
// the terms of any of the Free Software licenses or any of the Open Source
// Initiative approved licenses and Convey the resulting work. Corresponding
// source of such a combination shall include the source code for all other
// software used.
//
// This program is distributed WITHOUT ANY WARRANTY; without even the implied
// warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
//
// See COPYING file for full licensing terms.
// See https://www.nexedi.com/licensing for rationale and options.

package xneo
// node management & node table

import (
	"bytes"
	"context"
	"fmt"
	"sync"
	"time"

	"lab.nexedi.com/kirr/neo/go/neo/neonet"
	"lab.nexedi.com/kirr/neo/go/neo/proto"

	"lab.nexedi.com/kirr/neo/go/internal/log"
	"lab.nexedi.com/kirr/neo/go/internal/task"
	"lab.nexedi.com/kirr/neo/go/internal/xio"
)

// NodeTable represents known peer nodes in a cluster.
//
// It is
//
//	NID -> *PeerNode  ; = (.laddr, .state, ...) + .link
//
// mapping listing known nodes and associating their node ID with information
// about a peer.
//
// Master maintains such table and provides it to its peers to know each other:
//
//	- Primary Master view of cluster.
// 	- M tracks changes to nodeTab as nodes appear (connected to M) and go (disconnected from M).
// 	- M regularly broadcasts nodeTab content updates(?) to all nodes.
// 	  This way all nodes stay informed about their peers in cluster.
//
// Usage examples:
//
//	- C needs to connect/talk to a storage by nid
//	  (the nid itself is obtained from PartitionTable by oid).
//	- S pulls from other S.
//
// NOTE once a node was added to NodeTable its entry is never deleted: if e.g.
// a connection to node is lost associated entry is marked as having DOWN (XXX
// or UNKNOWN ?) node state.
type NodeTable struct {
	// XXX for PeerNode.Dial to work. see also comments vvv near "peer link"
	localNode *Node

	nodev   []*PeerNode // all known peers
}

//trace:event traceNodeChanged(nt *NodeTable, n *PeerNode)

// PeerNode represents a peer node in the cluster.
//
// It provides:
//
//	- peer's node info (nid, laddr, state, ...), and
// 	- link to the peer.
type PeerNode struct {
	nodeTab *NodeTable // we are peer of .nodeTab.localNode

	proto.NodeInfo // (.type, .laddr, .nid, .state, .idtime)	XXX also protect by mu?

	linkMu sync.Mutex
	link   *neonet.NodeLink // link to this peer; nil if not connected
	dialT  time.Time        // last dial finished at this time

	// dialer notifies waiters via this; reinitialized at each redial; nil while not dialing
	//
	// NOTE duplicates .link to have the following properties:
	//
	// 1. all waiters of current in-progress dial wakeup immediately after
	//    dial completes and get link/error from dial result.
	//
	// 2. any .Link() that sees .link=nil starts new redial with throttle
	//    to make sure peer is dialed not faster than δtRedial.
	//
	// (if we do not have dialing.link waiter will need to relock
	//  peer.linkMu and for some waiters chances are another .Link()
	//  already started redialing and they will have to wait again)
	dialing *dialed
}

// Len returns N(entries) in the table.
func (nt *NodeTable) Len() int {
	return len(nt.nodev)
}

// All returns all entries in the table as one slice.
func (nt *NodeTable) All() []*PeerNode {
	return nt.nodev
}

// Get finds node by node ID.
func (nt *NodeTable) Get(nid proto.NodeID) *PeerNode {
	// FIXME linear scan
	for _, node := range nt.nodev {
		if node.NID == nid {
			return node
		}
	}
	return nil
}

// TODO GetByAddress ?

// Update updates information about a node.
//
// it returns corresponding node entry for convenience.
func (nt *NodeTable) Update(nodeInfo proto.NodeInfo) *PeerNode {
	node := nt.Get(nodeInfo.NID)
	if node == nil {
		node = &PeerNode{nodeTab: nt}
		nt.nodev = append(nt.nodev, node)
	}

	node.NodeInfo = nodeInfo

	// XXX close link if .state becomes DOWN ?

	traceNodeChanged(nt, node)
	return node
}

// StorageList returns list of all storages in the node table.
func (nt *NodeTable) StorageList() []*PeerNode {
	// FIXME linear scan
	sl := []*PeerNode{}
	for _, nodev := range nt.nodev {
		if nodev.Type == proto.STORAGE {
			sl = append(sl, nodev)
		}
	}
	return sl
}



func (nt *NodeTable) String() string {
	buf := bytes.Buffer{}

	for _, n := range nt.nodev {
		// XXX recheck output
		// XXX +link ?
		fmt.Fprintf(&buf, "%s (%s)\t%s\t%s\t@ %s\n", n.NID, n.Type, n.State, n.Addr, n.IdTime)
	}

	return buf.String()
}

// ---- peer link ----

// TODO review peer link dialing / setting / accepting.
//
//	Keep in mind that in NEO in general case it is not client/server but peer-to-peer
//	e.g. when two S establish a link in between them to exchange/sync data.
//
//	Also the distinction between S and M should go away as every S should
//	be taught to also become M (and thus separate M nodes go away
//	completely) with constant reelection being happening in the background
//	like in raft.

// SetLink sets link to peer node.
// XXX
//
// See also: Link, ResetLink, Dial.
func (p *PeerNode) SetLink(link *neonet.NodeLink) {
	// XXX see Link about locking - whether it is needed here or not
	p.linkMu.Lock()
	p.link = link
	p.linkMu.Unlock()
}

// Link returns current link to peer node.
//
// If the link is not yet established - Link returns nil.
//
// See also: Dial.
func (p *PeerNode) Link() *neonet.NodeLink {
	// XXX do we need lock here?
	// XXX usages where Link is used (contrary to Dial) there is no need for lock
	p.linkMu.Lock()
	link := p.link
	p.linkMu.Unlock()
	return link
}

// ResetLink closes link to peer and sets it to nil.
func (p *PeerNode) ResetLink(ctx context.Context) {
	p.linkMu.Lock()
	link := p.link
	p.link = nil
	p.dialing = nil // XXX what if dialing is in progress? -> cancel dialing with err?
	p.linkMu.Unlock()

	if link != nil {
		log.Infof(ctx, "%v: closing link", link)
		err := link.Close()
		if err != nil {
			log.Error(ctx, err)
		}
	}
}

// dial does low-level work to dial peer
// XXX p.* reading without lock - ok?
func (p *PeerNode) dial(ctx context.Context) (_ *neonet.NodeLink, err error) {
	node := p.nodeTab.localNode
	mynid := node.MyInfo.NID // XXX locking
	defer task.Runningf(&ctx, "dial %s", p.NID)(&err)

	reqID := &proto.RequestIdentification{
		NodeType:	node.MyInfo.Type,
		NID:		mynid,
		Address:	node.MyInfo.Addr,
		ClusterName:	node.ClusterName,
		IdTime:		node.MyInfo.IdTime,
		DevPath:        nil,                    // XXX stub
		NewNID:         nil,			// XXX stub
	}
	link, accept, err := Dial(ctx, p.Type, node.Net, p.Addr.String(), reqID)
	if err != nil {
		return nil, err
	}

	// verify peer identifies as what we expect
	switch {
	// type is already checked by node.Dial

	case accept.MyNID != p.NID:
		err = fmt.Errorf("connected, but peer's nid is not %s (identifies as %s)", p.NID, accept.MyNID)

	case accept.YourNID != mynid:
		err = fmt.Errorf("connected, but peer gives us nid %s (our is %s)", accept.YourNID, mynid)
	}

	if err != nil {
		xio.LClose(ctx, link)
		link = nil
	}

	return link, err
}

// even if dialing a peer failed, we'll attempt redial after this timeout
const δtRedial = 3 * time.Second

// dialed is result of dialing a peer.
type dialed struct {
	link	*neonet.NodeLink
	err	error
	ready	chan struct{}
}

// Dial establishes link to the peer node.
//
// If the link was not yet established Dial dials the peer appropriately,
// handshakes, requests identification and checks that identification reply is
// as expected.
//
// Several Dial calls may be done in parallel - in any case only 1 link-level
// dial will be made and others will share established link.
//
// In case Dial returns an error - future Dial will attempt to reconnect with
// "don't reconnect too fast" throttling.
func (p *PeerNode) Dial(ctx context.Context) (*neonet.NodeLink, error) {
	p.linkMu.Lock()

	// ok if already connected
	if link := p.link; link != nil {
		p.linkMu.Unlock()
		return link, nil
	}

	// if dial is already in progress - wait for its completion
	if dialing := p.dialing; dialing != nil {
		p.linkMu.Unlock()

		select {
		case <-ctx.Done():
			return nil, ctx.Err()

		case <-dialing.ready:
			return dialing.link, dialing.err
		}
	}

	// otherwise this goroutine becomes responsible for (re)dialing the peer

	// XXX p.State != RUNNING
	// XXX p.Addr  != ""

	dialT := p.dialT
	dialing := &dialed{ready: make(chan struct{})}
	p.dialing = dialing
	p.linkMu.Unlock()

	go func() {
		link, err := func() (*neonet.NodeLink, error) {
			// throttle redialing if too fast
			δt := time.Now().Sub(dialT)
			if δt < δtRedial && !dialT.IsZero() {
				select {
				case <-ctx.Done():
					return nil, ctx.Err()

				case <-time.After(δtRedial - δt):
					// ok
				}
			}

			link, err := p.dial(ctx)
			dialT = time.Now()
			return link, err
		}()

		p.linkMu.Lock()
		p.link = link
		p.dialT = dialT
		p.dialing = nil
		p.linkMu.Unlock()

		dialing.link = link
		dialing.err = err
		close(dialing.ready)
	}()

	<-dialing.ready
	return dialing.link, dialing.err
}
