package alert

import (
	"context"
	"fmt"
	"net/http"
	"net/mail"
	"net/netip"
	"net/url"
	"regexp"
	"strconv"
	"strings"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/pingcap/log"
	"github.com/prometheus/alertmanager/template"
	"go.uber.org/zap"
	"gorm.io/gorm"

	"github.com/tidbcloud/account/idl/pbgen/proto/account"

	"github.com/tidbcloud/tiinf/sdk/auditlog"

	"github.com/pingcap-inc/clinic_cloud_server/pkg/alert/notification"
	"github.com/pingcap-inc/clinic_cloud_server/pkg/alert/repo"
	"github.com/pingcap-inc/clinic_cloud_server/pkg/common"
	"github.com/pingcap-inc/clinic_cloud_server/pkg/db"
	"github.com/pingcap-inc/clinic_cloud_server/pkg/entity"
)

var (
	// The name must be 4-64 characters that can only include numbers, letters, and hyphens, and the first and last character must be a letter or number.
	nameRegexp = regexp.MustCompile(`^[A-Za-z0-9][-A-Za-z0-9]{2,62}[A-Za-z0-9]$`)
	// The token must be 1-128 characters that can only include numbers, letters, hyphens, underscores and dots.
	tokenRegexp = regexp.MustCompile(`^[a-zA-Z0-9._\-]{1,128}$`)
	// Lark custom bot hook IDs contain only letters, numbers, hyphens, and underscores.
	larkWebhookIDRegexp = regexp.MustCompile(`^[A-Za-z0-9_-]+$`)
)

func getOrgIdAndProjectIdAndSubscriberIdFromPath(c *gin.Context) (uint64, uint64, uint, error) {
	tenantID, projectID, err := getOrgIdAndProjectIdFromPath(c)
	if err != nil {
		return 0, 0, 0, err
	}
	subscriberIDStr := c.Param("alertSubscriberId")
	if subscriberIDStr == "" {
		return 0, 0, 0, fmt.Errorf("alertSubscriberId is required")
	}
	subscriberID, err := strconv.ParseUint(subscriberIDStr, 10, 64)
	if err != nil {
		return 0, 0, 0, fmt.Errorf("invalid alertSubscriberId")
	}
	return tenantID, projectID, uint(subscriberID), nil
}

func getOrgIdAndProjectIdFromPath(c *gin.Context) (uint64, uint64, error) {
	tenantIDStr := c.Param("orgId")
	projectIDStr := c.Param("projectId")
	if tenantIDStr == "" {
		return 0, 0, fmt.Errorf("orgId is required")
	}
	if projectIDStr == "" {
		return 0, 0, fmt.Errorf("projectId is required")
	}
	tenantID, err := strconv.ParseUint(tenantIDStr, 10, 64)
	if err != nil {
		return 0, 0, fmt.Errorf("invalid orgId")
	}
	projectID, err := strconv.ParseUint(projectIDStr, 10, 64)
	if err != nil {
		return 0, 0, fmt.Errorf("invalid projectId")
	}
	return tenantID, projectID, nil
}

// ListAlertSubscribers
//
//	@Id				ListAlertSubscribers
//	@Tags			alert subscriber
//	@Description	list alert subscribers
//	@Produce		json
//	@Param			orgId		path		string	true	"organization id"
//	@Param			projectId	path		string	true	"project id"
//	@Success		200			{object}	entity.ListAlertSubscribersResponse
//	@Failure		400			{object}	common.BaseResp
//	@Failure		500			{object}	common.BaseResp
//	@Router			/api/alert/v1/orgs/{orgId}/projects/{projectId}/alertSubscribers [get]
func (s *Service) ListAlertSubscribers(c *gin.Context) {
	tenantID, projectID, err := getOrgIdAndProjectIdFromPath(c)
	if err != nil {
		common.FailWithCode(c, http.StatusBadRequest, err.Error())
		return
	}

	alertSubscribers, err := s.listAlertSubscribers(
		c, tenantID, projectID, entity.ClusterDeployTypeDedicated, true)
	if err != nil {
		log.Error("list alert subscribers failed",
			zap.Error(err),
			zap.Uint64("tenant_id", tenantID),
			zap.Uint64("project_id", projectID),
		)
		common.FailWithCode(c, http.StatusInternalServerError, "failed to list alert subscribers")
		return
	}
	common.Success(c, entity.ListAlertSubscribersResponse{
		AlertSubscribers: alertSubscribers,
		TotalSize:        len(alertSubscribers),
	})
}

