// Copyright 2018 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"
	"crypto/tls"
	"errors"
	"fmt"
	"net/http"
	"net/http/httputil"
	"net/url"
	"strings"
	"time"

	"github.com/cenkalti/backoff/v5"
	"github.com/gin-gonic/gin"
	"github.com/prometheus/client_golang/prometheus/promhttp"
	"github.com/rs/zerolog"
	"github.com/rs/zerolog/log"
	"github.com/urfave/cli/v3"
	"golang.org/x/sync/errgroup"

	"go.woodpecker-ci.org/woodpecker/v3/server"
	cron_scheduler "go.woodpecker-ci.org/woodpecker/v3/server/cron"
	"go.woodpecker-ci.org/woodpecker/v3/server/router"
	"go.woodpecker-ci.org/woodpecker/v3/server/router/middleware"
	"go.woodpecker-ci.org/woodpecker/v3/server/store"
	"go.woodpecker-ci.org/woodpecker/v3/server/web"
	"go.woodpecker-ci.org/woodpecker/v3/shared/logger"
	"go.woodpecker-ci.org/woodpecker/v3/version"
)

const (
	shutdownTimeout = time.Second * 5
)

var (
	stopServerFunc     context.CancelCauseFunc = func(error) {}
	shutdownCancelFunc context.CancelFunc      = func() {}
	shutdownCtx                                = context.Background()
)

