package notification

import (
	"bytes"
	"context"
	"crypto/hmac"
	"crypto/sha256"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"html"
	"io"
	"net/http"
	"regexp"
	"strconv"
	"strings"
	"sync"
	"time"
	"unicode/utf8"

	"github.com/pingcap/log"
	"github.com/prometheus/alertmanager/template"
	"go.uber.org/zap"
	"golang.org/x/time/rate"

	"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"
)

const (
	larkBotMessageTypeInteractive = "interactive"
	larkBotRateLimitCode          = 11232
	larkBotMaxPayloadBytes        = 20 * 1024
	larkBotMaxMessageBytes        = 12 * 1024
	larkBotRateLimitBurst         = 1
	larkBotRateLimitInterval      = 600 * time.Millisecond
)

var (
	larkHTMLLinkRegexp = regexp.MustCompile(`(?i)<a\s+href=["']([^"']+)["'][^>]*>(.*?)</a>`)
	larkHTMLTagRegexp  = regexp.MustCompile(`<[^>]+>`)
)

type larkBotPayload struct {
	Timestamp string   `json:"timestamp,omitempty"`
	Sign      string   `json:"sign,omitempty"`
	MsgType   string   `json:"msg_type"`
	Card      larkCard `json:"card"`
}

type larkCard struct {
	Config   larkCardConfig    `json:"config"`
	Header   larkCardHeader    `json:"header"`
	Elements []larkCardElement `json:"elements"`
}

type larkCardConfig struct {
	WideScreenMode bool `json:"wide_screen_mode"`
}

type larkCardHeader struct {
	Template string   `json:"template"`
	Title    larkText `json:"title"`
}

type larkCardElement struct {
	Tag  string   `json:"tag"`
	Text larkText `json:"text"`
}

type larkText struct {
	Tag     string `json:"tag"`
	Content string `json:"content"`
}

type larkBotResponse struct {
	Code          *int   `json:"code"`
	Msg           string `json:"msg"`
	StatusCode    *int   `json:"StatusCode"`
	StatusMessage string `json:"StatusMessage"`
}

type LarkNotification struct {
	notificationRepo *repo.AlertNotificationRepo
	rateLimiters     sync.Map
}

func (n *LarkNotification) Push(
	ctx context.Context,
	alert template.Alert,
	subscribers []entity.AlertSubscribersItem,
	alertMeta AlertMeta,
	dryRun bool,
) error {
	for _, subscriber := range subscribers {
		if subscriber.SubscriberType != entity.AlertSubscriberTypeLark {
			continue
		}
		var err error
		for retryTimes := 0; retryTimes < 3; retryTimes++ {
			if err = n.waitForRateLimit(ctx, subscriber); err != nil {
				break
			}
			var retry bool
			retry, err = n.handle(ctx, alert, subscriber, alertMeta, dryRun)
			if err != nil {
				logLarkSendError(alertMeta, subscriber, dryRun, retryTimes, err)
			}
			if dryRun || !retry {
				break
			}
			if waitErr := waitForLarkRetry(ctx, larkRetryDelay(retryTimes)); waitErr != nil {
				err = waitErr
				break
			}
		}
		if err != nil {
			return err
		}
	}
	return nil
}

func (n *LarkNotification) PushAsync(
	ctx context.Context,
	alert template.Alert,
	subscribers []entity.AlertSubscribersItem,
	alertMeta AlertMeta,
	dryRun bool,
) {
	asyncCtx := context.WithoutCancel(ctx)
	for _, subscriber := range subscribers {
		if subscriber.SubscriberType != entity.AlertSubscriberTypeLark {
			continue
		}
		go func(subscriber entity.AlertSubscribersItem) {
			for retryTimes := 0; retryTimes < 3; retryTimes++ {
				if err := n.waitForRateLimit(asyncCtx, subscriber); err != nil {
					logLarkSendError(alertMeta, subscriber, dryRun, retryTimes, err)
					break
				}
				retry, err := n.handle(asyncCtx, alert, subscriber, alertMeta, dryRun)
				if err != nil {
					logLarkSendError(alertMeta, subscriber, dryRun, retryTimes, err)
				}
				if dryRun || !retry {
					break
				}
				if err := waitForLarkRetry(asyncCtx, larkRetryDelay(retryTimes)); err != nil {
					logLarkSendError(alertMeta, subscriber, dryRun, retryTimes, err)
					break
				}
			}
		}(subscriber)
	}
}