// GetAlertSubscriberQuota
//
//	@Id				GetAlertSubscriberQuota
//	@Tags			alert subscriber
//	@Description	get alert subscriber quota
//	@Produce		json
//	@Param			orgId		path		string	true	"organization id"
//	@Param			projectId	path		string	true	"project id"
//	@Success		200			{object}	entity.GetAlertSubscriberQuotaResponse
//	@Failure		400			{object}	common.BaseResp
//	@Failure		500			{object}	common.BaseResp
//	@Router			/api/alert/v1/orgs/{orgId}/projects/{projectId}/alertSubscriberQuota [get]
func (s *Service) GetAlertSubscriberQuota(c *gin.Context) {
	tenantID, _, err := getOrgIdAndProjectIdFromPath(c)
	if err != nil {
		common.FailWithCode(c, http.StatusBadRequest, err.Error())
		return
	}
	supportPlan, maxAlertSubscribersCount, err := s.getAlertSubscriberQuota(c, tenantID)
	if err != nil {
		log.Error("get alert subscriber quota failed",
			zap.Error(err),
			zap.Uint64("tenant_id", tenantID),
		)
		common.FailWithCode(c, http.StatusInternalServerError, "failed to get alert subscriber quota")
		return
	}

	common.Success(c, entity.GetAlertSubscriberQuotaResponse{
		MaxAlertSubscribersCount: maxAlertSubscribersCount,
		SupportPlan:              supportPlan,
	})
}

// CreateAlertSubscriber
//
//	@Id				CreateAlertSubscriber
//	@Tags			alert subscriber
//	@Description	create alert subscriber
//	@Produce		json
//	@Param			orgId		path		string								true	"organization id"
//	@Param			projectId	path		string								true	"project id"
//	@Param			req			body		entity.CreateAlertSubscriberRequest	true	"request param"
//	@Success		200			{object}	common.BaseResp
//	@Router			/api/alert/v1/orgs/{orgId}/projects/{projectId}/alertSubscribers [post]
func (s *Service) CreateAlertSubscriber(c *gin.Context) {
	tenantID, projectID, err := getOrgIdAndProjectIdFromPath(c)
	if err != nil {
		common.FailWithCode(c, http.StatusBadRequest, err.Error())
		return
	}
	req := &entity.CreateAlertSubscriberRequest{}
	err = c.ShouldBindJSON(req)
	if err != nil {
		common.FailWithCode(c, http.StatusBadRequest, err.Error())
		return
	}
	logger := log.L().With(
		zap.Uint64("tenant_id", tenantID),
		zap.Uint64("project_id", projectID),
		zap.String("subscriber_type", string(req.SubscriberType)),
		zap.String("subscriber_name", req.Name),
		zap.Bool("dry_run", req.DryRun),
	)

	alertSubscribers, err := s.listAlertSubscribers(
		c.Request.Context(), tenantID, projectID, entity.ClusterDeployTypeDedicated, true)
	if err != nil {
		logger.Error("list alert subscribers failed", zap.Error(err))
		common.FailWithCode(c, http.StatusInternalServerError, "failed to list existent alert subscribers")
		return
	}
	item := entity.AlertSubscribersItem{
		SubscriberType: req.SubscriberType,
		Name:           req.Name,
		Email:          req.Email,
		SlackUrl:       req.SlackUrl,
		ZoomUrl:        req.ZoomUrl,
		ZoomToken:      req.ZoomToken,
		FlashDutyUrl:   req.FlashDutyUrl,
		PagerDutyKey:   req.PagerDutyKey,
		WebhookUrl:     req.WebhookUrl,
		LarkWebhookUrl: req.LarkWebhookUrl,
		LarkSecret:     req.LarkSecret,
	}
	supportPlan, maxAlertSubscribersCount, err := s.getAlertSubscriberQuota(c, tenantID)
	if err != nil {
		logger.Error("get alert subscriber quota failed", zap.Error(err))
		common.FailWithCode(c, http.StatusInternalServerError, "failed to get alert subscriber quota")
		return
	}
	if (item.SubscriberType != entity.AlertSubscriberTypeEmail) &&
		(supportPlan != entity.SupportPlanTypeEnterprise &&
			supportPlan != entity.SupportPlanTypePremium) {
		common.FailWithCode(c, http.StatusBadRequest, "need upgrade to enterprise or premium support plan")
		return
	}
	if len(alertSubscribers) >= maxAlertSubscribersCount {
		logger.Error("create alert subscriber failed, exceed the quota",
			zap.Int("count", len(alertSubscribers)),
		)
		common.FailWithCode(c, http.StatusBadRequest, "exceed the quota")
		return
	}
	if err := s.validateAlertSubscriber(
		c,
		logger,
		tenantID, projectID,
		entity.ClusterDeployTypeDedicated,
		item); err != nil {
		logger.Error("validate alert subscriber failed", zap.Error(err))
		common.FailWithCode(c, http.StatusBadRequest, err.Error())
		return
	}

	if req.DryRun {
		err = s.dryRunAlertSubscriber(c, item, dryRunAlert, tenantID, projectID, entity.ClusterDeployTypeDedicated)
		if err != nil {
			logger.Error("dry run alert subscriber failed", zap.Error(err))
			common.FailWithCode(c, http.StatusInternalServerError, "Test connection failed. Please check if the configuration is correct.")
			return
		}
		logger.Info("dry run alert subscriber success")
		common.Success(c, nil)
	} else {
		err = s.createOrUpdateAlertSubscriber(c,
			logger, tenantID, projectID, entity.ClusterDeployTypeDedicated, item)
		details := auditlog.Details{
			// auditlog.KeyUserEmail:     req.Email,
			auditlog.KeyTidbProjectId: projectID,
			auditlog.KeyError:         err,
		}
		auditErr := auditlog.
			NewEvent(c, auditlog.EventSubscribeAlerts).
			ParseGIN(c).
			SetOrgID(tenantID).
			SetResult(err == nil).
			SetDetails(details.Build()).
			Submit()
		if auditErr != nil {
			logger.Error("audit log failed", zap.Error(auditErr))
		}
		if err != nil {
			logger.Error("create alert subscriber failed", zap.Error(err))
			common.FailWithCode(c, http.StatusInternalServerError, err.Error())
			return
		}
		logger.Info("create alert subscriber success")
		common.Success(c, nil)
	}
}

