package config

import (
	"io/ioutil"
	"path/filepath"
	"sync/atomic"

	"github.com/pingcap/dbaas/pkg/ratecontroller/types"

	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/aws/session"
	"github.com/aws/aws-sdk-go/service/secretsmanager"
	"github.com/bwmarrin/snowflake"
	"github.com/creasty/defaults"
	"github.com/go-playground/validator/v10"
	"github.com/imdario/mergo"
	"github.com/pingcap/dbaas/pkg/constants"
	"github.com/pingcap/errors"
	"github.com/pingcap/log"
	"go.uber.org/zap"
	"gopkg.in/yaml.v2"
	"k8s.io/apimachinery/pkg/api/resource"
)

var globalConf CentralConfig
var topSQLTLSConf atomic.Value // stores TLSConfig for TopSQL TiDB storage

type CentralConfig struct {
	ServerConfig      ServerConfig      `yaml:"server"`
	SessionConfig     SessionConfig     `yaml:"session"`
	MysqlConfig       MysqlConfig       `yaml:"mysql"`
	GardenerConfig    GardenerConfig    `yaml:"gardener"`
	InfraAPIConfig    InfraAPIConfig    `yaml:"infraAPI"`
	AwsConfig         AwsConfig         `yaml:"aws"`
	BillingConfig     BillingConfig     `yaml:"billing"`
	ProxyConfig       ProxyConfig       `yaml:"proxy"`
	InfraConfig       InfraConfig       `yaml:"infra"`
	Auth0Config       Auth0Config       `yaml:"auth0"`
	DebugPortalConfig DebugPortalConfig `yaml:"debugPortal"`
	ZendeskConfig     ZendeskConfig     `yaml:"zendesk"`
	SalesforceConfig  SalesforceConfig  `yaml:"salesforce"`
	GrafanaConfig     GrafanaConfig     `yaml:"grafana"`
	ImportConfig      ImportConfig      `yaml:"import"`
	HibernationConfig HibernationConfig `yaml:"hibernation"`
	RateControlConfig RateControlConfig `yaml:"rateControl"`
}

type HibernationConfig struct {
	DryRun                       bool  `yaml:"dryRun" default:"false"`
	IdleDurationThresholdMinutes int64 `yaml:"idleDurationThresholdMinutes" default:"60"`
	CheckIdleIntervalMinutes     int64 `yaml:"checkIdleIntervalMinutes" default:"5"`
}

type RateControlConfigItem struct {
	Scene *types.Scene  `yaml:"scene"`
	Rates []*types.Rate `yaml:"rates"`
}

type RateControlConfig struct {
	InviteUser               RateControlConfigItem `yaml:"inviteUser"`
	RegistrationVerification RateControlConfigItem `yaml:"registrationVerification"`
	SendPasswordVerifyCode   RateControlConfigItem `yaml:"sendPasswordVerifyCode"`
}

