package alertbyoc

import (
	"context"
	"crypto/rand"
	"encoding/base64"
	"errors"
	"fmt"
	"net/http"
	"strconv"
	"strings"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/go-resty/resty/v2"
	"github.com/pingcap/log"
	"github.com/prometheus/alertmanager/template"
	"go.uber.org/zap"
	"gorm.io/gorm"

	"github.com/pingcap-inc/clinic_cloud_server/pkg/alert"
	"github.com/pingcap-inc/clinic_cloud_server/pkg/alert/notification"
	"github.com/pingcap-inc/clinic_cloud_server/pkg/alertbyoc/repo"
	"github.com/pingcap-inc/clinic_cloud_server/pkg/client/mgmt_svc"
	"github.com/pingcap-inc/clinic_cloud_server/pkg/common"
	"github.com/pingcap-inc/clinic_cloud_server/pkg/config"
	"github.com/pingcap-inc/clinic_cloud_server/pkg/db"
	"github.com/pingcap-inc/clinic_cloud_server/pkg/entity"

	nextgenv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/nextgen/v1"
)

type Service struct {
	cfg                   *config.Config
	mgmtSvcClient         *mgmt_svc.Client
	alertService          *alert.Service
	alertmanagerTokenRepo *repo.AlertmanagerTokenRepo
	httpCli               *resty.Client
}

func NewService(
	cfg *config.Config,
	mgmtSvcClient *mgmt_svc.Client,
	alertService *alert.Service,
) *Service {
	httpCli := resty.New().
		SetTransport(http.DefaultTransport).
		SetTimeout(60 * time.Second).
		SetBaseURL(cfg.JiralertConfig.Endpoint).
		OnAfterResponse(func(c *resty.Client, r *resty.Response) error {
			if !r.IsSuccess() {
				return fmt.Errorf("call jiralert service failed, url: %s, method: %s, status: %s, body: %s",
					r.Request.URL, r.Request.Method, r.Status(), string(r.Body()))
			}
			return nil
		})

	svc := &Service{
		cfg:                   cfg,
		httpCli:               httpCli,
		mgmtSvcClient:         mgmtSvcClient,
		alertService:          alertService,
		alertmanagerTokenRepo: &repo.AlertmanagerTokenRepo{},
	}

	return svc
}

func (s *Service) RegisterRouter(r *gin.RouterGroup) {
	r.GET("/health", func(g *gin.Context) { g.String(http.StatusOK, "") })
	r.POST("/alert", s.DistributeAlerts)
	r.GET("/orgs/:orgId/token", s.GetToken)
}

func (s *Service) RegisterInternalRouter(r *gin.RouterGroup) {
	r.POST("/orgs/:orgId/refreshToken", s.RefreshToken)
}

func (s *Service) BasicAuthMiddleware() gin.HandlerFunc {
	return func(c *gin.Context) {
		authHeader := c.GetHeader("Authorization")
		if authHeader == "" {
			c.Header("WWW-Authenticate", `Basic realm="Authorization Required"`)
			c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Missing Authorization header"})
			return
		}

		authParts := strings.Split(authHeader, " ")
		if len(authParts) != 2 || authParts[0] != "Basic" {
			c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Invalid Authorization format"})
			return
		}

		decoded, err := base64.StdEncoding.DecodeString(authParts[1])
		if err != nil {
			c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Invalid Base64 encoding"})
			return
		}
		credentials := strings.SplitN(string(decoded), ":", 2)
		if len(credentials) != 2 {
			c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Invalid credentials format"})
			return
		}
		username, password := credentials[0], credentials[1]
		token, err := s.getAlertmanagerTokenByUsername(c, username)
		if err != nil {
			c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Invalid credentials"})
			return
		}
		if password != token {
			c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Invalid credentials"})
			return
		}

		c.Next()
	}
}

func getCredentialFromRequest(c *gin.Context) (string, string, error) {
	authHeader := c.GetHeader("Authorization")
	if authHeader == "" {
		return "", "", errors.New("missing authorization header")
	}

	authParts := strings.Split(authHeader, " ")
	if len(authParts) != 2 || authParts[0] != "Basic" {
		return "", "", errors.New("invalid authorization format")
	}

	decoded, err := base64.StdEncoding.DecodeString(authParts[1])
	if err != nil {
		return "", "", errors.New("invalid base64 format")
	}

	credentials := strings.SplitN(string(decoded), ":", 2)
	if len(credentials) != 2 {
		return "", "", errors.New("invalid credentials format")
	}

	return credentials[0], credentials[1], nil
}

