// Copyright (C) 2016-2023  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
// dial/listen combined with request/accept identification

import (
	"context"
	"fmt"
	"net"

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

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

// Listener is LinkListener adapted to return NodeLink with requested identification on Accept.
type Listener interface {
	// from LinkListener:
	Close() error
	Addr() net.Addr

	// Accept accepts incoming connection from a peer.
	//
	// On success the link was handshaked and peer sent us RequestIdentification
	// packet which we did not yet answer.
	//
	// On success returned are:
	// - original peer request that carried identification
	// - requested identification packet
	//
	// After successful accept it is the caller responsibility to reply the request.
	//
	// NOTE established link is Request.Link().
	Accept(ctx context.Context) (*neonet.Request, *proto.RequestIdentification, error)
}

// NewListener wraps inner LinkListener into Listener.
func NewListener(inner neonet.LinkListener) Listener {
	return &listener{l: inner}
}

type listener struct {
	l neonet.LinkListener
}

func (l *listener) Accept(ctx context.Context) (_ *neonet.Request, msgID *proto.RequestIdentification, err error) {
	link, err := l.l.Accept(ctx)
	if err != nil {
		return nil, nil, err
	}

	// identify peer
	// the first conn must come with RequestIdentification packet
	defer xerr.Context(&err, "identify")
	var req neonet.Request
	err = xio.WithCloseOnErrCancel(ctx, link, func() error {
		var err error
		req, err = link.Recv1(/*XXX ctx*/)
		if err != nil {
			return err
		}

		switch msg := req.Msg.(type) {
		case *proto.RequestIdentification:
			msgID = msg
			return nil
		}

		emsg := &proto.Error{proto.PROTOCOL_ERROR, fmt.Sprintf("unexpected message %T", req.Msg)}
		req.Reply(emsg)	// ignore err
		return emsg
	})
	if err != nil {
		return nil, nil, err
	}
	return &req, msgID, err
}

func (l *listener) Close() error   { return l.l.Close() }
func (l *listener) Addr() net.Addr { return l.l.Addr() }


// Dial connects to another node in the cluster.
//
// It handshakes, requests identification and checks peer type. If successful returned are:
//
//	- established link
//	- accept identification reply
//
// Dial does not update NodeTab or its node entries in any way.
// For establishing links to peers present in NodeTab use PeerNode.Dial.
func Dial(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 = xio.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.

		conn, err := link.NewConn()
		if err != nil {
			return err
		}
		defer conn.Close()

		err = conn.Send(reqID)
		if err != nil {
			return err
		}

		// besides AcceptIdentification and Error
		// also expect NotPrimaryMaster if we are connecting to a master
		nerr := &proto.Error{}
		notPrimary := &proto.NotPrimaryMaster{}
		respv := []proto.Msg{accept, nerr}
		if typ == proto.MASTER {
			respv = append(respv, notPrimary)
		}

		which, err := conn.Expect(respv...)

		switch which {
		case 0:
			if accept.NodeType != typ {
				// TODO send Error to peer?
				return fmt.Errorf("accepted, but peer is not %s (identifies as %s)", typ, accept.NodeType)
			}
			return nil
		case 1:
			return nerr
		case 2:
			return notPrimary
		}
		return err

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

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