package server

import (
	"context"
	"crypto/tls"
	"errors"
	"fmt"
	"net"
	"net/http"
	"os"
	"time"

	"golang.org/x/crypto/acme/autocert"
	"golang.org/x/sync/errgroup"

	"github.com/gin-contrib/gzip"
	"github.com/gin-gonic/gin"

	"github.com/photoprism/photoprism/internal/api"
	"github.com/photoprism/photoprism/internal/config"
	"github.com/photoprism/photoprism/internal/server/process"
	"github.com/photoprism/photoprism/pkg/clean"
	"github.com/photoprism/photoprism/pkg/fs"
	"github.com/photoprism/photoprism/pkg/http/header"
	"github.com/photoprism/photoprism/pkg/txt"
)

// Start the REST API server using the configuration provided
func Start(ctx context.Context, conf *config.Config) {
	defer func() {
		if err := recover(); err != nil {
			log.Error(err)
		}
	}()

	start := time.Now()

	// Log the server process ID for troubleshooting purposes.
	log.Infof("server: started as pid %d", process.ID)

	// Set web server mode.
	if conf.HttpMode() != "" {
		gin.SetMode(conf.HttpMode())
	} else if !conf.Debug() {
		gin.SetMode(gin.ReleaseMode)
	}

	// Create new router engine without standard middleware.
	router := gin.New()

	// Set proxy from which headers related to the client and protocol can be trusted?
	if trustedProxies := conf.TrustedProxies(); len(trustedProxies) > 0 {
		if err := router.SetTrustedProxies(trustedProxies); err != nil {
			log.Warnf("server: %s", err)
		}

		router.RemoteIPHeaders = conf.ProxyClientHeaders()
	}

	// Set trusted platform client IP address header name?
	if trustedPlatform := conf.TrustedPlatform(); trustedPlatform != "" {
		router.TrustedPlatform = trustedPlatform

		// Enable support for HTTP/2 without TLS.
		router.UseH2C = true
	}

	// Register panic recovery middleware.
	router.Use(Recovery())

	// Register logger middleware if debug mode is enabled.
	if conf.Debug() {
		router.Use(Logger())
	}

	// Register compression middleware if enabled in the configuration.
	switch conf.HttpCompression() {
	case "br", "brotli":
		log.Infof("server: brotli compression is currently not supported")
	case "gzip":
		router.Use(gzip.Gzip(
			gzip.DefaultCompression,
			gzip.WithExcludedExtensions([]string{
				".png", ".gif", ".jpeg", ".jpg", ".webp", ".mp3", ".mp4", ".zip", ".gz",
			}),
			gzip.WithExcludedPaths([]string{
				conf.BaseUri("/health"),
				conf.BaseUri(config.ApiUri + "/t"),
				conf.BaseUri(config.ApiUri + "/folders/t"),
				conf.BaseUri(config.ApiUri + "/dl"),
				conf.BaseUri(config.ApiUri + "/zip"),
				conf.BaseUri(config.ApiUri + "/albums"),
				conf.BaseUri(config.ApiUri + "/labels"),
				conf.BaseUri(config.ApiUri + "/videos"),
			}),
		))
		log.Infof("server: enabled gzip compression")
	}

	// Register security middleware.
	router.Use(Security(conf))

	// Create REST API router group.
	APIv1 = router.Group(conf.BaseUri(config.ApiUri), APIMiddleware(conf))

	// Initialize package extensions.
	Ext().Init(router, conf)

	// Find and load templates.
	router.LoadHTMLFiles(conf.TemplateFiles()...)

	// Register application routes.
	registerRoutes(router, conf)

	// Register standard health check endpoints to determine whether the server is running.
	isLive := func(c *gin.Context) {
		c.Header(header.CacheControl, header.CacheControlNoStore)
		c.Header(header.AccessControlAllowOrigin, header.Any)
		c.JSON(http.StatusOK, api.NewHealthResponse("ok"))
	}
	router.Any(conf.BaseUri("/livez"), isLive)
	router.Any(conf.BaseUri("/health"), isLive)
	router.Any(conf.BaseUri("/healthz"), isLive)

	// Register "/readyz" endpoint to check if the server has been successfully initialized.
	isReady := func(c *gin.Context) {
		c.Header(header.CacheControl, header.CacheControlNoStore)
		c.Header(header.AccessControlAllowOrigin, header.Any)
		if conf.IsReady() {
			c.JSON(http.StatusOK, api.NewHealthResponse("ok"))
		} else {
			c.JSON(http.StatusServiceUnavailable, api.NewHealthResponse("service unavailable"))
		}
	}
	router.Any(conf.BaseUri("/readyz"), isReady)

	// Create a new HTTP server instance with no read or write timeout, except for reading the headers:
	// https://pkg.go.dev/net/http#Server
	server := &http.Server{
		ReadHeaderTimeout: time.Minute,
		ReadTimeout:       -1,
		WriteTimeout:      -1,
		Handler:           router,
	}

	var tlsErr error
	var tlsManager *autocert.Manager

	// Listen on a Unix domain socket instead of a TCP port?
	if unixSocket := conf.HttpSocket(); unixSocket != nil {
		var listener net.Listener
		var unixAddr *net.UnixAddr
		var err error

		// Check if the Unix socket already exists and delete it if the force flag is set.
		if fs.SocketExists(unixSocket.Path) {
			if !txt.Bool(unixSocket.Query().Get("force")) {
				Fail("server: %s socket %s already exists", clean.Log(unixSocket.Scheme), clean.Log(unixSocket.Path))
				return
			} else if removeErr := os.Remove(unixSocket.Path); removeErr != nil {
				Fail("server: %s socket %s already exists and cannot be deleted", clean.Log(unixSocket.Scheme), clean.Log(unixSocket.Path))
				return
			}
		}

		// Create a Unix socket and listen on it.
		if unixAddr, err = net.ResolveUnixAddr(unixSocket.Scheme, unixSocket.Path); err != nil {
			Fail("server: invalid %s socket (%s)", clean.Log(unixSocket.Scheme), err)
			return
		} else if listener, err = net.ListenUnix(unixSocket.Scheme, unixAddr); err != nil {
			Fail("server: failed to listen on %s socket (%s)", clean.Log(unixSocket.Scheme), err)
			return
		} else {
			// Update socket permissions?
			if mode := unixSocket.Query().Get("mode"); mode == "" {
				// Skip, no socket mode was specified.
			} else if modeErr := os.Chmod(unixSocket.Path, fs.ParseMode(mode, fs.ModeSocket)); modeErr != nil {
				log.Warnf(
					"server: failed to change permissions of %s socket %s (%s)",
					clean.Log(unixSocket.Scheme),
					clean.Log(unixSocket.Path),
					modeErr,
				)
			}

			// Listen on Unix socket, which should be automatically closed and removed after use:
			// https://pkg.go.dev/net#UnixListener.SetUnlinkOnClose.
			server.Addr = listener.Addr().String()

			log.Infof("server: listening on %s [%s]", unixSocket.Path, time.Since(start))

			// Start Web server.
			go StartHttp(server, listener)
		}
	} else if tlsManager, tlsErr = AutoTLS(conf); tlsErr == nil {
		log.Infof("server: starting in auto tls mode")

		tlsSocket := fmt.Sprintf("%s:%d", conf.HttpHost(), conf.HttpPort())
		tlsConfig := tlsManager.TLSConfig()
		tlsConfig.MinVersion = tls.VersionTLS12

		// Listen on HTTPS socket.
		server.Addr = tlsSocket
		server.TLSConfig = tlsConfig

		log.Infof("server: listening on %s [%s]", server.Addr, time.Since(start))

		// Start Web server.
		go StartAutoTLS(server, tlsManager, conf)
	} else if publicCert, privateKey := conf.TLS(); publicCert != "" && privateKey != "" {
		log.Infof("server: starting in tls mode")

		tlsSocket := fmt.Sprintf("%s:%d", conf.HttpHost(), conf.HttpPort())
		tlsConfig := &tls.Config{
			MinVersion: tls.VersionTLS12,
		}

		// Listen on HTTPS socket.
		server.Addr = tlsSocket
		server.TLSConfig = tlsConfig

		log.Infof("server: listening on %s [%s]", server.Addr, time.Since(start))

		// Start Web server.
		go StartTLS(server, publicCert, privateKey)
	} else {
		log.Infof("server: %s", tlsErr)

		tcpSocket := fmt.Sprintf("%s:%d", conf.HttpHost(), conf.HttpPort())

		if listener, err := net.Listen("tcp", tcpSocket); err != nil {
			Fail("server: %s", err)
			return
		} else {
			// Listen on HTTP socket.
			server.Addr = tcpSocket

			log.Infof("server: listening on %s [%s]", server.Addr, time.Since(start))

			// Start Web server.
			go StartHttp(server, listener)
		}
	}

	// Graceful web server shutdown.
	<-ctx.Done()
	log.Info("server: shutting down")
	err := server.Close()
	if err != nil {
		log.Errorf("server: shutdown failed (%s)", err)
	}
}