func run(ctx context.Context, c *cli.Command) error {
	if err := logger.SetupGlobalLogger(ctx, c, true); err != nil {
		return err
	}

	ctx, ctxCancel := context.WithCancelCause(ctx)
	stopServerFunc = func(err error) {
		if err != nil {
			log.Error().Err(err).Msg("shutdown of whole server")
		}
		stopServerFunc = func(error) {}
		shutdownCtx, shutdownCancelFunc = context.WithTimeout(shutdownCtx, shutdownTimeout)
		ctxCancel(err)
	}
	defer stopServerFunc(nil)
	defer shutdownCancelFunc()

	// set gin mode based on log level
	if zerolog.GlobalLevel() > zerolog.DebugLevel {
		gin.SetMode(gin.ReleaseMode)
	}

	if c.String("server-host") == "" {
		return fmt.Errorf("WOODPECKER_HOST is not properly configured")
	}

	if !strings.Contains(c.String("server-host"), "://") {
		return fmt.Errorf("WOODPECKER_HOST must be <scheme>://<hostname> format")
	}

	if _, err := url.Parse(c.String("server-host")); err != nil {
		return fmt.Errorf("could not parse WOODPECKER_HOST: %w", err)
	}

	if strings.Contains(c.String("server-host"), "://localhost") {
		log.Warn().Msg(
			"WOODPECKER_HOST should probably be publicly accessible (not localhost)",
		)
	}

	_store, err := backoff.Retry(ctx,
		func() (store.Store, error) {
			return setupStore(ctx, c)
		},
		backoff.WithBackOff(backoff.NewExponentialBackOff()),
		backoff.WithMaxTries(c.Uint("db-max-retries")),
		backoff.WithNotify(func(err error, delay time.Duration) {
			log.Error().Msgf("failed to setup store: %v: retry in %v", err, delay)
		}))
	if err != nil {
		return err
	}

	defer func() {
		if err := _store.Close(); err != nil {
			log.Error().Err(err).Msg("could not close store")
		}
	}()

	err = setupEvilGlobals(ctx, c, _store)
	if err != nil {
		return fmt.Errorf("can't setup globals: %w", err)
	}

	// wait for all services until one do stops with an error
	serviceWaitingGroup := errgroup.Group{}

	log.Info().Msgf("starting Woodpecker server with version '%s'", version.String())

	startMetricsCollector(ctx, _store)

	serviceWaitingGroup.Go(func() error {
		log.Info().Msg("starting cron service ...")
		if err := cron_scheduler.Run(ctx, _store); err != nil {
			go stopServerFunc(err)
			return err
		}
		log.Info().Msg("cron service stopped")
		return nil
	})

	// start the grpc server
	serviceWaitingGroup.Go(func() error {
		log.Info().Msg("starting grpc server ...")
		if err := runGrpcServer(ctx, c, _store); err != nil {
			// stop whole server as grpc is essential
			go stopServerFunc(err)
			return err
		}
		return nil
	})

	proxyWebUI := c.String("www-proxy")
	var webUIServe func(w http.ResponseWriter, r *http.Request)

	if proxyWebUI == "" {
		webEngine, err := web.New()
		if err != nil {
			log.Error().Err(err).Msg("failed to create web engine")
			return err
		}
		webUIServe = webEngine.ServeHTTP
	} else {
		origin, _ := url.Parse(proxyWebUI)

		director := func(req *http.Request) {
			req.Header.Add("X-Forwarded-Host", req.Host)
			req.Header.Add("X-Origin-Host", origin.Host)
			req.URL.Scheme = origin.Scheme
			req.URL.Host = origin.Host
		}

		proxy := &httputil.ReverseProxy{Director: director}
		webUIServe = proxy.ServeHTTP
	}

	// setup the server and start the listener
	handler := router.Load(
		webUIServe,
		middleware.Logger(time.RFC3339, true),
		middleware.Version,
		middleware.Store(_store),
	)

	if c.String("server-cert") != "" {
		// start the server with tls enabled
		serviceWaitingGroup.Go(func() error {
			tlsServer := &http.Server{
				Addr:    server.Config.Server.PortTLS,
				Handler: handler,
				TLSConfig: &tls.Config{
					NextProtos: []string{"h2", "http/1.1"},
				},
			}

			go func() {
				<-ctx.Done()
				log.Info().Msg("shutdown tls server ...")
				if err := tlsServer.Shutdown(shutdownCtx); err != nil { //nolint:contextcheck
					log.Error().Err(err).Msg("shutdown tls server failed")
				} else {
					log.Info().Msg("tls server stopped")
				}
			}()

			log.Info().Msg("starting tls server ...")
			err := tlsServer.ListenAndServeTLS(
				c.String("server-cert"),
				c.String("server-key"),
			)
			if err != nil && !errors.Is(err, http.ErrServerClosed) {
				log.Error().Err(err).Msg("TLS server failed")
				stopServerFunc(fmt.Errorf("TLS server failed: %w", err))
			}
			return err
		})

		// http to https redirect
		redirect := func(w http.ResponseWriter, req *http.Request) {
			serverURL, _ := url.Parse(server.Config.Server.Host)
			req.URL.Scheme = "https"
			req.URL.Host = serverURL.Host

			w.Header().Set("Strict-Transport-Security", "max-age=31536000")

			http.Redirect(w, req, req.URL.String(), http.StatusMovedPermanently)
		}

		serviceWaitingGroup.Go(func() error {
			redirectServer := &http.Server{
				Addr:    server.Config.Server.Port,
				Handler: http.HandlerFunc(redirect),
			}
			go func() {
				<-ctx.Done()
				log.Info().Msg("shutdown redirect server ...")
				if err := redirectServer.Shutdown(shutdownCtx); err != nil { //nolint:contextcheck
					log.Error().Err(err).Msg("shutdown redirect server failed")
				} else {
					log.Info().Msg("redirect server stopped")
				}
			}()

			log.Info().Msg("starting redirect server ...")
			if err := redirectServer.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
				log.Error().Err(err).Msg("redirect server failed")
				stopServerFunc(fmt.Errorf("redirect server failed: %w", err))
			}
			return nil
		})
	} else {
		// start the server without tls
		serviceWaitingGroup.Go(func() error {
			httpServer := &http.Server{
				Addr:    c.String("server-addr"),
				Handler: handler,
			}

			go func() {
				<-ctx.Done()
				log.Info().Msg("shutdown http server ...")
				if err := httpServer.Shutdown(shutdownCtx); err != nil { //nolint:contextcheck
					log.Error().Err(err).Msg("shutdown http server failed")
				} else {
					log.Info().Msg("http server stopped")
				}
			}()

			log.Info().Msg("starting http server ...")
			if err := httpServer.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
				log.Error().Err(err).Msg("http server failed")
				stopServerFunc(fmt.Errorf("http server failed: %w", err))
			}
			return err
		})
	}

	if metricsServerAddr := c.String("metrics-server-addr"); metricsServerAddr != "" {
		serviceWaitingGroup.Go(func() error {
			metricsRouter := gin.New()
			metricsRouter.GET("/metrics", gin.WrapH(promhttp.Handler()))

			metricsServer := &http.Server{
				Addr:    metricsServerAddr,
				Handler: metricsRouter,
			}

			go func() {
				<-ctx.Done()
				log.Info().Msg("shutdown metrics server ...")
				if err := metricsServer.Shutdown(shutdownCtx); err != nil { //nolint:contextcheck
					log.Error().Err(err).Msg("shutdown metrics server failed")
				} else {
					log.Info().Msg("metrics server stopped")
				}
			}()

			log.Info().Msg("starting metrics server ...")
			if err := metricsServer.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
				log.Error().Err(err).Msg("metrics server failed")
				stopServerFunc(fmt.Errorf("metrics server failed: %w", err))
			}
			return err
		})
	}

	return serviceWaitingGroup.Wait()
}
