package clustereventconsumer

import (
	"context"
	"fmt"
	"time"

	"github.com/samber/lo"
	"github.com/tidbcloud/account/idl/pbgen/proto/account"
	"github.com/tidbcloud/tiinf/errs"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"github.com/tidbcloud/tiinf/transport/tieventbus"
	"go.uber.org/zap"

	globaldedicatedv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/dedicated/v1"
	"github.com/tidbcloud/tidb-management-service/tidb-mgmt-service/domain/entity"
	eventutil "github.com/tidbcloud/tidb-management-service/utilities/clusterevent"
)

var (
	clusterEventEmailSubjectMap = map[eventutil.EventType]string{
		eventutil.EventTypeScheduledResume: "Your cluster %s will be auto resumed at %s",
		eventutil.EventTypeResumed:         "Your cluster %s has been resumed by TiDB Cloud at %s",
	}
	clusterEventEmailTemplate = map[eventutil.EventType]string{
		eventutil.EventTypeScheduledResume: "d-2e76b5128bd644d4a70cdfb60a8ab9a3",
		eventutil.EventTypeResumed:         "d-ba79c0686f1d44a6a04e8fde2216ab85",
	}

	clustereventEmailSence = map[eventutil.EventType]string{
		eventutil.EventTypeScheduledResume: "dedicated.will_auto_resume_paused_cluster",
		eventutil.EventTypeResumed:         "dedicated.auto_resumed_paused_cluster",
	}
)