func (s *Service) DistributeAlerts(c *gin.Context) {
	var payload template.Data
	if err := c.ShouldBindJSON(&payload); err != nil {
		common.FailWithCode(c, http.StatusBadRequest, err.Error())
		return
	}

	// verify alerts
	// get credentials from request
	username, _, err := getCredentialFromRequest(c)
	if err != nil {
		common.FailWithCode(c, http.StatusInternalServerError, err.Error())
		return
	}

	token, err := s.alertmanagerTokenRepo.Query(c, repo.AlertmanagerTokenQuery{
		Username: username,
	})
	if err != nil {
		common.FailWithCode(c, http.StatusInternalServerError, err.Error())
		return
	}
	tenantID := token.TenantID
	log.Info("get tenant id by username",
		zap.Uint64("tenant_id", tenantID))

	validAlerts := make([]template.Alert, 0)
	for _, alert := range payload.Alerts {
		alertTenant, err := s.getTenantIDFromAlert(c, alert)
		if err != nil {
			log.Error("failed to get tenant id from alert",
				zap.Error(err),
				zap.Any("alert", alert),
			)
			continue
		}
		log.Info("get tenant id from alert",
			zap.Uint64("tenant_id", alertTenant))
		if alertTenant == tenantID {
			validAlerts = append(validAlerts, alert)
		}
	}

	data := template.Data{
		Receiver:          payload.Receiver,
		Status:            payload.Status,
		Alerts:            validAlerts,
		GroupLabels:       payload.GroupLabels,
		CommonLabels:      payload.CommonLabels,
		CommonAnnotations: payload.CommonAnnotations,
		ExternalURL:       payload.ExternalURL,
	}
	log.Info("validate alerts",
		zap.Int("alert len", len(validAlerts)),
		zap.String("alert receiver", payload.Receiver))

	if strings.HasSuffix(data.Receiver, notification.AlertReceiverCustomer) {
		log.Info("send alerts to cloud server")
		err = s.pushAlertsToCloudServer(c, data)
	} else {
		log.Info("send alerts to jiralert")
		err = s.pushAlertsToJiralert(c, data)
	}

	if err != nil {
		common.FailWithCode(c, http.StatusInternalServerError, err.Error())
		return
	}

	common.Success(c, nil)
}

func (s *Service) GetToken(c *gin.Context) {
	orgID := c.Param("orgId")
	tenantID, err := strconv.ParseUint(orgID, 10, 64)
	if err != nil {
		log.Error("parse tenant id failed",
			zap.Error(err),
			zap.String("tenant_id", orgID),
		)
		common.FailWithCode(c, http.StatusInternalServerError, err.Error())
		return
	}
	log.Info("get tenantID", zap.Uint64("tenant_id", tenantID))

	username, password, err := s.getOrCreateAlertmanagerToken(c, tenantID)
	if err != nil {
		log.Error("get token failed",
			zap.Error(err),
			zap.String("tenant_id", orgID),
		)
		common.FailWithCode(c, http.StatusInternalServerError, err.Error())
		return
	}

	common.Success(c, entity.GetAlertmanagerTokenResponse{
		Username: username,
		Password: password,
	})
}

func (s *Service) RefreshToken(c *gin.Context) {
	orgID := c.Param("orgId")
	tenantID, err := strconv.ParseUint(orgID, 10, 64)
	if err != nil {
		log.Error("parse tenant id failed",
			zap.Error(err),
			zap.String("tenant_id", orgID),
		)
		common.FailWithCode(c, http.StatusInternalServerError, err.Error())
		return
	}

	err = s.refreshToken(c, tenantID)
	if err != nil {
		log.Error("refresh tokens failed", zap.Error(err), zap.Uint64("tenant_id", tenantID))
		common.FailWithCode(c, http.StatusInternalServerError, "failed to refresh tokens")
		return
	}
	common.Success(c, nil)
}

func (s *Service) getOrCreateAlertmanagerToken(ctx context.Context, tenantID uint64) (string, string, error) {
	// get token
	username, password, err := s.getAlertmanagerTokenByTenantID(ctx, tenantID)
	if err == nil {
		return username, password, nil
	}
	if err != nil && err != gorm.ErrRecordNotFound {
		return "", "", err
	}
	// if token doesn't exist then create and save
	log.Info("token doesn't exist, create", zap.Uint64("tenant_id", tenantID))
	username = fmt.Sprintf("customer-alertmanager-%d", tenantID)
	password, err = generateSecureToken(16)
	if err != nil {
		return "", "", err
	}
	err = db.Transaction(func(tx *gorm.DB) error {
		dbAlertmanagerToken := &db.AlertmanagerToken{
			TenantID: tenantID,
			Username: username,
			Password: password,
		}
		if err := s.alertmanagerTokenRepo.Save(ctx, dbAlertmanagerToken); err != nil {
			return fmt.Errorf("save alertmanager token failed, %w", err)
		}
		return nil
	})
	if err != nil {
		return "", "", err
	}
	return username, password, nil
}

func (s *Service) getAlertmanagerTokenByUsername(ctx context.Context, username string) (string, error) {
	token, err := s.alertmanagerTokenRepo.Query(ctx, repo.AlertmanagerTokenQuery{
		Username: username,
	})
	if err != nil {
		return "", err
	}
	return token.Password, nil
}

