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 FlashDutyPayload = `{
    "receiver": "flashduty",
    "status": "$STATUS",
    "alerts": [
        {
            "status": "$STATUS",
            "labels": {
                "cluster": "$CLUSTER_NAME",
				"project": "$PROJECT_NAME",
                "severity": "$SEVERITY"
            },
            "annotations": {
                "alertname": "$ALERT_NAME",
                "message": "$MESSAGE"
            },
            "startsAt": "$ISO_STARTS_AT"
        }
    ],
    "groupLabels": {
        "cluster": "$CLUSTER_NAME",
        "severity": "$SEVERITY"
    },
    "commonLabels": {
        "cluster": "$CLUSTER_NAME",
        "severity": "$SEVERITY"
    },
    "commonAnnotations": {
        "alertname": "$ALERT_NAME"
    }
}`

var DryRunFlashDutyPayload = `{
    "receiver": "flashduty",
    "status": "$STATUS",
    "alerts": [
        {
            "status": "$STATUS",
            "labels": {
                "severity": "$SEVERITY"
            },
            "annotations": {
                "alertname": "$ALERT_NAME",
                "message": "$MESSAGE"
            },
            "startsAt": "$ISO_STARTS_AT"
        }
    ],
    "groupLabels": {
        "severity": "$SEVERITY"
    },
    "commonLabels": {
        "severity": "$SEVERITY"
    }
}`

type FlashDutyNotification struct {
	notificationRepo *repo.AlertNotificationRepo
}

func (n *FlashDutyNotification) Push(
	ctx context.Context,
	alert template.Alert,
	subscribers []entity.AlertSubscribersItem,
	alertMeta AlertMeta,
	dryRun bool,
) error {
	for _, subscriber := range subscribers {
		if subscriber.SubscriberType != entity.AlertSubscriberTypeFlashDuty {
			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 flashduty 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 *FlashDutyNotification) PushAsync(
	ctx context.Context,
	alert template.Alert,
	subscribers []entity.AlertSubscribersItem,
	alertMeta AlertMeta,
	dryRun bool,
) {
	for _, subscriber := range subscribers {
		if subscriber.SubscriberType != entity.AlertSubscriberTypeFlashDuty {
			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 flashduty 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 *FlashDutyNotification) 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("flashduty_url", subscriber.FlashDutyUrl),
		zap.Bool("dry_run", dryRun),
	)
	logger.Info("start to send flashduty 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())
		}
	}()

	flashDutyUrl := subscriber.FlashDutyUrl
	client := http.Client{
		Timeout: 15 * time.Second,
	}
	flashDutyPayload := FlashDutyPayload
	if dryRun {
		flashDutyPayload = DryRunFlashDutyPayload
	}
	payload := replaceWebhookPayload(
		flashDutyPayload,
		alert,
		alertMeta.ProjectName,
		alertMeta.ClusterName,
	)
	req, err := http.NewRequest("POST", flashDutyUrl, 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 flashduty 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 flashduty notification failed, status code: %d, response: %w", resp.StatusCode, err)
		}
		return IsRetry(resp.StatusCode), fmt.Errorf("send flashduty notification failed, status code: %d, response: %s", resp.StatusCode, string(bodyBytes))
	}

	updateNotificationStatus(ctx, logger, n.notificationRepo, notificationRecord, "success", "")
	logger.Info("send flashduty notification success", zap.String("payload", payload))
	return false, nil
}
