// Copyright (C) 2019-2022  Nicola Murino
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published
// by the Free Software Foundation, version 3.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program.  If not, see <https://www.gnu.org/licenses/>.

// Package telemetry provides telemetry information for SFTPGo, such as:
//   - health information (for health checks)
//   - metrics
//   - profiling information
package telemetry

import (
	"crypto/tls"
	"log"
	"net/http"
	"path/filepath"
	"runtime"
	"time"

	"github.com/go-chi/chi/v5"

	"github.com/drakkan/sftpgo/v2/internal/common"
	"github.com/drakkan/sftpgo/v2/internal/logger"
	"github.com/drakkan/sftpgo/v2/internal/util"
)

const (
	logSender     = "telemetry"
	metricsPath   = "/metrics"
	pprofBasePath = "/debug"
)

var (
	router   *chi.Mux
	httpAuth common.HTTPAuthProvider
	certMgr  *common.CertManager
)

// Conf telemetry server configuration.
type Conf struct {
	// The port used for serving HTTP requests. 0 disable the HTTP server. Default: 0
	BindPort int `json:"bind_port" mapstructure:"bind_port"`
	// The address to listen on. A blank value means listen on all available network interfaces. Default: "127.0.0.1"
	BindAddress string `json:"bind_address" mapstructure:"bind_address"`
	// Enable the built-in profiler.
	// The profiler will be accessible via HTTP/HTTPS using the base URL "/debug/pprof/"
	EnableProfiler bool `json:"enable_profiler" mapstructure:"enable_profiler"`
	// Path to a file used to store usernames and password for basic authentication.
	// This can be an absolute path or a path relative to the config dir.
	// We support HTTP basic authentication and the file format must conform to the one generated using the Apache
	// htpasswd tool. The supported password formats are bcrypt ($2y$ prefix) and md5 crypt ($apr1$ prefix).
	// If empty HTTP authentication is disabled
	AuthUserFile string `json:"auth_user_file" mapstructure:"auth_user_file"`
	// If files containing a certificate and matching private key for the server are provided the server will expect
	// HTTPS connections.
	// Certificate and key files can be reloaded on demand sending a "SIGHUP" signal on Unix based systems and a
	// "paramchange" request to the running service on Windows.
	CertificateFile    string `json:"certificate_file" mapstructure:"certificate_file"`
	CertificateKeyFile string `json:"certificate_key_file" mapstructure:"certificate_key_file"`
	// TLSCipherSuites is a list of supported cipher suites for TLS version 1.2.
	// If CipherSuites is nil/empty, a default list of secure cipher suites
	// is used, with a preference order based on hardware performance.
	// Note that TLS 1.3 ciphersuites are not configurable.
	// The supported ciphersuites names are defined here:
	//
	// https://github.com/golang/go/blob/master/src/crypto/tls/cipher_suites.go#L52
	//
	// any invalid name will be silently ignored.
	// The order matters, the ciphers listed first will be the preferred ones.
	TLSCipherSuites []string `json:"tls_cipher_suites" mapstructure:"tls_cipher_suites"`
	// Defines the minimum TLS version. 13 means TLS 1.3, default is TLS 1.2
	MinTLSVersion int `json:"min_tls_version" mapstructure:"min_tls_version"`
}

// ShouldBind returns true if there service must be started
func (c Conf) ShouldBind() bool {
	if c.BindPort > 0 {
		return true
	}
	if filepath.IsAbs(c.BindAddress) && runtime.GOOS != "windows" {
		return true
	}
	return false
}

// Initialize configures and starts the telemetry server.
func (c Conf) Initialize(configDir string) error {
	var err error
	logger.Info(logSender, "", "initializing telemetry server with config %+v", c)
	authUserFile := getConfigPath(c.AuthUserFile, configDir)
	httpAuth, err = common.NewBasicAuthProvider(authUserFile)
	if err != nil {
		return err
	}
	certificateFile := getConfigPath(c.CertificateFile, configDir)
	certificateKeyFile := getConfigPath(c.CertificateKeyFile, configDir)
	initializeRouter(c.EnableProfiler)
	httpServer := &http.Server{
		Handler:           router,
		ReadHeaderTimeout: 30 * time.Second,
		ReadTimeout:       60 * time.Second,
		WriteTimeout:      60 * time.Second,
		IdleTimeout:       60 * time.Second,
		MaxHeaderBytes:    1 << 14, // 16KB
		ErrorLog:          log.New(&logger.StdLoggerWrapper{Sender: logSender}, "", 0),
	}
	if certificateFile != "" && certificateKeyFile != "" {
		keyPairs := []common.TLSKeyPair{
			{
				Cert: certificateFile,
				Key:  certificateKeyFile,
				ID:   common.DefaultTLSKeyPaidID,
			},
		}
		certMgr, err = common.NewCertManager(keyPairs, configDir, logSender)
		if err != nil {
			return err
		}
		config := &tls.Config{
			GetCertificate:           certMgr.GetCertificateFunc(common.DefaultTLSKeyPaidID),
			MinVersion:               util.GetTLSVersion(c.MinTLSVersion),
			NextProtos:               []string{"http/1.1", "h2"},
			CipherSuites:             util.GetTLSCiphersFromNames(c.TLSCipherSuites),
			PreferServerCipherSuites: true,
		}
		logger.Debug(logSender, "", "configured TLS cipher suites: %v", config.CipherSuites)
		httpServer.TLSConfig = config
		return util.HTTPListenAndServe(httpServer, c.BindAddress, c.BindPort, true, logSender)
	}
	return util.HTTPListenAndServe(httpServer, c.BindAddress, c.BindPort, false, logSender)
}

// ReloadCertificateMgr reloads the certificate manager
func ReloadCertificateMgr() error {
	if certMgr != nil {
		return certMgr.Reload()
	}
	return nil
}

func getConfigPath(name, configDir string) string {
	if !util.IsFileInputValid(name) {
		return ""
	}
	if name != "" && !filepath.IsAbs(name) {
		return filepath.Join(configDir, name)
	}
	return name
}
