package globalapi

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

	"github.com/pingcap/log"
	"github.com/tidbcloud/observability/pkg/common/structs"
	"go.uber.org/zap"
	corev1 "k8s.io/api/core/v1"
	"k8s.io/apimachinery/pkg/api/resource"

	"github.com/ddliu/go-httpclient"
	"github.com/gin-gonic/gin"
	"github.com/tidbcloud/observability/pkg/common/awsauth"
	"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/metrics"
	"github.com/tidbcloud/observability/pkg/globalservice"
	"github.com/tidbcloud/observability/pkg/regionalapi/apiinterceptor"
)

// HandleAlerts
// @Summary handle alerts
// @Description handle alerts
// @Tags ops
// @Accept json
// @Produce json
// @Param request body message.GlobalHandleAlertReq true " handle alert"
// @Success 202 {object} message.Result{data=message.GlobalHandleAlertResp}
// @Failure 500 {object} message.Result
// @Router /ops/events/alerts/ [post]
func HandleAlerts(c *gin.Context) {
	req := &message.GlobalHandleAlertReq{}
	err := c.ShouldBindJSON(req)
	if err != nil {
		c.JSON(errors.ParameterInvalid.GetHttpCode(), &message.Result{
			Code:    int(errors.ParameterInvalid),
			Message: err.Error(),
		})
	}

	if req.Status != "firing" || len(req.Alerts) == 0 {
		log.Debug("alerts received",
			zap.String("Notification_Status", req.Status),
			zap.Int("Alters_Number", len(req.Alerts)),
		)
		return
	}

	// get token
	token, err := awsauth.GetStsToken(c)
	if err != nil {
		awsauth.GetStsTokenCache().Flush()
		metrics.HandleGlobalBizErrorMetrics(fmt.Sprintf("gen sts token failed. error: %s", err.Error()))
		apiinterceptor.Fail(c, err)
		return
	}
	headers := map[string]string{
		constants.AuthorizationHeader: fmt.Sprintf("%s %s", constants.BearerPrefix, token),
		"Content-Type":                "application/json",
	}

	for _, alert := range req.Alerts {
		log.Debug("handle alert start")
		err = handleAlert(c, alert, headers)
		log.Debug("handle alert end")
		if err != nil {
			_, err = postAlertManager("http://self-monitoring-kube-prome-alertmanager.self-monitoring.svc.cluster.local:9093", generateNewAlert(alert))
			if err != nil {
				apiinterceptor.Fail(c, err)
				return
			}
		}
	}

	c.JSON(http.StatusOK, message.Result{})
}

func handleAlert(c *gin.Context, alert structs.Alert, headers map[string]string) error {
	id := alert.Labels[constants.O11YK8SIDLabel]
	var logicalRegion string
	if strings.HasPrefix(id, constants.AlertClusterIDPrefixProd) {
		logicalRegion = strings.TrimPrefix(id, constants.AlertClusterIDPrefixProd)
	} else if strings.HasPrefix(id, constants.AlertClusterIDPrefixStaging) {
		logicalRegion = strings.TrimPrefix(id, constants.AlertClusterIDPrefixStaging)
	} else if strings.HasPrefix(id, constants.AlertClusterIDPrefixDev) {
		logicalRegion = strings.TrimPrefix(id, constants.AlertClusterIDPrefixDev)
	} else {
		log.Debug("get alert", zap.String("O11y_K8s_ID", id))
		return nil
	}

	// get meta info of the logical region
	var vendor constants.ProvidedVendor
	var region, routeAddress string
	allRegions := globalservice.GetGlobalManager().GetGlobalMetaCache()
	for v, regions := range allRegions.Vendors {
		for _, re := range regions {
			for _, lr := range re.Routers {
				if logicalRegion == lr.LogicalRegionName {
					vendor = v
					region = re.Region
					routeAddress = lr.DnsAddress
				}
			}
		}
	}

	log.Info("get alert details",
		zap.String("Cluster_ID", id),
		zap.String("Vendor", string(vendor)),
		zap.String("Region", logicalRegion))

	if len(routeAddress) == 0 {
		err := errors.NewErrorf(errors.GetRegionalRouteAddressError,
			"failed to get the routing address of vendor `%s`, region `%s`, logicalRegion `%s`", string(vendor), region, logicalRegion)
		apiinterceptor.Fail(c, err)
		return err
	}
	// get service info
	serviceInfo := message.GetServiceInfoResp{}
	url := fmt.Sprintf("%s/api/v1/", routeAddress)

	httpResp, err := httpclient.Begin().WithHeaders(headers).Get(url)
	if err != nil {
		log.Error("handle alert httpclient error", zap.Error(err))
		return err
	}
	body, _ := io.ReadAll(httpResp.Body)

	type Result struct {
		Code int                        `json:"code"`
		Data message.GetServiceInfoResp `json:"data"`
	}

	result := &Result{
		Data: serviceInfo,
	}

	err = json.Unmarshal(body, &result)
	if err != nil {
		log.Error("handle alert unmarshal error", zap.Error(err))
		return err
	}

	err = handleAlertInRegion(alert, result.Data, AccessRegionReq{
		RouteAddress: routeAddress,
		Vendor:       string(vendor),
		Region:       region,
		Headers:      headers,
	})

	return err
}