func (n *LarkNotification) 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.Bool("dry_run", dryRun),
	)

	alertBytes, 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(alertBytes),
		Status:       "start",
	}
	updateNotificationStatus(ctx, logger, n.notificationRepo, notificationRecord, "start", "")
	defer func() {
		if err != nil {
			updateNotificationStatus(ctx, logger, n.notificationRepo, notificationRecord, "failure", err.Error())
		}
	}()

	timestamp := time.Now().Unix()
	payload := newLarkBotPayload(alert, alertMeta, subscriber.LarkSecret, timestamp, dryRun)
	body, err := json.Marshal(payload)
	if err != nil {
		return false, fmt.Errorf("failed to marshal lark payload: %w", err)
	}
	if len(body) > larkBotMaxPayloadBytes {
		return false, fmt.Errorf("lark payload exceeds %d bytes", larkBotMaxPayloadBytes)
	}

	req, err := http.NewRequestWithContext(
		ctx,
		http.MethodPost,
		subscriber.LarkWebhookUrl,
		bytes.NewReader(body),
	)
	if err != nil {
		return false, fmt.Errorf("failed to create lark request: %w", err)
	}
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("User-Agent", "TiDB-Cloud-Alert-Lark/1.0")

	client := http.Client{
		Timeout: 15 * time.Second,
		CheckRedirect: func(_ *http.Request, _ []*http.Request) error {
			return http.ErrUseLastResponse
		},
	}
	resp, err := client.Do(req)
	if err != nil {
		return true, fmt.Errorf("send lark notification failed: %w", err)
	}
	defer resp.Body.Close()
	responseBody, readErr := io.ReadAll(io.LimitReader(resp.Body, larkBotMaxPayloadBytes))
	if readErr != nil {
		return shouldRetryLarkHTTPStatus(resp.StatusCode), fmt.Errorf(
			"failed to read lark response with status %d: %w",
			resp.StatusCode,
			readErr,
		)
	}
	if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
		return shouldRetryLarkHTTPStatus(resp.StatusCode), fmt.Errorf(
			"lark returned http status %d: %s",
			resp.StatusCode,
			string(responseBody),
		)
	}

	code, message, parseErr := parseLarkBotResponse(responseBody)
	if parseErr != nil {
		return false, parseErr
	}
	if code != 0 {
		return shouldRetryLarkBusinessCode(code), fmt.Errorf(
			"lark returned business code %d: %s",
			code,
			message,
		)
	}

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

func newLarkBotPayload(
	alert template.Alert,
	alertMeta AlertMeta,
	secret string,
	timestamp int64,
	dryRun bool,
) larkBotPayload {
	message := replaceEmailMessage(
		alert.Annotations[AlertAnnotationContent],
		alert,
		alertMeta.ProjectName,
		alertMeta.ClusterName,
	)
	message = truncateUTF8ByBytes(toLarkMarkdown(message), larkBotMaxMessageBytes)
	severity := strings.ToLower(alert.Labels[AlertLabelSeverity])
	if severity == "major" {
		severity = "warning"
	}
	if severity == "" {
		severity = "info"
	}
	status := strings.ToUpper(alert.Status)

	var content strings.Builder
	fmt.Fprintf(&content, "**Status:** %s\n", status)
	fmt.Fprintf(&content, "**Severity:** %s\n", severity)
	fmt.Fprintf(&content, "**Cluster:** %s\n", alertMeta.ClusterName)
	if alertMeta.ProjectName != "" {
		fmt.Fprintf(&content, "**Project:** %s\n", alertMeta.ProjectName)
	}
	fmt.Fprintf(&content, "**Started at:** %s\n", alert.StartsAt.Format(alertTimeFormatISO))
	if !alert.EndsAt.IsZero() {
		fmt.Fprintf(&content, "**Ended at:** %s\n", alert.EndsAt.Format(alertTimeFormatISO))
	}
	fmt.Fprintf(&content, "\n**Message:**\n%s", message)

	payload := larkBotPayload{
		MsgType: larkBotMessageTypeInteractive,
		Card: larkCard{
			Config: larkCardConfig{WideScreenMode: true},
			Header: larkCardHeader{
				Template: larkCardTemplate(alert.Status, severity),
				Title: larkText{
					Tag:     "plain_text",
					Content: larkCardTitle(severity, alert.Annotations[AlertAnnotationSubject], dryRun),
				},
			},
			Elements: []larkCardElement{
				{
					Tag: "div",
					Text: larkText{
						Tag:     "lark_md",
						Content: content.String(),
					},
				},
			},
		},
	}
	if secret != "" {
		payload.Timestamp = strconv.FormatInt(timestamp, 10)
		payload.Sign = generateLarkBotSign(secret, timestamp)
	}
	return payload
}