type ServerConfig struct {
	// Log level: debug, info, warn, error, fatal
	LogLevel         string `yaml:"logLevel" default:"info" validate:"oneof=debug info warn error fatal"`
	EnableDBQueryLog bool   `yaml:"enableDBQueryLog" default:"false"`

	FrontendDNS string `yaml:"frontendDNS" validate:"required"`
	BackendDNS  string `yaml:"backendDNS" validate:"required"`

	EnableSwaggerUI bool `yaml:"enableSwaggerUI" default:"false"`

	ManifestDirPrefix string `yaml:"manifestDirPrefix"`
	Stage             string `yaml:"stage" validate:"required"`
	// central server cidr
	CentralCIDR []string `yaml:"centralCIDR" default:"[\"0.0.0.0/0\", \"0.0.0.0/1\"]"`
	GrpcPort    int      `yaml:"grpcPort" default:"10000"`
	//the central metrics port
	MetricsPort uint16 `yaml:"metricsPort" default:"9090"`
	// the snowflake node id
	SnowflakeID int64 `yaml:"snowflakeID" default:"1"`
	// session secret salt conf
	SaltSecret string `yaml:"saltSecret" required:"true"`
	// mailgun conf
	MailGunApiKey string `yaml:"mailGunApiKey" required:"true"`

	// recaptcha secret conf
	RecaptchaSecret string `yaml:"recaptchaSecret" required:"true"`

	SentryConf SentryConfig `yaml:"sentry"`

	SecretStore       *SecretConfig `yaml:"secret"`
	TopSQLSecretStore *SecretConfig `yaml:"topSQLSecret"`

	PocManualBackupReservedDays *int `yaml:"pocManualBackupReservedDays"`

	PocAutoBackupReservedDays *int `yaml:"pocAutoBackupReservedDays"`

	OnDemandAutoBackupReservedDays *int `yaml:"onDemandAutoBackupReservedDays"`

	EnablePublicCloudCrd *bool `yaml:"enablePublicCloudCrd"`

	ManagementPortalInitUserEmail *string `yaml:"managementPortalInitUserEmail" default:"cloud@pingcap.com"`

	// EnableGracefulShutdownWebhookOnAWS will deploy graceful-shutdown-tidb-webhook in tidb-admin namespace of shoot cluster,
	// and add health-checker sidecar for tidb pod, terminating or restarting TiDB node will not cause non-persistent connections abort.
	// EnableGracefulShutdownWebhookOnGCP is true by default, it has been fully tested.
	// https://github.com/tidbcloud/dbaas/tree/master/central/cmd/health-checker
	// https://github.com/tidbcloud/dbaas/tree/master/central/cmd/graceful-shutdown-tidb-webhook
	EnableGracefulShutdownWebhookOnAWS bool `yaml:"enableGracefulShutdownWebhookOnAWS" default:"false"`

	CentralRoleARN string `yaml:"centralRoleARN" validate:"required"`

	// Free trial user's initial credit.
	FreeTrialCreditAmount *int64 `yaml:"freeTrialCreditAmount"`

	// If the number of free trial users exceeds MaxFreeTrialOrgs, the system will reject the new user registration.
	MaxFreeTrialOrgs *int64 `yaml:"maxFreeTrialOrgs"`

	// listen addr
	AdminListenAddr   string `yaml:"adminListenAddr" default:"0.0.0.0:8080"`
	MetricsListenAddr string `yaml:"metricsListenAddr" default:"0.0.0.0:8080"`

	// thanos addr
	ThanosAddr string `yaml:"thanosAddr" default:"http://thanos-query.monitoring:9090"`
}

type SentryConfig struct {
	DSN         string `yaml:"dsn"`
	Environment string `yaml:"env"`
	LogLevel    string `yaml:"logLevel" default:"warn"`
}

type SessionConfig struct {
	// A secret to encrypt or derive sessions.
	// Currently only Debug Portal implements session.
	Secret string `yaml:"secret" validate:"required"`
}

type ImportConfig struct {
	MachineType string `yaml:"machineType" default:"c5.9xlarge"`
	// default use pingcap/tidb-lightning:{ClusterVersion}
	LightningImage   string `yaml:"lightningImage"`
	StorageRequest   string `yaml:"storageRequest" default:"3Ti"`
	SkipPreCheck     bool   `yaml:"skipPreCheck"`
	SkipBanMyBuckets bool   `yaml:"skipBanMyBuckets"`
}

type SecretConfig struct {
	SecretKey string `yaml:"secretKey"`
	Region    string `yaml:"region"`
}

type GrafanaConfig struct {
	Server           string `yaml:"server"  default:"https://grafana.oauth.dev.shared.aws.tidbcloud.com/"`
	APIKey           string `yaml:"apiKey"`
	TidbDashboardTag string `yaml:"tidbDashboardTag" default:"tidb-mixin"`
}

