// 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 neo
// a node driven by master

import (
	"context"
	"errors"
	"fmt"
	"sync"
	"time"

	"lab.nexedi.com/kirr/go123/xerr"
	"lab.nexedi.com/kirr/go123/xnet"
	"lab.nexedi.com/kirr/go123/xsync"

	"lab.nexedi.com/kirr/neo/go/internal/log"
	"lab.nexedi.com/kirr/neo/go/internal/task"
	"lab.nexedi.com/kirr/neo/go/internal/xcontext"
	"lab.nexedi.com/kirr/neo/go/neo/neonet"
	"lab.nexedi.com/kirr/neo/go/neo/proto"
	"lab.nexedi.com/kirr/neo/go/neo/xneo"
)

// _MasteredNode provides base functionality of a NEO node driven by master.
//
// It connects to master, identifies to it, and handles master messages about
// δNodeTab, δPartTab and δClusterState to update local replica of cluster
// state. The other messages from master are passed through to RecvM1. In other
// words _MasteredNode installs a kind of reception pipeline in between master
// and _MasteredNode user:
//
//
//	                       δNodeTab
//	                       δPartTab
//	                       δClusterState
//	                     ↑
//	        RecvM1   ---------------
//	 user <-------- | _MasteredNode | <- M
//	                 ---------------
//
// This pipeline is operated by TalkMaster.
// The connection to master is persisted by redial as needed.
type _MasteredNode struct {
	myInfo      proto.NodeInfo // type, laddr, nid, state, idtime
	ClusterName string
	Net         xnet.Networker // network AP we are sending/receiving on
	MasterAddr  string         // address of current master	 TODO -> masterRegistry

	// nodeTab/partTab/clusterState
	stateMu      sync.RWMutex
	state        xneo.ClusterState

	// operational state in node is maintained by TalkMaster.
	// users retrieve it via withOperational().	XXX recheck
	//
	// NOTE being operational means:
	// - link to master established and is ok
	// - .state is operational
	//
	// however master link is accessed separately (see ^^^ and masterLink)
	//
	// protected by .stateMu
	operational bool
	opReady	    chan struct{} // reinitialized each time state becomes non-operational


	flags _MasteredNodeFlags
	rxm   chan _RxM          // TalkMaster -> RecvM1
}

// _RxM represents a request or event received from master.
type _RxM struct {
	Req neonet.Request
	Err error          // event*
}

type _MasteredNodeFlags int
const (
	// δPartTabPassThrough tells RecvM1 not to filter out messages related
	// to partition table changes. When RecvM1 receives such messages there
	// are already processed internally to update .state.PartTab correspondingly.
	//
	// Storage uses this mode to receive δPartTab notifications to know
	// when to persist it.
	δPartTabPassThrough _MasteredNodeFlags = iota
)

// XXX doc
func newMasteredNode(typ proto.NodeType, clusterName string, net xnet.Networker, masterAddr string) *_MasteredNode {
	node := &_MasteredNode{
		myInfo: proto.NodeInfo{
			Type:   typ,
			Addr:   proto.Address{},
			NID:    0,
			IdTime: proto.IdTimeNone,
		},
		ClusterName: clusterName,

		Net:        net,
		MasterAddr: masterAddr,

		state: xneo.ClusterState{
			NodeTab: &xneo.NodeTable{},
			PartTab: &xneo.PartitionTable{},
			Code:    -1, // invalid
		},

		rxm: make(chan _RxM),
	}

	return node
}



// TalkMaster dials master, identifies to it, and receives master notifications and requests.
//
// Notifications to node/partition tables and cluster state are automatically
// handled, while other notifications and requests are passed through to RecvM1.
//
// The connection to master is persisted by redial as needed.
//
// f is called on every reconnection after identification and protocol prologue. XXX
func (node *_MasteredNode) TalkMaster(ctx context.Context, f func(context.Context, *neonet.NodeLink) error) (err error) {
	defer task.Runningf(&ctx, "talk master(%s)", node.MasterAddr)(&err)

	for {
		err := node.talkMaster1(ctx, f)
		log.Warning(ctx, err)	// XXX Warning ok? -> Error?
		// TODO if err == "reject identification / protocol error" -> shutdown client
		// TODO if err == shutdown -> return
		// TODO if err == "not a primary" -> try redirected addresss

		// exit on cancel / throttle reconnecting
		select {
		case <-ctx.Done():
			return ctx.Err()

		// XXX 1s hardcoded -> move out of here
		case <-time.After(1*time.Second):
			// ok
		}
	}
}

