package notification

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

	"github.com/pingcap/log"
	"github.com/prometheus/alertmanager/template"
	"go.uber.org/zap"

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

var SlackPayload = `{
    "blocks": [
        {
            "type": "header",
            "text": {
                "type": "plain_text",
                "text": "[$SEVERITY] $ALERT_NAME"
            }
        },
        {
            "type": "section",
            "fields": [
                {
                    "type": "mrkdwn",
                    "text": "*Status:* $STATUS"
                },
                {
                    "type": "mrkdwn",
                    "text": "*Cluster:* $CLUSTER_NAME"
                },
                {
                    "type": "mrkdwn",
                    "text": "*StartAt:* $STARTS_AT"
                }
            ]
        },
        {
            "type": "section",
            "text": {
                "type": "mrkdwn",
                "text": "*Message:* $MESSAGE"
            }
        }
    ]
}`

var DryRunSlackPayload = `{
    "blocks": [
        {
            "type": "header",
            "text": {
                "type": "plain_text",
                "text": "[$SEVERITY]$ALERT_NAME"
            }
        },
        {
            "type": "section",
            "text": {
                "type": "mrkdwn",
                "text": "*Message:* $MESSAGE"
            }
        }
    ]
}`

type SlackNotification struct {
	notificationRepo *repo.AlertNotificationRepo
}

func (n *SlackNotification) Push(
	ctx context.Context,
	alert template.Alert,
	subscribers []entity.AlertSubscribersItem,
	alertMeta AlertMeta,
	dryRun bool,
) error {
	for _, subscriber := range subscribers {
		if subscriber.SubscriberType != entity.AlertSubscriberTypeSlack {
			continue
		}
		var err error
		var retry bool
		// retry 3 times if failed
		for retryTimes := 0; retryTimes < 3; retryTimes++ {
			retry, err = n.handle(ctx, alert, subscriber, alertMeta, dryRun)
			if err != nil {
				log.Error("send slack notification failed",
					zap.Uint64("tenant_id", alertMeta.TenantId),
					zap.Uint64("cluster_id", alertMeta.ClusterId),
					zap.Uint("subscriber_id", subscriber.ID),
					zap.Bool("dry_run", dryRun),
					zap.Int("retry_times", retryTimes),
					zap.Error(err))
			}
			// if success or dryRun, break
			if dryRun || !retry {
				break
			}
		}
		if err != nil {
			return err
		}
	}
	return nil
}

func (n *SlackNotification) PushAsync(
	ctx context.Context,
	alert template.Alert,
	subscribers []entity.AlertSubscribersItem,
	alertMeta AlertMeta,
	dryRun bool,
) {
	for _, subscriber := range subscribers {
		if subscriber.SubscriberType != entity.AlertSubscriberTypeSlack {
			continue
		}
		go func(subscriber entity.AlertSubscribersItem) {
			// retry 3 times if failed
			for retryTimes := 0; retryTimes < 3; retryTimes++ {
				retry, err := n.handle(ctx, alert, subscriber, alertMeta, dryRun)
				if err != nil {
					log.Error("send slack notification failed",
						zap.Uint64("tenant_id", alertMeta.TenantId),
						zap.Uint64("cluster_id", alertMeta.ClusterId),
						zap.Uint("subscriber_id", subscriber.ID),
						zap.Bool("dry_run", dryRun),
						zap.Int("retry_times", retryTimes),
						zap.Error(err))
				}
				// if success or dryRun, break
				if dryRun || !retry {
					break
				}
			}
		}(subscriber)
	}
}

func (n *SlackNotification) handle(
	ctx context.Context,
	alert template.Alert,
	subscriber entity.AlertSubscribersItem,
	alertMeta AlertMeta,
	dryRun bool,
) (retry bool, err error) {
	logger := log.L().With(
		zap.Uint64("tenant_id", alertMeta.TenantId),
		zap.Uint64("cluster_id", alertMeta.ClusterId),
		zap.Uint("subscriber_id", subscriber.ID),
		zap.String("slack_url", subscriber.SlackUrl),
		zap.Bool("dry_run", dryRun),
	)
	logger.Info("start to send slack notification")
	alertStr, err := json.Marshal(alert)
	if err != nil {
		return false, fmt.Errorf("failed to marshal alert, %w", err)
	}
	notificationRecord := &db.AlertNotification{
		TenantID:     alertMeta.TenantId,
		ProjectID:    alertMeta.ProjectId,
		ClusterID:    alertMeta.ClusterId,
		SubscriberID: uint64(subscriber.ID),
		AlertContent: string(alertStr),
		Status:       "start",
	}
	updateNotificationStatus(ctx, logger, n.notificationRepo, notificationRecord, "start", "")
	defer func() {
		if err != nil {
			updateNotificationStatus(ctx, logger, n.notificationRepo, notificationRecord, "failure", err.Error())
		}
	}()

	slackUrl := subscriber.SlackUrl
	client := http.Client{
		Timeout: 15 * time.Second,
	}
	slackPayload := SlackPayload
	if dryRun {
		slackPayload = DryRunSlackPayload
	}
	payload := replaceWebhookPayload(
		slackPayload,
		alert,
		alertMeta.ProjectName,
		alertMeta.ClusterName,
	)
	payload = replaceHtmlLink(payload)
	req, err := http.NewRequest("POST", slackUrl, strings.NewReader(payload))
	if err != nil {
		return false, fmt.Errorf("failed to create http request, %w", err)
	}
	req.Header.Set("Content-Type", "application/json")
	resp, err := client.Do(req)
	if err != nil {
		return true, fmt.Errorf("send to slack failed, err: %w", err)
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusOK {
		bodyBytes, err := io.ReadAll(resp.Body)
		if err != nil {
			return IsRetry(resp.StatusCode), fmt.Errorf("send slack notification failed, status code: %d, response: %w", resp.StatusCode, err)
		}
		return IsRetry(resp.StatusCode), fmt.Errorf("send slack notification failed, status code: %d, response: %s", resp.StatusCode, string(bodyBytes))
	}

	updateNotificationStatus(ctx, logger, n.notificationRepo, notificationRecord, "success", "")
	logger.Info("send slack notification success")
	return false, nil
}

func IsRetry(statusCode int) bool {
	return statusCode >= 500 && statusCode <= 599
}

func updateNotificationStatus(
	ctx context.Context,
	logger *zap.Logger,
	notificationRepo *repo.AlertNotificationRepo,
	notificationRecord *db.AlertNotification,
	status string,
	message string,
) {
	notificationRecord.Status = status
	if message != "" {
		notificationRecord.Message = message
	}
	err := notificationRepo.Save(ctx, notificationRecord)
	if err != nil {
		logger.Warn("update alert notification status failed",
			zap.String("status", status),
			zap.Error(err))
	}
}