// infra configurations
type InfraConfig struct {
	//The shoot local proxy image
	ProxyImage string `yaml:"proxyImage" required:"true"`
	//The shoot local reporter image
	ReporterImage string `yaml:"reporterImage" required:"true"`
	// The Top SQL Agent image
	TopSQLAgentImage string `yaml:"topsqlAgentImage" required:"true"`
	//The shoot local SLI reporter image
	SLIReporterImage string `yaml:"sliReporterImage" required:"true"`
	// The identity helper image
	IdentityImage string `yaml:"identityImage" required:"true"`
	// node usage reporter sqs message
	SQSEndpoint string `yaml:"sqsEndpoint" required:"true"`
	// SLI reporter sqs message queue
	SLISQSEndpoint string `yaml:"sliSqsEndpoint" required:"true"`
	// dbaas central service region
	Region             string `yaml:"region" required:"true" default:"us-west-2"`
	CMKArn             string `yaml:"cmkArn" required:"true"`
	AwsPublicHZDomain  string `yaml:"awsPublicHZDomain" required:"true"`
	GcpPublicHZDomain  string `yaml:"gcpPublicHZDomain" required:"true"`
	AwsPublicHZID      string `yaml:"awsPublicHZID" required:"true"`
	GcpPublicHZID      string `yaml:"gcpPublicHZID" required:"true"`
	GcpProject         string `yaml:"gcpProject" required:"true"`
	GcpCertManagerRole string `yaml:"gcpCertManagerRole" required:"true"`

	// cert-manager controller image repository
	CertManagerRepository string `yaml:"certManagerRepository" required:"true"`

	ConfigServiceURL string `yaml:"configServiceURL" required:"true" default:"http://config-service.gitops:8090"`
}

type ZendeskConfig struct {
	EnableZendesk   bool   `yaml:"enableZendesk" default:"false"`
	ZendeskEmail    string `yaml:"zendeskEmail"`
	ZendeskDomain   string `yaml:"zendeskDomain"`
	ZendeskAPIToken string `yaml:"zendeskAPIToken"`
}

type SalesforceConfig struct {
	EnableSalesforce       bool   `yaml:"enableSalesforce" default:"false"`
	SalesforceDomain       string `yaml:"salesforceDomain"`
	SalesforceClientID     string `yaml:"salesforceClientID"`
	SalesforceClientSecret string `yaml:"salesforceClientSecret"`
	SalesforceUsername     string `yaml:"salesforceUsername"`
	SalesforcePassword     string `yaml:"salesforcePassword"`
}

type Auth0Config struct {
	Auth0ManagementClientID     string `yaml:"auth0ManagementClientID" required:"true"`
	Auth0ManagementClientSecret string `yaml:"auth0ManagementClientSecret" required:"true"`
	Auth0DatabaseConnectionName string `yaml:"auth0DatabaseConnectionName" required:"true" default:"custom-email"`

	Auth0Domain   string `yaml:"auth0Domain" required:"true"`
	Auth0Audience string `yaml:"auth0Audience" required:"true"`
	Auth0Secret   string `yaml:"auth0Secret" required:"true"`

	ManagementPortalAuth0Domain         string `yaml:"managementPortalAuth0Domain" required:"true"`
	ManagementPortalAuth0Audience       string `yaml:"managementPortalAuth0Audience" required:"true"`
	ManagementPortalAuth0Secret         string `yaml:"managementPortalAuth0Secret" required:"true"`
	ManagementPortalAuth0ClientAudience string `yaml:"managementPortalAuth0ClientAudience" required:"true"`
}

type DebugPortalConfig struct {
	OAuthClientID         string      `yaml:"oAuthClientID" required:"true"`
	OAuthClientSecret     string      `yaml:"oAuthClientSecret" required:"true"`
	OAuthScopes           string      `yaml:"oAuthScopes" required:"true"`
	OAuthAuthorizationURL string      `yaml:"oAuthAuthorizationURL" required:"true"`
	OAuthTokenURL         string      `yaml:"oAuthTokenURL" required:"true"`
	OAuthUserInfoURL      string      `yaml:"oAuthUserInfoURL" required:"true"`
	OAuthStateSecret      string      `yaml:"oAuthStateSecret" required:"true"`
	SignOutURL            string      `yaml:"signOutURL" required:"true"`
	DisableAuthN          bool        `yaml:"disableAuthN" required:"true"`
	SlowQueryDB           MysqlConfig `yaml:"slowQueryDB" required:"true"`
	TopSqlTidbConfig      MysqlConfig `yaml:"topSQLTiDB" required:"true"`
}

