package main

import (
	"context"
	"fmt"
	"io/ioutil"
	"log"
	"net/http"
	"os"
	"os/signal"
	"strconv"
	"strings"
	"syscall"
	"time"

	"code.justin.tv/live/plucky/api/assets"
	"code.justin.tv/live/plucky/api/rpc"
	"code.justin.tv/live/plucky/api/server"
	"code.justin.tv/live/plucky/api/util"
	"github.com/urfave/cli/v2"

	"github.com/rs/cors"
)

// Version is a human readable string that tags a release build.
// This is meant to be set by linker flags at build time.
var Version = "1.dev"

// GitCommit is the revision of the source code used to create this build.
// This is meant to be set by linker flags at build time.
var GitCommit = "000000"

const (
	portFlag     = "port"
	logLevelFlag = "log-level"
)

const (
	logLevelFlagDebug = "debug"
	logLevelFlagInfo  = "info"
	logLevelFlagError = "error"
)

const (
	defaultPort     = 8080
	defaultLogLevel = logLevelFlagInfo
)

const description = `plucky is a Rollbar-like app that reads errors from CloudWatch Logs.

Example 1 - View StreamSchedule production errors

	mwinit --aea
	ada credentials update --account 524336105177 --role LowRiskReadOnly --once
	plucky --service streamschedule

	# View the app in a browser at http://localhost:8080/streamschedule/prod

Example 2 - View Nexus production errors

	mwinit --aea
	ada credentials update --account 981671725203 --role LowRiskReadOnly --once
	plucky --service nexus

	# View the app in a browser at http://localhost:8080/nexus/prod

Example 3 - View Sheflie's production errors

	mwinit --aea
	ada credentials update --account 039370619915 --role LowRiskReadyOnly --once
	plucky --service shelfie

	# View the app in a browser at http://localhost:8080/shelfie/prod
`

func main() {
	err := run(os.Args, runWithConfig)
	if err != nil {
		log.Fatal(err)
	}
}

func run(args []string, handler func(conf *config) error) error {
	app := &cli.App{
		Usage:       "starts a server that aggregates VXP service errors",
		UsageText:   "plucky --service streamschedule|nexus|raids|shelfie [--stage prod|beta] [--port PORT] [--log-level debug|info|error]",
		Description: description,
		Flags: []cli.Flag{
			&cli.IntFlag{
				Name:        portFlag,
				DefaultText: strconv.Itoa(defaultPort),
			},
			&cli.StringFlag{
				Name:        logLevelFlag,
				Usage:       strings.Join([]string{logLevelFlagError, logLevelFlagInfo, logLevelFlagDebug}, ", "),
				DefaultText: defaultLogLevel,
			},
		},
		Action: func(c *cli.Context) error {
			config, err := parseConfig(c)
			if err != nil {
				return err
			}

			return handler(config)

		},
		Version: fmt.Sprintf("%s (%s)", Version, GitCommit),
	}

	return app.Run(args)
}

type config struct {
	logLevel util.LogLevel
	port     int
}

func parseConfig(c *cli.Context) (*config, error) {
	port := c.Int(portFlag)
	if port == 0 {
		port = defaultPort
	}

	level, err := getValidStringOrDefault(c, logLevelFlag, defaultLogLevel, []string{logLevelFlagError, logLevelFlagInfo, logLevelFlagDebug})
	if err != nil {
		return nil, err
	}

	var logLevel util.LogLevel
	switch level {
	case logLevelFlagDebug:
		logLevel = util.LogLevelDebug
	case logLevelFlagInfo:
		logLevel = util.LogLevelInfo
	default:
		logLevel = util.LogLevelError
	}

	return &config{
		logLevel: logLevel,
		port:     port,
	}, nil
}

func getValidString(c *cli.Context, flagName string, validValues []string) (string, error) {
	value := c.String(flagName)

	for _, validValue := range validValues {
		if value == validValue {
			return value, nil
		}
	}
	return "", fmt.Errorf(
		"\"%s\" is not a valid value for the %s flag. valid values: %s",
		value, flagName, strings.Join(validValues, ", "))
}

func getValidStringOrDefault(c *cli.Context, flagName, defaultValue string, validValues []string) (string, error) {
	value := c.String(flagName)
	if value == "" {
		return defaultValue, nil
	}

	return getValidString(c, flagName, validValues)
}

func getClientURL(port int, service *server.Service) string {
	return fmt.Sprintf("http://localhost:%d/%s/items", port, service.URLSlug)
}

func runWithConfig(conf *config) error {
	address := fmt.Sprintf(":%d", conf.port)
	httpServer := &http.Server{
		Addr: address,
	}

	twirpHandlers, err := server.NewServer(&server.ServerConfig{
		Level:     conf.logLevel,
		Version:   Version,
		GitCommit: GitCommit,
		Services: []*server.Service{
			server.ServiceNexusProd,
			server.ServiceRaidsProd,
			server.ServiceStreamScheduleProd,
		},
	})
	if err != nil {
		return err
	}
	twirpServer := rpc.NewPluckyServer(twirpHandlers, nil)

	// Add middleware around the twirp server to add CORS headers to responses.
	corsHandler := cors.Default().Handler(twirpServer)

	mux := http.NewServeMux()
	mux.Handle("/app/", http.StripPrefix("/app/", http.FileServer(assets.Assets)))
	mux.Handle(twirpServer.PathPrefix(), corsHandler)
	mux.HandleFunc("/", func(writer http.ResponseWriter, request *http.Request) {
		file, err := assets.Assets.Open("/index.html")
		if err != nil {
			log.Println(err)
			writer.WriteHeader(500)
			return
		}
		defer file.Close()

		buff, err := ioutil.ReadAll(file)
		if err != nil {
			log.Println(err)
			writer.WriteHeader(500)
			return
		}

		writer.Write(buff)
	})
	httpServer.Handler = mux

	go func() {
		sigChan := make(chan os.Signal, 1)
		signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM, syscall.SIGUSR2)
		<-sigChan

		ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
		defer cancel()

		err := httpServer.Shutdown(ctx)
		if err != nil {
			log.Println(err)
		}
	}()

	log.Printf("serving at \"%s\"", address)

	err = httpServer.ListenAndServe()
	if err != nil && err != http.ErrServerClosed {
		return err
	}

	log.Println("exiting")
	return nil
}