func (node *_MasteredNode) talkMaster1(ctx context.Context, f func(context.Context, *neonet.NodeLink) error) error {
	reqID := &proto.RequestIdentification{
		NodeType:	node.myInfo.Type,
		NID:		node.myInfo.NID,
		Address:	node.myInfo.Addr,
		ClusterName:	node.ClusterName,
		IdTime:		node.myInfo.IdTime,	// XXX ok?
		DevPath:        nil,                    // XXX stub
		NewNID:         nil,			// XXX stub
	}
	mlink, accept, err := dialNode(ctx, proto.MASTER, node.Net, node.MasterAddr, reqID)
	if err != nil {
		return err
	}

	return xcontext.WithCloseOnErrCancel(ctx, mlink, func() (err error) {
		if accept.YourNID != node.myInfo.NID {
			log.Infof(ctx, "master told us to have nid=%s", accept.YourNID)
			node.myInfo.NID = accept.YourNID // XXX locking ?
		}

		// master pushes whole nodeTab and partTab to us right after identification

		// nodeTab
		mnt := proto.NotifyNodeInformation{}
		_, err = mlink.Expect1(&mnt)
		if err != nil {
			return fmt.Errorf("after identification: %w", err)
		}

		// partTab	XXX not to S and secondary M?
		// https://lab.nexedi.com/nexedi/neoppod/blob/v1.12-69-gd98205d0/neo/master/handlers/__init__.py#L60-67
		mpt := proto.SendPartitionTable{}
		_, err = mlink.Expect1(&mpt)
		if err != nil {
			return fmt.Errorf("after identification: %w", err)
		}
		pt := xneo.PartTabFromDump(mpt.PTid, mpt.RowList) // TODO handle mpt.NumReplicas
		log.Infof(ctx, "master initialized us with next parttab:\n%s", pt)

		// update cluster state

		// XXX locking
		node.stateMu.Lock()
		err = node.updateNodeTab(ctx, &mnt)
		node.state.PartTab = pt
		// XXX update "operational"
/*
	// update .operational + notify those who was waiting for it
	opready := c.updateOperational()
	c.node.StateMu.Unlock()
	opready()
*/
		node.stateMu.Unlock()
		if err != nil { // might be command to shutdown
			return err
		}

		// XXX update .masterLink + notify waiters

		wg := xsync.NewWorkGroup(ctx)
		// receive and handle notifications from master
		wg.Go(func(ctx context.Context) error {
			defer task.Running(&ctx, "rx")(&err)
			for {
				req, err := mlink.Recv1()
				if err != nil {
					return err
				}
				err = node.recvMaster1(ctx, req) // req ownership is passed in
				if err != nil {
					return err
				}
			}
		})
		// run user code
		wg.Go(func(ctx context.Context) error {
			return f(ctx, mlink)
		})

		return wg.Wait()
	})

}

// recvMaster1 handles 1 message from master.
func (node *_MasteredNode) recvMaster1(ctx context.Context, req neonet.Request) (err error) {
	// messages for state changes are handled internally
	δstate := true
	switch req.Msg.(type) {
	default: δstate = false
	case *proto.SendPartitionTable:     // whole partTab
	case *proto.NotifyPartitionChanges: // δ(partTab)
	case *proto.NotifyNodeInformation:  // δ(nodeTab)
	case *proto.NotifyClusterState:
	}

	if δstate {
		δpt, err := node.recvδstate(ctx, req.Msg)
		toRecvM1 := false
		if δpt && (node.flags & δPartTabPassThrough != 0) {
			toRecvM1 = true
		}
		if !toRecvM1 {
			req.Close()
			return err
		}
	}

	// pass request -> RecvM1
	// NOTE req ownership is passed into RecvM1 caller who becomes responsible to close it
	select {
	case <-ctx.Done():
		req.Close()
		return ctx.Err()

	case node.rxm <- _RxM{Req: req}:
		// ok
	}

	return nil
}

// RecvM1 receives request from master filtered through δstate handler.
//
// XXX link down ?
// XXX kill err?
func (node *_MasteredNode) RecvM1() (neonet.Request, error) {
	rx := <-node.rxm
	// XXX close -> EOF?
	return rx.Req, rx.Err
}

//trace:event traceClusterStateChanged(cs *proto.ClusterState)

// recvδstate handles reception of δstate messages.
func (node *_MasteredNode) recvδstate(ctx context.Context, msg proto.Msg) (δpt bool, err error) {
	δpt = false

	node.stateMu.Lock()
	// XXX defer unlock ?

	switch msg := msg.(type) {
	default:
		node.stateMu.Unlock()
		panic(fmt.Sprintf("unexpected message: %T", msg))

	// <- whole partTab
	case *proto.SendPartitionTable:
		δpt = true
		pt := xneo.PartTabFromDump(msg.PTid, msg.RowList) // FIXME handle msg.NumReplicas
		// XXX logging under lock ok?
		log.Infof(ctx, "parttab update: %s", pt)
		node.state.PartTab = pt

	// <- δ(partTab)
	case *proto.NotifyPartitionChanges:
		δpt = true
		panic("TODO δ(partTab)")

	// <- δ(nodeTab)
	case *proto.NotifyNodeInformation:
		err = node.updateNodeTab(ctx, msg) // XXX recheck return (might be command to shutdown)

	case *proto.NotifyClusterState:
		log.Infof(ctx, "state update: %s", msg.State)
		node.state.Code = msg.State
		traceClusterStateChanged(&node.state.Code)
	}

/* XXX kill
	if δpt && node.OnNotifyδPartTab != nil {
		err = node.OnNotifyδPartTab(node.state.PartTab)
		// XXX err -> return without notify?
		panic("TODO")
	}
*/

	// update .operational + notify those who was waiting for it
	opready := node.updateOperational()
	node.stateMu.Unlock()
	opready()

	return δpt, err
}

