package api

import (
	"context"
	"encoding/json"
	"fmt"
	"html"
	"net/http"
	"strconv"
	"strings"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/google/uuid"
	"github.com/prometheus/alertmanager/template"
	"github.com/tidbcloud/account/idl/pbgen/proto/account"
	"github.com/tidbcloud/metrics-server/central/db"
	"github.com/tidbcloud/metrics-server/central/db/dbutil/useralerts"
	"github.com/tidbcloud/metrics-server/central/db/entity"
	"github.com/tidbcloud/metrics-server/central/grpc/grpcli"
	"github.com/tidbcloud/metrics-server/idl/pbgen/proto/core"
	"github.com/tidbcloud/pkgv2/log"
	"go.uber.org/zap"
	"gorm.io/gorm"
)

const (
	alertTimeFormatFull  = "2006-01-02 15:04:05 -07:00"
	alertTimeFormatShort = "01-02 15:04"
)

// FireAlert sends an alert to the user
func FireAlert(c *gin.Context) {
	var payload template.Data
	if err := c.ShouldBindJSON(&payload); err != nil {
		Fail(c, "", err, http.StatusBadRequest)
		return
	}
	// payload example here: https://github.com/pingcap/dead-mans-switch/blob/master/manifest/payload.json
	if payload.Status != ALERT_STATUS_FIRING {
		Success(c, http.StatusOK, "")
		return
	}

	for _, alert := range payload.Alerts {
		sendAlertToUsers(c.Request.Context(), alert)
	}

	Success(c, http.StatusOK, "")
}

func FireAlertWebhook(c *gin.Context) {
	var payload template.Data
	if err := c.ShouldBindJSON(&payload); err != nil {
		Fail(c, "", err, http.StatusBadRequest)
		return
	}

	for _, alert := range payload.Alerts {
		sendAlertToWebhook(c.Request.Context(), alert)
	}

	Success(c, http.StatusOK, "")
}

func sendAlertToUsers(ctx context.Context, alert template.Alert) {
	logger := log.WithContextL(ctx)
	logger.Info("received alert", zap.String("type", "email"), zap.Any("labels", alert.Labels), zap.Any("annotations", alert.Annotations))

	var (
		userEmails []string
		cluster    *core.Cluster
		project    *account.GetProjectByIDResponse
		clusterID  uint64
		err        error
	)

	cluster, project, err = getClusterAndProjectFromAlert(ctx, alert)
	if err != nil {
		logger.Error("failed to get cluster and project from alert", zap.Any("alert", alert), zap.Error(err))
		return
	}

	err = db.Transaction(func(tx *gorm.DB) error {
		userAlertService := &useralerts.DefaultUserAlertsServiceImpl{}
		userAlerts, err := userAlertService.GetEmailUserAlertsByProjectID(ctx, tx, entity.ID(project.Project.Id))
		if err != nil {
			return err
		}

		for _, i := range userAlerts {
			user, err := grpcli.GetUserByID(ctx, &account.GetUserByIDRequest{UserId: uint64(i.UserID)})
			if err != nil {
				logger.Error("no user info found, alert cannot be sent", zap.Any("userAlert", i))
				continue
			}
			userEmails = append(userEmails, user.User.Email)
		}

		return nil
	})
	if err != nil {
		logger.Error("failed to query cluster by clusterID", zap.Uint64("clusterID", clusterID), zap.Error(err))
		return
	}

	if len(userEmails) == 0 {
		logger.Info("no user emails found, skip this alert", zap.Uint64("clusterID", clusterID))
		return
	}

	subject := alert.Annotations[AlertSubjectKeyName]
	content := alert.Annotations[AlertContentKeyName]
	if subject == "" || content == "" {
		logger.Error("bad subject or content, so this alert will be skipped", zap.String("subject", subject), zap.String("content", content))
		return
	}
	starts := alert.StartsAt
	content = replaceMessage(content, alert, cluster, project)

	vars := map[string]string{
		"subject": fmt.Sprintf("%s at %s", subject, starts.Format(alertTimeFormatShort)),
		"content": fmt.Sprintf("Alert time: %s <br />Message: %s", starts.Format(alertTimeFormatFull), content),
	}

	// SendMailWithTemplate
	// mailFacade := utils.GetMailFacade()
	// err = mailFacade.SendMailWithTemplate(ctx, userEmails, subject, TNUserAlertIsTriggered, vars, nil)
	// logger.Info("send alert email to users", zap.String("subject", subject), zap.Any("emails", userEmails), zap.Any("alert", alert), zap.String("clusterName", cluster.Name), zap.Error(err))

	// SendEmail with account grpc
	_, err = grpcli.SendEmail(ctx, &account.SendEmailReq{
		OrgId:        project.Project.TenantId,
		Scene:        NotificationSceneUserAlert,
		Identifier:   uuid.New().String(),
		Subject:      subject,
		Emails:       userEmails,
		TemplateId:   SendgridTemplateName2id[TNUserAlertIsTriggered],
		TemplateVars: vars,
	})

	if err != nil {
		logger.Error("send alert email to users failed", zap.String("subject", subject), zap.Any("emails", userEmails), zap.Any("alert", alert), zap.Error(err))
	} else {
		logger.Info("send alert email to users", zap.String("subject", subject), zap.Any("emails", userEmails), zap.Any("alert", alert))
	}
}