type BillingConfig struct {
	Auth0Domain       string `yaml:"auth0Domain" required:"true"`
	Auth0ClientID     string `yaml:"auth0ClientID" required:"true"`
	Auth0ClientSecret string `yaml:"auth0ClientSecret" required:"true"`
	Auth0Audience     string `yaml:"auth0Audience" required:"true"`

	// The billing system base url
	BillingBaseURL string `yaml:"billingBaseURL" default:"http://billing:18888" required:"true"`
}

type ProxyConfig struct {
	Auth0Domain       string `yaml:"auth0Domain" required:"true"`
	Auth0ClientID     string `yaml:"auth0ClientID" required:"true"`
	Auth0ClientSecret string `yaml:"auth0ClientSecret" required:"true"`
	Auth0Audience     string `yaml:"auth0Audience" required:"true"`
}

type AwsConfig struct {
	PCAArn                  string `yaml:"pcaArn" required:"true"`
	PCARegion               string `yaml:"pcaRegion" required:"true"`
	Region                  string `yaml:"region" required:"true"`
	AccountID               string `yaml:"accountID" required:"true"`
	ProjectID               string `yaml:"projectID" required:"true"`
	ReporterAccessKeyID     string `yaml:"reporterAccessKeyID" required:"true"`
	ReporterSecretAccessKey string `yaml:"reporterSecretAccessKey" required:"true"`
}

type GardenerConfig struct {
	GcpMasterType  string `yaml:"gcpMasterType" default:"n1-standard-2"`
	GcpMasterCount int32  `yaml:"gcpMasterCount" default:"1"`
	AwsMasterType  string `yaml:"awsMasterType" default:"t3.medium"`
	AwsMasterCount int32  `yaml:"awsMasterCount" default:"1"`

	GardenerProjectName string `yaml:"gardenerProjectName" default:"core" required:"true"`
	// The base domain of gardener
	GardenBaseDomain string `yaml:"gardenBaseDomain" default:"garden.shared.aws.tidbcloud.com"`
	// kubeconfig file for base cluster, ignore if in cluster
	KubeConfigPath    string `yaml:"kubeConfigPath"`
	NodeImage         string `yaml:"nodeImage"`
	StorageNodeImage  string `yaml:"storageNodeImage"`
	GcpShootVPCCidr   string `yaml:"gcpShootVpcCidr" default:"10.250.0.0/16"`
	GcpInternalLBCidr string `yaml:"gcpInternalLBCidr" default:"10.251.0.0/16"`
}

// InfraAPIConfig is the configration items for infra API operations.
type InfraAPIConfig struct {
	// kubeconfig file for the infra API server, ignore this to use the default secret
	KubeConfigPath string `yaml:"kubeConfigPath"`

	// ref https://github.com/tidbcloud/infra-api/pull/79
	// Enable this too keep the 'key' of resources do not change in infra-api.
	// will use projectID@dbaas as projectID@infra-api
	ProjectIDAsInfraProjectID bool `yaml:"projectIDAsInfraProjectID"`

	// flags to control switch to use infra-api
	// these flags can be removed after migrated to infra-api
	EnableNetwork            bool `yaml:"enableNetwork"`
	EnableBackupRestoreRead  bool `yaml:"enableBackupRestoreRead"`
	EnableBackupRestoreWrite bool `yaml:"enableBackupRestoreWrite"`
	EnableNetworkPeerRead    bool `yaml:"enableNetworkPeerRead"`
	EnableNetworkPeerWrite   bool `yaml:"enableNetworkPeerWrite"`
	EnableClusterRead        bool `yaml:"enableClusterRead"`
	EnableClusterWrite       bool `yaml:"enableClusterWrite"`
	EnableImportRead         bool `yaml:"enableImportRead"`
	EnableImportWrite        bool `yaml:"enableImportWrite"`
}

// Validate return error if the config is not valid.
func (ic *InfraAPIConfig) Validate() error {
	if ic.EnableClusterWrite && !ic.EnableNetwork {
		return errors.New("must enable network if enable cluster")
	}
	return nil
}