// func (s *Service) UpdateAlertSubscriber(c *gin.Context) {
// 	tenantID, projectID, subscriberID, err := getOrgIdAndProjectIdAndSubscriberIdFromPath(c)
// 	if err != nil {
// 		common.FailWithCode(c, http.StatusBadRequest, err.Error())
// 		return
// 	}
// 	req := &entity.UpdateAlertSubscriberRequest{}
// 	err = c.ShouldBindJSON(req)
// 	if err != nil {
// 		common.FailWithCode(c, http.StatusBadRequest, err.Error())
// 		return
// 	}
// 	logger := log.L().With(
// 		zap.Uint64("tenant_id", tenantID),
// 		zap.Uint64("project_id", projectID),
// 		zap.Uint("subscriber_id", subscriberID),
// 		zap.Any("request", req),
// 	)
// 	originalAlertSubscribers, err := s.alertSubscriberRepo.Load(
// 		c.Request.Context(),
// 		subscriberID)
// 	if err != nil {
// 		logger.Error("load alert subscriber failed", zap.Error(err))
// 		common.FailWithCode(c, http.StatusInternalServerError, "failed to load alert subscriber")
// 		return
// 	}
// 	if originalAlertSubscribers.ProjectID != projectID {
// 		logger.Error("alert subscriber not found in the project",
// 			zap.Any("alert_subscriber", originalAlertSubscribers),
// 		)
// 		common.FailWithCode(c, http.StatusNotFound, "alert subscriber not found in the project")
// 		return
// 	}
// 	item := entity.AlertSubscribersItem{
// 		ID:             subscriberID,
// 		SubscriberType: entity.AlertSubscriberType(originalAlertSubscribers.SubscriberType),
// 		Name:           req.Name,
// 		Email:          req.Email,
// 		SlackUrl:       req.SlackUrl,
// 		ZoomUrl:        req.ZoomUrl,
// 		ZoomToken:      req.ZoomToken,
// 	}
// 	if err := s.validateAlertSubscriber(
// 		c.Request.Context(),
// 		logger,
// 		tenantID, projectID,
// 		item); err != nil {
// 		logger.Error("validate alert subscriber failed", zap.Error(err))
// 		common.FailWithCode(c, http.StatusBadRequest, err.Error())
// 		return
// 	}
// 	if req.DryRun {
// 		err = s.dryRunAlertSubscriber(c.Request.Context(), item, tenantID)
// 		if err != nil {
// 			logger.Error("dry run alert subscriber failed", zap.Error(err))
// 			common.FailWithCode(c, http.StatusInternalServerError, err.Error())
// 			return
// 		}
// 		c.JSON(http.StatusOK, nil)
// 	} else {
// 		err = s.createOrUpdateAlertSubscriber(c.Request.Context(),
// 			logger, tenantID, projectID, item)
// 		if err != nil {
// 			logger.Error("update alert subscriber failed", zap.Error(err))
// 			common.FailWithCode(c, http.StatusInternalServerError, err.Error())
// 			return
// 		}
// 		c.JSON(http.StatusOK, nil)
// 	}
// }

// DeleteAlertSubscriber
//
//	@Id				DeleteAlertSubscriber
//	@Tags			alert subscriber
//	@Description	delete alert subscriber
//	@Produce		json
//	@Param			orgId				path		string	true	"organization id"
//	@Param			projectId			path		string	true	"project id"
//	@Param			alertSubscriberId	path		string	true	"alert subscriber id"
//	@Success		200					{object}	common.BaseResp
//	@Failure		400					{object}	common.BaseResp
//	@Failure		500					{object}	common.BaseResp
//	@Router			/api/alert/v1/orgs/{orgId}/projects/{projectId}/alertSubscribers/{alertSubscriberId} [delete]
func (s *Service) DeleteAlertSubscriber(c *gin.Context) {
	tenantID, projectID, subscriberID, err := getOrgIdAndProjectIdAndSubscriberIdFromPath(c)
	if err != nil {
		common.FailWithCode(c, http.StatusBadRequest, err.Error())
		return
	}
	logger := log.L().With(
		zap.Uint64("tenant_id", tenantID),
		zap.Uint64("project_id", projectID),
		zap.Uint("subscriber_id", subscriberID),
	)
	err = s.deleteAlertSubscriber(c.Request.Context(),
		tenantID, projectID,
		entity.ClusterDeployTypeDedicated,
		entity.AlertSubscribersItem{ID: subscriberID},
	)
	details := auditlog.Details{
		// auditlog.KeyUserEmail:     req.Email,
		auditlog.KeyTidbProjectId: projectID,
		auditlog.KeyError:         err,
	}
	auditErr := auditlog.NewEvent(c, auditlog.EventUnsubscribeAlerts).
		ParseGIN(c).
		SetOrgID(tenantID).
		SetResult(err == nil).
		SetDetails(details.Build()).
		Submit()
	if auditErr != nil {
		logger.Error("audit log failed", zap.Error(auditErr))
	}
	if err != nil {
		logger.Error("delete alert subscriber failed",
			zap.Error(err),
		)
		common.FailWithCode(c, http.StatusInternalServerError, err.Error())
	}
	logger.Info("delete alert subscriber success")
	common.Success(c, nil)
}

