// Copyright 2018 Project Harbor Authors
//
// 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 config provide config for core api and other modules
// Before accessing user settings, need to call Load()
// For system settings, no need to call Load()
package config

import (
	"crypto/tls"
	"crypto/x509"
	"encoding/json"
	"errors"
	"fmt"
	"io/ioutil"
	"net/http"
	"os"
	"strings"

	"github.com/goharbor/harbor/src/common"
	comcfg "github.com/goharbor/harbor/src/common/config"
	"github.com/goharbor/harbor/src/common/models"
	"github.com/goharbor/harbor/src/common/secret"
	"github.com/goharbor/harbor/src/common/utils/log"
	"github.com/goharbor/harbor/src/core/promgr"
	"github.com/goharbor/harbor/src/core/promgr/pmsdriver"
	"github.com/goharbor/harbor/src/core/promgr/pmsdriver/admiral"
	"github.com/goharbor/harbor/src/core/promgr/pmsdriver/local"
)

const (
	defaultKeyPath                     = "/etc/core/key"
	defaultTokenFilePath               = "/etc/core/token/tokens.properties"
	defaultRegistryTokenPrivateKeyPath = "/etc/core/private_key.pem"
)

var (
	// SecretStore manages secrets
	SecretStore *secret.Store
	// GlobalProjectMgr is initialized based on the deploy mode
	GlobalProjectMgr promgr.ProjectManager
	keyProvider      comcfg.KeyProvider
	// AdmiralClient is initialized only under integration deploy mode
	// and can be passed to project manager as a parameter
	AdmiralClient *http.Client
	// TokenReader is used in integration mode to read token
	TokenReader admiral.TokenReader
	// defined as a var for testing.
	defaultCACertPath = "/etc/core/ca/ca.crt"
	cfgMgr            *comcfg.CfgManager
)

// Init configurations
func Init() error {
	// init key provider
	initKeyProvider()

	cfgMgr = comcfg.NewDBCfgManager()

	log.Info("init secret store")
	// init secret store
	initSecretStore()
	log.Info("init project manager based on deploy mode")
	// init project manager based on deploy mode
	if err := initProjectManager(); err != nil {
		log.Errorf("Failed to initialise project manager, error: %v", err)
		return err
	}
	return nil
}

// InitWithSettings init config with predefined configs, and optionally overwrite the keyprovider
func InitWithSettings(cfgs map[string]interface{}, kp ...comcfg.KeyProvider) {
	Init()
	cfgMgr = comcfg.NewInMemoryManager()
	cfgMgr.UpdateConfig(cfgs)
	if len(kp) > 0 {
		keyProvider = kp[0]
	}
}

func initKeyProvider() {
	path := os.Getenv("KEY_PATH")
	if len(path) == 0 {
		path = defaultKeyPath
	}
	log.Infof("key path: %s", path)

	keyProvider = comcfg.NewFileKeyProvider(path)
}

func initSecretStore() {
	m := map[string]string{}
	m[JobserviceSecret()] = secret.JobserviceUser
	SecretStore = secret.NewStore(m)
}

func initProjectManager() error {
	var driver pmsdriver.PMSDriver
	if WithAdmiral() {
		log.Debugf("Initialising Admiral client with certificate: %s", defaultCACertPath)
		content, err := ioutil.ReadFile(defaultCACertPath)
		if err != nil {
			return err
		}
		pool := x509.NewCertPool()
		if ok := pool.AppendCertsFromPEM(content); !ok {
			return fmt.Errorf("failed to append cert content into cert worker")
		}
		AdmiralClient = &http.Client{
			Transport: &http.Transport{
				Proxy: http.ProxyFromEnvironment,
				TLSClientConfig: &tls.Config{
					RootCAs: pool,
				},
			},
		}

		// integration with admiral
		log.Info("initializing the project manager based on PMS...")
		path := os.Getenv("SERVICE_TOKEN_FILE_PATH")
		if len(path) == 0 {
			path = defaultTokenFilePath
		}
		log.Infof("service token file path: %s", path)
		TokenReader = &admiral.FileTokenReader{
			Path: path,
		}
		driver = admiral.NewDriver(AdmiralClient, AdmiralEndpoint(), TokenReader)
	} else {
		// standalone
		log.Info("initializing the project manager based on local database...")
		driver = local.NewDriver()
	}
	GlobalProjectMgr = promgr.NewDefaultProjectManager(driver, true)
	return nil

}

