package integration

import (
	"context"
	"errors"
	"fmt"
	"strings"

	"net/http"

	"github.com/ngaut/log"
	"github.com/shiningrush/fastflow/pkg/mod"
	"github.com/tidbcloud/metrics-integration/global/internal/domain/entity"
	"github.com/tidbcloud/metrics-integration/global/internal/infrastructure/account"
	"github.com/tidbcloud/metrics-integration/global/internal/infrastructure/di"
	"github.com/tidbcloud/metrics-integration/global/internal/infrastructure/grpc/grpcli"
	"github.com/tidbcloud/metrics-integration/global/pkg/cvt"
	"github.com/tidbcloud/pkgv2/secret/encrypt"
	"github.com/tidbcloud/tiinf/sdk/auditlog"
	"go.uber.org/zap"
	"gorm.io/gorm"
)

// CreateNewrelicIntegration enables the newrelic integration for a given customer project.
// @param payload - types of data that Newrelic will collect.
func CreateNewrelicIntegration(ctx context.Context, clusterId entity.ID, site entity.NewrelicSite, apiKey string, creator string) (entity.Integration, error) {
	var err error

	// auditlog
	defer func() {
		details := auditlog.Details{
			auditlog.KeyDescription:     "a metrics integration is created",
			auditlog.KeyIntegrationType: "newrelic",
			auditlog.KeyError:           err,
		}
		user := account.GetUserFromContext(ctx)
		eventType := auditlog.EventCreateMetricsIntegration
		details[auditlog.KeyApiKey] = MaskNewrelicAPIKey(apiKey)
		details[auditlog.KeySite] = string(site)
		eventSubmitErr := auditlog.NewEvent(ctx, eventType).SetResult(err == nil).SetDetails(details.Build()).SetOrgID(user.TenantID).Submit()
		if eventSubmitErr != nil {
			log.Error(ctx, "CreateIntegration submit event fail",
				zap.Uint64("tenantID", user.TenantID),
				zap.Uint64("userID", user.ID),
				zap.Error(eventSubmitErr))
		}
	}()

	// get infos from cluster service
	cluster, err := grpcli.GetClusterByID(ctx, uint64(clusterId))
	if err != nil {
		return entity.Integration{}, fmt.Errorf("failed to get cluster info: %w", err)
	}
	region := cluster.Region
	cloudProviderType, err := normalizeIntegrationCloudProviderType(cluster.CloudProvider)
	if err != nil {
		return entity.Integration{}, err
	}

	// Map api models to db entity models.
	maskedKey := MaskNewrelicAPIKey(apiKey)
	newrelicIntegration := entity.Integration{
		TenantID:             entity.ID(cluster.OrganizationId),
		ProjectID:            entity.ID(cluster.ProjectId),
		ClusterID:            clusterId,
		Region:               region,
		CloudProviderType:    cloudProviderType,
		ProviderType:         entity.IntegrationTypeNewrelic,
		Status:               entity.IntegrationStatusActive,
		StatusReason:         "",
		Payloads:             entity.IntegrationPayloads{Payloads: []entity.IntegrationPayload{entity.IntegrationPayloadMetrics}},
		NewrelicSite:         site,
		MaskedNewrelicApiKey: maskedKey,
	}

	// Encrypt the api key with AWS KMS (with the customized master key).
	if err := di.DiCall(func(encryptor encrypt.Encryptor) error {
		encryptedKey, err := encrypt.EncryptBase64(encryptor, encrypt.Secret(apiKey))
		if err != nil {
			return fmt.Errorf("failed to encrypt Newrelic API key: %w", err)
		}
		newrelicIntegration.EncryptedNewrelicApiKey = encryptedKey
		return nil
	}); err != nil {
		return entity.Integration{}, fmt.Errorf("failed to encrypt Newrelic API key: %w", err)
	}

	// Create the GeneralIntegration and Integration in a single transaction and
	// reject creation if a cluster+provider already exists.
	if err := di.DiCall(func(db *gorm.DB) error {
		err := db.Transaction(func(tx *gorm.DB) error {
			// 1) Check whether a GeneralIntegration or Integration already exists for this cluster+provider
			var gen entity.GeneralIntegration
			if err := tx.Where("cluster_id = ? AND provider_type = ?", clusterId, entity.IntegrationTypeNewrelic).First(&gen).Error; err == nil {
				return fmt.Errorf("an integration of this type already exists for the cluster")
			} else if !errors.Is(err, gorm.ErrRecordNotFound) {
				return err
			}

			var existing entity.Integration
			if err := tx.Where("cluster_id = ? AND provider_type = ?", clusterId, entity.IntegrationTypeNewrelic).First(&existing).Error; err == nil {
				return fmt.Errorf("an integration of this type already exists for the cluster")
			} else if !errors.Is(err, gorm.ErrRecordNotFound) {
				return err
			}

			// 2) Create new GeneralIntegration
			genPtr := &entity.GeneralIntegration{
				TenantID:     entity.ID(cluster.OrganizationId),
				ProjectID:    entity.ID(cluster.ProjectId),
				ClusterID:    entity.ID(clusterId),
				ProviderType: entity.IntegrationTypeNewrelic,
				Creator:      creator,
			}
			if err := tx.Create(genPtr).Error; err != nil {
				return fmt.Errorf("create general integration failed: %w", err)
			}

			// Use genPtr.ID as Integration.ID
			newrelicIntegration.Updatable = entity.Updatable{Base: entity.Base{ID: genPtr.ID}}

			// 3) Create Integration
			if err := tx.Create(&newrelicIntegration).Error; err != nil {
				return err
			}

			return nil
		})
		if err != nil {
			return err
		}

		// workflow: create-pipeline
		if _, err := mod.GetCommander().RunDagWithTags("create-pipeline", map[string]string{
			"id": fmt.Sprintf("%d", newrelicIntegration.ID),
		}, map[string]string{}); err != nil {
			return err
		}
		return nil
	}); err != nil {
		return entity.Integration{}, err
	}

	return newrelicIntegration, nil
}

// ValidateNewrelicAPIKey validates the API key through Newrelic site.
func ValidateNewrelicAPIKey(_ context.Context, apiKey string, site entity.NewrelicSite) (valid bool, err error) {
	req, err := http.NewRequest(http.MethodPost, cvt.GetNewrelicSiteLogEp(site), nil)
	if err != nil {
		return false, err
	}

	req.Header.Add("Content-Type", "application/json")
	req.Header.Add("Api-Key", apiKey)

	resp, err := http.DefaultClient.Do(req)

	if err != nil {
		return false, err
	}

	if resp.StatusCode != http.StatusAccepted {
		switch resp.StatusCode {
		case http.StatusForbidden:
			return false, fmt.Errorf("please check the key or region")
		case http.StatusInternalServerError:
			return false, fmt.Errorf("newRelic's API service is temporarily unavailable, please try again later")
		default:
			return false, fmt.Errorf("%s", resp.Status)
		}
	}

	return true, nil
}

// MaskNewrelicAPIKey masks the suffix of an API key and only keeps the first 1/8 characters.
func MaskNewrelicAPIKey(k string) string {
	l := len(k)
	maskLen := l / 8
	var b strings.Builder
	b.Grow(l)
	b.WriteString(k[:maskLen])
	for i := 0; i < (l-maskLen)/2; i++ {
		b.WriteString("·")
	}
	return b.String()
}
