/*
Package bind provides a convenient way to bind to sockets. It exposes a flag in
the default flag set named "bind" which provides syntax to bind TCP and UNIX
sockets. It also supports binding to arbitrary file descriptors passed by a
parent (for instance, systemd), and for binding to Einhorn sockets (including
Einhorn ACK support).

If the value passed to bind contains a colon, as in ":8000" or "127.0.0.1:9001",
it will be treated as a TCP address. If it begins with a "/" or a ".", it will
be treated as a path to a UNIX socket. If it begins with the string "fd@", as in
"fd@3", it will be treated as a file descriptor (useful for use with systemd,
for instance). If it begins with the string "einhorn@", as in "einhorn@0", the
corresponding einhorn socket will be used.

If an option is not explicitly passed, the implementation will automatically
select between using "einhorn@0", "fd@3", and ":8000", depending on whether
Einhorn or systemd (or neither) is detected.

This package is a teensy bit magical, and goes out of its way to Do The Right
Thing in many situations, including in both development and production. If
you're looking for something less magical, you'd probably be better off just
calling net.Listen() the old-fashioned way.
*/
package bind

import (
	"flag"
	"fmt"
	"log"
	"net"
	"os"
	"strconv"
	"strings"
	"sync"
)

var bind string

func init() {
	einhornInit()
	systemdInit()
}

// DefaultBind specifies the fallback used for WithFlag() if it is
// unable to discover an environment hint (after checking $GOJI_BIND,
// Einhorn, systemd, and $PORT).
//
// If DefaultBind is overridden, it must be set before calling
// WithFlag().
var DefaultBind = ":8000"

// WithFlag adds a standard flag to the global flag instance that
// allows configuration of the default socket. Users who call
// Default() must call this function before flags are parsed, for
// example in an init() block.
//
// When selecting the default bind string, this function will examine
// its environment for hints about what port to bind to, selecting the
// GOJI_BIND environment variable, Einhorn, systemd, the PORT
// environment variable, and the value of DefaultBind, in order. In
// most cases, this means that the default behavior of the default
// socket will be reasonable for use in your circumstance.
func WithFlag() {
	s := Sniff()
	if s == "" {
		s = DefaultBind
	}
	flag.StringVar(&bind, "bind", s,
		`Address to bind on. If this value has a colon, as in ":8000" or
		"127.0.0.1:9001", it will be treated as a TCP address. If it
		begins with a "/" or a ".", it will be treated as a path to a
		UNIX socket. If it begins with the string "fd@", as in "fd@3",
		it will be treated as a file descriptor (useful for use with
		systemd, for instance). If it begins with the string "einhorn@",
		as in "einhorn@0", the corresponding einhorn socket will be
		used. If an option is not explicitly passed, the implementation
		will automatically select among "einhorn@0" (Einhorn), "fd@3"
		(systemd), and ":8000" (fallback) based on its environment.`)
}

// Sniff attempts to select a sensible default bind string by examining its
// environment. It examines the GOJI_BIND environment variable, Einhorn,
// systemd, and the PORT environment variable, in that order, selecting the
// first plausible option. It returns the empty string if no sensible default
// could be extracted from the environment.
func Sniff() string {
	if bind := os.Getenv("GOJI_BIND"); bind != "" {
		return bind
	} else if usingEinhorn() {
		return "einhorn@0"
	} else if usingSystemd() {
		return "fd@3"
	} else if port := os.Getenv("PORT"); port != "" {
		return ":" + port
	}
	return ""
}

func listenTo(bind string) (net.Listener, error) {
	if strings.Contains(bind, ":") {
		return net.Listen("tcp", bind)
	} else if strings.HasPrefix(bind, ".") || strings.HasPrefix(bind, "/") {
		return net.Listen("unix", bind)
	} else if strings.HasPrefix(bind, "fd@") {
		fd, err := strconv.Atoi(bind[3:])
		if err != nil {
			return nil, fmt.Errorf("error while parsing fd %v: %v",
				bind, err)
		}
		f := os.NewFile(uintptr(fd), bind)
		defer f.Close()
		return net.FileListener(f)
	} else if strings.HasPrefix(bind, "einhorn@") {
		fd, err := strconv.Atoi(bind[8:])
		if err != nil {
			return nil, fmt.Errorf(
				"error while parsing einhorn %v: %v", bind, err)
		}
		return einhornBind(fd)
	}

	return nil, fmt.Errorf("error while parsing bind arg %v", bind)
}

// Socket parses and binds to the specified address. If Socket encounters an
// error while parsing or binding to the given socket it will exit by calling
// log.Fatal.
func Socket(bind string) net.Listener {
	l, err := listenTo(bind)
	if err != nil {
		log.Fatal(err)
	}
	return l
}

// Default parses and binds to the default socket as given to us by the flag
// module. If there was an error parsing or binding to that socket, Default will
// exit by calling `log.Fatal`.
func Default() net.Listener {
	return Socket(bind)
}

// I'm not sure why you'd ever want to call Ready() more than once, but we may
// as well be safe against it...
var ready sync.Once

// Ready notifies the environment (for now, just Einhorn) that the process is
// ready to receive traffic. Should be called at the last possible moment to
// maximize the chances that a faulty process exits before signaling that it's
// ready.
func Ready() {
	ready.Do(func() {
		einhornAck()
	})
}