func (s *Service) listAlertSubscribers(
	ctx context.Context,
	tenantID, targetID uint64,
	clusterType entity.ClusterDeployType,
	maskToken bool,
) ([]entity.AlertSubscribersItem, error) {
	alertSubscribers, err := s.alertSubscriberRepo.Query(ctx,
		tenantID, targetID, string(clusterType), repo.AlertSubscriberQuery{})
	if err != nil {
		return nil, err
	}
	if len(alertSubscribers) == 0 {
		return []entity.AlertSubscribersItem{}, nil
	}

	userIds := make([]uint64, 0, len(alertSubscribers))
	for _, subscriber := range alertSubscribers {
		subscriber := subscriber
		if subscriber.SubscriberType == "EMAIL" {
			userIds = append(userIds, subscriber.UserID)
		}
	}
	users, err := s.accountClient.ListUsers(ctx, userIds)
	if err != nil {
		return nil, fmt.Errorf("get users failed,  %w", err)
	}
	userIdToEmail := make(map[uint64]string, len(users))
	for _, user := range users {
		if user != nil {
			if user.DeletedAt != nil {
				log.Info("user was deleted",
					zap.Uint64("user_id", user.Id),
					zap.Time("deleted_at", user.DeletedAt.AsTime()),
				)
			} else {
				user := user
				userIdToEmail[user.Id] = user.Email
			}
		}
	}

	result := make([]entity.AlertSubscribersItem, 0, len(alertSubscribers))
	for _, subscriber := range alertSubscribers {
		subscriber := subscriber
		data, err := convertDBAlertSubscriber(*subscriber, userIdToEmail, maskToken)
		if err != nil {
			log.Warn("convert alert subscriber failed",
				zap.Error(err),
				zap.Any("alert_subscriber", subscriber),
			)
			continue
		}
		result = append(result, *data)
	}
	return result, nil
}

func convertDBAlertSubscriber(
	dbAlertSubscriber db.AlertSubscriber,
	userIdToEmail map[uint64]string,
	maskToken bool,
) (*entity.AlertSubscribersItem, error) {
	data := &entity.AlertSubscribersItem{
		ID:             dbAlertSubscriber.ID,
		SubscriberType: entity.AlertSubscriberType(dbAlertSubscriber.SubscriberType),
	}
	switch data.SubscriberType {
	case entity.AlertSubscriberTypeEmail:
		if email, ok := userIdToEmail[dbAlertSubscriber.UserID]; ok {
			data.Name = email
			data.Email = email
		} else {
			return nil, fmt.Errorf("no user email found for user id(%d), subscriber id: %d", dbAlertSubscriber.UserID, dbAlertSubscriber.ID)
		}
	case entity.AlertSubscriberTypeSlack:
		data.Name = dbAlertSubscriber.Name
		data.SlackUrl = dbAlertSubscriber.SlackUrl
	case entity.AlertSubscriberTypeZoom:
		data.Name = dbAlertSubscriber.Name
		data.ZoomUrl = dbAlertSubscriber.ZoomUrl
		if maskToken {
			data.ZoomToken = MaskString(dbAlertSubscriber.ZoomToken)
		} else {
			data.ZoomToken = dbAlertSubscriber.ZoomToken
		}
	case entity.AlertSubscriberTypeFlashDuty:
		data.Name = dbAlertSubscriber.Name
		data.FlashDutyUrl = dbAlertSubscriber.FlashDutyUrl
	case entity.AlertSubscriberTypePagerDuty:
		data.Name = dbAlertSubscriber.Name
		if maskToken {
			data.PagerDutyKey = MaskString(dbAlertSubscriber.PagerDutyKey)
		} else {
			data.PagerDutyKey = dbAlertSubscriber.PagerDutyKey
		}
	case entity.AlertSubscriberTypeWebhook:
		data.Name = dbAlertSubscriber.Name
		if maskToken {
			data.WebhookUrl = MaskURL(dbAlertSubscriber.WebhookUrl)
		} else {
			data.WebhookUrl = dbAlertSubscriber.WebhookUrl
		}
	case entity.AlertSubscriberTypeLark:
		data.Name = dbAlertSubscriber.Name
		if maskToken {
			data.LarkWebhookUrl = MaskURL(dbAlertSubscriber.LarkWebhookUrl)
			if dbAlertSubscriber.LarkSecret != "" {
				data.LarkSecret = MaskString(dbAlertSubscriber.LarkSecret)
			}
		} else {
			data.LarkWebhookUrl = dbAlertSubscriber.LarkWebhookUrl
			data.LarkSecret = dbAlertSubscriber.LarkSecret
		}
	default:
		return nil, fmt.Errorf("unknown subscriber type, id: %d, type: %s", dbAlertSubscriber.ID, dbAlertSubscriber.SubscriberType)
	}
	return data, nil
}

