package regionalservice

import (
	"context"
	"fmt"
	"time"

	promv1 "github.com/prometheus/client_golang/api/prometheus/v1"
	"github.com/prometheus/common/model"
	v1 "github.com/tidbcloud/observability/api/regionalserver/resourceworker/v1"
	"github.com/tidbcloud/observability/pkg/common/constants"
	"github.com/tidbcloud/observability/pkg/common/errors"
	"github.com/tidbcloud/observability/pkg/common/message"
	"github.com/tidbcloud/observability/pkg/common/structs"
	"github.com/tidbcloud/observability/pkg/common/utils"
	"github.com/tidbcloud/observability/pkg/resourcemanager"
)

const (
	vmSelectUrlTemplate                 = "http://vmselect-vmcluster.observability:8481/select/%d:%d/prometheus"
	vmAlertUrlTemplate                  = "http://vmalert-%s.%s:8080"
	sharedVMAlertUrl                    = "http://vmalert-vmalert-shared.observability:8080"
	prometheusUrlTemplate               = "http://svc-%s.%s:9090"
	historyPromQLTmpl     string        = "ALERTS_FOR_STATE * ignoring(alertstate) group_left(alertstate) ALERTS{severity=~\"%s.*\",visibility=\"external\",alertstate!=\"pending\"}"
	maxTimeRange          time.Duration = 7 * 24 * time.Hour
	// step controls the granularity of historical data that we need to analyze.
	step = 1 * time.Minute
)

func (p *ClusterHandler) ListClusterExternalAlerts(ctx context.Context, req message.QueryExternalAlertsReq) (resp message.QueryExternalAlertsResp, err error) {

	cluster, err := checkAndGetCluster(ctx, req)
	if err != nil {
		return resp, err
	}

	queryURL, alertURL, err := getServerURL(ctx, cluster)
	if err != nil {
		return resp, err
	}

	var alerts []*structs.UserAlertInfo
	// push down alert filters as much as possible to gain better latency.
	// Sort by state first, then by severity
	// if contains closed alerts, sorted by activeAt.
	switch req.State {
	case structs.AlertStateClosed:
		// if startTime is nil, we assume that there are no Closed alerts.
		alerts, err = retrieveHistoryAlerts(ctx, queryURL, req.Severity, req.StartTime, req.EndTime)
		if err != nil {
			return resp, err
		}
		structs.SortUserAlertsByActiveAtDesc(alerts)
	case structs.AlertStateAll:
		alerts, err = getPromAPIAlerts(ctx, alertURL, req.Severity, req)
		if err != nil {
			return resp, err
		}
		history, err := retrieveHistoryAlerts(ctx, queryURL, req.Severity, req.StartTime, req.EndTime)
		if err != nil {
			return resp, err
		}
		alerts = append(alerts, history...)
		structs.SortUserAlertsByActiveAtDesc(alerts)
	default:
		alerts, err = getPromAPIAlerts(ctx, alertURL, req.Severity, req)
		if err != nil {
			return resp, err
		}
		structs.SortUserAlertsBySeverityAndActiveAt(alerts)
	}

	resp.Total = len(alerts)
	resp.Alerts = utils.PaginateSlice(alerts, req.Limit, req.Page)
	return resp, nil
}

func checkAndGetCluster(ctx context.Context, req message.QueryExternalAlertsReq) (*v1.Cluster, error) {
	if req.EndTime != nil && req.StartTime != nil {
		if *req.EndTime-*req.StartTime > int64(maxTimeRange/time.Second) {
			return nil, errors.NewErrorf(errors.ParameterInvalid, "time range exceeds maximum range of 7d")
		}
	}

	existed, e := resourcemanager.GetClusterReaderWriter(ctx).QueryClusters(ctx, req.TenantID, req.ProjectID, req.ApplicationID, req.BizType, message.ContinueRequest{})
	if e != nil {
		return nil, e
	} else if len(existed.Items) == 0 {
		return nil, errors.NewErrorf(errors.ClusterNotFound, "cluster not found, tenantID = %s, projectID = %s, applicationID = %s", req.TenantID, req.ProjectID, req.ApplicationID)
	}

	return &existed.Items[0], nil
}