//gocyclo:ignore
func handleAlertInRegion(alert structs.Alert, serviceInfo message.GetServiceInfoResp, regionReq AccessRegionReq) (err error) {
	req := message.ModifyMonitorResourceReq{}
	maxSpec := globalservice.GetGlobalManager().GetGlobalMetaCache().MaxSpec
	// if maxSpec is empty then return
	if maxSpec == "" {
		err = errors.New(errors.ConfigInvalid, "maxSpec is invalid")
		log.Error("invalid maxSpec configuration", zap.Error(err))
		return
	}

	var id string
	if alert.Status != "firing" {
		log.Debug("handle non-firing alert", zap.String("Alert_Status", alert.Status))
		return
	}

	if alert.Labels["alertname"] == constants.PrometheusCPUAlert && alert.Labels["container"] == "prometheus" {
		pod := alert.Labels["pod"]
		log.Info("handle PrometheusCPUAlert",
			zap.String("Alert_Name", alert.Labels["alertname"]),
			zap.String("Pod", pod))
		if strings.HasPrefix(pod, "prometheus-") && strings.HasSuffix(pod, "-0") {
			id = pod[11 : len(pod)-2]
		} else {
			log.Info("alert pod is not the prometheus of a cluster")
			return
		}
		req, err = handleCPUAlert(regionReq, serviceInfo.AvailableSpecs, maxSpec, id)
	} else if alert.Labels["alertname"] == constants.PrometheusMemoryAlert && strings.HasPrefix(alert.Labels["pod"], "prometheus-") {
		pod := alert.Labels["pod"]
		log.Info("handle PrometheusMemoryAlert",
			zap.String("Alert_Name", alert.Labels["alertname"]),
			zap.String("Pod", pod))
		if strings.HasPrefix(pod, "prometheus-") && strings.HasSuffix(pod, "-0") {
			id = pod[11 : len(pod)-2]
		} else {
			log.Info("alert pod is not the prometheus of a cluster")
			return
		}
		req, err = handleMemoryAlert(regionReq, serviceInfo.AvailableSpecs, maxSpec, id)
	} else if (alert.Labels["alertname"] == constants.VMAlertMemoryAlert || alert.Labels["alertname"] == constants.VMAlertOOMAlert) && strings.HasPrefix(alert.Labels["pod"], "vmalert-") {
		pod := alert.Labels["pod"]
		log.Info("handle VMAlert",
			zap.String("Alert_Name", alert.Labels["alertname"]),
			zap.String("Pod", pod))
		if strings.HasPrefix(pod, "vmalert-") && !strings.Contains(pod, "shared") {
			id = pod[8:44]
		} else {
			log.Info("alert pod is not the vmalert of a cluster")
			return
		}
		req, err = handleMemoryAlert(regionReq, serviceInfo.AvailableSpecs, maxSpec, id)
	} else if alert.Labels["alertname"] == constants.PrometheusPersistentAlert && strings.HasPrefix(alert.Labels["persistentvolumeclaim"], "prometheus-") {
		ns := alert.Labels["namespace"]
		pvcName := alert.Labels["persistentvolumeclaim"]
		id = pvcName[11:strings.Index(pvcName, "-db-prometheus-")]
		log.Info("handle PrometheusPersistentAlert",
			zap.String("Alert_Name", alert.Labels["alertname"]),
			zap.String("PVC_Name", pvcName))
		req, err = handleStorageAlert(regionReq, id, ns, pvcName)
	} else {
		log.Debug("can not handle this alert")
		return
	}
	if err != nil {
		log.Error("handle alert in region failed", zap.Error(err))
		return
	}
	req.CommonReqInfo = structs.CommonReqInfo{
		Vendor: constants.ProvidedVendor(regionReq.Vendor),
		Region: regionReq.Region,
	}

	regionalApiUrl := fmt.Sprintf("%s/api/v1/clusters/%s/monitor-resource?vendor=%s&region=%s", regionReq.RouteAddress, id, regionReq.Vendor, regionReq.Region)
	body, _ := json.Marshal(req)

	resp, err := httpclient.Begin().WithHeaders(regionReq.Headers).PostJson(regionalApiUrl, body)
	if err != nil {
		log.Error("httpclient post failed", zap.Error(err))
		return
	}
	if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusAccepted {
		err = errors.NewErrorf(errors.HttpClientResponseError, "handle alert in regional failed")
		log.Error("handle alert in regional failed", zap.Error(err), zap.Int("Status_Code", resp.StatusCode))
	}
	return err
}