func genTiDBCloudDomain(alert template.Alert) string {

	o11yBizType := alert.Labels[AlertO11yBizTypeKeyName]

	switch {

	case strings.HasPrefix(o11yBizType, "dev"):
		return fmt.Sprintf("dev.%s", Domain)

	case strings.HasPrefix(o11yBizType, "staging"):
		return fmt.Sprintf("staging.%s", Domain)
	}
	return Domain
}

func sendAlertToWebhook(ctx context.Context, alert template.Alert) {
	logger := log.WithContextL(ctx)
	logger.Info("received alert", zap.String("type", "webhook"), zap.Any("labels", alert.Labels), zap.Any("annotations", alert.Annotations))

	var (
		cluster    *core.Cluster
		project    *account.GetProjectByIDResponse
		userAlerts []entity.UserAlert
		err        error
	)

	cluster, project, err = getClusterAndProjectFromAlert(ctx, alert)
	if err != nil {
		logger.Error("failed to get cluster and project from alert", zap.Any("alert", alert), zap.Error(err))
		return
	}

	err = db.Transaction(func(tx *gorm.DB) error {
		userAlertService := &useralerts.DefaultUserAlertsServiceImpl{}
		userAlerts, err = userAlertService.GetWebhookUserAlertsByProjectID(ctx, tx, entity.ID(project.Project.Id))
		if err != nil {
			return err
		}
		return nil
	})
	if err != nil {
		logger.Error("failed to query cluster by clusterID", zap.Uint64("clusterID", cluster.Id), zap.Error(err))
		return
	}

	if len(userAlerts) == 0 {
		logger.Info("no user emails found, skip this alert", zap.Uint64("clusterID", cluster.Id))
		return
	}

	subject := alert.Annotations[AlertSubjectKeyName]
	message := alert.Annotations[AlertContentKeyName]
	if subject == "" || message == "" {
		logger.Error("bad subject or message, so this alert will be skipped", zap.String("subject", subject), zap.String("message", message))
		return
	}
	for _, userAlert := range userAlerts {
		go func(userAlert entity.UserAlert) {
			client := http.Client{
				Timeout: 15 * time.Second,
			}
			payload := replaceWebhookPayload(userAlert.WebhookCustomPayload, alert, cluster, project)
			req, err := http.NewRequest("POST", userAlert.WebhookUrl, strings.NewReader(payload))
			if err != nil {
				logger.Error("failed to create http request", zap.Error(err), zap.Any("userAlert", userAlert))
			}
			header := make(map[string]string)
			err = json.Unmarshal([]byte(userAlert.WebhookCustomHeader), &header)
			if err != nil {
				logger.Error("failed to unmarshal webhook header", zap.Error(err), zap.Any("userAlert", userAlert))
			}
			for k, v := range header {
				req.Header.Set(k, v)
			}
			resp, err := client.Do(req)
			if err != nil {
				logger.Error("failed to send webhook", zap.Error(err), zap.Any("userAlert", userAlert))
			}
			if resp.StatusCode != http.StatusOK {
				logger.Error("failed to send webhook",
					zap.Any("userAlert", userAlert),
					zap.Int("statusCode", resp.StatusCode),
					zap.String("payload", payload),
					zap.Any("header", header),
				)
			}
		}(userAlert)
	}
}

