package twitchserver

import (
	"context"
	"math/rand"
	"net"
	"net/http"
	_ "net/http/pprof" // importing twitchhttp implies intent to host pprof metrics, so we automatically enable it.
	"os"
	"runtime"
	"syscall"
	"time"

	"github.com/cactus/go-statsd-client/statsd"
	"github.com/zenazn/goji/graceful"

	"code.justin.tv/common/chitin"
	"code.justin.tv/common/config"
	"code.justin.tv/common/gometrics"
	"code.justin.tv/feeds/ctxlog"
	"code.justin.tv/foundation/xray"

	"goji.io"
	"goji.io/middleware"
	"goji.io/pat"
)

var stdSignals = []os.Signal{os.Interrupt, syscall.SIGUSR2, syscall.SIGTERM}

// Server allows simple definition of an HTTP service which meets Twitch requirements for production readiness.
func init() {
	config.Register(map[string]string{
		"bind-address":       ":8000",
		"debug-bind-address": ":6000",
	})
	rand.Seed(time.Now().UnixNano())
}

// ServerConfig configures a twitchhttp server.
// All configuration is optional and will fall back to reasonable defaults
type ServerConfig struct {
	Addr      string         // defaults to config.Resolve("bind-address")
	DebugAddr string         // defaults to config.Resolve("debug-address")
	Statter   statsd.Statter // defaults to config.Statsd()

	// Timeout configuration. These values default to 0 (ignored by graceful.Server)
	// Documentation for these values can be found here: https://golang.org/pkg/net/http/#Server
	ReadTimeout       time.Duration
	ReadHeaderTimeout time.Duration
	WriteTimeout      time.Duration
	IdleTimeout       time.Duration

	// GracefulTimeout is a duration which a server will attempt to shut down gracefully before shutting down abruptly. Defaults to 10s.
	GracefulTimeout time.Duration
	// ctxlog will append logging dimensions to the context using this key. A logger may
	// want to log those context values.
	DimensionKey interface{}
}

// NewConfig creates a new configuration using the config values bind-address and debug-bind-address.
func NewConfig() *ServerConfig {
	return fillConfig(&ServerConfig{})
}

func fillConfig(conf *ServerConfig) *ServerConfig {
	if conf == nil {
		conf = NewConfig()
	}
	if conf.Addr == "" {
		conf.Addr = config.Resolve("bind-address")
	}
	if conf.DebugAddr == "" {
		conf.DebugAddr = config.Resolve("debug-bind-address")
	}
	if conf.Statter == nil {
		conf.Statter = config.Statsd()
	}
	if conf.GracefulTimeout == 0 {
		conf.GracefulTimeout = time.Second * 10
	}
	if conf.DimensionKey == nil {
		conf.DimensionKey = new(int)
	}
	return conf
}

// NewServer allocates and returns a Server. The /debug/running endpoint is registered for load balancer health checks.
func NewServer() *goji.Mux {
	debugRouter := goji.SubMux()
	debugRouter.HandleFunc(pat.Get("/running"), healthcheck)

	router := goji.NewMux()
	router.Use(RollbarMiddleware)
	router.Use(SkipMiddleware(debugRouter))
	router.Handle(pat.New("/debug/*"), debugRouter)

	return router
}

// Generate a base handler which wraps chitin, xray, and ctxlog
func baseHandler(h http.Handler, ctxLog *ctxlog.Ctxlog) (http.Handler, error) {
	handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		ctx := chitin.Context(w, r)
		r = r.WithContext(ctx)
		h.ServeHTTP(w, r)
	})

	traceCtx, err := chitin.ExperimentalTraceContext(context.Background())
	if err != nil {
		return nil, err
	}

	ctxlogHandler := &ctxlog.CtxHandler{
		Next:      handler,
		Ctxlog:    ctxLog,
		Logger:    &defaultLogger{},
		IDFetcher: xray.TraceID,
	}

	xrayHandler := xray.Handler(ctxlogHandler)

	chitinHandler := chitin.Handler(xrayHandler, chitin.SetBaseContext(traceCtx))

	return chitinHandler, nil
}

// Serve accepts connnections to the listener and dispatches HTTP requests, as well as starting a debug server for pprof debugging. Go runtime metrics begin emitting to statsd.
func Serve(ln net.Listener, h http.Handler, serverConfig *ServerConfig) error {
	serverConfig = fillConfig(serverConfig)

	if serverConfig.DebugAddr != "" {
		go func() {
			logger.Printf("debug listening on %v", serverConfig.DebugAddr)
			logger.Print(http.ListenAndServe(serverConfig.DebugAddr, nil))
		}()
	}

	gometrics.Monitor(serverConfig.Statter, time.Second*5)

	graceful.Timeout(serverConfig.GracefulTimeout)
	graceful.PreHook(func() {
		logger.Println("Shutting down server...")
	})
	graceful.PostHook(func() {
		logger.Println("Server is shut down...")
	})

	ctxLog := &ctxlog.Ctxlog{
		CtxDims:       &ctxDimensions{serverConfig.DimensionKey},
		StartingIndex: rand.Int63(),
	}
	handler, err := baseHandler(h, ctxLog)
	if err != nil {
		return err
	}

	logger.Printf("application listening on %v", serverConfig.Addr)
	server := &graceful.Server{
		Addr:              serverConfig.Addr,
		Handler:           handler,
		ReadTimeout:       serverConfig.ReadTimeout,
		ReadHeaderTimeout: serverConfig.ReadHeaderTimeout,
		WriteTimeout:      serverConfig.WriteTimeout,
		IdleTimeout:       serverConfig.IdleTimeout,
	}

	err = server.Serve(ln)

	if err != nil {
		return err
	}
	graceful.Wait()
	return nil
}

