package entity

import (
	"encoding/json"
	"slices"
	"strconv"
	"strings"
	"time"

	tablefilter "github.com/pingcap/tidb/pkg/util/table-filter"
	"github.com/tidbcloud/pkgv2/bizerr"
	"github.com/tidbcloud/serverless-service/internal/domain/constant"
	regionalserverlessv1 "github.com/tidbcloud/tidb-management-service/api/spec/regional/serverless/v1"
	tidbmgmtservicev1 "github.com/tidbcloud/tidb-management-service/api/spec/tidb_mgmt_service/v1"
)

const (
	AuditLogFilterRuleMaxDisplayNameLength = 255
	FilterRuleUsersMaxNumber               = 3
	MinAuditlogRotationIntervalSeconds     = 600                // 10 minute
	MaxAuditlogRotationIntervalSeconds     = 86400              // 24 hours
	DefaultAuditlogRotationIntervalSeconds = 3600               // 1 hour
	MinAuditlogRotationSizeBytes           = 1 * 1024 * 1024    // 1 MB
	MaxAuditlogRotationSizeBytes           = 1024 * 1024 * 1024 // 1024 MB
	DefaultAuditlogRotationSizeBytes       = 100 * 1024 * 1024  // 100 MB
)

type AuditLogFilterRule struct {
	ID        string            `gorm:"column:id;type:varchar(255);primaryKey;not null"`
	TenantID  string            `gorm:"column:tenant_id;type:varchar(255);not null"`
	ClusterID string            `gorm:"column:cluster_id;type:varchar(255);not null;index:idx_cluster_id"`
	Name      string            `gorm:"column:name;type:varchar(255);not null"`
	Users     []string          `gorm:"column:users;type:json;serializer:json;not null"`
	Filters   []*AuditLogFilter `gorm:"column:filters;type:json;serializer:json;not null"`
	Disabled  *bool             `gorm:"column:disabled;default:false"`
	CreatedAt time.Time         `gorm:"column:created_at;type:timestamp;not null;autoCreateTime;index:idx_created_at"`
	UpdatedAt *time.Time        `gorm:"column:updated_at;type:timestamp;null;default:null;autoUpdateTime"`
}

func (f *AuditLogFilterRule) TableName() string {
	return "auditlog_filters"
}

// The filter follows the tidb-cse definition:
type AuditLogFilter struct {
	Classes        []string `json:"class,omitempty"`
	ClassesExclude []string `json:"class_excl,omitempty"`
	Tables         []string `json:"table,omitempty"`
	TablesExclude  []string `json:"table_excl,omitempty"`
	StatusCodes    []int32  `json:"status_code,omitempty"`
}

type AuditLogFilterRuleFilter struct {
	ClusterID string
	Limit     *int32
	Offset    *int64
	OrderBy   string
}

func (f *AuditLogFilterRule) FillWithCluster(cluster *tidbmgmtservicev1.Cluster) {
	f.TenantID = strconv.FormatUint(cluster.OrganizationId, 10)
	f.ClusterID = strconv.FormatUint(cluster.ClusterId, 10)
}

// convert the filter in filterRule to content, example: { "filter": [ { "class": ["QUERY_DML"], "table": ["mysql.*"] } ] }'
func (f *AuditLogFilterRule) ToContent() (string, error) {
	// empty filter will be converted to {} to filter all the logs
	if len(f.Filters) == 0 {
		return "{}", nil
	}

	// Create a map to hold the filter data
	filterData := map[string]any{
		"filter": f.Filters,
	}

	// Marshal the filter data into a JSON string
	content, err := json.Marshal(filterData)
	if err != nil {
		return "", err
	}

	return string(content), nil
}

func (f *AuditLogFilterRule) Validate(userPrefix string) bizerr.BizError {
	if len(f.Name) > AuditLogFilterRuleMaxDisplayNameLength {
		return bizerr.New(
			bizerr.ErrCodeParams,
			"filter rule name exceeds maximum length of %d characters",
			AuditLogFilterRuleMaxDisplayNameLength)
	}
	if err := f.ValidateUser(userPrefix); err != nil {
		return err
	}
	return f.ValidateFilter()
}