func generateLarkBotSign(secret string, timestamp int64) string {
	stringToSign := fmt.Sprintf("%d\n%s", timestamp, secret)
	mac := hmac.New(sha256.New, []byte(stringToSign))
	return base64.StdEncoding.EncodeToString(mac.Sum(nil))
}

func parseLarkBotResponse(body []byte) (int, string, error) {
	var response larkBotResponse
	if err := json.Unmarshal(body, &response); err != nil {
		return 0, "", fmt.Errorf("failed to decode lark response: %w", err)
	}
	if response.Code != nil {
		return *response.Code, response.Msg, nil
	}
	if response.StatusCode != nil {
		return *response.StatusCode, response.StatusMessage, nil
	}
	return 0, "", fmt.Errorf("lark response does not contain a business code")
}

func shouldRetryLarkHTTPStatus(statusCode int) bool {
	return statusCode == http.StatusTooManyRequests || IsRetry(statusCode)
}

func shouldRetryLarkBusinessCode(code int) bool {
	return code == larkBotRateLimitCode
}

func larkRetryDelay(retryTimes int) time.Duration {
	return time.Second * time.Duration(1<<retryTimes)
}

func waitForLarkRetry(ctx context.Context, delay time.Duration) error {
	timer := time.NewTimer(delay)
	defer timer.Stop()
	select {
	case <-ctx.Done():
		return ctx.Err()
	case <-timer.C:
		return nil
	}
}

func (n *LarkNotification) waitForRateLimit(
	ctx context.Context,
	subscriber entity.AlertSubscribersItem,
) error {
	key := strconv.FormatUint(uint64(subscriber.ID), 10)
	if subscriber.ID == 0 {
		key = subscriber.LarkWebhookUrl
	}
	newLimiter := rate.NewLimiter(
		rate.Every(larkBotRateLimitInterval),
		larkBotRateLimitBurst,
	)
	value, _ := n.rateLimiters.LoadOrStore(key, newLimiter)
	return value.(*rate.Limiter).Wait(ctx)
}

func larkCardTitle(severity, subject string, dryRun bool) string {
	prefix := fmt.Sprintf("[%s]", strings.ToUpper(severity))
	if dryRun {
		prefix = "[TEST]"
	}
	return fmt.Sprintf("%s %s", prefix, subject)
}

func larkCardTemplate(status, severity string) string {
	if strings.EqualFold(status, "resolved") {
		return "green"
	}
	switch strings.ToLower(severity) {
	case "critical":
		return "red"
	case "warning", "major":
		return "orange"
	default:
		return "blue"
	}
}

func toLarkMarkdown(message string) string {
	message = larkHTMLLinkRegexp.ReplaceAllString(message, `[$2]($1)`)
	message = strings.NewReplacer(
		"<br>", "\n",
		"<br/>", "\n",
		"<br />", "\n",
		"</br>", "\n",
	).Replace(message)
	message = larkHTMLTagRegexp.ReplaceAllString(message, "")
	return html.UnescapeString(message)
}

func truncateUTF8ByBytes(value string, maxBytes int) string {
	if len(value) <= maxBytes {
		return value
	}
	value = value[:maxBytes]
	for !utf8.ValidString(value) {
		value = value[:len(value)-1]
	}
	return value + "…"
}

func logLarkSendError(
	alertMeta AlertMeta,
	subscriber entity.AlertSubscribersItem,
	dryRun bool,
	retryTimes int,
	err error,
) {
	log.Error("send lark 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),
	)
}
