package config

import (
	"fmt"
	"net/url"
	"strings"

	"github.com/tidbcloud/pkgv2/gormutil"
)

type CentralConfig struct {
	Env          string       `mapstructure:"env"`
	ServerConfig ServerConfig `mapstructure:"server"`

	// cloud configs
	MysqlConfig gormutil.Config `mapstructure:"mysql"`

	InfraConfig InfraConfig `mapstructure:"infra"`

	// internal configs
	BillingConfig   BillingConfig      `mapstructure:"billing"`
	GrpcConfig      GrpcConfig         `mapstructure:"grpc"`
	O11yConfig      O11yConfig         `mapstructure:"o11y"`
	PremiumDMConfig PremiumDMConfig    `mapstructure:"premium_dm"`
	ImportConfig    ImportIntoConfig   `mapstructure:"importInto"`
	DataPipeline    DataPipelineConfig `mapstructure:"dataPipeline"`

	DebugConfig DebugConfig `mapstructure:"debugConfig"`
}

func (p *CentralConfig) Get() *CentralConfig {
	return p
}

type O11yConfig struct {
	GlobalServerDomain string `mapstructure:"globalServerDomain" validate:"required"`
	BizType            string `mapstructure:"bizType" validate:"required"`
	Auth0Domain        string `mapstructure:"auth0Domain" validate:"required"`
	Auth0ClientID      string `mapstructure:"auth0ClientID" validate:"required"`
	Auth0Audience      string `mapstructure:"auth0Audience" validate:"required"`
}

type DebugConfig struct {
	Enable            bool   `mapstructure:"enable"  default:"false"`
	DisableKafka      bool   `mapstructure:"disableKafka"  default:"false"`
	KafkaProxyAddress string `mapstructure:"kafkaProxyAddress"  default:"127.0.0.1:9092"`
	InfraConfig       string `mapstructure:"infraConfig"  default:""`
	// EnableConfigCenter whether to use config center to replace gitops
	EnableConfigCenter bool `mapstructure:"enableConfigCenter"  default:"false"`
	// TiCDCServiceServer is the address of TiCDC service server
	TiCDCServiceServer string `mapstructure:"tiCDCServiceServer" default:""`
}

type GrpcConfig struct {
	AccountProviderAddr string `mapstructure:"accountProviderSvcAddr" default:"kubernetes:///account-provider-grpc:10000"`
	AccountAclSvcAddr   string `mapstructure:"accountAclSvcAddr" default:"kubernetes:///account-acl-grpc:10000"`
	TiDBMgmtSvcAddr     string `mapstructure:"tidbMgmtSvcAddr" default:"kubernetes:///tidb-mgmt-service.tidb-management-service:10001"`
	ConfigCenterAddr    string `mapstructure:"configCenterAddr" default:"kubernetes:///config-center.devops:8080"`
	MgmtSvcProxyAddr    string `mapstructure:"mgmtSvcProxyAddr" default:"http://tidb-mgmt-service.tidb-management-service:8080"`
	MgmtSvcProxyToken   string `mapstructure:"mgmtSvcProxyToken" default:""`
}

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

	GrpcPort      int `mapstructure:"grpcPort" default:"10000"`
	HttpPort      int `mapstructure:"httpPort" default:"11000"`
	InnerHttpPort int `mapstructure:"innerHttpPort" default:"12000"`

	// the central metrics port
	MetricsPort uint16 `mapstructure:"metricsPort" default:"9090"`
	// session secret salt conf
	SaltSecret string `mapstructure:"saltSecret" required:"true"`

	SecretStore *SecretConfig `mapstructure:"secret"`

	// RBACBypassUserEmails bypasses RBAC permission checks for listed user emails.
	RBACBypassUserEmails []string `mapstructure:"rbacBypassUserEmails"`

	// general allowlist config
	FeaturesSwitchers FeaturesSwitchersConfig `mapstructure:"featuresSwitchersConfig"`
}