// GetCfgManager return the current config manager
func GetCfgManager() *comcfg.CfgManager {
	if cfgMgr == nil {
		return comcfg.NewDBCfgManager()
	}
	return cfgMgr
}

// Load configurations
func Load() error {
	return cfgMgr.Load()
}

// Upload save all system configurations
func Upload(cfg map[string]interface{}) error {
	return cfgMgr.UpdateConfig(cfg)
}

// GetSystemCfg returns the system configurations
func GetSystemCfg() (map[string]interface{}, error) {
	sysCfg := cfgMgr.GetAll()
	if len(sysCfg) == 0 {
		return nil, errors.New("can not load system config, the database might be down")
	}
	return sysCfg, nil
}

// AuthMode ...
func AuthMode() (string, error) {
	err := cfgMgr.Load()
	if err != nil {
		log.Errorf("failed to load config, error %v", err)
		return "db_auth", err
	}
	return cfgMgr.Get(common.AUTHMode).GetString(), nil
}

// TokenPrivateKeyPath returns the path to the key for signing token for registry
func TokenPrivateKeyPath() string {
	path := os.Getenv("TOKEN_PRIVATE_KEY_PATH")
	if len(path) == 0 {
		path = defaultRegistryTokenPrivateKeyPath
	}
	return path
}

// LDAPConf returns the setting of ldap server
func LDAPConf() (*models.LdapConf, error) {
	err := cfgMgr.Load()
	if err != nil {
		return nil, err
	}
	return &models.LdapConf{
		LdapURL:               cfgMgr.Get(common.LDAPURL).GetString(),
		LdapSearchDn:          cfgMgr.Get(common.LDAPSearchDN).GetString(),
		LdapSearchPassword:    cfgMgr.Get(common.LDAPSearchPwd).GetString(),
		LdapBaseDn:            cfgMgr.Get(common.LDAPBaseDN).GetString(),
		LdapUID:               cfgMgr.Get(common.LDAPUID).GetString(),
		LdapFilter:            cfgMgr.Get(common.LDAPFilter).GetString(),
		LdapScope:             cfgMgr.Get(common.LDAPScope).GetInt(),
		LdapConnectionTimeout: cfgMgr.Get(common.LDAPTimeout).GetInt(),
		LdapVerifyCert:        cfgMgr.Get(common.LDAPVerifyCert).GetBool(),
	}, nil
}

// LDAPGroupConf returns the setting of ldap group search
func LDAPGroupConf() (*models.LdapGroupConf, error) {
	err := cfgMgr.Load()
	if err != nil {
		return nil, err
	}
	return &models.LdapGroupConf{
		LdapGroupBaseDN:              cfgMgr.Get(common.LDAPGroupBaseDN).GetString(),
		LdapGroupFilter:              cfgMgr.Get(common.LDAPGroupSearchFilter).GetString(),
		LdapGroupNameAttribute:       cfgMgr.Get(common.LDAPGroupAttributeName).GetString(),
		LdapGroupSearchScope:         cfgMgr.Get(common.LDAPGroupSearchScope).GetInt(),
		LdapGroupAdminDN:             cfgMgr.Get(common.LDAPGroupAdminDn).GetString(),
		LdapGroupMembershipAttribute: cfgMgr.Get(common.LDAPGroupMembershipAttribute).GetString(),
	}, nil
}

// TokenExpiration returns the token expiration time (in minute)
func TokenExpiration() (int, error) {
	return cfgMgr.Get(common.TokenExpiration).GetInt(), nil
}

// RobotTokenDuration returns the token expiration time of robot account (in minute)
func RobotTokenDuration() int {
	return cfgMgr.Get(common.RobotTokenDuration).GetInt()
}

// ExtEndpoint returns the external URL of Harbor: protocol://host:port
func ExtEndpoint() (string, error) {
	return cfgMgr.Get(common.ExtEndpoint).GetString(), nil
}