func (s *Service) getAlertmanagerTokenByTenantID(ctx context.Context, tenantID uint64) (string, string, error) {
	token, err := s.alertmanagerTokenRepo.Query(ctx, repo.AlertmanagerTokenQuery{
		TenantId: tenantID,
	})
	if err != nil {
		return "", "", err
	}
	return token.Username, token.Password, nil
}

func (s *Service) getTenantIDFromAlert(
	ctx context.Context,
	alert template.Alert,
) (uint64, error) {
	// get tenant id from `tenant` label
	if tenantIDStr, exist := alert.Labels[notification.AlertLabelTenant]; exist {
		tenantID, err := strconv.ParseUint(tenantIDStr, 10, 64)
		if err == nil {
			return tenantID, nil
		}
	}
	// get tenant id from `o11y_tenant_id` label
	if tenantIDStr, exist := alert.Labels[notification.AlertLabelO11yTenantID]; exist {
		tenantID, err := strconv.ParseUint(tenantIDStr, 10, 64)
		if err == nil {
			return tenantID, nil
		}
	}

	clusterIDStr, exist := alert.Labels[notification.AlertLabelTiDBClusterID]
	if !exist {
		clusterIDStr = alert.Labels[notification.AlertLabelClusterID]
	}
	clusterID, err := strconv.ParseUint(clusterIDStr, 10, 64)
	if err != nil {
		return 0, fmt.Errorf("failed to parse cluster_id from alert labels: %w, cluser_id: %s", err, clusterIDStr)
	}

	cluster, err := s.mgmtSvcClient.GetCluster(ctx, clusterID)
	log.Info("get cluster by id", zap.Uint64("cluster_id", clusterID), zap.Error(err))

	if err != nil {
		// if cluster not found, try to get resource pool
		resourcePool, err := s.mgmtSvcClient.GetResourcePool(ctx, clusterID)
		if err != nil {
			return 0, fmt.Errorf("failed to get resource pool by id: %w, resource_pool_id: %d", err, clusterID)
		}
		rpp := resourcePool.GetResourcePool()
		if rpp.GetOwnerType() == nextgenv1.ResourcePool_ORGANIZATION {
			return rpp.GetOwnerId(), nil
		}
		return 0, fmt.Errorf("resource pool owner_type is not organization (owner_type=%v), resource_pool_id: %d",
			rpp.GetOwnerType(), clusterID)
	}
	return cluster.OrganizationId, nil
}

func (s *Service) pushAlertsToCloudServer(ctx context.Context, payload template.Data) error {
	s.alertService.PushAlerts(ctx, payload, "")
	return nil
}

func (s *Service) pushAlertsToJiralert(ctx context.Context, payload template.Data) error {
	// convert alert receiver to jiralert receiver
	receiver := s.cfg.JiralertConfig.Receiver.Critical
	if strings.HasSuffix(payload.Receiver, notification.AlertReceiverPingCAPWarning) {
		receiver = s.cfg.JiralertConfig.Receiver.Warning
	} else if strings.HasSuffix(payload.Receiver, notification.AlertReceiverPingCAPMajor) {
		receiver = s.cfg.JiralertConfig.Receiver.Major
	}
	payload.Receiver = receiver
	log.Info("convert alert receiver to jiralert receiver",
		zap.String("receiver", payload.Receiver))

	_, err := s.httpCli.R().
		SetContext(ctx).
		SetBody(payload).
		Post("/alert")
	return err
}

func (s *Service) createOrUpdateAlertmanagerToken(ctx context.Context,
	tenantId uint64,
	username string,
	token string,
) error {
	err := db.Transaction(func(tx *gorm.DB) error {
		existingToken, err := s.alertmanagerTokenRepo.Query(ctx, repo.AlertmanagerTokenQuery{
			TenantId: tenantId,
		})
		if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
			return fmt.Errorf("query alertmanager token failed, %w", err)
		}

		if existingToken != nil {
			existingToken.Username = username
			existingToken.Password = token

			if err := s.alertmanagerTokenRepo.Save(ctx, existingToken); err != nil {
				return fmt.Errorf("update alertmanager token failed, %w", err)
			}
		} else {
			dbAlertmanagerToken := &db.AlertmanagerToken{
				TenantID: tenantId,
				Username: username,
				Password: token,
			}

			if err := s.alertmanagerTokenRepo.Save(ctx, dbAlertmanagerToken); err != nil {
				return fmt.Errorf("save alertmanager token failed, %w", err)
			}
		}
		return nil
	})
	if err != nil {
		return err
	}
	return nil
}

func generateSecureToken(length int) (string, error) {
	b := make([]byte, length)
	_, err := rand.Read(b)
	if err != nil {
		return "", err
	}

	return base64.URLEncoding.WithPadding(base64.NoPadding).EncodeToString(b), nil
}