type FeaturesSwitchersConfig struct {
	// PublicFeatures public available feature, if a feature is defined here, same feature defined in
	// EnabledFeatureByDeployment and FeaturesAllowList will not take effects.
	PublicFeatures []string `mapstructure:"publicFeatures"`
	// EnabledFeatureByDeployment enable features by defined deployment, i.e. which region and which provider
	// if a feature is enabled by this, same feature defined in FeaturesAllowList will not take effects
	EnabledFeatureByDeployment map[string]DeploymentMatcher `mapstructure:"enabledFeatureByDeployment"`
	// FeaturesAllowList enable feature by cluster/project/tenant id, if input cluster matches any of defined
	// cluster/project/tenant id of some feature, the feature is enabled.
	FeaturesAllowList map[string]AllowListMatcher `mapstructure:"featuresAllowList"`
}

type AllowListMatcher struct {
	TenantID          []string           `mapstructure:"tenantID"`
	ProjectID         []string           `mapstructure:"projectID"`
	ClusterID         []string           `mapstructure:"clusterID"`
	DeploymentMatcher *DeploymentMatcher `mapstructure:"deploymentMatcher"`
}

type ImportIntoConfig struct {
	MaxRetryTimesOnExecImportingQuerySQL uint `mapstructure:"maxRetryTimesOnExecImportingQuerySQL" yaml:"maxRetryTimesOnExecImportingQuerySQL" default:"3"`
	MaxImportTasksRetrievedOnWorker      int  `mapstructure:"maxImportTasksRetrievedOnWorker" yaml:"maxImportTasksRetrievedOnWorker" default:"100"`

	WriteEnabled  FeatureGate `mapstructure:"writeEnabled" yaml:"writeEnabled"`
	WorkerEnabled FeatureGate `mapstructure:"workerEnabled" yaml:"workerEnabled"`
}

// SecretString is a configuration value that must never reach a log. The whole
// CentralConfig is dumped with zap.Reflect at debug level, and zap encodes a
// reflected value as JSON, so a plain string field would be printed verbatim.
// This type marshals as a placeholder instead; call Reveal to use the value.
type SecretString string

func (s SecretString) MarshalJSON() ([]byte, error) { return []byte(`"[REDACTED]"`), nil }

// String is what fmt verbs and zap.Stringer see.
func (s SecretString) String() string { return "[REDACTED]" }

// Reveal returns the underlying secret. Every call site is a place to check
// that the value is not about to be logged or returned to a caller.
func (s SecretString) Reveal() string { return string(s) }

// DataPipelineConfig is the Data Pipeline runtime configuration. The Lake HMAC
// credential lives here alongside the base URL it authenticates against, and
// is loaded from the environment-specific secret store like every other
// secret-backed setting; SecretString keeps it out of the config debug dump.
type DataPipelineConfig struct {
	LakeBaseURL    string       `mapstructure:"lakeBaseURL" yaml:"lakeBaseURL" default:""`
	LakeHMACSecret SecretString `mapstructure:"lakeHMACSecret" yaml:"lakeHMACSecret" default:""`

	ObservationIntervalSeconds uint32 `mapstructure:"observationIntervalSeconds" yaml:"observationIntervalSeconds" default:"30" validate:"gte=5,lte=3600"`
	ObservationStaleSeconds    uint32 `mapstructure:"observationStaleSeconds" yaml:"observationStaleSeconds" default:"120" validate:"gte=15,lte=86400"`
}

// ValidateRuntime validates cross-field and secret-backed configuration. Lake
// settings are only required once a base URL is configured, so a deployment
// that does not run Data Pipeline starts without them.
func (c DataPipelineConfig) ValidateRuntime() error {
	if c.ObservationStaleSeconds < c.ObservationIntervalSeconds {
		return fmt.Errorf("data pipeline observation stale threshold must not be shorter than its interval")
	}
	baseURL := strings.TrimSpace(c.LakeBaseURL)
	if baseURL == "" {
		// Data Pipeline is simply not configured in this deployment.
		return nil
	}
	parsed, err := url.Parse(baseURL)
	if err != nil {
		return fmt.Errorf("parse data pipeline Lake base URL: %w", err)
	}
	if (parsed.Scheme != "http" && parsed.Scheme != "https") || parsed.Host == "" ||
		parsed.User != nil || parsed.RawQuery != "" || parsed.Fragment != "" ||
		(parsed.Path != "" && parsed.Path != "/") {
		return fmt.Errorf("data pipeline Lake base URL must be an HTTP(S) origin")
	}
	if strings.TrimSpace(c.LakeHMACSecret.Reveal()) == "" {
		return fmt.Errorf("data pipeline Lake HMAC secret is required when a Lake base URL is configured")
	}
	return nil
}

