package console

import (
	"context"
	"strconv"
	"strings"
	"unicode/utf8"

	"github.com/tidbcloud/pkgv2/bizerr"
	"github.com/tidbcloud/serverless-service/idl/pbgen/proto/console"
	"github.com/tidbcloud/serverless-service/internal/errorcode"
	globalserverlessv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/serverless/v1"
	tidbmgmtservicev1 "github.com/tidbcloud/tidb-management-service/api/spec/tidb_mgmt_service/v1"
	"go.uber.org/zap"
)

// maxMigrationTaskNameLen defines the maximum allowed length for a migration task name.
const maxMigrationTaskNameLen = 64

func validateCreateTaskReq(req *console.CreateMigrationTaskReq) bizerr.BizError {
	// Validate task name
	req.Name = strings.TrimSpace(req.GetName())
	if req.GetName() == "" {
		return bizerr.New(bizerr.ErrCodeParams, "task name should not be empty")
	}
	if !utf8.ValidString(req.GetName()) {
		return bizerr.New(bizerr.ErrCodeParams, "task name contains invalid UTF-8 characters")
	}
	if len(req.GetName()) > maxMigrationTaskNameLen {
		return bizerr.New(
			bizerr.ErrCodeParams,
			"task name is too long, maximum %d characters allowed",
			maxMigrationTaskNameLen,
		)
	}

	// Validate mode
	if req.GetMode() == console.TaskMode_MODE_UNSPECIFIED {
		return bizerr.New(bizerr.ErrCodeParams, "DM task mode is required")
	}

	// Validate sources
	if len(req.GetSources()) != 1 {
		return bizerr.New(bizerr.ErrCodeParams, "only one source is supported currently")
	}

	for i, source := range req.GetSources() {
		if bizErr := validateSource(source, i, req.FullInstanceMigration); bizErr != nil {
			return bizErr
		}
	}

	// Validate target database
	if req.GetTarget() == nil {
		return bizerr.New(bizerr.ErrCodeParams, "target_db is required")
	}
	if bizErr := validateTargetDatabase(req.GetTarget()); bizErr != nil {
		return bizErr
	}

	return nil
}

func validateGetSourceTablesReq(req *console.MigrationSourceDBConnProfile) bizerr.BizError {
	return validateConnProfile(req.GetConnProfile(), 0)
}

func validateMigrationSourceConnReq(req *console.MigrationSourceDBConnProfile) bizerr.BizError {
	return validateConnProfile(req.GetConnProfile(), 0)
}

func validateMigrationTargetConnReq(req *console.MigrationTargetDBConnProfile) bizerr.BizError {
	req.ClusterId = strings.TrimSpace(req.GetClusterId())
	req.User = strings.TrimSpace(req.GetUser())

	if req.User == "" {
		return bizerr.New(bizerr.ErrCodeParams, "user is required")
	}
	if req.Password == "" {
		return bizerr.New(bizerr.ErrCodeParams, "password is required")
	}
	return nil
}

func (s *DMService) validateCluster(
	ctx context.Context,
	clusterID string,
	logger *zap.Logger,
) (*tidbmgmtservicev1.Cluster, bizerr.BizError) {
	clusterIDUint, err := strconv.ParseUint(clusterID, 10, 64)
	if err != nil {
		return nil, bizerr.New(bizerr.ErrCodeParams, "Invalid cluster ID: %s", clusterID)
	}

	cluster, bizErr := s.mgmtSvc.GetCluster(ctx, clusterIDUint, tidbmgmtservicev1.ClusterFilter_CLUSTER_VIEW_BASIC)
	if bizErr != nil {
		logger.Error("GetClusterByID failed", zap.String("cluster_id", clusterID), zap.Error(bizErr))
		return nil, bizErr
	}

	if cluster.GetServerlessCluster().
		GetUserStatus() !=
		globalserverlessv1.UserClusterStatus_USER_CLUSTER_STATUS_NORMAL {
		return nil, bizerr.New(
			errorcode.ErrCodeClusterNotRunning,
			"Cluster ID %s is not in normal state, please try again later.",
			clusterID,
		)
	}
	// check if the cluster is essential plan
	if cluster.GetServerlessCluster().GetCluster().GetAutoScaling().GetMinRcu() == 0 &&
		cluster.GetServerlessCluster().GetCluster().GetAutoScaling().GetMaxRcu() == 0 {
		return nil, bizerr.New(
			bizerr.ErrCodeParams,
			"Current Data Migration service is only supported for Essential plan clusters.",
		)
	}
	return cluster, nil
}