func getClusterDetail(regionReq AccessRegionReq, id string) (cluster message.DetailClusterResp, err error) {
	url := fmt.Sprintf("%s/api/v1/clusters/%s?vendor=%s&region=%s", regionReq.RouteAddress, id, regionReq.Vendor, regionReq.Region)
	httpResp, err := httpclient.Begin().WithHeaders(regionReq.Headers).Get(url)

	if err != nil {
		log.Error("get cluster detail http error", zap.Error(err))
		return
	}
	body, _ := io.ReadAll(httpResp.Body)
	type Result struct {
		Code int                       `json:"code"`
		Data message.DetailClusterResp `json:"data"`
	}

	result := &Result{
		Data: cluster,
	}

	err = json.Unmarshal(body, &result)
	if err != nil {
		log.Error("unmarshal cluster detail response failed", zap.Error(err))
		return
	}
	return result.Data, err
}

type AccessRegionReq struct {
	RouteAddress string
	Vendor       string
	Region       string
	Headers      map[string]string
}

func handleCPUAlert(regionReq AccessRegionReq, availableSpecs map[string]corev1.ResourceRequirements, maxSpec string, id string) (req message.ModifyMonitorResourceReq, err error) {
	// get cluster detail
	cluster, err := getClusterDetail(regionReq, id)
	if err != nil {
		log.Error("failed to get cluster detail", zap.Error(err))
		return
	}

	// determine last scale time
	hourTime := time.Now().Add(-time.Hour * 1).Unix()
	t, err := strconv.ParseInt(cluster.LastScaleTime["spec"], 10, 64)
	if err != nil {
		log.Error("failed to parse cluster spec,Is config right?", zap.Error(err))
		return
	}
	if t >= hourTime {
		err = errors.NewErrorf(errors.MethodNotSupported, "can not scale because the last scale time was less than an hour")
		return
	}
	// get new specID
	newSpecID, err := getNewSpecIDCpu(cluster.ObservabilityCluster, availableSpecs)
	if err != nil {
		return
	}

	request := availableSpecs[newSpecID]
	// determine if the new spec exceeds the max spec

	maxCpu, _ := resource.ParseQuantity(maxSpec[0:strings.Index(maxSpec, "C")])
	maxMemory, _ := resource.ParseQuantity(maxSpec[strings.Index(maxSpec, "C")+1:] + "i")

	if request.Requests.Cpu().AsApproximateFloat64() >= maxCpu.AsApproximateFloat64() || request.Requests.Memory().AsApproximateFloat64() >= maxMemory.AsApproximateFloat64() {
		err = errors.NewErrorf(errors.MethodNotSupported, "can not scale as it will exceed the max spec limit")
		return
	}
	// generate scale request
	req = message.ModifyMonitorResourceReq{
		ID:           id,
		ResourceType: corev1.ResourceCPU,
		ResourceMeta: map[corev1.ResourceName]message.ResourceMeta{
			corev1.ResourceCPU: {
				SpecID: newSpecID,
			},
		},
	}
	return
}