func (s *Service) getAlertSubscriberQuota(ctx context.Context,
	tenantID uint64,
) (entity.SupportPlanType, int, error) {
	supportPlanStr, err := s.billingClient.GetSupportPlan(tenantID)
	if err != nil {
		return "", 0, fmt.Errorf("get support plan failed, %w", err)
	}
	supportPlan := entity.SupportPlanType(supportPlanStr)
	quota, err := s.alertQuotaRepo.Load(ctx, tenantID)
	if err != nil {
		return "", 0, fmt.Errorf("load alert quota failed, %w", err)
	}
	maxAlertSubscribersCount := 15
	if quota != nil && quota.MaxSubscriberCount > 0 {
		maxAlertSubscribersCount = quota.MaxSubscriberCount
	}
	return supportPlan, maxAlertSubscribersCount, nil
}

func (s *Service) validateAlertSubscriber(
	ctx context.Context,
	logger *zap.Logger,
	tenantId, targetId uint64,
	clusterType entity.ClusterDeployType,
	target entity.AlertSubscribersItem,
) error {
	switch target.SubscriberType {
	case entity.AlertSubscriberTypeEmail:
		if target.Email == "" {
			return fmt.Errorf("email is required")
		}
		if len(target.Email) > 255 {
			return fmt.Errorf("email is too long")
		}
		if _, err := mail.ParseAddress(target.Email); err != nil {
			return fmt.Errorf("invalid email address, %w", err)
		}
		// check user is in this tenant
		user, err := s.accountClient.GetUserByEmail(ctx,
			&account.GetUserByEmailRequest{Email: target.Email})
		if err != nil {
			logger.Error("validate alert subscriber failed, get user by email failed",
				zap.Error(err),
			)
			return fmt.Errorf("no user found by the email %s", target.Email)
		}
		if user.Status != account.UserStatus_USER_STATUS_ACTIVE {
			return fmt.Errorf("no active user of current project has the email %s", target.Email)
		}
		orgs, err := s.accountClient.ListOrgsByUser(ctx,
			&account.ListOrgsByUserRequest{UserId: user.Id})
		if err != nil {
			logger.Error("validate alert subscriber failed, get orgs by user failed",
				zap.Error(err),
			)
			return fmt.Errorf("no organization found by the user email %s", target.Email)
		}
		matchOrg := false
		for _, org := range orgs {
			if org.Id == tenantId {
				matchOrg = true
				break
			}
		}
		if !matchOrg {
			return fmt.Errorf("no active user of current project has the email %s with org id %d", target.Email, tenantId)
		}
		if err := s.checkDuplicateAlertSubscriberByUserId(ctx,
			tenantId, targetId, user.Id, clusterType, target); err != nil {
			return err
		}
	case entity.AlertSubscriberTypeSlack:
		if target.Name == "" {
			return fmt.Errorf("name is required")
		}
		if !nameRegexp.MatchString(target.Name) {
			return fmt.Errorf("name is invalid")
		}
		if target.SlackUrl == "" {
			return fmt.Errorf("slack url is required")
		}
		if len(target.SlackUrl) > 255 {
			return fmt.Errorf("slack url is too long")
		}
		if err := checkURL(target.SlackUrl); err != nil {
			return fmt.Errorf("slack url is invalid: %w", err)
		}
		if err := s.checkDuplicateAlertSubscriberByName(ctx,
			tenantId, targetId, clusterType, target); err != nil {
			return err
		}
	case entity.AlertSubscriberTypeZoom:
		if target.Name == "" {
			return fmt.Errorf("name is required")
		}
		if !nameRegexp.MatchString(target.Name) {
			return fmt.Errorf("name is invalid")
		}
		if target.ZoomUrl == "" {
			return fmt.Errorf("zoom url is required")
		}
		if len(target.ZoomUrl) > 255 {
			return fmt.Errorf("zoom url is too long")
		}
		if err := checkURL(target.ZoomUrl); err != nil {
			return fmt.Errorf("zoom url is invalid: %w", err)
		}
		if target.ZoomToken == "" {
			return fmt.Errorf("zoom token is required")
		}
		if !tokenRegexp.MatchString(target.ZoomToken) {
			return fmt.Errorf("zoom token is invalid")
		}
		if err := s.checkDuplicateAlertSubscriberByName(ctx,
			tenantId, targetId, clusterType, target); err != nil {
			return err
		}
	case entity.AlertSubscriberTypeFlashDuty:
		if target.Name == "" {
			return fmt.Errorf("name is required")
		}
		if !nameRegexp.MatchString(target.Name) {
			return fmt.Errorf("name is invalid")
		}
		if target.FlashDutyUrl == "" {
			return fmt.Errorf("flash duty url is required")
		}
		if len(target.FlashDutyUrl) > 255 {
			return fmt.Errorf("flash duty url is too long")
		}
		if err := checkURL(target.FlashDutyUrl); err != nil {
			return fmt.Errorf("flash duty url is invalid: %w", err)
		}
		if err := s.checkDuplicateAlertSubscriberByName(ctx,
			tenantId, targetId, clusterType, target); err != nil {
			return err
		}
	case entity.AlertSubscriberTypePagerDuty:
		if target.Name == "" {
			return fmt.Errorf("name is required")
		}
		if !nameRegexp.MatchString(target.Name) {
			return fmt.Errorf("name is invalid")
		}
		if target.PagerDutyKey == "" {
			return fmt.Errorf("pagerduty integration key is required")
		}
		if len(target.PagerDutyKey) > 255 {
			return fmt.Errorf("pagerduty integration key is too long")
		}
		if err := s.checkDuplicateAlertSubscriberByName(ctx,
			tenantId, targetId, clusterType, target); err != nil {
			return err
		}
	case entity.AlertSubscriberTypeWebhook:
		if target.Name == "" {
			return fmt.Errorf("name is required")
		}
		if !nameRegexp.MatchString(target.Name) {
			return fmt.Errorf("name is invalid")
		}
		if target.WebhookUrl == "" {
			return fmt.Errorf("webhook url is required")
		}
		if len(target.WebhookUrl) > 2048 {
			return fmt.Errorf("webhook url is too long")
		}
		if err := checkURL(target.WebhookUrl); err != nil {
			return fmt.Errorf("webhook url is invalid: %w", err)
		}
		if err := s.checkDuplicateAlertSubscriberByName(ctx,
			tenantId, targetId, clusterType, target); err != nil {
			return err
		}
	case entity.AlertSubscriberTypeLark:
		if target.Name == "" {
			return fmt.Errorf("name is required")
		}
		if !nameRegexp.MatchString(target.Name) {
			return fmt.Errorf("name is invalid")
		}
		if target.LarkWebhookUrl == "" {
			return fmt.Errorf("lark webhook url is required")
		}
		if len(target.LarkWebhookUrl) > 2048 {
			return fmt.Errorf("lark webhook url is too long")
		}
		if err := checkLarkWebhookURL(target.LarkWebhookUrl); err != nil {
			return fmt.Errorf("lark webhook url is invalid: %w", err)
		}
		if len(target.LarkSecret) > 255 {
			return fmt.Errorf("lark secret is too long")
		}
		if err := s.checkDuplicateAlertSubscriberByName(ctx,
			tenantId, targetId, clusterType, target); err != nil {
			return err
		}
	default:
		return fmt.Errorf("unknown subscriber type: %s", target.SubscriberType)
	}
	return nil
}