// StartHttp starts the Web server in http mode.
func StartHttp(s *http.Server, l net.Listener) {
	if err := s.Serve(l); err != nil {
		if errors.Is(err, http.ErrServerClosed) {
			log.Info("server: shutdown complete")
		} else {
			log.Errorf("server: %s", err)
		}
	}
}

// StartTLS starts the Web server in https mode.
func StartTLS(s *http.Server, httpsCert, privateKey string) {
	if err := s.ListenAndServeTLS(httpsCert, privateKey); err != nil {
		if errors.Is(err, http.ErrServerClosed) {
			log.Info("server: shutdown complete")
		} else {
			log.Errorf("server: %s", err)
		}
	}
}

// StartAutoTLS starts the Web server with auto tls enabled.
func StartAutoTLS(s *http.Server, m *autocert.Manager, conf *config.Config) {
	var g errgroup.Group

	g.Go(func() error {
		redirectSrv := &http.Server{
			Addr:              fmt.Sprintf("%s:%d", conf.HttpHost(), conf.HttpPort()),
			Handler:           m.HTTPHandler(http.HandlerFunc(redirect)),
			ReadHeaderTimeout: time.Minute,
			ReadTimeout:       5 * time.Second,
			WriteTimeout:      10 * time.Second,
		}

		return redirectSrv.ListenAndServe()
	})

	g.Go(func() error {
		return s.ListenAndServeTLS("", "")
	})

	if err := g.Wait(); err != nil {
		if errors.Is(err, http.ErrServerClosed) {
			log.Info("server: shutdown complete")
		} else {
			log.Errorf("server: %s", err)
		}
	}
}

func redirect(w http.ResponseWriter, req *http.Request) {
	target := "https://" + req.Host + req.RequestURI

	http.Redirect(w, req, target, httpsRedirect)
}