func handleMemoryAlert(regionReq AccessRegionReq, availableSpecs map[string]corev1.ResourceRequirements, maxSpec string, id string) (req message.ModifyMonitorResourceReq, err error) {
	cluster, err := getClusterDetail(regionReq, id)
	if err != nil {
		log.Error("failed to get cluster detail", zap.Error(err))
		return
	}

	hourTime := time.Now().Add(-time.Hour * 1).Unix()
	t, err := strconv.ParseInt(cluster.LastScaleTime["spec"], 10, 64)
	if err != nil {
		log.Error("failed to parse cluster spec,Is config right?", zap.Error(err))
		return
	}
	if t >= hourTime {
		err = errors.NewErrorf(errors.MethodNotSupported, "can not scale because the last scale time was less than an hour")
		return
	}

	// get new specID
	newSpecID, err := getNewSpecIDMemory(cluster.ObservabilityCluster, availableSpecs)
	if err != nil {
		return
	}
	request := availableSpecs[newSpecID]
	// determine if the new spec exceeds the max spec
	maxCpu, _ := resource.ParseQuantity(maxSpec[0:strings.Index(maxSpec, "C")])
	maxMemory, _ := resource.ParseQuantity(maxSpec[strings.Index(maxSpec, "C")+1:] + "i")
	if request.Requests.Cpu().AsApproximateFloat64() >= maxCpu.AsApproximateFloat64() || request.Requests.Memory().AsApproximateFloat64() >= maxMemory.AsApproximateFloat64() {
		err = errors.NewErrorf(errors.MethodNotSupported, "can not scale as it will exceed the max spec limit")
		return
	}

	req = message.ModifyMonitorResourceReq{
		ID:           id,
		ResourceType: corev1.ResourceMemory,
		ResourceMeta: map[corev1.ResourceName]message.ResourceMeta{
			corev1.ResourceMemory: {
				SpecID: newSpecID,
			},
		},
	}
	return
}

func handleStorageAlert(regionReq AccessRegionReq, id string, ns string, pvcName string) (req message.ModifyMonitorResourceReq, err error) {
	cluster, err := getClusterDetail(regionReq, id)
	if err != nil {
		log.Error("failed to get cluster detail", zap.Error(err))
		return
	}

	hourTime := time.Now().Add(-time.Hour * 1).Unix()
	t, err := strconv.ParseInt(cluster.LastScaleTime["storage"], 10, 64)
	if t >= hourTime {
		err = errors.NewErrorf(errors.MethodNotSupported, "can not scale because the last scale time was less than an hour")
		return
	}
	req = message.ModifyMonitorResourceReq{
		ID:           id,
		ResourceType: corev1.ResourceStorage,
		ResourceMeta: map[corev1.ResourceName]message.ResourceMeta{
			corev1.ResourceStorage: {
				Namespace: ns,
				PVCName:   pvcName,
			},
		},
	}
	return
}