// ExtURL returns the external URL: host:port
func ExtURL() (string, error) {
	endpoint, err := ExtEndpoint()
	if err != nil {
		log.Errorf("failed to load config, error %v", err)
	}
	l := strings.Split(endpoint, "://")
	if len(l) > 0 {
		return l[1], nil
	}
	return endpoint, nil
}

// SecretKey returns the secret key to encrypt the password of target
func SecretKey() (string, error) {
	return keyProvider.Get(nil)
}

// SelfRegistration returns the enablement of self registration
func SelfRegistration() (bool, error) {
	return cfgMgr.Get(common.SelfRegistration).GetBool(), nil
}

// RegistryURL ...
func RegistryURL() (string, error) {
	return cfgMgr.Get(common.RegistryURL).GetString(), nil
}

// InternalJobServiceURL returns jobservice URL for internal communication between Harbor containers
func InternalJobServiceURL() string {
	return strings.TrimSuffix(cfgMgr.Get(common.JobServiceURL).GetString(), "/")
}

// InternalCoreURL returns the local harbor core url
func InternalCoreURL() string {
	return strings.TrimSuffix(cfgMgr.Get(common.CoreURL).GetString(), "/")
}

// LocalCoreURL returns the local harbor core url
func LocalCoreURL() string {
	return cfgMgr.Get(common.CoreLocalURL).GetString()
}

// InternalTokenServiceEndpoint returns token service endpoint for internal communication between Harbor containers
func InternalTokenServiceEndpoint() string {
	return InternalCoreURL() + "/service/token"
}

// InternalNotaryEndpoint returns notary server endpoint for internal communication between Harbor containers
// This is currently a conventional value and can be unaccessible when Harbor is not deployed with Notary.
func InternalNotaryEndpoint() string {
	return cfgMgr.Get(common.NotaryURL).GetString()
}

// InitialAdminPassword returns the initial password for administrator
func InitialAdminPassword() (string, error) {
	return cfgMgr.Get(common.AdminInitialPassword).GetString(), nil
}

// OnlyAdminCreateProject returns the flag to restrict that only sys admin can create project
func OnlyAdminCreateProject() (bool, error) {
	return cfgMgr.Get(common.ProjectCreationRestriction).GetString() == common.ProCrtRestrAdmOnly, nil
}

// Email returns email server settings
func Email() (*models.Email, error) {
	err := cfgMgr.Load()
	if err != nil {
		return nil, err
	}
	return &models.Email{
		Host:     cfgMgr.Get(common.EmailHost).GetString(),
		Port:     cfgMgr.Get(common.EmailPort).GetInt(),
		Username: cfgMgr.Get(common.EmailUsername).GetString(),
		Password: cfgMgr.Get(common.EmailPassword).GetString(),
		SSL:      cfgMgr.Get(common.EmailSSL).GetBool(),
		From:     cfgMgr.Get(common.EmailFrom).GetString(),
		Identity: cfgMgr.Get(common.EmailIdentity).GetString(),
		Insecure: cfgMgr.Get(common.EmailInsecure).GetBool(),
	}, nil
}

// Database returns database settings
func Database() (*models.Database, error) {
	database := &models.Database{}
	database.Type = cfgMgr.Get(common.DatabaseType).GetString()
	postgresql := &models.PostGreSQL{
		Host:         cfgMgr.Get(common.PostGreSQLHOST).GetString(),
		Port:         cfgMgr.Get(common.PostGreSQLPort).GetInt(),
		Username:     cfgMgr.Get(common.PostGreSQLUsername).GetString(),
		Password:     cfgMgr.Get(common.PostGreSQLPassword).GetString(),
		Database:     cfgMgr.Get(common.PostGreSQLDatabase).GetString(),
		SSLMode:      cfgMgr.Get(common.PostGreSQLSSLMode).GetString(),
		MaxIdleConns: cfgMgr.Get(common.PostGreSQLMaxIdleConns).GetInt(),
		MaxOpenConns: cfgMgr.Get(common.PostGreSQLMaxOpenConns).GetInt(),
	}
	database.PostGreSQL = postgresql

	return database, nil
}