// ListenAndServe starts the application server, as well as a debug server for pprof debugging. Go runtime metrics begin emitting to statsd.
func ListenAndServe(h http.Handler, serverConfig *ServerConfig) error {
	serverConfig = fillConfig(serverConfig)
	ln, err := net.Listen("tcp", serverConfig.Addr)
	if err != nil {
		return err
	}
	return Serve(tcpKeepAliveListener{ln.(*net.TCPListener)}, h, serverConfig)
}

// AddDefaultSignalHandlers will handle OS signals in an opinionated but sane manner. At the moment, that means
//
// SIGUSR2 will gracefully shut down
// SIGINT will gracefully shut down
// SIGTERM will gracefully shut down
func AddDefaultSignalHandlers() {
	graceful.AddSignal(stdSignals...)
}

// AddShutdownSignal will add listeners to OS signals. When any of those signals are received, the http server will shut down gracefully.
func AddShutdownSignal(sig ...os.Signal) {
	graceful.AddSignal(sig...)
}

// Shutdown will gracefully shut down the HTTP server. No future requests will be accepted, but
// in-progress requests will complete.
//
// timeout is required because of limitations with the graceful library. A timeout of 0 will disable the feature.
func Shutdown(t time.Duration) {
	if t > 0 {
		go func() {
			time.Sleep(t)
			graceful.ShutdownNow()
		}()
	}
	graceful.Shutdown()
}

// ShutdownNow will shut down the server immediately. In-progress requests will be cut short, and
// consuming sockets will receive an EOF error. In general, you'll want to use Shutdown.
func ShutdownNow() {
	graceful.ShutdownNow()
}

// RollbarMiddleware intercepts panics and sends them to rollbar before re-panicing
func RollbarMiddleware(h http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		defer func() {
			if p := recover(); p != nil {
				if config.RollbarErrorLogger() != nil {
					config.RollbarErrorLogger().RequestPanic(r, p)
				} else {
					// This error handling is copied from golang's net/http package
					// https://github.com/golang/go/blob/0ec62565f911575beaf7d047dfe1eae2ae02bf67/src/net/http/server.go#L1468
					// It allows us to log the error while still returning a response to the client
					const size = 64 << 10
					buf := make([]byte, size)
					buf = buf[:runtime.Stack(buf, false)]
					logger.Printf("http: panic serving %v: %v\n%s", r.RemoteAddr, p, buf)
				}

				w.WriteHeader(http.StatusInternalServerError)
			}
		}()

		h.ServeHTTP(w, r)
	})
}

// SkipMiddleware takes a handler, and if the request is being routed to that handler
// it will skip all remaining middleware and immediately execute the handler.
//
// The order in which middleware is registered determines which middleware this will skip.
// For more information refer to https://godoc.org/goji.io#Mux.Use
func SkipMiddleware(desiredHandler http.Handler) func(http.Handler) http.Handler {
	return func(h http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			if handler := middleware.Handler(r.Context()); handler == desiredHandler {
				handler.ServeHTTP(w, r)
				return
			}

			h.ServeHTTP(w, r)
		})
	}
}

func healthcheck(w http.ResponseWriter, r *http.Request) {
	w.Header().Add("Cache-Control", "no-cache, no-store, must-revalidate")
	_, err := w.Write([]byte("OK"))
	if err != nil {
		logger.Printf("health check failed to respond: %v", err)
	}
}

// tcpKeepAliveListener sets TCP keep-alive timeouts on accepted
// connections. It's used by ListenAndServe and ListenAndServeTLS so
// dead TCP connections (e.g. closing laptop mid-download) eventually
// go away.
// Lifted from goji and net/http.
type tcpKeepAliveListener struct {
	*net.TCPListener
}

func (ln tcpKeepAliveListener) Accept() (c net.Conn, err error) {
	tc, err := ln.AcceptTCP()
	if err != nil {
		return nil, err
	}
	if err := tc.SetKeepAlive(true); err != nil {
		return nil, err
	}
	if err := tc.SetKeepAlivePeriod(3 * time.Minute); err != nil {
		return nil, err
	}
	return tc, nil
}