func (m *AllowListMatcher) Match(tenantID, projectID, clusterID uint64, region, infraProvider string) bool {
	tenantIDStr, projectIDStr, clusterIDStr := fmt.Sprintf("%d", tenantID),
		fmt.Sprintf("%d", projectID), fmt.Sprintf("%d", clusterID)

	matched := IsContain(m.TenantID, tenantIDStr) || IsContain(m.ProjectID, projectIDStr) ||
		IsContain(m.ClusterID, clusterIDStr)
	if m.DeploymentMatcher != nil {
		return matched && m.DeploymentMatcher.Match(region, infraProvider)
	}

	return matched
}

// DeploymentMatcher match by how a feature is deployed.
type DeploymentMatcher struct {
	Regions   []string `mapstructure:"regions"`
	Providers []string `mapstructure:"providers"` // this is infra provider, like eks, dev-tier, aws, gcp
}

// Match target cluster.
// todo: quite fixed, maybe support this by expression.
func (m *DeploymentMatcher) Match(region string, provider string) bool {
	return IsContain(m.Regions, region) && IsContain(m.Providers, provider)
}

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

// infra configurations
type InfraConfig struct {
	// dataflow service region
	Region string `mapstructure:"region" required:"true" default:"us-west-2"`
	CMKArn string `mapstructure:"cmkArn" required:"true"`
}

type BillingConfig struct {
	Billing2BaseURL   string `mapstructure:"billing2BaseURL" default:"http://billing2:18888" required:"true"`
	Billing2URLPrefix string `mapstructure:"billing2URLPrefix" default:"/api/v1/billing" required:"true"`
}

type PremiumDMConfig struct {
	ImportThreads int `mapstructure:"importThreads" default:"16"`
	ExportThreads int `mapstructure:"exportThreads" default:"4"`
	ReplThreads   int `mapstructure:"replThreads"   default:"16"`
	ReplBatch     int `mapstructure:"replBatch"     default:"100"`

	// PrecheckSkipConfigs skips specific precheck items for clusters matched by Matcher.
	// Each entry specifies a matcher and a list of DM checker task names to skip (e.g. version_checking).
	// Order matters: the first matching entry is applied.
	PrecheckSkipConfigs PrecheckSkipConfigs `mapstructure:"precheckSkipConfigs"`
}

// PrecheckSkipConfig skips specific precheck items for clusters matched by Matcher.
type PrecheckSkipConfig struct {
	Matcher   AllowListMatcher `mapstructure:"matcher"`
	SkipItems []string         `mapstructure:"skipItems"`
}

// PrecheckSkipConfigs is a list of PrecheckSkipConfig entries.
type PrecheckSkipConfigs []PrecheckSkipConfig

// GetSkippedItems returns all precheck items that should be skipped for the given tenant/project/cluster.
// The first matching entry wins.
func (c PrecheckSkipConfigs) GetSkippedItems(tenantID, projectID, clusterID uint64) map[string]bool {
	skipped := make(map[string]bool)
	for _, conf := range c {
		if !conf.Matcher.Match(tenantID, projectID, clusterID, "", "") {
			continue
		}

		for _, item := range conf.SkipItems {
			skipped[item] = true
		}
		break
	}
	return skipped
}

// ApplyBlacklist removes any items in the blacklist for the given tenant/project/cluster from items.
func (c PrecheckSkipConfigs) ApplyBlacklist(items []string, tenantID, projectID, clusterID uint64) []string {
	skipped := c.GetSkippedItems(tenantID, projectID, clusterID)

	result := make([]string, 0, len(items))
	for _, item := range items {
		if skipped[item] {
			continue
		}

		result = append(result, item)
	}
	return result
}

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

func IsContain(items []string, item string) bool {
	for _, v := range items {
		if v == item {
			return true
		}
	}

	return false
}