// NeedInit returns true if we need to initialize the infra API.
func (ic *InfraAPIConfig) NeedInit() bool {
	return ic.EnableNetwork || ic.EnableBackupRestoreRead || ic.EnableBackupRestoreWrite || ic.EnableNetworkPeerRead || ic.EnableNetworkPeerWrite ||
		ic.EnableClusterRead ||
		ic.EnableClusterWrite || ic.EnableImportRead || ic.EnableImportWrite
}

type MysqlConfig struct {
	Host     string `yaml:"host" default:"host"`
	User     string `yaml:"user" default:"root"`
	Port     uint16 `yaml:"port" default:"4000"`
	Password string `yaml:"password" required:"true"`
	Database string `yaml:"database" default:"dbaas"`
}

type TLSConfig struct {
	TlsCA   string `json:"ca.crt"`
	TlsCert string `json:"tls.crt"`
	TlsKey  string `json:"tls.key"`
}

// use a single instance of Validate, it caches struct info
var validate = validator.New()

// InitConfig parse the central configuration file and init the global config var
func InitConfig(path string) error {
	yamlFile, err := ioutil.ReadFile(filepath.Clean(path))
	if err != nil {
		return errors.Annotate(err, "failed to read config file")
	}
	err = yaml.Unmarshal(yamlFile, &globalConf)
	if err != nil {
		return errors.Annotate(err, "failed to unmarshal config file")
	}

	// let's load some secret from aws and merge it
	if globalConf.ServerConfig.SecretStore != nil {
		secretConfig, err := GetSecretFromAwsSecretManager(globalConf.ServerConfig.SecretStore.SecretKey, globalConf.ServerConfig.SecretStore.Region)
		if err != nil {
			return errors.Annotate(err, "failed to load secret config from aws")
		}
		var s CentralConfig
		if err := yaml.Unmarshal([]byte(secretConfig), &s); err != nil {
			log.Error("failed to unmarshal secret config", zap.String("secretConfig", secretConfig))
			return errors.Annotate(err, "failed to unmarshal secret config")
		}
		if err := mergo.Merge(&globalConf, s); err != nil {
			return errors.Annotate(err, "failed to merge secret config")
		}
	}
	setSecretDefault()

	if err := defaults.Set(&globalConf); err != nil {
		return errors.Annotate(err, "failed to set default values")
	}

	// PocManualBackupReservedDays,  PocAutoBackupReservedDays and OnDemandAutoBackupReservedDays should not be nil
	if globalConf.ServerConfig.PocManualBackupReservedDays == nil || globalConf.ServerConfig.PocAutoBackupReservedDays == nil || globalConf.ServerConfig.OnDemandAutoBackupReservedDays == nil {
		return errors.New("manual/auto backup reserved days can not be empty")
	}

	if globalConf.ServerConfig.FreeTrialCreditAmount == nil {
		return errors.New("freeTrialCreditAmount can not be empty")
	}
	if globalConf.ServerConfig.MaxFreeTrialOrgs == nil {
		return errors.New("MaxFreeTrialOrgs can not be empty")
	}

	if _, err := resource.ParseQuantity(globalConf.ImportConfig.StorageRequest); err != nil {
		return errors.Annotatef(err, "invalid import.storageRequest value: %s", globalConf.ImportConfig.StorageRequest)
	}

	//validate the values
	if err := validate.Struct(&globalConf); err != nil {
		return errors.Annotate(err, "failed to validate the config values")
	}

	initInfraContext()
	err = initSnowflakeNode(globalConf.ServerConfig.SnowflakeID)
	if err != nil {
		return errors.Annotate(err, "initialize snowflake node failed")
	}

	err = globalConf.InfraAPIConfig.Validate()
	if err != nil {
		return err
	}

	return nil
}

func Global() *CentralConfig {
	return &globalConf
}

func LoadTopSQLTLS() *TLSConfig {
	conf := topSQLTLSConf.Load().(TLSConfig)
	return &conf
}

func StoreTopSQLTLS(conf TLSConfig) {
	topSQLTLSConf.Store(conf)
}

