// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package setting

import (
	"fmt"
	"net/url"
	"os"
	"path/filepath"
	"strings"

	"forgejo.org/modules/auth/password/hash"
	"forgejo.org/modules/generate"
	"forgejo.org/modules/jwtx"
	"forgejo.org/modules/keying"
	"forgejo.org/modules/log"
)

var (
	// Security settings
	InstallLock                        bool
	SecretKey                          string
	InternalToken                      string // internal access token
	LogInRememberDays                  int
	GlobalTwoFactorRequirement         TwoFactorRequirementType
	CookieRememberName                 string
	ReverseProxyAuthUser               string
	ReverseProxyAuthEmail              string
	ReverseProxyAuthFullName           string
	ReverseProxyLimit                  int
	ReverseProxyTrustedProxies         []string
	MinPasswordLength                  int
	ImportLocalPaths                   bool
	DisableGitHooks                    bool
	DisableWebhooks                    bool
	OnlyAllowPushIfGiteaEnvironmentSet bool
	PasswordComplexity                 []string
	PasswordHashAlgo                   string
	PasswordCheckPwn                   bool
	SuccessfulTokensCacheSize          int
	DisableQueryAuthToken              bool
)

/*
 * key loading is a two-stage process to avoid complications for unit tests:
 *
 * For symmetric keys, we want to add a random key to the configuration. We would
 * not want to change the configuration after loading has completed to maintain
 * isolation. So from this perspective, we would want to initialize keys only
 * during setting.load...From()
 *
 * For asymmetric keys, we want to create a random private key _file_.
 * Doing so during the setting load phase, however, creates key files for unit
 * tests, because they usually complete the full settings load phase, but do not
 * initialize modules which they do not need. Depending on the test case, it
 * might not provide a writable AppDataPath, or it would leave private key files
 * in the source tree. All of this can be avoided with
 * specifically adjusting the ini loaded for unit tests, but adds considerable
 * friction.
 *
 * So to avoid all this, we split key loading in two phases:
 * - settings parse the config and save missing symmetric keys
 * - module init takes the parsed config, creates missing asymmetric keys and
 *   creates the actual signingkey objects
 *
 * jwtx.SigningKeyCfg and jwtx.KeyCfg are used for handover
 */

// loadSecret load the secret from ini by uriKey or verbatimKey, only one of them could be set
// If the secret is loaded from uriKey (file), the file should be non-empty, to guarantee the behavior stable and clear.
func loadSecret(sec ConfigSection, uriKey, verbatimKey string) string {
	// don't allow setting both URI and verbatim string
	uri := sec.Key(uriKey).String()
	verbatim := sec.Key(verbatimKey).String()
	if uri != "" && verbatim != "" {
		log.Fatal("Cannot specify both %s and %s", uriKey, verbatimKey)
	}

	// if we have no URI, use verbatim
	if uri == "" {
		return verbatim
	}
	verbatim, err := loadSecretFromURI(uri)
	if err == nil {
		return verbatim
	}
	log.Fatal("%s: %w", uriKey, err)
	// unreached
	return ""
}

func loadSecretFromURI(uri string) (string, error) {
	tempURI, err := url.Parse(uri)
	if err != nil {
		return "", fmt.Errorf("Failed to parse %s: %v", uri, err)
	}
	switch tempURI.Scheme {
	case "file":
		path := tempURI.RequestURI()
		if !filepath.IsAbs(path) {
			path = filepath.Join(AppDataPath, path)
		}
		buf, err := os.ReadFile(path)
		if err != nil {
			return "", fmt.Errorf("Failed to read %s: %v", path, err)
		}
		val := strings.TrimSpace(string(buf))
		if val == "" {
			// The file shouldn't be empty, otherwise we can not know whether the user has ever set the KEY or KEY_URI
			// For example: if INTERNAL_TOKEN_URI=file:///empty-file,
			// Then if the token is re-generated during installation and saved to INTERNAL_TOKEN
			// Then INTERNAL_TOKEN and INTERNAL_TOKEN_URI both exist, that's a fatal error (they shouldn't)
			return "", fmt.Errorf("Failed to read %s: the file is empty", path)
		}
		return val, nil

	// only file URIs are allowed
	default:
		return "", fmt.Errorf("Unsupported URI-Scheme %q in %q", tempURI.Scheme, uri)
	}
}

