package main

import (
	"context"
	"fmt"
	"log"
	"net/http"
	"time"

	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/aws/session"
	"github.com/aws/aws-sdk-go/service/sqs"
	"go.uber.org/zap"

	"code.justin.tv/websocket-edge/server/internal/autoprof"
	"code.justin.tv/websocket-edge/server/internal/environment"
	"code.justin.tv/websocket-edge/server/internal/gqlsubs"
	"code.justin.tv/websocket-edge/server/internal/handlers"
	"code.justin.tv/websocket-edge/server/internal/logs"
	"code.justin.tv/websocket-edge/server/internal/metrics"
	"code.justin.tv/websocket-edge/server/internal/queue"
	"code.justin.tv/websocket-edge/server/internal/server"
)

// gitCommit is the short commit hash of the source tree (e.g. "5d85cffc8"). It is set at build time with -ldflags.
var gitCommit string

// buildDate is the date and time that this executable was built. It is set at build time with -ldflags.
var buildDate string

// The name of the service.
var serviceName = "websocket-edge"

// How long we'll wait for connections to close before shutting down.
var shutdownTimeout = 120 * time.Second

var ballast []byte //nolint:go-lint,unused

func main() {
	var logger logs.Logger
	logger, err := logs.New(environment.IsLocal())
	if err != nil {
		log.Fatalf("Unable to create logger: %v\n", err)
	}
	logger = logger.With(zap.String("commit", gitCommit), zap.String("buildDate", buildDate))

	logFn := func(key string, err error, msg string) {
		logger.Error(msg, zap.Error(err), zap.String("key", key))
	}
	conf := loadConfig(logFn)

	var statter metrics.Statter
	if environment.IsLocal() {
		statter = &metrics.LogStatter{Logger: logger}
	} else {
		statter, err = metrics.New(logger, serviceName, environment.Environment(), gitCommit, conf.awsRegion)
		if err != nil {
			logger.Fatal("Unable to create statter", zap.Error(err))
		}
	}

	ballast = makeBallast()

	var queueClient queue.Queue
	session, err := session.NewSession(&aws.Config{
		Region: aws.String("us-west-2"),
	})
	if err != nil {
		logger.Fatal("Unable to create aws session.", zap.Error(err))
	}
	if environment.IsLocal() {
		queueClient = &queue.Noop{}
	} else {
		sqsClient := sqs.New(session)
		queueClient = queue.NewSQSQueue(sqsClient, statter, conf.sqsQueueURL)
		logger.Info("SQS Client configured", zap.String("queueURL", conf.sqsQueueURL))
	}

	err = autoprof.Go(context.Background(), session, conf.autoprofBucket, func(err error) error {
		logger.Error("Autoprof error", zap.Error(err))
		return err
	})
	if err != nil {
		logger.Warn("Autoprof disabled", zap.Error(err))
	}

	tr := &http.Transport{
		MaxIdleConnsPerHost: 1000,
		IdleConnTimeout:     60 * time.Second,
		MaxConnsPerHost:     2000,
	}
	httpClient := &http.Client{
		Transport: tr,
	}
	var subsClient gqlsubs.Client = gqlsubs.New(logger, statter, httpClient, conf.gqlSubsURL)

	handlerShutdownDone := make(chan interface{}, 1)
	handler, handlerShutdown := handlers.New(logger, statter, queueClient, subsClient, handlerShutdownDone)

	srv, shutdown := server.New(fmt.Sprintf(":%d", conf.servingPort), handler, logger.NewStdLogger())

	go func() {
		logger.Info("Server started", zap.Int64("port", conf.servingPort))

		if err := srv.ListenAndServe(); err != http.ErrServerClosed {
			logger.Error("Encountered an http server error.", zap.Error(err))
		}
	}()

	// shutdown receives a signal upon SIGINT or SIGTERM os signals.
	<-shutdown
	logger.Info("Beginning shutdown.")
	shutdownStart := time.Now()
	close(handlerShutdown)

	var timedOut bool
	srv.Shutdown(10 * time.Second)
	select {
	case <-handlerShutdownDone:
		logger.Info("Connections closed gracefully.")
		timedOut = false
	case <-time.After(shutdownTimeout):
		logger.Info("Timed out while waiting for connections to close.")
		timedOut = true
	}

	// We already waited up to shutdownTimeout seconds. Wait only shutdownTimeout/2 seconds for remaining shutdown.
	closeAll(shutdownTimeout/2, logger, statter)

	logger.Info("Shutdown complete.",
		zap.Duration("shutdownSeconds", time.Since(shutdownStart)),
		zap.Bool("timedOut", timedOut))
}

// Creates a large heap allocation - See https://git-aws.internal.justin.tv/edge/visage/pull/3538
// Ballast is not used during local development.
func makeBallast() []byte {
	var n int
	if !environment.IsLocal() {
		n = 10 << 30 // 10 GB
	}
	return make([]byte, n)
}

type closer interface {
	Close() error
}

// closeAll attempts to Close all closers, waiting up to the specified
// timeout before stopping.
func closeAll(timeout time.Duration, logger logs.Logger, closers ...closer) {
	start := time.Now()

	n := len(closers)
	errC := make(chan error, n)
	for _, c := range closers {
		go func(c closer) {
			errC <- c.Close()
		}(c)
	}

	quit := time.After(timeout)
	for i := 0; i < n; i++ {
		select {
		case err := <-errC:
			if err != nil {
				logger.Error("Unable to close", zap.Error(err))
			}
		case <-quit:
			logger.Warn("Shutdown timeout.", zap.Duration("elapsed", time.Since(start)))
			close(errC)
		}
	}
}