func checkURL(urlStr string) error {
	parsedURL, err := url.Parse(urlStr)
	if err != nil {
		return err
	}
	if parsedURL.Scheme != "http" && parsedURL.Scheme != "https" {
		return fmt.Errorf("url scheme must be http or https")
	}
	if parsedURL.Host == "" {
		return fmt.Errorf("url host is required")
	}
	if parsedURL.User != nil {
		return fmt.Errorf("url userinfo is not allowed")
	}
	hostname := strings.ToLower(strings.TrimSuffix(parsedURL.Hostname(), "."))
	if hostname == "localhost" {
		return fmt.Errorf("url cannot be localhost address")
	}
	if addr, parseErr := netip.ParseAddr(hostname); parseErr == nil {
		addr = addr.Unmap()
		if !addr.IsGlobalUnicast() || addr.IsPrivate() ||
			netip.MustParsePrefix("100.64.0.0/10").Contains(addr) {
			return fmt.Errorf("url cannot use a non-public IP address")
		}
	}
	return nil
}

func (s *Service) createOrUpdateAlertSubscriber(ctx context.Context,
	logger *zap.Logger,
	tenantId, targetId uint64,
	clusterType entity.ClusterDeployType,
	subscriber entity.AlertSubscribersItem,
) error {
	var userId uint64
	if subscriber.SubscriberType == entity.AlertSubscriberTypeEmail {
		user, err := s.accountClient.GetUserByEmail(ctx,
			&account.GetUserByEmailRequest{Email: subscriber.Email})
		if err != nil {
			return fmt.Errorf("get user by email %s failed, %w", subscriber.Email, err)
		}
		userId = user.Id
	}
	err := db.Transaction(func(tx *gorm.DB) error {
		dbAlertSubscriber := &db.AlertSubscriber{
			TenantID:       tenantId,
			SubscriberType: string(subscriber.SubscriberType),
		}
		if clusterType == entity.ClusterDeployTypeNextGen {
			dbAlertSubscriber.ClusterID = targetId
		} else {
			dbAlertSubscriber.ProjectID = targetId
		}
		// check duplicate email/name
		switch subscriber.SubscriberType {
		case entity.AlertSubscriberTypeEmail:
			if err := s.checkDuplicateAlertSubscriberByUserId(ctx,
				tenantId, targetId, userId, clusterType, subscriber); err != nil {
				return err
			}
			dbAlertSubscriber.UserID = userId
		case entity.AlertSubscriberTypeSlack:
			if err := s.checkDuplicateAlertSubscriberByName(ctx,
				tenantId, targetId, clusterType, subscriber); err != nil {
				return err
			}
			dbAlertSubscriber.Name = subscriber.Name
			dbAlertSubscriber.SlackUrl = subscriber.SlackUrl
		case entity.AlertSubscriberTypeZoom:
			if err := s.checkDuplicateAlertSubscriberByName(ctx,
				tenantId, targetId, clusterType, subscriber); err != nil {
				return err
			}
			dbAlertSubscriber.Name = subscriber.Name
			dbAlertSubscriber.ZoomUrl = subscriber.ZoomUrl
			dbAlertSubscriber.ZoomToken = subscriber.ZoomToken
		case entity.AlertSubscriberTypeFlashDuty:
			if err := s.checkDuplicateAlertSubscriberByName(ctx,
				tenantId, targetId, clusterType, subscriber); err != nil {
				return err
			}
			dbAlertSubscriber.Name = subscriber.Name
			dbAlertSubscriber.FlashDutyUrl = subscriber.FlashDutyUrl
		case entity.AlertSubscriberTypePagerDuty:
			if err := s.checkDuplicateAlertSubscriberByName(ctx,
				tenantId, targetId, clusterType, subscriber); err != nil {
				return err
			}
			dbAlertSubscriber.Name = subscriber.Name
			dbAlertSubscriber.PagerDutyKey = subscriber.PagerDutyKey
		case entity.AlertSubscriberTypeWebhook:
			if err := s.checkDuplicateAlertSubscriberByName(ctx,
				tenantId, targetId, clusterType, subscriber); err != nil {
				return err
			}
			dbAlertSubscriber.Name = subscriber.Name
			dbAlertSubscriber.WebhookUrl = subscriber.WebhookUrl
		case entity.AlertSubscriberTypeLark:
			if err := s.checkDuplicateAlertSubscriberByName(ctx,
				tenantId, targetId, clusterType, subscriber); err != nil {
				return err
			}
			dbAlertSubscriber.Name = subscriber.Name
			dbAlertSubscriber.LarkWebhookUrl = subscriber.LarkWebhookUrl
			dbAlertSubscriber.LarkSecret = subscriber.LarkSecret
		}

		if err := s.alertSubscriberRepo.Save(ctx, dbAlertSubscriber); err != nil {
			return fmt.Errorf("save alert subscriber failed, %w", err)
		}
		return nil
	})
	if err != nil {
		return err
	}
	return nil
}