// createSymmeticSigningKey creates a new symmetric signing key and saves it to
// the setting named cfgSecret (usually [PFX_]SECRET) in section cfgSection
func createSymmeticSigningKeyCfg(rootCfg ConfigProvider, cfgSection, cfgSecret string) (*[]byte, error) {
	jwtSecretBytes, jwtSecretBase64 := generate.NewJwtSecret()
	saveCfg, err := rootCfg.PrepareSaving()
	if err == nil {
		rootCfg.Section(cfgSection).Key(cfgSecret).SetValue(jwtSecretBase64)
		saveCfg.Section(cfgSection).Key(cfgSecret).SetValue(jwtSecretBase64)
		err = saveCfg.Save()
	}
	if err != nil {
		return nil, fmt.Errorf("save %s.%s failed: %v", cfgSection, cfgSecret, err)
	}
	return &jwtSecretBytes, nil
}

// loadSymmeticSigningKey loads a signing key and creates it unless present
// loads from [pfx]SECRET_URI
// loads from or saves to [pfx]SECRET
// in section sec
func loadSymmeticSigningKeyCfg(rootCfg ConfigProvider, sec ConfigSection, pfx string) (*[]byte, error) {
	cfgSecretURI := pfx + "SECRET_URI"
	cfgSecret := pfx + "SECRET"

	secretBase64 := loadSecret(sec, cfgSecretURI, cfgSecret)
	secret, err := generate.DecodeJwtSecret(secretBase64)
	if err == nil {
		return &secret, nil
	}

	log.Info("[%s] %s or %s failed loading: %v - creating new key", sec.Name(), cfgSecret, cfgSecretURI, err)
	return createSymmeticSigningKeyCfg(rootCfg, sec.Name(), cfgSecret)
}

// loadAsymmeticSigningKey loads a signing key from [pfx]SIGNING_PRIVATE_KEY_FILE
// or creates it if it does not exist
func loadAsymmeticSigningKeyPath(sec ConfigSection, pfx, defaultFile string) *string {
	cfgFile := pfx + "SIGNING_PRIVATE_KEY_FILE"
	keyPath := sec.Key(cfgFile).MustString(defaultFile)
	if !filepath.IsAbs(keyPath) {
		keyPath = filepath.Join(AppDataPath, keyPath)
	}
	return &keyPath
}

type checkKeyCfg func(rootCfg ConfigProvider, cfgSection, pfx string) error

func onlyAsymmetric() checkKeyCfg {
	return func(rootCfg ConfigProvider, cfgSection, pfx string) error {
		sec := rootCfg.Section(cfgSection)
		cfgAlg := pfx + "SIGNING_ALGORITHM"

		if sec.HasKey(cfgAlg) {
			alg := sec.Key(cfgAlg).String()
			if !jwtx.IsValidAsymmetricAlgorithm(alg) {
				return fmt.Errorf("Unexpected algorithm: %s = %s, needs to be one of %v",
					cfgAlg, alg, jwtx.ValidAsymmetricAlgorithms)
			}
		}

		noCfg := []string{
			pfx + "SECRET_URI",
			pfx + "SECRET",
		}

		for _, cfg := range noCfg {
			if sec.HasKey(cfg) {
				return fmt.Errorf("Invalid config key: %s - must be removed", cfg)
			}
		}

		return nil
	}
}

// loadSigningKey() loads a or creates signing key based on settings in section cfgSection
// [pfx]SIGNING_ALGORITHM determines the algorithm
// [pfx]SECRET is a literal secret for symmetric algorithms
// [pfx]SECRET_URI is the uri of a secret for symmetric algorithms
// [pfx]SIGNING_PRIVATE_KEY_FILE is a file with a private key for asymmetric algorithms
//
// [pfx]SECRET might get written to literally in the config if needed but not present