// CoreSecret returns a secret to mark harbor-core when communicate with
// other component
func CoreSecret() string {
	return os.Getenv("CORE_SECRET")
}

// JobserviceSecret returns a secret to mark Jobservice when communicate with
// other component
// TODO replace it with method of SecretStore
func JobserviceSecret() string {
	return os.Getenv("JOBSERVICE_SECRET")
}

// WithNotary returns a bool value to indicate if Harbor's deployed with Notary
func WithNotary() bool {
	return cfgMgr.Get(common.WithNotary).GetBool()
}

// WithClair returns a bool value to indicate if Harbor's deployed with Clair
func WithClair() bool {
	return cfgMgr.Get(common.WithClair).GetBool()
}

// ClairEndpoint returns the end point of clair instance, by default it's the one deployed within Harbor.
func ClairEndpoint() string {
	return cfgMgr.Get(common.ClairURL).GetString()
}

// ClairDB return Clair db info
func ClairDB() (*models.PostGreSQL, error) {
	clairDB := &models.PostGreSQL{
		Host:     cfgMgr.Get(common.ClairDBHost).GetString(),
		Port:     cfgMgr.Get(common.ClairDBPort).GetInt(),
		Username: cfgMgr.Get(common.ClairDBUsername).GetString(),
		Password: cfgMgr.Get(common.ClairDBPassword).GetString(),
		Database: cfgMgr.Get(common.ClairDB).GetString(),
		SSLMode:  cfgMgr.Get(common.ClairDBSSLMode).GetString(),
	}
	return clairDB, nil
}

// ClairAdapterEndpoint returns the endpoint of clair adapter instance, by default it's the one deployed within Harbor.
func ClairAdapterEndpoint() string {
	return cfgMgr.Get(common.ClairAdapterURL).GetString()
}

// AdmiralEndpoint returns the URL of admiral, if Harbor is not deployed with admiral it should return an empty string.
func AdmiralEndpoint() string {
	if cfgMgr.Get(common.AdmiralEndpoint).GetString() == "NA" {
		return ""
	}
	return cfgMgr.Get(common.AdmiralEndpoint).GetString()
}

// ScanAllPolicy returns the policy which controls the scan all.
func ScanAllPolicy() models.ScanAllPolicy {
	var res models.ScanAllPolicy
	log.Infof("Scan all policy %v", cfgMgr.Get(common.ScanAllPolicy).GetString())
	if err := json.Unmarshal([]byte(cfgMgr.Get(common.ScanAllPolicy).GetString()), &res); err != nil {
		log.Errorf("Failed to unmarshal the value in configuration for Scan All policy, error: %v, returning the default policy", err)
		return models.DefaultScanAllPolicy
	}
	return res
}

// WithAdmiral returns a bool to indicate if Harbor's deployed with admiral.
func WithAdmiral() bool {
	return len(AdmiralEndpoint()) > 0
}

// UAASettings returns the UAASettings to access UAA service.
func UAASettings() (*models.UAASettings, error) {
	err := cfgMgr.Load()
	if err != nil {
		return nil, err
	}
	us := &models.UAASettings{
		Endpoint:     cfgMgr.Get(common.UAAEndpoint).GetString(),
		ClientID:     cfgMgr.Get(common.UAAClientID).GetString(),
		ClientSecret: cfgMgr.Get(common.UAAClientSecret).GetString(),
		VerifyCert:   cfgMgr.Get(common.UAAVerifyCert).GetBool(),
	}
	return us, nil
}

// ReadOnly returns a bool to indicates if Harbor is in read only mode.
func ReadOnly() bool {
	return cfgMgr.Get(common.ReadOnly).GetBool()
}

// WithChartMuseum returns a bool to indicate if chartmuseum is deployed with Harbor.
func WithChartMuseum() bool {
	return cfgMgr.Get(common.WithChartMuseum).GetBool()
}

// GetChartMuseumEndpoint returns the endpoint of the chartmuseum service
// otherwise an non nil error is returned
func GetChartMuseumEndpoint() (string, error) {
	chartEndpoint := strings.TrimSpace(cfgMgr.Get(common.ChartRepoURL).GetString())
	if len(chartEndpoint) == 0 {
		return "", errors.New("empty chartmuseum endpoint")
	}
	return chartEndpoint, nil
}