// ValidateUser will check user the same tidb-cse
func (f *AuditLogFilterRule) ValidateUser(userPrefix string) bizerr.BizError {
	if len(f.Users) > FilterRuleUsersMaxNumber {
		return bizerr.New(
			bizerr.ErrCodeParams,
			"you can only set up to %d users for a filter rule",
			FilterRuleUsersMaxNumber,
		)
	}
	for _, user := range f.Users {
		if err := normalizeUserAndHost(user); err != nil {
			return err
		}
	}
	// rules in serverless
	for _, user := range f.Users {
		if !strings.HasPrefix(user, "%") && !strings.HasPrefix(user, userPrefix) {
			return bizerr.New(
				bizerr.ErrCodeParams,
				"User name must start with `%s.` (use `%s.%s` instead), see https://docs.pingcap.com/tidbcloud/select-cluster-tier#user-name-prefix for more details.",
				userPrefix,
				userPrefix,
				user,
			)
		}
	}
	return nil
}

const UserHostSep = "@"

func normalizeUserAndHost(userAndHost string) bizerr.BizError {
	parts := strings.Split(userAndHost, UserHostSep)
	if len(parts) != 1 && len(parts) != 2 {
		return bizerr.New(bizerr.ErrCodeParams, "illegal user format: '%s'", userAndHost)
	}
	return nil
}

var eventClasses = []string{
	"CONNECTION",
	"CONNECT",
	"DISCONNECT",
	"CHANGE_USER",
	"QUERY",
	"TRANSACTION",
	"EXECUTE",
	"QUERY_DML",
	"INSERT",
	"REPLACE",
	"UPDATE",
	"DELETE",
	"LOAD_DATA",
	"SELECT",
	"QUERY_DDL",
	"AUDIT",
	"AUDIT_SET_SYS_VAR",
	"AUDIT_FUNC_CALL",
	"AUDIT_ENABLE",
	"AUDIT_DISABLE",
}

// ValidateFilter will check filter the same tidb-cse
func (f *AuditLogFilterRule) ValidateFilter() bizerr.BizError {
	for _, f := range f.Filters {
		for _, c := range f.Classes {
			if !isValidEventClass(c) {
				return bizerr.New(bizerr.ErrCodeParams, "invalid event class name '%s'", c)
			}
		}
		for _, c := range f.ClassesExclude {
			if !isValidEventClass(c) {
				return bizerr.New(bizerr.ErrCodeParams, "invalid event class name '%s'", c)
			}
		}
		for _, t := range f.Tables {
			if err := parseTableFilter(t); err != nil {
				return err
			}
		}
		for _, t := range f.TablesExclude {
			if err := parseTableFilter(t); err != nil {
				return err
			}
		}
	}
	return nil
}

func isValidEventClass(s string) bool {
	return slices.Contains(eventClasses, s)
}

// follows the tidb-cse method
func parseTableFilter(table string) bizerr.BizError {
	// forbid wildcards '!' and '@' for safety,
	// please see https://github.com/pingcap/tidb/pkg-tools/tree/master/pkg/table-filter for more details.
	arg := strings.TrimLeft(table, " \t")
	if arg == "" || arg[0] == '!' || arg[0] == '@' || arg[0] == '/' {
		return bizerr.New(bizerr.ErrCodeParams, "invalid table format '%s'", table)
	}

	_, err := tablefilter.Parse([]string{arg})
	if err != nil {
		return bizerr.New(bizerr.ErrCodeParams, "invalid table format '%s'", table)
	}

	return nil
}

type AuditLogConfig struct {
	ClusterID               string                    `gorm:"column:cluster_id;type:varchar(255);not null;primaryKey"`
	TenantID                string                    `gorm:"column:tenant_id;type:varchar(255);not null"`
	ProjectID               string                    `gorm:"column:project_id;type:varchar(255);not null"`
	CloudProvider           string                    `gorm:"column:cloud_provider;type:varchar(32);not null"`
	Region                  string                    `gorm:"column:region;type:varchar(64);not null"`
	Kubernetes              string                    `gorm:"column:kubernetes;type:varchar(32);not null"`
	Enabled                 *bool                     `gorm:"column:enabled;type:tinyint(1);not null"`
	Unredacted              *bool                     `gorm:"column:unredacted;type:tinyint(1);;not null"`
	CloudStorage            *CloudStorage             `gorm:"column:cloud_storage;type:json;serializer:json;default:null"`
	CloudStorageType        constant.CloudStorageType `gorm:"column:cloud_storage_type;type:varchar(32);not null"`
	RotationIntervalSeconds int64                     `gorm:"column:rotation_interval_seconds;type:bigint(20);default:0"`
	RotationSizeBytes       int64                     `gorm:"column:rotation_size_bytes;type:bigint(20);default:0"`
	UpdatedAt               *time.Time                `gorm:"column:updated_at;type:timestamp;null;default:null;autoUpdateTime"`
}