func loadSigningKeyCfg(rootCfg ConfigProvider, cfgSection, pfx, defaultAlg, defaultPrivateKeyFile string, checks ...checkKeyCfg) (*jwtx.SigningKeyCfg, error) {
	for _, check := range checks {
		err := check(rootCfg, cfgSection, pfx)
		if err != nil {
			return nil, err
		}
	}

	sec := rootCfg.Section(cfgSection)
	cfgAlg := pfx + "SIGNING_ALGORITHM"

	algorithm := sec.Key(cfgAlg).MustString(defaultAlg)

	cfg := jwtx.SigningKeyCfg{Algorithm: algorithm}
	var err error

	if jwtx.IsValidSymmetricAlgorithm(algorithm) {
		cfg.SecretBytes, err = loadSymmeticSigningKeyCfg(rootCfg, sec, pfx)
	} else if jwtx.IsValidAsymmetricAlgorithm(algorithm) {
		cfg.PrivateKeyPath = loadAsymmeticSigningKeyPath(sec, pfx, defaultPrivateKeyFile)
	} else {
		err = fmt.Errorf("invalid algorithm: %s = %s", cfgAlg, algorithm)
	}

	return &cfg, err
}

func loadKeyCfg(rootCfg ConfigProvider, cfgSection, pfx, defaultAlg, defaultPrivateKeyFile string, checks ...checkKeyCfg) (*jwtx.KeyCfg, error) {
	signing, err := loadSigningKeyCfg(rootCfg, cfgSection, pfx, defaultAlg, defaultPrivateKeyFile, checks...)
	if err != nil {
		err = fmt.Errorf("[%s] %v", cfgSection, err)
		return nil, err
	}
	return &jwtx.KeyCfg{Signing: signing}, nil
}

// generateSaveInternalToken generates and saves the internal token to app.ini
func generateSaveInternalToken(rootCfg ConfigProvider) {
	token, err := generate.NewInternalToken()
	if err != nil {
		log.Fatal("Error generate internal token: %v", err)
	}

	InternalToken = token
	saveCfg, err := rootCfg.PrepareSaving()
	if err != nil {
		log.Fatal("Error saving internal token: %v", err)
	}
	rootCfg.Section("security").Key("INTERNAL_TOKEN").SetValue(token)
	saveCfg.Section("security").Key("INTERNAL_TOKEN").SetValue(token)
	if err = saveCfg.Save(); err != nil {
		log.Fatal("Error saving internal token: %v", err)
	}
}