func (s *Service) checkDuplicateAlertSubscriberByUserId(
	ctx context.Context,
	tenantId, targetId, userId uint64,
	clusterType entity.ClusterDeployType,
	subscriber entity.AlertSubscribersItem,
) error {
	if subscriber.SubscriberType != entity.AlertSubscriberTypeEmail {
		return fmt.Errorf("only email alert subscriber can be checked")
	}
	existItems, err := s.alertSubscriberRepo.Query(ctx,
		tenantId, targetId, string(clusterType), repo.AlertSubscriberQuery{
			UserId: userId,
		})
	if err != nil {
		return fmt.Errorf("query alert subscriber failed, %w", err)
	}
	if subscriber.ID == 0 {
		if len(existItems) > 0 {
			return fmt.Errorf("cannot create duplicate alert subscriber with the same email")
		}
	} else {
		if len(existItems) > 0 {
			for _, item := range existItems {
				if item.ID != subscriber.ID {
					return fmt.Errorf("already exists alert subscriber with the same email")
				}
			}
		}
	}

	return nil
}

func (s *Service) checkDuplicateAlertSubscriberByName(
	ctx context.Context,
	tenantId, targetId uint64,
	clusterType entity.ClusterDeployType,
	subscriber entity.AlertSubscribersItem,
) error {
	existItems, err := s.alertSubscriberRepo.Query(ctx,
		tenantId, targetId, string(clusterType), repo.AlertSubscriberQuery{
			Name:           subscriber.Name,
			SubscriberType: string(subscriber.SubscriberType),
		})
	if err != nil {
		return fmt.Errorf("query alert subscriber failed, %w", err)
	}
	if subscriber.ID == 0 {
		if len(existItems) > 0 {
			return fmt.Errorf("cannot create duplicate alert subscriber with the same name")
		}
	} else {
		if len(existItems) > 0 {
			for _, item := range existItems {
				if item.ID != subscriber.ID {
					return fmt.Errorf("already exists alert subscriber with the same name")
				}
			}
		}
	}
	return nil
}

