package notification

import (
	"bytes"
	"context"
	"crypto/sha256"
	"encoding/hex"
	"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"
)

const webhookReceiverName = "webhook"

// webhookPayload mirrors the Alertmanager webhook payload so that customers can
// point an existing Alertmanager-compatible receiver at us without writing an
// adapter. Only the customer-visible label/annotation subset is exposed, the
// same set FlashDutyPayload uses.
//
// dryRun is an extension used by the test-connection flow. Generic Alertmanager
// receivers ignore unknown top-level fields.
type webhookPayload struct {
	Receiver string         `json:"receiver"`
	Status   string         `json:"status"`
	Alerts   []webhookAlert `json:"alerts"`
	// GroupKey is hashed, see hashGroupKey.
	GroupKey          string            `json:"groupKey,omitempty"`
	GroupLabels       map[string]string `json:"groupLabels"`
	CommonLabels      map[string]string `json:"commonLabels"`
	CommonAnnotations map[string]string `json:"commonAnnotations"`
	DryRun            bool              `json:"dryRun,omitempty"`
}

type webhookAlert struct {
	Status      string            `json:"status"`
	Labels      map[string]string `json:"labels"`
	Annotations map[string]string `json:"annotations"`
	StartsAt    time.Time         `json:"startsAt"`
	EndsAt      *time.Time        `json:"endsAt,omitempty"`
}

type WebhookNotification struct {
	notificationRepo *repo.AlertNotificationRepo
}

func (n *WebhookNotification) Push(
	ctx context.Context,
	alert template.Alert,
	subscribers []entity.AlertSubscribersItem,
	alertMeta AlertMeta,
	dryRun bool,
) error {
	for _, subscriber := range subscribers {
		if subscriber.SubscriberType != entity.AlertSubscriberTypeWebhook {
			continue
		}
		var err error
		var retry bool
		for retryTimes := 0; retryTimes < 3; retryTimes++ {
			retry, err = n.handle(ctx, alert, subscriber, alertMeta, dryRun)
			if err != nil {
				log.Error("send webhook 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 dryRun || !retry {
				break
			}
		}
		if err != nil {
			return err
		}
	}
	return nil
}

func (n *WebhookNotification) 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.AlertSubscriberTypeWebhook {
			continue
		}
		go func(subscriber entity.AlertSubscribersItem) {
			for retryTimes := 0; retryTimes < 3; retryTimes++ {
				retry, err := n.handle(asyncCtx, alert, subscriber, alertMeta, dryRun)
				if err != nil {
					log.Error("send webhook 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 dryRun || !retry {
					break
				}
			}
		}(subscriber)
	}
}

func (n *WebhookNotification) 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())
		}
	}()

	payload := newWebhookPayload(alert, alertMeta, dryRun)
	body, err := json.Marshal(payload)
	if err != nil {
		return false, fmt.Errorf("failed to marshal webhook payload: %w", err)
	}
	req, err := http.NewRequestWithContext(ctx, http.MethodPost, subscriber.WebhookUrl, bytes.NewReader(body))
	if err != nil {
		return false, fmt.Errorf("failed to create webhook request: %w", err)
	}
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("User-Agent", "TiDB-Cloud-Alert-Webhook/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 webhook notification failed: %w", err)
	}
	defer resp.Body.Close()
	if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
		responseBody, readErr := io.ReadAll(io.LimitReader(resp.Body, 4<<10))
		if readErr != nil {
			return shouldRetryWebhook(resp.StatusCode), fmt.Errorf(
				"webhook returned status %d and response could not be read: %w",
				resp.StatusCode, readErr)
		}
		return shouldRetryWebhook(resp.StatusCode), fmt.Errorf(
			"webhook returned status %d: %s", resp.StatusCode, string(responseBody))
	}

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

func newWebhookPayload(alert template.Alert, alertMeta AlertMeta, dryRun bool) webhookPayload {
	severity := alert.Labels[AlertLabelSeverity]
	if strings.ToLower(severity) == "major" {
		severity = "warning"
	}
	alertName := alert.Annotations[AlertAnnotationSubject]

	labels := map[string]string{
		"cluster":  alertMeta.ClusterName,
		"severity": severity,
	}
	if alertMeta.ProjectName != "" {
		labels["project"] = alertMeta.ProjectName
	}

	item := webhookAlert{
		Status: alert.Status,
		Labels: labels,
		Annotations: map[string]string{
			"alertname": alertName,
			"message": replaceEmailMessage(
				alert.Annotations[AlertAnnotationContent],
				alert,
				alertMeta.ProjectName,
				alertMeta.ClusterName,
			),
		},
		StartsAt: alert.StartsAt,
	}
	if !alert.EndsAt.IsZero() {
		item.EndsAt = &alert.EndsAt
	}

	// Alertmanager groups upstream, but we still dispatch one alert per call, so
	// alerts holds a single element and the common/group fields are derived from
	// it. This matches FlashDutyPayload.
	return webhookPayload{
		Receiver: webhookReceiverName,
		Status:   alert.Status,
		Alerts:   []webhookAlert{item},
		GroupKey: hashGroupKey(alertMeta.GroupKey),
		GroupLabels: map[string]string{
			"cluster":  alertMeta.ClusterName,
			"severity": severity,
		},
		CommonLabels: map[string]string{
			"cluster":  alertMeta.ClusterName,
			"severity": severity,
		},
		CommonAnnotations: map[string]string{"alertname": alertName},
		DryRun:            dryRun,
	}
}

// hashGroupKey turns Alertmanager's group key into an opaque identifier.
//
// The raw key cannot be forwarded: it embeds the route matchers and the group
// labels verbatim, which means tidb_cluster_id, o11y_biz_type, visibility, the
// internal rule name and the pre-mapping severity. Hashing keeps what
// customers actually need -- a stable id that is identical for every
// notification of the same group, so firing and resolved can be paired and our
// retries deduplicated -- while exposing none of it.
func hashGroupKey(groupKey string) string {
	if groupKey == "" {
		return ""
	}
	sum := sha256.Sum256([]byte(groupKey))
	return hex.EncodeToString(sum[:])[:16]
}

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