// validateSource validates a single source configuration
func validateSource(source *console.Source, index int, migrationFull bool) bizerr.BizError {
	if source == nil {
		return bizerr.New(bizerr.ErrCodeParams, "source[%d] is empty", index)
	}

	if bizErr := validateConnProfile(source.GetConnProfile(), index); bizErr != nil {
		return bizErr
	}

	// For full instance migration, set both block and allow rules to nil so that all non-system databases and tables are included.
	// Note: Do not use '*' — the system database filter only takes effect when no block or allow rules are defined.
	if migrationFull {
		source.BaRules = nil
	}
	// Validate block allow rules (optional but if present must be valid)
	if source.GetBaRules() != nil {
		if bizErr := validateBlockAllowRules(source.GetBaRules()); bizErr != nil {
			return bizErr
		}
	}

	if source.GetBinlogPos() < 0 {
		return bizerr.New(bizerr.ErrCodeParams, "source[%d] binlog_pos must be non-negative", index)
	}

	return nil
}

// validateConnProfile validates connection profile
func validateConnProfile(connProfile *console.ConnProfile, sourceIndex int) bizerr.BizError {
	if connProfile == nil {
		return bizerr.New(bizerr.ErrCodeParams, "source[%d] conn_profile is required", sourceIndex)
	}
	if connProfile.GetConnType() == console.ConnType_CONN_TYPE_UNSPECIFIED {
		return bizerr.New(bizerr.ErrCodeParams, "conn_profile.conn_type is required")
	}

	// Validate host
	host := strings.TrimSpace(connProfile.GetHost())
	id := strings.TrimSpace(connProfile.GetEndpointId())
	if connProfile.GetConnType() == console.ConnType_PUBLIC && host == "" {
		return bizerr.New(bizerr.ErrCodeParams, "source[%d] host is required for public connection", sourceIndex)
	}
	if connProfile.GetConnType() == console.ConnType_PRIVATE_LINK && id == "" {
		return bizerr.New(
			bizerr.ErrCodeParams,
			"source[%d] endpoint_id is required for private connection",
			sourceIndex,
		)
	}

	// Validate user
	connProfile.User = strings.TrimSpace(connProfile.GetUser())
	if connProfile.User == "" {
		return bizerr.New(bizerr.ErrCodeParams, "source[%d] user is empty", sourceIndex)
	}
	return nil
}

// validateBlockAllowRules validates block/allow rules for table filtering
// Wildcards are not supported, besides the special case of "*" or "~.*" for matching all tables.
func validateBlockAllowRules(rules *console.BlockAllowRules) bizerr.BizError {
	if rules == nil {
		return nil
	}

	// Validate do_dbs
	for i := range rules.GetDoDbs() {
		rules.GetDoDbs()[i] = strings.TrimSpace(rules.GetDoDbs()[i])
		if bizErr := validateNoWildcard(rules.GetDoDbs()[i], "do_dbs contains unsupported wildcard characters"); bizErr != nil {
			return bizErr
		}
	}

	// Validate do_tables
	for i := range rules.GetDoTables() {
		rules.GetDoTables()[i].Schema = strings.TrimSpace(rules.GetDoTables()[i].GetSchema())
		rules.GetDoTables()[i].Table = strings.TrimSpace(rules.GetDoTables()[i].GetTable())
		table := rules.GetDoTables()[i]
		if bizErr := validateNoWildcard(table.Schema, "do_tables.schema contains unsupported wildcard characters"); bizErr != nil {
			return bizErr
		}

		// Skip validation for patterns that match all tables
		if table.Table == "~.*" || table.Table == "*" {
			continue
		}
		if bizErr := validateNoWildcard(table.Table, "do_tables.table contains unsupported wildcard characters"); bizErr != nil {
			return bizErr
		}
	}

	return nil
}

// validateTargetDatabase validates target database configuration
func validateTargetDatabase(targetDb *console.Target) bizerr.BizError {
	if targetDb == nil {
		return bizerr.New(bizerr.ErrCodeParams, "target_db is required")
	}

	// Validate user
	targetDb.User = strings.TrimSpace(targetDb.GetUser())
	if targetDb.User == "" {
		return bizerr.New(bizerr.ErrCodeParams, "target_db user is empty")
	}

	// Validate password
	if targetDb.Password == "" {
		return bizerr.New(bizerr.ErrCodeParams, "target_db password is required")
	}

	return nil
}

func validateCreateEndpointReq(req *console.CreateEndpointReq) bizerr.BizError {
	if strings.TrimSpace(req.ServiceName) == "" {
		return bizerr.New(bizerr.ErrCodeParams, "service_name is required")
	}
	return nil
}

// validateNoWildcard ensures the provided value does not include unsupported wildcard characters.
func validateNoWildcard(value, errMsg string) bizerr.BizError {
	if value == "" {
		return nil
	}
	if strings.ContainsAny(value, "*?[]") {
		return bizerr.New(bizerr.ErrCodeParams, "%s", errMsg)
	}
	return nil
}