func (s *Service) deleteAlertSubscriber(ctx context.Context,
	tenantId, targetId uint64,
	clusterType entity.ClusterDeployType,
	subscriber entity.AlertSubscribersItem,
) error {
	err := db.Transaction(func(tx *gorm.DB) error {
		originalAlertSubscribers, err := s.alertSubscriberRepo.Load(ctx, subscriber.ID)
		if err != nil {
			return fmt.Errorf("load alert subscriber failed, %w", err)
		}

		if clusterType == entity.ClusterDeployTypeNextGen {
			if originalAlertSubscribers.ClusterID != targetId {
				return fmt.Errorf("alert subscriber %d not found in the cluster %d", subscriber.ID, targetId)
			}
		} else {
			if originalAlertSubscribers.ProjectID != targetId {
				return fmt.Errorf("alert subscriber %d not found in the project %d", subscriber.ID, targetId)
			}
		}
		if err := s.alertSubscriberRepo.Delete(ctx, &db.AlertSubscriber{
			Model: gorm.Model{
				ID: subscriber.ID,
			},
		}); err != nil {
			return fmt.Errorf("delete alert subscriber failed, %w", err)
		}
		return nil
	})
	if err != nil {
		return err
	}
	return nil
}

var dryRunAlert = template.Alert{
	Status: "firing",
	Labels: map[string]string{
		notification.AlertLabelSeverity: "info",
	},
	Annotations: map[string]string{
		notification.AlertAnnotationSubject: "Welcome to Alert Subscription on TiDB Cloud",
		notification.AlertAnnotationContent: `Welcome to the alert subscription feature on TiDB Cloud. This is a test alert notification to confirm that we can successfully send alert notifications to you.

Please confirm that you wish to subscribe to alerts for the [TiDBProjectName] project. This address will receive notifications whenever an alert is triggered within this project.

If everything looks correct, please remember to save the current alert subscriber setup on the page.

If you did not request this test or have any concerns, please contact our <a href="https://docs.pingcap.com/tidbcloud/tidb-cloud-support">support team</a>.`,
	},
	StartsAt: time.Now(),
}

func (s *Service) dryRunAlertSubscriber(
	ctx context.Context,
	subscriber entity.AlertSubscribersItem,
	alert template.Alert,
	tenantId uint64,
	targetId uint64,
	clusterType entity.ClusterDeployType,
) error {
	alertMeta := notification.AlertMeta{
		TenantId: tenantId,
	}
	if clusterType == entity.ClusterDeployTypeNextGen {
		cluster, err := s.mgmtSvcClient.GetCluster(ctx, targetId)
		if err != nil {
			return fmt.Errorf("failed to get cluster: %w, cluster_id: %d", err, targetId)
		}
		alertMeta.ClusterId = targetId
		alertMeta.ClusterName = cluster.ClusterName
		alertMeta.ProjectId = 0
		alertMeta.ProjectName = "testProject"
	} else {
		project, bizErr := s.accountClient.GetProject(ctx, targetId)
		if bizErr != nil {
			return fmt.Errorf("failed to get project: %w, project_id: %d", bizErr, targetId)
		}
		alertMeta.ProjectId = targetId
		alertMeta.ProjectName = project.Name
		alertMeta.ClusterId = 0
		alertMeta.ClusterName = "testCluster"
	}

	err := s.notificationClient.Push(ctx,
		alert,
		[]entity.AlertSubscribersItem{subscriber},
		alertMeta,
		true)
	if err != nil {
		return err
	}
	return nil
}

// MaskString masks the prefix of a string and only keeps the last 1/8 characters.
func MaskString(k string) string {
	l := len(k)
	maskLen := l / 8
	var b strings.Builder
	b.Grow(l)
	for i := 0; i < (l-maskLen)/2; i++ {
		b.WriteString("·")
	}
	b.WriteString(k[l-maskLen:])
	return b.String()
}

func MaskURL(rawURL string) string {
	parsedURL, err := url.Parse(rawURL)
	if err != nil || parsedURL.Host == "" {
		return MaskString(rawURL)
	}
	parsedURL.RawQuery = ""
	parsedURL.Fragment = ""
	segments := strings.Split(parsedURL.Path, "/")
	if len(segments) > 0 {
		segments[len(segments)-1] = MaskString(segments[len(segments)-1])
		parsedURL.Path = strings.Join(segments, "/")
	}
	return parsedURL.Scheme + "://" + parsedURL.Host + parsedURL.Path
}

func checkLarkWebhookURL(rawURL string) error {
	if err := checkURL(rawURL); err != nil {
		return err
	}
	parsedURL, err := url.Parse(rawURL)
	if err != nil {
		return err
	}
	if parsedURL.Scheme != "https" {
		return fmt.Errorf("lark webhook url must use https")
	}
	hostname := strings.ToLower(strings.TrimSuffix(parsedURL.Hostname(), "."))
	if hostname != "open.larksuite.com" && hostname != "open.feishu.cn" {
		return fmt.Errorf("unsupported lark webhook host")
	}
	if parsedURL.RawQuery != "" || parsedURL.Fragment != "" {
		return fmt.Errorf("lark webhook url cannot contain query or fragment")
	}
	const hookPathPrefix = "/open-apis/bot/v2/hook/"
	if !strings.HasPrefix(parsedURL.EscapedPath(), hookPathPrefix) {
		return fmt.Errorf("invalid lark webhook path")
	}
	hookID := strings.TrimPrefix(parsedURL.EscapedPath(), hookPathPrefix)
	if hookID == "" || !larkWebhookIDRegexp.MatchString(hookID) {
		return fmt.Errorf("invalid lark webhook id")
	}
	return nil
}