// updateOperational updates .operational from current state.
//
// Must be called with .stateMu lock held.
//
// Returned sendReady func must be called by updateOperational caller after
// .node.StateMu lock is released - it will close current .opReady this way
// notifying .operational waiters.
func (node *_MasteredNode) updateOperational() (sendReady func()) {
	operational := node.state.IsOperational()

	//fmt.Printf("\nupdateOperatinal: %v\n", operational)
	//fmt.Println(node.partTab)
	//fmt.Println(node.nodeTab)

	var opready chan struct{}
	if operational != node.operational {
		node.operational = operational
		if operational {
			opready = node.opReady // don't close from under stateMu
		} else {
			node.opReady = make(chan struct{}) // remake for next operational waiters
		}
	}

	return func() {
		if opready != nil {
			//fmt.Println("updateOperational - notifying %v\n", opready)
			close(opready)
		}
	}
}


// WithOperational runs f during when cluster state is/becomes operational.
// The cluster state is guaranteed not to change during f run.
func (node *_MasteredNode) WithOperational(ctx context.Context, f func(cs *xneo.ClusterState) error) error {
	for {
		node.stateMu.RLock()
		if node.operational {
			//fmt.Printf("withOperation -> ready\n");
			break
		}

		ready := node.opReady
		node.stateMu.RUnlock()

		//fmt.Printf("withOperational - waiting on %v\n", ready)

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

		case <-ready:
			// ok - try to relock and read again.
		}
	}

	// node.operational=y and node.stateMu is rlocked
	defer node.stateMu.RUnlock()
	return f(&node.state)
}

var cmdShutdown = errors.New("master told us to shutdown")

// updateNodeTab applies updates to .nodeTab from message and logs changes appropriately.
// the only possible error is cmdShutdown.
// must be called under .stateMu.
func (node *_MasteredNode) updateNodeTab(ctx context.Context, msg *proto.NotifyNodeInformation) error {
	// XXX msg.IdTime ?
	for _, nodeInfo := range msg.NodeList {
		log.Infof(ctx, "node update: %v", nodeInfo)
		node.state.NodeTab.Update(nodeInfo)

		// we have to provide IdTime when requesting identification to other peers
		// (e.g. Spy checks this is what master broadcast them and if not replies "unknown by master")
		if nodeInfo.NID == node.myInfo.NID {
			// XXX recheck locking
			// XXX do .myInfo = nodeInfo ?
			node.myInfo.IdTime = nodeInfo.IdTime

			// NEO/py currently employs this hack
			// FIXME -> better it be separate command and handled cleanly
			if nodeInfo.State == proto.DOWN {
				err := cmdShutdown
				log.Info(ctx, err)
				return err
			}
		}
	}

	// FIXME logging under lock ok? (if caller took e.g. .stateMu before applying updates)
	log.Infof(ctx, "full nodetab:\n%s", node.state.NodeTab)
	return nil
}


// XXX Dial + request identification + verify peer type
func dialNode(ctx context.Context, typ proto.NodeType, net xnet.Networker, addr string, reqID *proto.RequestIdentification) (_ *neonet.NodeLink, _ *proto.AcceptIdentification, err error) {
	defer task.Runningf(&ctx, "dial %s (%s)", addr, typ)(&err)

	link, err := neonet.DialLink(ctx, net, addr)
	if err != nil {
		return nil, nil, err
	}

	log.Info(ctx, "dialed ok; requesting identification...")
	defer xerr.Contextf(&err, "%s: request identification", link)

	accept := &proto.AcceptIdentification{}
	err = xcontext.WithCloseOnErrCancel(ctx, link, func() error {
		// FIXME error if peer sends us something with another connID
		// (currently we ignore and serveRecv will deadlock)
		//
		// XXX solution could be:
		// link.CloseAccept()
		// link.Ask1(reqID, accept)
		// link.Listen()
		// XXX but there is a race window in between recv in ask and listen
		// start, and if peer sends new connection in that window it will be rejected.
		//
		// TODO thinking.
		err = link.Ask1(reqID, accept)
		if err != nil {
			return err
		}

		if accept.NodeType != typ {
			// XXX send Error to peer?
			return fmt.Errorf("accepted, but peer is not %v (identifies as %v)", typ, accept.NodeType)
		}

		return nil
	})
	if err != nil {
		return nil, nil, err
	}

	log.Info(ctx, "identification accepted")
	return link, accept, nil
}


// XXX = Dial node by NID, verify it accepts wit "MyNID" == NID, YourNID == NID we sent
// func dialNID