func getClusterAndProjectFromAlert(ctx context.Context, alert template.Alert) (*core.Cluster, *account.GetProjectByIDResponse, error) {
	var (
		cluster   *core.Cluster
		project   *account.GetProjectByIDResponse
		clusterID uint64
		err       error
	)

	clusterIDStr := alert.Labels[AlertTiDBClusterIDKeyName]
	if clusterIDStr == "" {
		clusterIDStr = alert.Labels[AlertClusterIDKeyName]
	}
	clusterID, err = strconv.ParseUint(clusterIDStr, 10, 64)
	if err != nil {
		return nil, nil, fmt.Errorf("failed to parse cluster_id from alert labels: %w, cluser_id: %s", err, clusterIDStr)
	}

	cluster, err = grpcli.ListClusterByID(ctx, clusterID)
	if err != nil {
		return nil, nil, fmt.Errorf("failed to list cluster by id: %w, cluster_id: %d", err, clusterID)
	}

	project, err = grpcli.GetProjectByID(ctx, &account.GetProjectByIDRequest{Id: cluster.ProjectId})
	if err != nil {
		return nil, nil, fmt.Errorf("failed to get project by id: %w, project_id: %d", err, cluster.ProjectId)
	}

	return cluster, project, nil
}

func replaceMessage(message string, alert template.Alert, cluster *core.Cluster, project *account.GetProjectByIDResponse) string {
	message = strings.ReplaceAll(message, ClusterAnnotationToReplace, html.EscapeString(cluster.Name))
	message = strings.ReplaceAll(message, DomainAnnotationToReplace, html.EscapeString(genTiDBCloudDomain(alert)))
	message = strings.ReplaceAll(message, ProjectAnnotationToReplace, html.EscapeString(project.Project.Name))
	return message
}

func replaceWebhookPayload(payload string, alert template.Alert, cluster *core.Cluster, project *account.GetProjectByIDResponse) string {
	payload = strings.ReplaceAll(payload, WebhookVariableAlertName, escapeString(alert.Annotations[AlertSubjectKeyName]))
	payload = strings.ReplaceAll(payload, WebhookVariableMessage, escapeString(replaceMessage(alert.Annotations[AlertContentKeyName], alert, cluster, project)))
	payload = strings.ReplaceAll(payload, WebhookVariableSeverity, escapeString(alert.Labels["severity"]))
	payload = strings.ReplaceAll(payload, WebhookVariableStartsAt, escapeString(alert.StartsAt.Format(alertTimeFormatFull)))
	payload = strings.ReplaceAll(payload, WebhookVariableStatus, escapeString(alert.Status))
	payload = strings.ReplaceAll(payload, WebhookVariablesClusterName, escapeString(cluster.Name))
	payload = strings.ReplaceAll(payload, WebhookVariablesProjectName, escapeString(project.Project.Name))
	return payload
}

func escapeString(s string) string {
	return strings.ReplaceAll(s, "\"", "\\\"")
}
