// Copyright 2019 Drone IO, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package main

import (
	"context"
	"flag"
	"fmt"

	"github.com/drone/drone/cmd/drone-server/bootstrap"
	"github.com/drone/drone/cmd/drone-server/config"
	"github.com/drone/drone/core"
	"github.com/drone/drone/metric/sink"
	"github.com/drone/drone/operator/runner"
	"github.com/drone/drone/service/canceler/reaper"
	"github.com/drone/drone/server"
	"github.com/drone/drone/trigger/cron"
	"github.com/drone/signal"

	"github.com/joho/godotenv"
	"github.com/sirupsen/logrus"
	"golang.org/x/sync/errgroup"

	_ "github.com/go-sql-driver/mysql"
	_ "github.com/lib/pq"
	_ "github.com/mattn/go-sqlite3"
)

func main() {
	var envfile string
	flag.StringVar(&envfile, "env-file", ".env", "Read in a file of environment variables")
	flag.Parse()

	godotenv.Load(envfile)
	config, err := config.Environ()
	if err != nil {
		logger := logrus.WithError(err)
		logger.Fatalln("main: invalid configuration")
	}

	initLogging(config)
	ctx := signal.WithContext(
		context.Background(),
	)

	// if trace level logging is enabled, output the
	// configuration parameters.
	if logrus.IsLevelEnabled(logrus.TraceLevel) {
		fmt.Println(config.String())
	}

	app, err := InitializeApplication(config)
	if err != nil {
		logger := logrus.WithError(err)
		logger.Fatalln("main: cannot initialize server")
	}

	// optionally bootstrap the system with administrative or
	// machine users configured in the environment.
	err = bootstrap.New(app.users).Bootstrap(ctx, &core.User{
		Login:   config.Users.Create.Username,
		Machine: config.Users.Create.Machine,
		Admin:   config.Users.Create.Admin,
		Hash:    config.Users.Create.Token,
	})
	if err != nil {
		logger := logrus.WithError(err)
		logger.Fatalln("cannot bootstrap user account")
	}

	g := errgroup.Group{}
	g.Go(func() error {
		logrus.WithFields(
			logrus.Fields{
				"proto": config.Server.Proto,
				"host":  config.Server.Host,
				"port":  config.Server.Port,
				"url":   config.Server.Addr,
				"acme":  config.Server.Acme,
			},
		).Infoln("starting the http server")
		return app.server.ListenAndServe(ctx)
	})

	// launches the datadog sink in a goroutine. If the sink
	// is disabled, the goroutine exits immediately without error.
	g.Go(func() (err error) {
		if !config.Datadog.Enabled {
			return nil
		}
		return app.sink.Start(ctx)
	})

	// launches the cron runner in a goroutine. If the cron
	// runner is disabled, the goroutine exits immediately
	// without error.
	g.Go(func() (err error) {
		if config.Cron.Disabled {
			return nil
		}
		logrus.WithField("interval", config.Cron.Interval.String()).
			Infoln("starting the cron scheduler")
		return app.cron.Start(ctx, config.Cron.Interval)
	})

	// launches the reaper in a goroutine. If the reaper
	// is disabled, the goroutine exits immediately
	// without error.
	g.Go(func() (err error) {
		if config.Cleanup.Disabled {
			return nil
		}
		logrus.WithField("interval", config.Cleanup.Interval.String()).
			Infoln("starting the zombie build reaper")
		return app.reaper.Start(ctx, config.Cleanup.Interval)
	})

	// launches the build runner in a goroutine. If the local
	// runner is disabled (because nomad or kubernetes is enabled)
	// then the goroutine exits immediately without error.
	g.Go(func() (err error) {
		if app.runner == nil {
			return nil
		}
		logrus.WithField("threads", config.Runner.Capacity).
			Infoln("main: starting the local build runner")
		return app.runner.Start(ctx, config.Runner.Capacity)
	})

	if err := g.Wait(); err != nil {
		logrus.WithError(err).Fatalln("program terminated")
	}
}

// helper function configures the logging.
func initLogging(c config.Config) {
	if c.Logging.Debug {
		logrus.SetLevel(logrus.DebugLevel)
	}
	if c.Logging.Trace {
		logrus.SetLevel(logrus.TraceLevel)
	}
	if c.Logging.Text {
		logrus.SetFormatter(&logrus.TextFormatter{
			ForceColors:   c.Logging.Color,
			DisableColors: !c.Logging.Color,
		})
	} else {
		logrus.SetFormatter(&logrus.JSONFormatter{
			PrettyPrint: c.Logging.Pretty,
		})
	}
}

// application is the main struct for the Drone server.
type application struct {
	cron   *cron.Scheduler
	reaper *reaper.Reaper
	sink   *sink.Datadog
	runner *runner.Runner
	server *server.Server
	users  core.UserStore
}

// newApplication creates a new application struct.
func newApplication(
	cron *cron.Scheduler,
	reaper *reaper.Reaper,
	sink *sink.Datadog,
	runner *runner.Runner,
	server *server.Server,
	users core.UserStore) application {
	return application{
		users:  users,
		cron:   cron,
		sink:   sink,
		server: server,
		runner: runner,
		reaper: reaper,
	}
}