// getPromAPIAlerts will call prometheus alerts api, and filter the user alerts out with desired condition.
func getPromAPIAlerts(ctx context.Context, url, severity string, req message.QueryExternalAlertsReq) ([]*structs.UserAlertInfo, error) {
	promAlerts, err := resourcemanager.GetPrometheusAPIClient().Alerts(url, ctx)
	if err != nil {
		return nil, err
	}

	endTime := req.EndTime
	alerts := []*structs.UserAlertInfo{}
	for _, alert := range promAlerts.Alerts {
		// TODO: pushdown some filters like tidbClusterId to vmalert.
		if alert.Labels[constants.ExternalLabelBizType] != "" && alert.Labels[constants.ExternalLabelBizType] != model.LabelValue(req.BizType) ||
			alert.Labels[constants.ExternalLabelTenantID] != "" && alert.Labels[constants.ExternalLabelTenantID] != model.LabelValue(req.TenantID) ||
			alert.Labels[constants.ExternalLabelTiDBClusterID] != "" && alert.Labels[constants.ExternalLabelTiDBClusterID] != model.LabelValue(req.ApplicationID) ||
			alert.State != promv1.AlertStateFiring ||
			alert.Labels[constants.UserAlertLabelKey] != constants.UserAlertLabelValue ||
			endTime != nil && alert.ActiveAt.Unix() > *endTime {
			continue
		}

		// special rules for severity: if severity is warning, we keep only warning and major alerts.
		if severity == "warning" &&
			string(alert.Labels[constants.UserAlertSeverityKey]) != "warning" &&
			string(alert.Labels[constants.UserAlertSeverityKey]) != "major" {
			continue
		}

		if severity == "critical" && string(alert.Labels[constants.UserAlertSeverityKey]) != severity {
			continue
		}
		alerts = append(alerts, &structs.UserAlertInfo{
			AlertName: string(alert.Annotations[constants.UserAlertDescriptionKey]),
			Message:   string(alert.Annotations[constants.UserAlertMessageKey]),
			ActiveAt:  alert.ActiveAt.Unix(),
			Duration:  time.Since(alert.ActiveAt).Round(time.Second).String(),
			Value:     alert.Value,
			RuleName:  string(alert.Labels[constants.UserAlertNameKey]),
			Severity:  structs.Severity(alert.Labels[constants.UserAlertSeverityKey]),
			State:     structs.AlertStateActive,
			// TODO: better to refactor the alert labels to avoid this kind of hard code.
			// maybe a specific label with labels templating will be better.
			Changefeed: string(alert.Labels[constants.UserAlertChangefeedKey]),
			JobID:      string(alert.Labels[constants.UserAlertJobIDKey]),
		})
	}
	return alerts, nil
}