// TODO: unify the usage of setDefault, see viper.SetDefault(), defaults.Set(), and this one
func setSecretDefault() {
	if globalConf.Auth0Config.Auth0Domain == "" {
		globalConf.Auth0Config.Auth0Domain = constants.Auth0Domain
	}
	if globalConf.Auth0Config.Auth0Secret == "" {
		globalConf.Auth0Config.Auth0Secret = constants.Auth0Secret
	}
	if globalConf.Auth0Config.Auth0Audience == "" {
		globalConf.Auth0Config.Auth0Audience = constants.Auth0CentralAudience
	}
	if globalConf.Auth0Config.ManagementPortalAuth0ClientAudience == "" {
		globalConf.Auth0Config.ManagementPortalAuth0ClientAudience = constants.Auth0CentralM2MAPI
	}
	if globalConf.Auth0Config.ManagementPortalAuth0Domain == "" {
		globalConf.Auth0Config.ManagementPortalAuth0Domain = globalConf.Auth0Config.Auth0Domain
	}
	if globalConf.Auth0Config.ManagementPortalAuth0Secret == "" {
		globalConf.Auth0Config.ManagementPortalAuth0Secret = globalConf.Auth0Config.Auth0Secret
	}
	if globalConf.Auth0Config.ManagementPortalAuth0Audience == "" {
		globalConf.Auth0Config.ManagementPortalAuth0Audience = globalConf.Auth0Config.Auth0Audience
	}
	if globalConf.BillingConfig.Auth0Domain == "" {
		globalConf.BillingConfig.Auth0Domain = constants.Auth0Domain
	}
	if globalConf.BillingConfig.Auth0ClientID == "" {
		globalConf.BillingConfig.Auth0ClientID = constants.Auth0BillingClientID
	}
	if globalConf.BillingConfig.Auth0Audience == "" {
		globalConf.BillingConfig.Auth0Audience = constants.Auth0BillingM2MAPI
	}
	if globalConf.BillingConfig.Auth0ClientSecret == "" {
		globalConf.BillingConfig.Auth0ClientSecret = constants.Auth0BillingClientSecret
	}
	if globalConf.ProxyConfig.Auth0Domain == "" {
		globalConf.ProxyConfig.Auth0Domain = constants.Auth0Domain
	}
	if globalConf.ProxyConfig.Auth0ClientID == "" {
		globalConf.ProxyConfig.Auth0ClientID = constants.Auth0ProxyClientID
	}
	if globalConf.ProxyConfig.Auth0Audience == "" {
		globalConf.ProxyConfig.Auth0Audience = constants.Auth0ProxyM2MAPI
	}
	if globalConf.ProxyConfig.Auth0ClientSecret == "" {
		globalConf.ProxyConfig.Auth0ClientSecret = constants.Auth0ProxyClientSecret
	}
	if globalConf.GrafanaConfig.APIKey == "" {
		globalConf.GrafanaConfig.APIKey = constants.GrafanaApiKey
	}
}

func GetSecretFromAwsSecretManager(secretName, region string) (string, error) {
	awsConfig := aws.NewConfig().WithMaxRetries(3).WithRegion(region)
	opts := session.Options{Config: *awsConfig}
	sess, err := session.NewSessionWithOptions(opts)
	if err != nil {
		return "", err
	}
	svc := secretsmanager.New(sess)
	input := &secretsmanager.GetSecretValueInput{
		SecretId:     aws.String(secretName),
		VersionStage: aws.String("AWSCURRENT"), // VersionStage defaults to AWSCURRENT if unspecified
	}
	result, err := svc.GetSecretValue(input)
	if err != nil {
		return "", err
	}
	return *result.SecretString, nil
}

var node *snowflake.Node

// initSnowflakeNode init a snowflake node
func initSnowflakeNode(nodeId int64) error {
	var err error
	node, err = snowflake.NewNode(nodeId)
	if err != nil {
		return err
	}
	return err
}

// GetSnowflakeNode return a snowflake node
func GetSnowflakeNode() *snowflake.Node {
	// for ut test cases
	if node == nil {
		_ = initSnowflakeNode(1)
	}
	return node
}

func IsGracefulShutdownWebhookEnabled(provider Cloud) bool {
	switch provider {
	case Gcp:
		return true
	case Aws:
		return Global().ServerConfig.EnableGracefulShutdownWebhookOnAWS
	default:
		return false
	}
}
