package clustereventconsumer

import (
	"context"
	"errors"
	"fmt"
	"strconv"
	"time"

	"github.com/tidbcloud/tiinf/monitor/tilog"
	"github.com/tidbcloud/tiinf/transport/tieventbus"
	"go.uber.org/zap"
	"gorm.io/gorm"

	"github.com/tidbcloud/tidb-management-service/tidb-mgmt-service/domain/adapter/billing"
	eventutil "github.com/tidbcloud/tidb-management-service/utilities/clusterevent"
)

const operator = "tidb-mgmt-service"

var clusterEventSubjectMap = map[eventutil.EventType]string{
	eventutil.EventTypeLaunched: "Cluster %s is active and ready for use.",
	// eventutil.EventTypeScheduledResume: "Cluster %s will auto-resumed by TiDB Cloud.",
}

// sendToUserNotificationCenter sends cluster event to the user notification center.
// Deprecated. FIXME: This consumer is already implemented in billing system. Logic here causes duplicated notification. Need to delete.
func (c *clusterEventConsumerManagerImp) sendToUserNotificationCenter(ctx context.Context, e *tieventbus.Event) error {
	if c.uncConfig == nil {
		tilog.Warn(ctx, "unc: user notification center is not configured")
		return nil
	}

	eventBody, ok := e.Data.(*eventutil.ClusterEvent)
	if !ok {
		tilog.Warn(ctx, "unc: event is not a ClusterEvent", zap.String("event id", e.ID), zap.Any("event", e))
		return nil
	}

	if eventBody.ClusterType == eventutil.ClusterTypeTidbxResourcePool {
		tilog.Info(ctx,
			"no need to handle cluster events of resource pool",
			zap.Any("event body", eventBody))
		return nil
	}

	subject, exist := clusterEventSubjectMap[eventBody.EventType]
	if !exist {
		tilog.Info(
			ctx,
			"unc: not-launched event omitted",
			zap.String("event id", e.ID),
			zap.Uint64("cluster id", eventBody.ClusterID),
			zap.String("event type", string(eventBody.EventType)),
		)
		return nil
	}

	if eventBody.IsDevTier {
		tilog.Info(
			ctx,
			"unc: serverless event omitted",
			zap.String("event id", e.ID),
			zap.Uint64("cluster id", eventBody.ClusterID),
			zap.String("event type", string(eventBody.EventType)),
		)
		return nil
	}

	t := time.Unix(0, eventBody.EventCreatedAtMs*int64(time.Millisecond))

	var (
		clusterName string
		userID      uint64
	)
	cluster, err := c.clusterRepo.GetCluster(ctx, eventBody.ClusterID, false)
	if err != nil {
		if !errors.Is(err, gorm.ErrRecordNotFound) {
			return fmt.Errorf("unc: failed to get cluster. cluster id=%d. error=%w", eventBody.ClusterID, err)
		}

		tilog.Warn(ctx, "unc: cluster not found in mgmt svc.", zap.Uint64("cluster id", eventBody.ClusterID))
		// Call core-svc to get dedicated cluster info.
		// Delete this after migrating to tidb-mgmt-service completely.
		coreCluster, err := c.coreSvc.GetCluster(ctx, eventBody.ClusterID)
		if err != nil {
			return fmt.Errorf(
				"unc: failed to get cluster info from core-svc. cluster id=%d. error=%w",
				eventBody.ClusterID,
				err,
			)
		}
		clusterName = coreCluster.GetName()
		userID = coreCluster.CreatedBy
	} else {
		clusterName = cluster.DisplayName
		userID, err = strconv.ParseUint(cluster.UserId, 10, 64)
		if err != nil {
			return fmt.Errorf("unc: failed to parse user id. user id=%s. error=%w", cluster.UserId, err)
		}
	}

	// We request project name from account service here.
	// This causes ugly unnecessary inter-service dependency and is a temporary solution.
	// We should store project name in the cluster event in the future.
	p, err := c.accountCli.GetProject(ctx, eventBody.ProjectID)
	if err != nil {
		return fmt.Errorf("unc: failed to get project. project id=%d. error=%w", eventBody.ProjectID, err)
	}
	u, err := c.accountCli.GetUserById(ctx, userID)
	if err != nil {
		return fmt.Errorf("unc: failed to get user. user id=%d. error=%w", eventBody.OperatorID, err)
	}

	switch eventBody.EventType {
	case eventutil.EventTypeLaunched:
		subject = fmt.Sprintf(subject, clusterName)
	case eventutil.EventTypeScheduledResume:
		subject = fmt.Sprintf(subject, clusterName)
	}

	notificationContentTemplateArgs := map[string]string{
		"param_tenant_id":           strconv.FormatUint(eventBody.TenantID, 10),
		"param_project_id":          strconv.FormatUint(eventBody.ProjectID, 10),
		"param_cluster_id":          strconv.FormatUint(eventBody.ClusterID, 10),
		"param_operator_id":         strconv.FormatUint(userID, 10),
		"param_user_email":          u.GetEmail(),
		"param_is_dev_tier":         strconv.FormatBool(eventBody.IsDevTier),
		"param_status":              eventBody.Status,
		"param_event_type":          string(eventBody.EventType),
		"param_event_created_at_ms": t.Format(time.RFC3339),
		"param_cluster_name":        clusterName,
		"param_project_name":        p.Name,
		"_subject":                  subject,
	}

	for _, sc := range c.uncConfig.UNCSendingConfigs {
		sendToOneScopeType := func(scopeType string, targetIDList, roles []string) error {
			uncEvent := billing.UNCEvent{
				TriggerID:    sc.TriggerID,
				WorkFlowID:   sc.WorkFlowID,
				Payload:      notificationContentTemplateArgs,
				ScopeType:    scopeType,
				TargetIDList: targetIDList,
				Roles:        roles,
				IsSendNow:    true,
				IsTest:       sc.IsTest,
				Operator:     operator,
			}

			if err := c.billingCli.SendClusterEventToUNC(ctx, eventBody.TenantID, &uncEvent); err != nil {
				return fmt.Errorf("unc: failed to send UNC event. error=%w", err)
			}

			return nil
		}

		if err := sendToOneScopeType("ORG", []string{strconv.FormatUint(eventBody.TenantID, 10)}, []string{billing.OrgRoleOwner}); err != nil {
			return err
		}
		if err := sendToOneScopeType(
			"PROJECT",
			[]string{strconv.FormatUint(eventBody.ProjectID, 10)},
			[]string{billing.ProjectRoleOwner, billing.ProjectRoleDev, billing.ProjectRoleReadOnly},
		); err != nil {
			return err
		}
	}

	return nil
}
