package server

import (
	"context"
	"crypto/tls"
	"log"
	"net"
	"net/http"
	"os"
	"time"

	"github.com/pkg/errors"
)

// ServerShutdownTimeout is the default time to wait before closing
// connections on shutdown.
const ServerShutdownTimeout = 60 * time.Second

// Server is a incomplete component that implements a basic HTTP/HTTPS
// server.
type Server struct {
	*http.Server
	listener   *net.TCPListener
	reloadCh   chan net.Listener
	shutdownCh chan struct{}
}

// New creates a new HTTP/HTTPS server configured with the passed
// address, http.Handler and tls.Config.
func New(addr string, handler http.Handler, tlsConfig *tls.Config) *Server {
	return &Server{
		reloadCh:   make(chan net.Listener),
		shutdownCh: make(chan struct{}),
		Server:     newHTTPServer(addr, handler, tlsConfig),
	}
}

// newHTTPServer creates a new http.Server with the TCP address, handler and
// tls.Config.
func newHTTPServer(addr string, handler http.Handler, tlsConfig *tls.Config) *http.Server {
	return &http.Server{
		Addr:         addr,
		Handler:      handler,
		TLSConfig:    tlsConfig,
		WriteTimeout: 15 * time.Second,
		ReadTimeout:  15 * time.Second,
		IdleTimeout:  15 * time.Second,
		ErrorLog:     log.New(os.Stderr, "", log.Ldate|log.Ltime|log.Llongfile),
	}
}

// ListenAndServe listens on the TCP network address srv.Addr and then calls
// Serve to handle requests on incoming connections.
func (srv *Server) ListenAndServe() error {
	ln, err := net.Listen("tcp", srv.Addr)
	if err != nil {
		return err
	}

	return srv.Serve(ln)
}

// Serve runs Serve or ServeTLS on the underlying http.Server and listen to
// channels to reload or shutdown the server.
func (srv *Server) Serve(ln net.Listener) error {
	var err error
	// Store the current listener.
	// In reloads we'll create a copy of the underlying os.File so the close of the server one does not affect the copy.
	srv.listener = ln.(*net.TCPListener)

	for {
		// Start server
		if srv.TLSConfig == nil || (len(srv.TLSConfig.Certificates) == 0 && srv.TLSConfig.GetCertificate == nil) {
			log.Printf("Serving HTTP on %s ...", srv.Addr)
			err = srv.Server.Serve(ln)
		} else {
			log.Printf("Serving HTTPS on %s ...", srv.Addr)
			err = srv.Server.ServeTLS(ln, "", "")
		}

		// log unexpected errors
		if err != http.ErrServerClosed {
			log.Println(errors.Wrap(err, "unexpected error"))
		}

		select {
		case ln = <-srv.reloadCh:
			srv.listener = ln.(*net.TCPListener)
		case <-srv.shutdownCh:
			return http.ErrServerClosed
		}
	}
}

// Shutdown gracefully shuts down the server without interrupting any active
// connections.
func (srv *Server) Shutdown() error {
	ctx, cancel := context.WithTimeout(context.Background(), ServerShutdownTimeout)
	defer cancel()              // release resources if Shutdown ends before the timeout
	defer close(srv.shutdownCh) // close shutdown channel
	return srv.Server.Shutdown(ctx)
}

func (srv *Server) reloadShutdown() error {
	ctx, cancel := context.WithTimeout(context.Background(), ServerShutdownTimeout)
	defer cancel() // release resources if Shutdown ends before the timeout
	return srv.Server.Shutdown(ctx)
}

// Reload reloads the current server with the configuration of the passed
// server.
func (srv *Server) Reload(ns *Server) error {
	var err error
	var ln net.Listener

	if srv.Addr != ns.Addr {
		// Open new address
		ln, err = net.Listen("tcp", ns.Addr)
		if err != nil {
			return errors.WithStack(err)
		}
	} else {
		// Get a copy of the underlying os.File
		fd, err := srv.listener.File()
		if err != nil {
			return errors.WithStack(err)
		}
		// Make sure to close the copy
		defer fd.Close()

		// Creates a new listener copying fd
		ln, err = net.FileListener(fd)
		if err != nil {
			return errors.WithStack(err)
		}
	}

	// Close old server without sending a signal
	if err := srv.reloadShutdown(); err != nil {
		return err
	}

	// Update old server
	srv.Server = ns.Server
	srv.reloadCh <- ln
	return nil
}

// Forbidden writes on the http.ResponseWriter a text/plain forbidden
// response.
func (srv *Server) Forbidden(w http.ResponseWriter) {
	header := w.Header()
	header.Set("Content-Type", "text/plain; charset=utf-8")
	header.Set("Content-Length", "11")
	w.WriteHeader(http.StatusForbidden)
	w.Write([]byte("Forbidden.\n"))
}