func loadSecurityFrom(rootCfg ConfigProvider) {
	sec := rootCfg.Section("security")
	InstallLock = HasInstallLock(rootCfg)
	LogInRememberDays = sec.Key("LOGIN_REMEMBER_DAYS").MustInt(31)
	SecretKey = loadSecret(sec, "SECRET_KEY_URI", "SECRET_KEY")
	if SecretKey == "" {
		// FIXME: https://github.com/go-gitea/gitea/issues/16832
		// Until it supports rotating an existing secret key, we shouldn't move users off of the widely used default value
		SecretKey = "!#@FDEWREWR&*(" //nolint:gosec
	}
	keying.Init([]byte(SecretKey))

	GlobalTwoFactorRequirement = NewTwoFactorRequirementType(sec.Key("GLOBAL_TWO_FACTOR_REQUIREMENT").String())

	CookieRememberName = sec.Key("COOKIE_REMEMBER_NAME").MustString("persistent")

	ReverseProxyAuthUser = sec.Key("REVERSE_PROXY_AUTHENTICATION_USER").MustString("X-WEBAUTH-USER")
	ReverseProxyAuthEmail = sec.Key("REVERSE_PROXY_AUTHENTICATION_EMAIL").MustString("X-WEBAUTH-EMAIL")
	ReverseProxyAuthFullName = sec.Key("REVERSE_PROXY_AUTHENTICATION_FULL_NAME").MustString("X-WEBAUTH-FULLNAME")

	ReverseProxyLimit = sec.Key("REVERSE_PROXY_LIMIT").MustInt(1)
	ReverseProxyTrustedProxies = sec.Key("REVERSE_PROXY_TRUSTED_PROXIES").Strings(",")
	if len(ReverseProxyTrustedProxies) == 0 {
		ReverseProxyTrustedProxies = []string{"127.0.0.0/8", "::1/128"}
	}

	MinPasswordLength = sec.Key("MIN_PASSWORD_LENGTH").MustInt(8)
	ImportLocalPaths = sec.Key("IMPORT_LOCAL_PATHS").MustBool(false)
	DisableGitHooks = sec.Key("DISABLE_GIT_HOOKS").MustBool(true)
	DisableWebhooks = sec.Key("DISABLE_WEBHOOKS").MustBool(false)
	OnlyAllowPushIfGiteaEnvironmentSet = sec.Key("ONLY_ALLOW_PUSH_IF_GITEA_ENVIRONMENT_SET").MustBool(true)

	// Ensure that the provided default hash algorithm is a valid hash algorithm
	var algorithm *hash.PasswordHashAlgorithm
	PasswordHashAlgo, algorithm = hash.SetDefaultPasswordHashAlgorithm(sec.Key("PASSWORD_HASH_ALGO").MustString(""))
	if algorithm == nil {
		log.Fatal("The provided password hash algorithm was invalid: %s", sec.Key("PASSWORD_HASH_ALGO").MustString(""))
	}

	PasswordCheckPwn = sec.Key("PASSWORD_CHECK_PWN").MustBool(false)
	SuccessfulTokensCacheSize = sec.Key("SUCCESSFUL_TOKENS_CACHE_SIZE").MustInt(20)

	InternalToken = loadSecret(sec, "INTERNAL_TOKEN_URI", "INTERNAL_TOKEN")
	if InstallLock && InternalToken == "" {
		// if Gitea has been installed but the InternalToken hasn't been generated (upgrade from an old release), we should generate
		// some users do cluster deployment, they still depend on this auto-generating behavior.
		generateSaveInternalToken(rootCfg)
	}

	cfgdata := sec.Key("PASSWORD_COMPLEXITY").Strings(",")
	if len(cfgdata) == 0 {
		cfgdata = []string{"off"}
	}
	PasswordComplexity = make([]string, 0, len(cfgdata))
	for _, name := range cfgdata {
		name := strings.ToLower(strings.Trim(name, `"`))
		if name != "" {
			PasswordComplexity = append(PasswordComplexity, name)
		}
	}

	sectionHasDisableQueryAuthToken := sec.HasKey("DISABLE_QUERY_AUTH_TOKEN")

	// TODO: default value should be true in future releases
	DisableQueryAuthToken = sec.Key("DISABLE_QUERY_AUTH_TOKEN").MustBool(false)

	// warn if the setting is set to false explicitly
	if sectionHasDisableQueryAuthToken && !DisableQueryAuthToken {
		log.Warn("Enabling Query API Auth tokens is not recommended. DISABLE_QUERY_AUTH_TOKEN will be removed in Forgejo v13.0.0.")
	}
}

type TwoFactorRequirementType string

// llu:TrKeysSuffix admin.config.global_2fa_requirement.
const (
	NoneTwoFactorRequirement  TwoFactorRequirementType = "none"
	AllTwoFactorRequirement   TwoFactorRequirementType = "all"
	AdminTwoFactorRequirement TwoFactorRequirementType = "admin"
)

func NewTwoFactorRequirementType(twoFactorRequirement string) TwoFactorRequirementType {
	switch twoFactorRequirement {
	case AllTwoFactorRequirement.String():
		return AllTwoFactorRequirement
	case AdminTwoFactorRequirement.String():
		return AdminTwoFactorRequirement
	default:
		return NoneTwoFactorRequirement
	}
}

func (r TwoFactorRequirementType) String() string {
	return string(r)
}

func (r TwoFactorRequirementType) IsNone() bool {
	return r == NoneTwoFactorRequirement
}

func (r TwoFactorRequirementType) IsAll() bool {
	return r == AllTwoFactorRequirement
}

func (r TwoFactorRequirementType) IsAdmin() bool {
	return r == AdminTwoFactorRequirement
}