// GetRedisOfRegURL returns the URL of Redis used by registry
func GetRedisOfRegURL() string {
	return os.Getenv("_REDIS_URL_REG")
}

// GetPortalURL returns the URL of portal
func GetPortalURL() string {
	url := os.Getenv("PORTAL_URL")
	if len(url) == 0 {
		return common.DefaultPortalURL
	}
	return url
}

// GetRegistryCtlURL returns the URL of registryctl
func GetRegistryCtlURL() string {
	url := os.Getenv("REGISTRYCTL_URL")
	if len(url) == 0 {
		return common.DefaultRegistryCtlURL
	}
	return url
}

// GetClairHealthCheckServerURL returns the URL of
// the health check server of Clair
func GetClairHealthCheckServerURL() string {
	url := os.Getenv("CLAIR_HEALTH_CHECK_SERVER_URL")
	if len(url) == 0 {
		return common.DefaultClairHealthCheckServerURL
	}
	return url
}

// HTTPAuthProxySetting returns the setting of HTTP Auth proxy.  the settings are only meaningful when the auth_mode is
// set to http_auth
func HTTPAuthProxySetting() (*models.HTTPAuthProxy, error) {
	if err := cfgMgr.Load(); err != nil {
		return nil, err
	}
	return &models.HTTPAuthProxy{
		Endpoint:            cfgMgr.Get(common.HTTPAuthProxyEndpoint).GetString(),
		TokenReviewEndpoint: cfgMgr.Get(common.HTTPAuthProxyTokenReviewEndpoint).GetString(),
		VerifyCert:          cfgMgr.Get(common.HTTPAuthProxyVerifyCert).GetBool(),
		SkipSearch:          cfgMgr.Get(common.HTTPAuthProxySkipSearch).GetBool(),
	}, nil

}

// OIDCSetting returns the setting of OIDC provider, currently there's only one OIDC provider allowed for Harbor and it's
// only effective when auth_mode is set to oidc_auth
func OIDCSetting() (*models.OIDCSetting, error) {
	if err := cfgMgr.Load(); err != nil {
		return nil, err
	}
	scopeStr := cfgMgr.Get(common.OIDCScope).GetString()
	extEndpoint := strings.TrimSuffix(cfgMgr.Get(common.ExtEndpoint).GetString(), "/")
	scope := []string{}
	for _, s := range strings.Split(scopeStr, ",") {
		scope = append(scope, strings.TrimSpace(s))
	}

	return &models.OIDCSetting{
		Name:         cfgMgr.Get(common.OIDCName).GetString(),
		Endpoint:     cfgMgr.Get(common.OIDCEndpoint).GetString(),
		VerifyCert:   cfgMgr.Get(common.OIDCVerifyCert).GetBool(),
		ClientID:     cfgMgr.Get(common.OIDCCLientID).GetString(),
		ClientSecret: cfgMgr.Get(common.OIDCClientSecret).GetString(),
		GroupsClaim:  cfgMgr.Get(common.OIDCGroupsClaim).GetString(),
		RedirectURL:  extEndpoint + common.OIDCCallbackPath,
		Scope:        scope,
	}, nil
}

// NotificationEnable returns a bool to indicates if notification enabled in harbor
func NotificationEnable() bool {
	return cfgMgr.Get(common.NotificationEnable).GetBool()
}

// QuotaPerProjectEnable returns a bool to indicates if quota per project enabled in harbor
func QuotaPerProjectEnable() bool {
	return cfgMgr.Get(common.QuotaPerProjectEnable).GetBool()
}

// QuotaSetting returns the setting of quota.
func QuotaSetting() (*models.QuotaSetting, error) {
	if err := cfgMgr.Load(); err != nil {
		return nil, err
	}
	return &models.QuotaSetting{
		CountPerProject:   cfgMgr.Get(common.CountPerProject).GetInt64(),
		StoragePerProject: cfgMgr.Get(common.StoragePerProject).GetInt64(),
	}, nil
}