// Copyright 2023 Harness, 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 server

import (
	"context"
	"fmt"
	"os"
	"os/signal"
	"syscall"
	"time"

	"github.com/harness/gitness/app/pipeline/logger"
	"github.com/harness/gitness/profiler"
	"github.com/harness/gitness/types"
	"github.com/harness/gitness/version"

	"github.com/joho/godotenv"
	"github.com/mattn/go-isatty"
	"github.com/rs/zerolog"
	"github.com/rs/zerolog/log"
	"golang.org/x/sync/errgroup"
	"gopkg.in/alecthomas/kingpin.v2"
)

type command struct {
	envfile     string
	enableCI    bool
	initializer func(context.Context, *types.Config) (*System, error)
}

func (c *command) run(*kingpin.ParseContext) error {
	// Create context that listens for the interrupt signal from the OS.
	ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
	defer stop()

	// load environment variables from file.
	// no error handling needed when file is not present
	_ = godotenv.Load(c.envfile)

	// create the system configuration store by loading
	// data from the environment.
	config, err := LoadConfig()
	if err != nil {
		return fmt.Errorf("encountered an error while loading configuration: %w", err)
	}

	// configure the log level
	SetupLogger(config)

	// configure profiler
	SetupProfiler(config)

	// add logger to context
	log := log.Logger.With().Logger()
	ctx = log.WithContext(ctx)

	// initialize system
	system, err := c.initializer(ctx, config)
	if err != nil {
		return fmt.Errorf("encountered an error while wiring the system: %w", err)
	}

	// bootstrap the system
	err = system.bootstrap(ctx)
	if err != nil {
		return fmt.Errorf("encountered an error while bootstrapping the system: %w", err)
	}

	// gCtx is canceled if any of the following occurs:
	// - any go routine launched with g encounters an error
	// - ctx is canceled
	g, gCtx := errgroup.WithContext(ctx)

	g.Go(func() error {
		// initialize metric collector
		if system.services.MetricCollector != nil {
			if err := system.services.MetricCollector.Register(gCtx); err != nil {
				log.Error().Err(err).Msg("failed to register metric collector")
				return err
			}
		}

		if system.services.RepoSizeCalculator != nil {
			if err := system.services.RepoSizeCalculator.Register(gCtx); err != nil {
				log.Error().Err(err).Msg("failed to register repo size calculator")
				return err
			}
		}

		if err := system.services.Cleanup.Register(gCtx); err != nil {
			log.Error().Err(err).Msg("failed to register cleanup service")
			return err
		}

		return system.services.JobScheduler.Run(gCtx)
	})

	// start server
	gHTTP, shutdownHTTP := system.server.ListenAndServe()
	g.Go(gHTTP.Wait)
	if c.enableCI {
		// start populating plugins
		g.Go(func() error {
			err := system.resolverManager.Populate(ctx)
			if err != nil {
				log.Error().Err(err).Msg("could not populate plugins")
			}
			return nil
		})
		// start poller for CI build executions.
		g.Go(func() error {
			system.poller.Poll(
				logger.WithWrappedZerolog(ctx),
				config.CI.ParallelWorkers,
			)
			return nil
		})
	}

	if config.SSH.Enable {
		g.Go(func() error {
			log.Err(system.sshServer.ListenAndServe()).Send()
			return nil
		})
	}

	log.Info().
		Str("host", config.HTTP.Host).
		Int("port", config.HTTP.Port).
		Str("revision", version.GitCommit).
		Str("repository", version.GitRepository).
		Stringer("version", version.Version).
		Msg("server started")

	// wait until the error group context is done
	<-gCtx.Done()

	// restore default behavior on the interrupt signal and notify user of shutdown.
	stop()
	log.Info().Msg("shutting down gracefully (press Ctrl+C again to force)")

	// shutdown servers gracefully
	shutdownCtx, cancel := context.WithTimeout(context.Background(), config.GracefulShutdownTime)
	defer cancel()

	if sErr := shutdownHTTP(shutdownCtx); sErr != nil {
		log.Err(sErr).Msg("failed to shutdown http server gracefully")
	}

	if config.SSH.Enable {
		if err := system.sshServer.Shutdown(shutdownCtx); err != nil {
			log.Err(err).Msg("failed to shutdown ssh server gracefully")
		}
	}

	// shutdown instrumentation
	err = system.services.Instrumentation.Close(shutdownCtx)
	if err != nil {
		log.Err(err).Msg("failed to close instrumentation gracefully")
	}
	// shutdown job scheduler
	system.services.JobScheduler.WaitJobsDone(shutdownCtx)

	log.Info().Msg("wait for subroutines to complete")
	err = g.Wait()

	return err
}

// SetupLogger configures the global logger from the loaded configuration.
func SetupLogger(config *types.Config) {
	// configure the log level
	switch {
	case config.Trace:
		zerolog.SetGlobalLevel(zerolog.TraceLevel)
	case config.Debug:
		zerolog.SetGlobalLevel(zerolog.DebugLevel)
	default:
		zerolog.SetGlobalLevel(zerolog.InfoLevel)
	}

	// configure time format (ignored if running in terminal)
	zerolog.TimeFieldFormat = time.RFC3339Nano

	// if the terminal is a tty we should output the
	// logs in pretty format
	if isatty.IsTerminal(os.Stdout.Fd()) {
		log.Logger = log.Output(
			zerolog.ConsoleWriter{
				Out:        os.Stderr,
				NoColor:    false,
				TimeFormat: "15:04:05.999",
			},
		)
	}
}

func SetupProfiler(config *types.Config) {
	profilerType, parsed := profiler.ParseType(config.Profiler.Type)
	if !parsed {
		log.Info().Msgf("No valid profiler so skipping profiling ['%s']", config.Profiler.Type)
		return
	}

	gitnessProfiler, _ := profiler.New(profilerType)
	gitnessProfiler.StartProfiling(config.Profiler.ServiceName, version.Version.String())
}

// Register the server command.
func Register(app *kingpin.Application, initializer func(context.Context, *types.Config) (*System, error)) {
	c := new(command)
	c.initializer = initializer

	cmd := app.Command("server", "starts the server").
		Action(c.run)

	cmd.Arg("envfile", "load the environment variable file").
		Default("").
		StringVar(&c.envfile)

	cmd.Flag("enable-ci", "start ci runners for build executions").
		Default("true").
		Envar("ENABLE_CI").
		BoolVar(&c.enableCI)
}