func (a *AuditLogConfig) TableName() string {
	return "auditlog_config"
}

func (a *AuditLogConfig) FillWithCluster(cluster *tidbmgmtservicev1.Cluster) {
	a.ProjectID = strconv.FormatUint(cluster.ProjectId, 10)
	a.TenantID = strconv.FormatUint(cluster.OrganizationId, 10)
	a.ClusterID = strconv.FormatUint(cluster.ClusterId, 10)
	a.CloudProvider = cluster.CloudProvider
	a.Region = cluster.Region
	a.Kubernetes = cluster.GetServerlessCluster().GetCluster().GetKubernetes()
}

func (a *AuditLogConfig) HideSecret() {
	switch a.CloudStorageType {
	case constant.CloudStorageTypeS3:
		s3 := a.CloudStorage.S3
		if s3.AccessKey != nil {
			s3.AccessKey.SecretAccessKey = ""
		}
	case constant.CloudStorageTypeOSS:
		oss := a.CloudStorage.OSS
		if oss.AccessKey != nil {
			oss.AccessKey.AccessKeySecret = ""
		}
	case constant.CloudStorageTypeAzureBlob:
		azureBlob := a.CloudStorage.AzureBlob
		if azureBlob.SasToken != "" {
			azureBlob.SasToken = ""
		}
	case constant.CloudStorageTypeGCS:
		gcs := a.CloudStorage.GCS
		if gcs.ServiceAccountKey != "" {
			gcs.ServiceAccountKey = ""
		}
	}
}

func (a *AuditLogConfig) Validate(cluster *tidbmgmtservicev1.Cluster) bizerr.BizError {
	if a.RotationIntervalSeconds != 0 {
		if a.RotationIntervalSeconds%60 != 0 {
			return bizerr.New(bizerr.ErrCodeParams, "rotation interval must be in minutes")
		}
		if a.RotationIntervalSeconds < MinAuditlogRotationIntervalSeconds ||
			a.RotationIntervalSeconds > MaxAuditlogRotationIntervalSeconds {
			return bizerr.New(
				bizerr.ErrCodeParams,
				"rotation interval minutes must be between %d and %d",
				MinAuditlogRotationIntervalSeconds/60,
				MaxAuditlogRotationIntervalSeconds/60,
			)
		}
	}

	if a.RotationSizeBytes != 0 {
		if a.RotationSizeBytes%(1024*1024) != 0 {
			return bizerr.New(bizerr.ErrCodeParams, "rotation size must be in MiB")
		}
		if a.RotationSizeBytes < MinAuditlogRotationSizeBytes ||
			a.RotationSizeBytes > MaxAuditlogRotationSizeBytes {
			return bizerr.New(
				bizerr.ErrCodeParams,
				"rotation size mib must be between %d and %d",
				MinAuditlogRotationSizeBytes/1024/1024,
				MaxAuditlogRotationSizeBytes/1024/1024,
			)
		}
	}
	return a.CloudStorage.Validate(cluster.CloudProvider)
}

func (a *AuditLogConfig) ToRegional() *regionalserverlessv1.ServerlessRegionalServiceClusterInfo_AuditLogConfig {
	auditLogConfig := &regionalserverlessv1.ServerlessRegionalServiceClusterInfo_AuditLogConfig{
		Enabled:      a.Enabled,
		Unredacted:   a.Unredacted,
		CloudStorage: a.CloudStorage.ToRegionalCloudStorage(),
	}
	if a.RotationIntervalSeconds > 0 {
		auditLogConfig.RotationIntervalSeconds = &a.RotationIntervalSeconds
	}
	if a.RotationSizeBytes > 0 {
		auditLogConfig.RotationSizeBytes = &a.RotationSizeBytes
	}

	return auditLogConfig
}
