// Copyright (C) 2018-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 main
// routines common to several subcommands

import (
	"bytes"
	"context"
	"flag"
	"net/http"
	"io"
	"fmt"

	"github.com/soheilhy/cmux"

	"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/xio"
	"lab.nexedi.com/kirr/neo/go/neo/neonet"

	// for http://.../debug/pprof
	_ "net/http/pprof"
)

// netFlags installs common network flags and returns function to setup network
// and create selected networker.
func netFlags(flags *flag.FlagSet) (netSetup func(context.Context) (xnet.Networker, error)) {
	// XXX also support $NEO_<K> envvars?
	fca   := flags.String("ca",   "", "path to CA certificate")
	fcert := flags.String("cert", "", "path to node certificate")
	fkey  := flags.String("key",  "", "path to node private key")

	flonode := flags.String("lonode", "", "<net>/<host> for this node on lonet network")

	return func(ctx context.Context) (xnet.Networker, error) {
		return neonet.Join(ctx, neonet.Config{
			CA:   *fca,
			Cert: *fcert,
			Key:  *fkey,

			LoNode: *flonode,
		})
	}
}

// neoMatch tells whether incoming stream starts like a NEO protocol handshake word.
func neoMatch(r io.Reader) bool {
	var b [4]byte
	n, _ := io.ReadFull(r, b[:])
	if n < 4 {
		return false
	}

	switch {
	// 00 00 00 v - original handshake for NEO <= 1.12
	case bytes.Equal(b[:3], []byte{0,0,0}):
		return true

	// NEO handshake after switch to MessagePack (= start of msgpack encoding for [2](b"NEO", version) )
	case bytes.Equal(b[:],  []byte{0x92, 0xc4, 3, 'N'}):
		return true

	default:
		return false
	}
}

// listenAndServe runs service on laddr.
//
// It starts listening, multiplexes incoming connection to NEO and HTTP
// protocols, passes NEO connections to serve and passes HTTP connection to
// default HTTP mux.
//
// Default HTTP mux can be assumed to contain /debug/pprof and the like.
func listenAndServe(ctx context.Context, net xnet.Networker, laddr string, serve func(ctx context.Context, l xnet.Listener) error) (err error) {
	l, err := net.Listen(ctx, laddr)
	if err != nil {
		return err
	}
	defer func() { // just in case if mux.Close does not close l
		__ := l.Close()
		err = xerr.First(err, __)
	}()

	log.Infof(ctx, "listening at %s ...", l.Addr())
	log.Flush()

	mux := cmux.New(xnet.BindCtxL(l, ctx))
	neoL  := mux.Match(neoMatch)
	httpL := mux.Match(cmux.HTTP1(), cmux.HTTP2())
	miscL := mux.Match(cmux.Any())

	wg := xsync.NewWorkGroup(ctx)

	wg.Go(func(ctx context.Context) error {
		return xio.WithCloseOnRetCancel(ctx, &noErrCloser{mux},
			mux.Serve,
		)
	})

	wg.Go(func(ctx context.Context) error {
		return serve(ctx, xnet.WithCtxL(neoL))
	})

	wg.Go(func(ctx context.Context) error {
		srv := &http.Server{}
		return xio.WithCloseOnRetCancel(ctx, srv, func() error {
			// TODO ^^^ better Shutdown instead of Close
			return srv.Serve(httpL)
		})
	})

	wg.Go(func(ctx context.Context) error {
		return xio.WithCloseOnRetCancel(ctx, miscL, func() error {
			for {
				conn, err := miscL.Accept()
				if err != nil {
					return err
				}

				// got something unexpected - grab the header (which we
				// already have read), log it and reject the connection.
				b := make([]byte, 1024)
				// must not block as some data is already there in cmux buffer
				n, _ := conn.Read(b)
				subj := fmt.Sprintf("strange connection from %s:", conn.RemoteAddr())
				serr := "peer sent nothing"
				if n > 0 {
					serr = fmt.Sprintf("peer sent %q", b[:n])
				}
				log.Warningf(ctx, "%s: %s", subj, serr)

				xio.LClose(ctx, conn)
			}
		})
	})

	return wg.Wait()
}

// noErrCloser turns `Close()` -> `Close() err` that always returns nil.
type noErrCloser struct {
	c interface { Close() }
}

func (c *noErrCloser) Close() error {
	c.c.Close()
	return nil
}
