// Copyright (C) 2017 Michael J. Fromberger. All Rights Reserved.

// Package server provides support routines for running jrpc2 servers.
package server

import (
	"context"
	"net"
	"sync"

	"github.com/creachadair/jrpc2"
	"github.com/creachadair/jrpc2/channel"
)

// Service is the interface used by the Loop function to start up a server.
// The methods of this interface allow the instance to manage its state:
//
//   - The Assigner method is called before the server is started, to to
//     initialize the service.
//
//   - The Finish method is called after the server exits to clean up.
type Service interface {
	// This method is called to create an assigner and initialize the service
	// for use.  If it reports an error, the server is not started.
	Assigner() (jrpc2.Assigner, error)

	// This method is called when the server for this service has exited.
	// The arguments are the assigner returned by the Assigner method and the
	// server exit status.
	Finish(jrpc2.Assigner, jrpc2.ServerStatus)
}

// Static wraps a [jrpc2.Assigner] to trivially implement the Service interface.
func Static(m jrpc2.Assigner) func() Service { return static{methods: m}.new }

type static struct{ methods jrpc2.Assigner }

func (s static) new() Service                            { return s }
func (s static) Assigner() (jrpc2.Assigner, error)       { return s.methods, nil }
func (static) Finish(jrpc2.Assigner, jrpc2.ServerStatus) {}

// An Accepter obtains client connections from an external source and
// constructs channels from them.
type Accepter interface {
	// Accept blocks until a connection is available, or until ctx ends.
	// If a connection is found, Accept  returns a new channel for it.
	Accept(ctx context.Context) (channel.Channel, error)
}

// NetAccepter adapts a [net.Listener] to the Accepter interface, using f as
// the channel framing.
func NetAccepter(lst net.Listener, f channel.Framing) Accepter {
	return netAccepter{Listener: lst, newChannel: f}
}

type netAccepter struct {
	net.Listener
	newChannel channel.Framing
}

func (n netAccepter) Accept(ctx context.Context) (channel.Channel, error) {
	// A net.Listener does not obey a context, so simulate it by closing the
	// listener if ctx ends.
	ok := make(chan struct{})
	defer close(ok)
	go func() {
		select {
		case <-ctx.Done():
			n.Listener.Close()
		case <-ok:
			return
		}
	}()

	conn, err := n.Listener.Accept()
	if err != nil {
		return nil, err
	}
	return n.newChannel(conn, conn), nil
}

// Loop obtains connections from lst and starts a server for each using a
// service instance returned by newService and the given options. Each server
// runs in a new goroutine.
//
// If lst is closed or otherwise reports an error, the loop will terminate.
// The error will be reported to the caller of Loop once any active servers
// have returned. In addition, if ctx ends, any active servers will be stopped.
func Loop(ctx context.Context, lst Accepter, newService func() Service, opts *LoopOptions) error {
	serverOpts := opts.serverOpts()
	log := func(string, ...any) {}
	if serverOpts != nil && serverOpts.Logger != nil {
		log = serverOpts.Logger.Printf
	}

	var wg sync.WaitGroup
	for {
		ch, err := lst.Accept(ctx)
		if err != nil {
			if channel.IsErrClosing(err) {
				err = nil
			} else {
				log("Error accepting new connection: %v", err)
			}
			wg.Wait()
			return err
		}
		wg.Go(func() {
			svc := newService()
			assigner, err := svc.Assigner()
			if err != nil {
				log("Service initialization failed: %v", err)
				return
			}

			sctx, cancel := context.WithCancel(ctx)
			defer cancel()

			srv := jrpc2.NewServer(assigner, serverOpts).Start(ch)
			go func() { <-sctx.Done(); srv.Stop() }()

			stat := srv.WaitStatus()
			svc.Finish(assigner, stat)
			if stat.Err != nil {
				log("Server exit: %v", stat.Err)
			}
		})
	}
}

// LoopOptions control the behaviour of the Loop function.  A nil *LoopOptions
// provides default values as described.
type LoopOptions struct {
	// If non-nil, these options are used when constructing the server to
	// handle requests on an inbound connection.
	ServerOptions *jrpc2.ServerOptions
}

func (o *LoopOptions) serverOpts() *jrpc2.ServerOptions {
	if o == nil {
		return nil
	}
	return o.ServerOptions
}