func getNewSpecIDCpu(cluster structs.ObservabilityCluster, availableSpecs map[string]corev1.ResourceRequirements) (newSpecID string, err error) {
	nowSpec := availableSpecs[cluster.MonitorInfo.SpecID]
	sortedSpecs := getSpecsArray(availableSpecs)
	sort.Sort(sortedSpecs)
	for _, s := range sortedSpecs {
		if nowSpec.Requests.Cpu().AsApproximateFloat64() < s.Resource.Requests.Cpu().AsApproximateFloat64() && nowSpec.Requests.Memory().AsApproximateFloat64() <= s.Resource.Requests.Memory().AsApproximateFloat64() {
			newSpecID = s.SpecID
			break
		}
	}

	if len(newSpecID) == 0 {
		err = errors.NewErrorf(errors.MethodNotSupported, "%s", fmt.Sprintf("Failed to scale the monitor spec for cluster %s because there is no available bigger spec", cluster.ID))
		return
	}
	return
}

func getNewSpecIDMemory(cluster structs.ObservabilityCluster, availableSpecs map[string]corev1.ResourceRequirements) (newSpecID string, err error) {
	spec := cluster.MonitorInfo.SpecID
	if len(spec) == 0 {
		spec = "vmalert.default"
	}
	nowSpec := availableSpecs[spec]
	sortedSpecs := getSpecsArray(availableSpecs)
	sort.Sort(sortedSpecs)
	for _, s := range sortedSpecs {
		if nowSpec.Requests.Memory().AsApproximateFloat64() < s.Resource.Requests.Memory().AsApproximateFloat64() && nowSpec.Requests.Cpu().AsApproximateFloat64() <= s.Resource.Requests.Cpu().AsApproximateFloat64() {
			newSpecID = s.SpecID
			break
		}
	}

	if len(newSpecID) == 0 {
		err = errors.NewErrorf(errors.MethodNotSupported, "%s", fmt.Sprintf("Failed to scale the monitor spec for cluster %s because there is no available bigger spec", cluster.ID))
		return
	}
	return
}

type AvailableSpecs struct {
	SpecID   string
	Resource corev1.ResourceRequirements
}
type SpecsArray []AvailableSpecs

func (s SpecsArray) Len() int {
	return len(s)
}

func (s SpecsArray) Less(i, j int) bool {
	if s[i].Resource.Requests.Cpu().AsApproximateFloat64() < s[j].Resource.Requests.Cpu().AsApproximateFloat64() {
		return true
	} else if s[i].Resource.Requests.Cpu().AsApproximateFloat64() == s[j].Resource.Requests.Cpu().AsApproximateFloat64() &&
		s[i].Resource.Requests.Memory().AsApproximateFloat64() < s[j].Resource.Requests.Memory().AsApproximateFloat64() {
		return true
	} else {
		return false
	}
}

func (s SpecsArray) Swap(i, j int) {
	s[i], s[j] = s[j], s[i]
}

func getSpecsArray(specs map[string]corev1.ResourceRequirements) SpecsArray {
	var specsArr = make(SpecsArray, 0)

	for id, spec := range specs {
		specsArr = append(specsArr, AvailableSpecs{
			SpecID:   id,
			Resource: spec,
		})
	}
	return specsArr
}

type Alert struct {
	Labels       map[string]string `json:"labels"`
	Annotations  map[string]string `json:"annotations"`
	GeneratorURL string            `json:"generatorURL" validate:"optional"`
}

func postAlertManager(domain string, alert Alert) (*httpclient.Response, error) {
	api := fmt.Sprintf("%s/api/v1/alerts", domain)
	req := make([]*Alert, 0)
	req = append(req, &alert)
	body, err := json.Marshal(req)
	if err != nil {
		return nil, err
	}
	headers := map[string]string{
		"Content-Type": "application/json",
	}
	resp, err := httpclient.Begin().WithHeaders(headers).PostJson(api, body)
	log.Info("send new alert to alertmanager")
	return resp, err
}

func generateNewAlert(alert structs.Alert) Alert {
	req := Alert{
		Labels:      alert.Labels,
		Annotations: alert.Annotations,
	}
	// promote the alert severity to critical
	req.Labels["severity"] = "critical"

	return req
}