// sendToClusterEventEmail sends the `EventTypeLaunched` event to the cluster event email.
func (c *clusterEventConsumerManagerImp) sendToClusterEventEmail(ctx context.Context, e *tieventbus.Event) error {
	if c.accountCli == nil {
		tilog.Warn(ctx, "account client is not configured")
		return nil
	}

	eventBody, ok := e.Data.(*eventutil.ClusterEvent)
	if !ok {
		tilog.Warn(ctx, "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
	}

	// get email subject

	subject, exist := clusterEventEmailSubjectMap[eventBody.EventType]
	if !exist {
		tilog.Info(
			ctx,
			"not get email subject, skip it",
			zap.String("event id", e.ID),
			zap.Uint64("cluster id", eventBody.ClusterID),
			zap.String("event type", string(eventBody.EventType)),
		)
		return nil
	}

	// send email not need support core-svc
	cluster, err := c.clusterRepo.GetCluster(ctx, eventBody.ClusterID, true)
	if err != nil {
		tilog.Error(ctx, "failed to get cluster", zap.Uint64("cluster id", eventBody.ClusterID), zap.Error(err))
		return err
	}

	tilog.Info(
		ctx,
		"send email",
		zap.String("event id", e.ID),
		zap.Uint64("cluster id", eventBody.ClusterID),
		zap.String("event type", string(eventBody.EventType)),
	)
	switch eventBody.EventType {
	case eventutil.EventTypeScheduledResume,
		eventutil.EventTypeResumed:
		// skip the non-normal pause plan
		if eventBody.EventType == eventutil.EventTypeResumed &&
			eventBody.ExtraInfo.PausePlanType != globaldedicatedv1.Cluster_PausePlan_NORMAL.String() {
			return nil
		}

		// get global cluster
		globalCluster, err := c.dedicatedCli.GetCluster(
			ctx,
			&globaldedicatedv1.GetClusterRequest{ClusterId: eventBody.ClusterID},
		)
		if err != nil {
			tilog.Error(
				ctx,
				"failed to get cluster from dedicated global",
				zap.Uint64("cluster id", eventBody.ClusterID),
				zap.Error(err),
			)
			return err
		}

		// get project
		projectInfo, err := c.accountCli.GetProject(ctx, cluster.ProjectId)
		if err != nil {
			tilog.Error(ctx, "failed to get project", zap.Uint64("project id", cluster.ProjectId), zap.Error(err))
			return err
		}

		offset, err := c.accountCli.GetTimeZoneByProjectID(ctx, cluster.ProjectId)
		if err != nil {
			tilog.Error(
				ctx,
				"failed to get project timezone offset",
				zap.Uint64("project id", cluster.ProjectId),
				zap.Error(err),
			)
			return err
		}

		// At present, all users in project are Project Owner.
		projectUsers, err := c.accountCli.ListProjectOwnerUsers(ctx, cluster.ProjectId)
		if err != nil {
			tilog.Error(ctx, "failed to get project users", zap.Uint64("project id", cluster.ProjectId), zap.Error(err))
			return err
		}

		orgUsers, err := c.accountCli.ListOrgOwnerUsers(ctx, cluster.OrganizationId)
		if err != nil {
			tilog.Error(ctx, "failed to get org users", zap.Uint64("org id", cluster.OrganizationId), zap.Error(err))
			return err
		}

		// get all user emails
		userEmails := make([]string, 0, len(projectUsers)+len(orgUsers))
		for _, user := range append(projectUsers, orgUsers...) {
			userEmails = append(userEmails, user.GetEmail())
		}
		userEmails = lo.Union(userEmails)

		templateVars := make(map[string]string)
		switch eventBody.EventType {
		case eventutil.EventTypeScheduledResume:
			// if pause plan is nil, skip it
			if globalCluster.GetPausePlan() == nil {
				tilog.Error(
					ctx,
					"pause plan is nil",
					zap.Uint64("cluster id", eventBody.ClusterID),
					zap.Any("global cluster", globalCluster),
				)
				return nil
			}

			templateVars = map[string]string{
				"ClusterName": cluster.DisplayName,
				"ProjectName": projectInfo.GetName(),
				"ResumeTime": convertTimeToStringByUTCOffset(
					globalCluster.GetPausePlan().GetScheduledResumeTime().Seconds,
					offset,
				),
				"PausedTime": convertTimeToStringByUTCOffset(
					globalCluster.GetPausePlan().GetPauseTime().Seconds,
					offset,
				),
			}
			subject = fmt.Sprintf(
				subject,
				cluster.DisplayName,
				convertTimeToStringByUTCOffset(globalCluster.GetPausePlan().GetScheduledResumeTime().Seconds, offset),
			)
		case eventutil.EventTypeResumed:
			templateVars = map[string]string{
				"ClusterName": cluster.DisplayName,
				"ProjectName": projectInfo.GetName(),
				"ResumedTime": convertTimeToStringByUTCOffset(
					eventBody.EventCreatedAtMs/1000,
					offset,
				), // convert ms to ss
			}
			subject = fmt.Sprintf(
				subject,
				cluster.DisplayName,
				convertTimeToStringByUTCOffset(eventBody.EventCreatedAtMs, offset),
			)
		}

		err = c.sendClusterEventEmail(ctx, cluster, eventBody, subject, userEmails, templateVars)
		if err != nil {
			tilog.Error(
				ctx,
				"failed to send cluster Event email",
				zap.Uint64("cluster id", eventBody.ClusterID),
				zap.Error(err),
			)
			return err
		}
	}

	return nil
}

func (c *clusterEventConsumerManagerImp) sendClusterEventEmail(
	ctx context.Context,
	cluster *entity.Cluster,
	eventBody *eventutil.ClusterEvent,
	subject string,
	userEmails []string,
	templateVars map[string]string,
) error {
	identifier := emailIdentifier(
		cluster.OrganizationId,
		cluster.ProjectId,
		cluster.ClusterId,
		fmt.Sprintf("%s-%d", eventBody.EventType, eventBody.EventCreatedAtMs),
	)

	req := &account.SendEmailReq{
		Scene:        clustereventEmailSence[eventBody.EventType],
		Identifier:   identifier,
		Emails:       userEmails,
		TemplateId:   clusterEventEmailTemplate[eventBody.EventType],
		TemplateVars: templateVars,
		Subject:      subject,
	}

	_, err := c.accountCli.SendEmail(ctx, req)
	if err != nil {
		if int64(err.ErrCode()) == errs.ErrCodeConflict {
			tilog.Info(ctx, "email already sent, skip")
			return nil
		}
		tilog.Error(ctx, "failed to send email", zap.Error(err))
		return err
	}

	tilog.Info(ctx, "send email successfully", zap.String("identifier", identifier), zap.Strings("emails", userEmails))
	return nil
}

// convertTimeToStringByUTCOffset converts time to string by UTC offset
// e.g. 2023-05-02 19:00:00
// May 2, 2023 18:00 UTC-01:00
// May 2, 2023 19:00 UTC±00:00
// May 3, 2023 03:00 UTC+08:00
func convertTimeToStringByUTCOffset(t int64, offset int32) string {
	location := time.FixedZone("UTC", int(offset)*3600)
	tt := time.Unix(t, 0)
	tt = tt.In(location)
	var sign string

	switch {
	case offset > 0:
		sign = "+"
	case offset < 0:
		sign = "-"
		offset = -offset
	default:
		sign = "±"
	}

	return fmt.Sprintf("%s UTC%s%02d:00", tt.Format("Jan 2, 2006 15:04"), sign, offset)
}

func emailIdentifier(orgId, projectId, clusterId uint64, key string) string {
	return fmt.Sprintf("%d-%d-%d-%s", orgId, projectId, clusterId, key)
}