func retrieveHistoryAlerts(ctx context.Context, url string, severity string, start, end *int64) ([]*structs.UserAlertInfo, error) {
	alertEvents := []*structs.UserAlertInfo{}
	if start == nil || end == nil {
		return alertEvents, nil
	}
	q := renderPromQL(severity)
	r := promv1.Range{
		Start: time.Unix(*start, 0),
		End:   time.Unix(*end, 0),
		Step:  step,
	}

	result, _, err := resourcemanager.GetPrometheusAPIClient().QueryRange(url, ctx, q, r)
	if err != nil {
		return alertEvents, fmt.Errorf("failed to run a range query Prometheus for alerts: %w", err)
	}
	if samples, ok := result.(model.Matrix); ok {
		// loop for every timeseries
		for _, sample := range samples {
			if len(sample.Values) == 0 {
				continue
			}
			// value is the unix timestamp for seconds
			startTs, endTs := sample.Values[0].Value, sample.Values[0].Timestamp
			// loop for every datapoints
			for _, pair := range sample.Values {
				// this means a timestamp change happens, we treat this as a new alert triggering.
				if pair.Value != startTs {
					activeAt := time.Unix(int64(startTs), 0)
					// construct the Alert Event and append it to the result.
					alertEvent := &structs.UserAlertInfo{
						AlertName:  string(sample.Metric[constants.UserAlertDescriptionKey]),
						Message:    string(sample.Metric[constants.UserAlertMessageKey]),
						ActiveAt:   int64(startTs),
						Duration:   endTs.Time().Sub(activeAt).Round(time.Second).String(),
						RuleName:   string(sample.Metric[constants.UserAlertNameKey]),
						State:      structs.AlertStateClosed,
						Severity:   structs.Severity(sample.Metric[constants.UserAlertSeverityKey]),
						Changefeed: string(sample.Metric[constants.UserAlertChangefeedKey]),
						JobID:      string(sample.Metric[constants.UserAlertJobIDKey]),
					}
					alertEvents = append(alertEvents, alertEvent)
					// update the startTs as a new Alert Event triggered.
					startTs = pair.Value
				}
				endTs = pair.Timestamp
			}
			// if last datapoint is 2-step away from end, we treat it a closed alert.
			// this is useful for the case that vmalert might not be able to send the timeseries to vmstorage.
			if *end-endTs.Unix() > 2*int64(step.Seconds()) {
				activeAt := time.Unix(int64(startTs), 0)
				alertEvent := &structs.UserAlertInfo{
					AlertName:  string(sample.Metric[constants.UserAlertDescriptionKey]),
					Message:    string(sample.Metric[constants.UserAlertMessageKey]),
					RuleName:   string(sample.Metric[constants.UserAlertNameKey]),
					ActiveAt:   activeAt.Unix(),
					Duration:   endTs.Time().Sub(activeAt).Round(time.Second).String(),
					State:      structs.AlertStateClosed,
					Severity:   structs.Severity(sample.Metric[constants.UserAlertSeverityKey]),
					Changefeed: string(sample.Metric[constants.UserAlertChangefeedKey]),
					JobID:      string(sample.Metric[constants.UserAlertJobIDKey]),
				}
				alertEvents = append(alertEvents, alertEvent)
			}
		}
	}
	return alertEvents, nil
}

func getServerURL(ctx context.Context, cluster *v1.Cluster) (queryURL string, alertURL string, err error) {
	if cluster.Labels[constants.LabelShareMode] == string(constants.SHARED) {
		vmSelectURL := fmt.Sprintf(vmSelectUrlTemplate, cluster.Spec.VictoriaAccount.AccountID, cluster.Spec.VictoriaAccount.ProjectID)
		vmRule, err := resourcemanager.GetPrometheusRulesReaderWriter(ctx, true).GetPrometheusRule(ctx, cluster.Labels[constants.LabelClusterID])
		if err != nil {
			return "", "", err
		}
		if vmRule.Labels == nil || vmRule.Labels[constants.LabelMultiTenancyRule] != "true" {
			// for non-shared vmalert, we need to use vmalert url to query alerts.
			vmAlertURL := fmt.Sprintf(vmAlertUrlTemplate, cluster.Name, cluster.Namespace)
			return vmSelectURL, vmAlertURL, nil
		}
		return vmSelectURL, sharedVMAlertUrl, nil
	}
	return fmt.Sprintf(prometheusUrlTemplate, cluster.Name, cluster.Namespace), fmt.Sprintf(prometheusUrlTemplate, cluster.Name, cluster.Namespace), nil
}

func renderPromQL(severity string) string {
	// map warning to warning|major, as webconsole only show warning to users.
	var severityRE = severity
	if severity == "warning" || severity == "major" {
		severityRE = "warning|major"
	}
	return fmt.Sprintf(historyPromQLTmpl, severityRE)
}
