package clustermaintenance

import (
	"context"
	"fmt"
	"slices"
	"strconv"
	"strings"
	"time"

	userv1 "github.com/tidbcloud/aisupport/packages/sdk-go/contracts/api/core-server/user/v1"
	maintent "github.com/tidbcloud/devops/entity/maintenance"

	"github.com/tidbcloud/account/idl/pbgen/proto/account"
	"github.com/tidbcloud/devops/entity/event"
	"github.com/tidbcloud/devops/entity/message"
	"github.com/tidbcloud/devops/pkg/infra/clients/accountcli"
	"github.com/tidbcloud/devops/pkg/infra/clients/coresvccli"
	"github.com/tidbcloud/devops/pkg/infra/clients/larkcli"
	larknotify "github.com/tidbcloud/devops/pkg/infra/clients/larkcli/notify"
	"github.com/tidbcloud/devops/pkg/infra/config"
	"github.com/tidbcloud/devops/pkg/infra/consts"
	"github.com/tidbcloud/devops/pkg/infra/eventbus"
	"github.com/tidbcloud/devops/pkg/infra/utils/container"
	"github.com/tidbcloud/devops/pkg/infra/utils/pointer"
	"github.com/tidbcloud/devops/pkg/infra/utils/repoopt"
	"github.com/tidbcloud/devops/pkg/service"
	cmxapi "github.com/tidbcloud/devops/pkg/service/clustermaintenance/api"
	"github.com/tidbcloud/devops/pkg/service/clustermaintenance/repo"
	msgapi "github.com/tidbcloud/devops/pkg/service/message/api"
	tenantSvc "github.com/tidbcloud/devops/pkg/service/tenant"

	"github.com/go-logr/logr"
	"github.com/tidbcloud/pkgv2/db"
	mgmtproto "github.com/tidbcloud/tidb-management-service/api/spec/tidb_mgmt_service/v1"
	mgmtevent "github.com/tidbcloud/tidb-management-service/utilities/clusterevent"
	"go.uber.org/dig"
	"gorm.io/gorm"
)

const (
	groupKey = "devops_cluster_maintenance_service"
)

var (
	ExecutionStatusMap = map[maintent.ClusterMaintenanceExecutionStatus]mgmtproto.MaintenanceTaskExecution_State{
		maintent.ClusterMaintenanceExecutionStatusPending:  mgmtproto.MaintenanceTaskExecution_PENDING,
		maintent.ClusterMaintenanceExecutionStatusRunning:  mgmtproto.MaintenanceTaskExecution_RUNNING,
		maintent.ClusterMaintenanceExecutionStatusDone:     mgmtproto.MaintenanceTaskExecution_DONE,
		maintent.ClusterMaintenanceExecutionStatusCanceled: mgmtproto.MaintenanceTaskExecution_CANCELED,
	}

	TaskStatusMap = map[maintent.ClusterMaintenanceTaskStatus]mgmtproto.MaintenanceTask_State{
		maintent.ClusterMaintenanceTaskStatusPending: mgmtproto.MaintenanceTask_PENDING,
		maintent.ClusterMaintenanceTaskStatusRunning: mgmtproto.MaintenanceTask_RUNNING,
		maintent.ClusterMaintenanceTaskStatusClosed:  mgmtproto.MaintenanceTask_DONE,
	}
)

type ServiceParam struct {
	dig.In

	Conf           *config.RootConfig
	Logger         logr.Logger
	CoresvcCli     *coresvccli.CoreSvcCli
	AccountCli     accountcli.Client
	DBProvider     db.TransProvider
	LarkClient     *larkcli.LarkClient
	UserService    userv1.UserServiceClient
	MessageService msgapi.MessageServiceAPI
	TenantSvc      tenantSvc.TenantServiceAPI
	EventBus       *eventbus.EventBus
}

type ServiceResult struct {
	dig.Out

	API     cmxapi.ServiceAPI
	Service service.Service `group:"service"`
}

type svc struct {
	*service.JobService

	conf        config.ClusterMaintenanceConfig
	logger      logr.Logger
	coreSvcCli  *coresvccli.CoreSvcCli
	larkCli     *larkcli.LarkClient
	accountCli  accountcli.Client
	msgSvc      msgapi.EmailServiceAPI
	dbp         db.TransProvider
	userSvc     userv1.UserServiceClient
	tenantSvc   tenantSvc.TenantServiceAPI
	eventBus    *eventbus.EventBus
	durationMap map[string]time.Duration // this map is to parse the time in the config to time duration. e.g. "5 minute" and "4 day".

	repo *repo.Repo
}

func NewService(p ServiceParam) (ServiceResult, error) {
	base := service.NewJobService("ClusterMaintenanceService", service.WithDummy(p.Conf.Services.ClusterMaintenance.Dummy))
	if p.Conf.Services.ClusterMaintenance.Dummy {
		return ServiceResult{
			API:     &cmxapi.DummyServiceAPI{},
			Service: base,
		}, nil
	}

	// init the duration map
	durationMap := make(map[string]time.Duration)
	durationMap["day"] = time.Hour * time.Duration(24)
	durationMap["hour"] = time.Hour * time.Duration(1)
	durationMap["minute"] = time.Minute * time.Duration(1)
	// check the interval format
	firstInterval := p.Conf.Services.ClusterMaintenance.FirstInterval
	nextInterval := p.Conf.Services.ClusterMaintenance.NextInterval
	partsFirst := strings.Fields(firstInterval)
	partsNext := strings.Fields(nextInterval)
	if len(partsFirst) != 2 || len(partsNext) != 2 {
		return ServiceResult{}, fmt.Errorf("invalid interval format: first interval %s and next interval %s", firstInterval, nextInterval)
	}
	_, err := strconv.Atoi(partsFirst[0])
	if err != nil {
		return ServiceResult{}, fmt.Errorf("invalid interval value: %v", err)
	}
	_, exists := durationMap[partsFirst[1]]
	if !exists {
		return ServiceResult{}, fmt.Errorf("invalid interval format: first interval %s", firstInterval)
	}
	_, err = strconv.Atoi(partsNext[0])
	if err != nil {
		return ServiceResult{}, fmt.Errorf("invalid interval value: %v", err)
	}
	_, exists = durationMap[partsNext[1]]
	if !exists {
		return ServiceResult{}, fmt.Errorf("invalid interval format: next interval %s", nextInterval)
	}

	s := &svc{
		JobService:  base,
		conf:        p.Conf.Services.ClusterMaintenance,
		logger:      p.Logger.WithName("ClusterMaintenanceService"),
		coreSvcCli:  p.CoresvcCli,
		larkCli:     p.LarkClient,
		accountCli:  p.AccountCli,
		msgSvc:      p.MessageService,
		dbp:         p.DBProvider,
		userSvc:     p.UserService,
		eventBus:    p.EventBus,
		durationMap: durationMap,
		tenantSvc:   p.TenantSvc,

		repo: repo.NewRepo(p.DBProvider),
	}

	s.eventBus.DBaasMaintenanceWindowEvent.Subscribe(groupKey, s.HandleDBaasMaintenanceWindowEvent)
	s.AddJob(
		service.NewConstantCronJob("SendEmailToUpdatedProjects", *s.conf.MaintenanceEmailCheckInternal, s.CheckEamilInterval),
	)

	return ServiceResult{API: s, Service: s}, nil
}

func (s *svc) HandleDBaasMaintenanceWindowEvent(uuid string, ev *mgmtevent.MaintenanceWindowEvent) error {
	s.logger.Info("receive maintenance event", "event type", ev.EventType)

	repoOpts := s.repo.Maintenance.Options()

	switch ev.EventType {
	case mgmtevent.MaintenanceWindowEventTypeTaskRunning:
		ctx := context.TODO()

		err := s.dbp.Transaction(ctx, func(ctx context.Context) error {
			// find maintenance from repo
			instanceID := ev.MaintenanceTask.ID
			tasksInRepo, err := s.repo.Maintenance.List(ctx, repoOpts.InInstanceIDs([]uint64{instanceID}))
			if err != nil {
				return err
			}

			taskInRepo := findMaintenanceByInstanceID(tasksInRepo, instanceID)
			if taskInRepo == nil {
				s.logger.Info("ignore maintenance task event because not found in repo", "event", ev)
				return nil
			}

			// find executions from core svc
			req := &mgmtproto.QueryMaintenanceTaskExecutionsRequest{
				Filter: &mgmtproto.QueryMaintenanceTaskExecutionsRequest_Filter{
					MaintenanceTaskIds: []uint64{taskInRepo.InstanceID},
				},
			}
			resp, err := s.coreSvcCli.Mgmt.Maintenance.QueryMaintenanceTaskExecutions(ctx, req)
			if err != nil {
				return err
			}

			// notify via lark
			param := larknotify.MaintenanceTaskRunningParam{
				ProjectID:    ev.ProjectID,
				Clusters:     container.TransformSlice(resp.MaintenanceTaskExecutions, func(e *mgmtproto.MaintenanceTaskExecution) uint64 { return e.ClusterId }),
				ScheduleTime: time.Unix(ev.MaintenanceTask.ScheduledApplyTime, 0),
				OnCallers:    []string{}, // TODO
				DetailLink:   maintenanceLink(taskInRepo.ID),
			}
			_, err = s.larkCli.Notify.MaintenanceTaskRunning(ctx, s.conf.NotifyChatID, param)
			if err != nil {
				return fmt.Errorf("notify via lark failed: %w", err)
			}

			err = s.repo.Maintenance.Patch(ctx, taskInRepo, func(current *maintent.ClusterMaintenanceInRepo) error {
				current.LarkNotifiedAt = pointer.Pointer(time.Now())
				return nil
			})
			if err != nil {
				return err
			}

			// publish event
			pubEv := assembleEvent(taskInRepo.ID, taskInRepo.InstanceID, "core-svc", event.ClusterMaintenanceEventTypeTaskRun)
			err = s.eventBus.ClusterMaintenanceEvent.Publish(pubEv)
			if err != nil {
				return fmt.Errorf("publish event failed: %w", err)
			}

			return nil
		})
		if err != nil {
			return err
		}
		// notify customer
		req := &mgmtproto.GetMaintenanceTaskRequest{
			MaintenanceTaskId: ev.MaintenanceTask.ID,
		}
		resp, err := s.coreSvcCli.Mgmt.Maintenance.GetMaintenanceTask(ctx, req)
		if err != nil {
			return err
		}
		reqExecutions := mgmtproto.ListMaintenanceTaskExecutionsRequest{
			MaintenanceTaskId: ev.MaintenanceTask.ID,
		}
		executions, err := s.coreSvcCli.Mgmt.Maintenance.ListMaintenanceTaskExecutions(ctx, &reqExecutions)
		if err != nil {
			return err
		}
		s.notifyProjectCustomers(ctx, resp, executions.MaintenanceTaskExecutions, s.conf.StartedTemplateID, mgmtproto.MaintenanceTaskExecution_PENDING)
		return err
	case mgmtevent.MaintenanceWindowEventTypeTaskUpdated, mgmtevent.MaintenanceWindowEventTypeTaskCreated:
		// update the schedule time in db.
		ctx := context.TODO()

		err := s.dbp.Transaction(ctx, func(ctx context.Context) error {
			instanceID := ev.MaintenanceTask.ID
			scheduleTime := time.Unix(ev.MaintenanceTask.ScheduledApplyTime, 0)
			tasksInRepo, err := s.repo.Maintenance.List(ctx, repoOpts.InInstanceIDs([]uint64{instanceID}))
			if err != nil {
				return err
			}

			taskInRepo := findMaintenanceByInstanceID(tasksInRepo, instanceID)
			if taskInRepo == nil {
				s.logger.Info("ignore maintenance task event because not found in repo", "event", ev)
				return nil
			}

			partsFirst := strings.Fields(s.conf.FirstInterval)
			value, _ := strconv.Atoi(partsFirst[0])
			if time.Now().Add(time.Duration(value) * s.durationMap[partsFirst[1]]).After(scheduleTime) {
				// notify customer
				req := &mgmtproto.GetMaintenanceTaskRequest{
					MaintenanceTaskId: ev.MaintenanceTask.ID,
				}
				resp, err := s.coreSvcCli.Mgmt.Maintenance.GetMaintenanceTask(ctx, req)
				if err != nil {
					return err
				}
				reqExecutions := mgmtproto.ListMaintenanceTaskExecutionsRequest{
					MaintenanceTaskId: ev.MaintenanceTask.ID,
				}
				executions, err := s.coreSvcCli.Mgmt.Maintenance.ListMaintenanceTaskExecutions(ctx, &reqExecutions)
				if err != nil {
					return err
				}
				s.notifyProjectCustomers(ctx, resp, executions.MaintenanceTaskExecutions, s.conf.BeforeTemplateID, mgmtproto.MaintenanceTaskExecution_PENDING)
				err = s.repo.Maintenance.Patch(ctx, taskInRepo, func(current *maintent.ClusterMaintenanceInRepo) error {
					current.LastNotifiedCustomerDate = pointer.Pointer(time.Now())
					return nil
				})
				if err != nil {
					return err
				}
			}
			return s.repo.Maintenance.Patch(ctx, taskInRepo, func(current *maintent.ClusterMaintenanceInRepo) error {
				current.ScheduledApplyTime = &scheduleTime
				return nil
			})
		})
		return err
	case mgmtevent.MaintenanceWindowEventTypeTaskDone:
		ctx := context.TODO()
		// notify customer
		req := &mgmtproto.GetMaintenanceTaskRequest{
			MaintenanceTaskId: ev.MaintenanceTask.ID,
		}
		resp, err := s.coreSvcCli.Mgmt.Maintenance.GetMaintenanceTask(ctx, req)
		if err != nil {
			return err
		}
		reqExecutions := mgmtproto.ListMaintenanceTaskExecutionsRequest{
			MaintenanceTaskId: ev.MaintenanceTask.ID,
		}
		executions, err := s.coreSvcCli.Mgmt.Maintenance.ListMaintenanceTaskExecutions(ctx, &reqExecutions)
		if err != nil {
			return err
		}
		s.notifyProjectCustomers(ctx, resp, executions.MaintenanceTaskExecutions, s.conf.CompletedTemplateID, mgmtproto.MaintenanceTaskExecution_DONE)
		return err
	default:
		return nil
	}
}

func (s *svc) handleIntervalEmailSend(ctx context.Context, interval string) error {
	err := s.dbp.Transaction(ctx, func(ctx context.Context) error {
		clause := fmt.Sprintf("scheduled_apply_time < CURRENT_TIMESTAMP + INTERVAL %s AND (last_notified_customer_date + INTERVAL %s < scheduled_apply_time OR last_notified_customer_date is NULL)", interval, interval)
		opts := []repoopt.Option{
			&repoopt.Raw{
				RawApply: func(db *gorm.DB) *gorm.DB {
					return db.Where(clause)
				},
			},
		}

		tasksInRepo, err := s.repo.Maintenance.List(ctx, opts...)
		if err != nil {
			return err
		}
		for _, task := range tasksInRepo {
			req := &mgmtproto.GetMaintenanceTaskRequest{
				MaintenanceTaskId: task.InstanceID,
			}
			resp, err := s.coreSvcCli.Mgmt.Maintenance.GetMaintenanceTask(ctx, req)
			if err != nil {
				return err
			}
			reqExecutions := mgmtproto.ListMaintenanceTaskExecutionsRequest{
				MaintenanceTaskId: task.InstanceID,
			}
			executions, err := s.coreSvcCli.Mgmt.Maintenance.ListMaintenanceTaskExecutions(ctx, &reqExecutions)
			if err != nil {
				return err
			}
			err = s.notifyProjectCustomers(ctx, resp, executions.MaintenanceTaskExecutions, s.conf.BeforeTemplateID, mgmtproto.MaintenanceTaskExecution_PENDING)
			if err != nil {
				return err
			}
			err = s.repo.Maintenance.Patch(ctx, task, func(current *maintent.ClusterMaintenanceInRepo) error {
				current.LastNotifiedCustomerDate = pointer.Pointer(time.Now())
				return nil
			})
			if err != nil {
				return err
			}
		}

		return nil
	})
	return err
}

func (s *svc) CheckEamilInterval(ctx context.Context) error {
	err := s.handleIntervalEmailSend(ctx, s.conf.FirstInterval)
	if err != nil {
		return err
	}
	return s.handleIntervalEmailSend(ctx, s.conf.NextInterval)
}

func (s *svc) notifyProjectCustomers(ctx context.Context,
	maintenanceTask *mgmtproto.MaintenanceTask,
	executions []*mgmtproto.MaintenanceTaskExecution,
	templateID string,
	state mgmtproto.MaintenanceTaskExecution_State) error {
	projectInfo, err := s.accountCli.GetProject(ctx, maintenanceTask.ProjectId)
	if err != nil {
		return err
	}
	usersResp, err := s.accountCli.GetProjectUsersByProjectID(ctx, maintenanceTask.ProjectId)
	if err != nil {
		return err
	}
	tenant, err := s.tenantSvc.GetTenant(ctx, projectInfo.TenantId)
	if err != nil {
		return err
	}
	tamEmail := tenant.TAMName
	salesOwner := tenant.SalesOwnerEmail
	if tamEmail != "" {
		usersResp = append(usersResp, &account.User{Email: tamEmail})
	}
	if salesOwner != "" && salesOwner != tamEmail {
		usersResp = append(usersResp, &account.User{Email: salesOwner})
	}
	for _, user := range usersResp {
		applyTimePointer := maintenanceTask.ScheduledApplyTime.AsTime()
		templateVars := buildBeforeTemplateVars(user,
			&applyTimePointer,
			projectInfo,
			maintenanceTask,
			executions,
			state)
		err = s.msgSvc.SendEmail(ctx, msgapi.SendEmailParam{
			Name: fmt.Sprintf("Maintenance Notification for Task '%s'", maintenanceTask.Name),
			Target: message.EmailTarget{
				Users: &message.EmailTargetUsers{
					Emails: []string{user.Email},
				},
			},
			Template: message.EmailTemplate{
				ID:        templateID,
				Variables: templateVars,
			},
			Reason:      "send maintenance email to notify customer",
			Applicant:   consts.InternalVirtualEmail,
			FromAddress: s.conf.EmailFromAddress,
		})
		if err != nil {
			s.logger.Error(err, "send maintenance email failed", "user", user.Email, "taskID", maintenanceTask.MaintenanceTaskId)
			continue
		}
	}

	return nil
}

func buildBeforeTemplateVars(user *account.User, scheduleTime *time.Time,
	project *account.Project,
	maintenanceTask *mgmtproto.MaintenanceTask,
	executions []*mgmtproto.MaintenanceTaskExecution,
	state mgmtproto.MaintenanceTaskExecution_State) map[string]string {
	templateVars := make(map[string]string)
	templateVars["userName"] = user.FirstName + " " + user.LastName
	templateVars["dateTime"] = scheduleTime.Format("2006-01-02T15:04:05Z")
	templateVars["projectName"] = project.Name
	templateVars["taskList"] = maintenanceTask.Description

	clusterCount := 0
	clusterList := make([]uint64, 0)
	opsCanceledClusterCountCount := 0
	opsCanceledClusterList := make([]uint64, 0)
	for _, execution := range executions {
		if execution.State == mgmtproto.MaintenanceTaskExecution_CANCELED {
			opsCanceledClusterCountCount += 1
			opsCanceledClusterList = append(opsCanceledClusterList, execution.ClusterId)
		}
		if execution.State == state {
			clusterCount += 1
			clusterList = append(clusterList, execution.ClusterId)
		}
	}
	templateVars["clusterList"] = formatUint64Slice(clusterList)
	templateVars["clusterCount"] = strconv.FormatInt(int64(clusterCount), 10)
	templateVars["opsCanceledClusterCountCount"] = strconv.FormatInt(int64(opsCanceledClusterCountCount), 10)
	templateVars["opsCanceledClusterList"] = formatUint64Slice(opsCanceledClusterList)

	return templateVars
}

func formatUint64Slice(nums []uint64) string {
	strSlice := make([]string, len(nums))
	for i, u := range nums {
		strSlice[i] = strconv.FormatUint(u, 10)
	}
	return strings.Join(strSlice, ", ")
}

func findMaintenanceByInstanceID(tasksInRepo []*maintent.ClusterMaintenanceInRepo, instanceID uint64) *maintent.ClusterMaintenanceInRepo {
	index := slices.IndexFunc(tasksInRepo, func(t *maintent.ClusterMaintenanceInRepo) bool { return t.InstanceID == instanceID })
	if index < 0 {
		return nil
	}

	return tasksInRepo[index]
}

func findMaintenanceInCoreSvcByInstanceID(tasksInCS []*mgmtproto.MaintenanceTask, instanceID uint64) *mgmtproto.MaintenanceTask {
	index := slices.IndexFunc(tasksInCS, func(t *mgmtproto.MaintenanceTask) bool { return t.MaintenanceTaskId == instanceID })
	if index < 0 {
		return nil
	}

	return tasksInCS[index]
}

func actionOperator(ctx context.Context) string {
	return consts.GetOperatorFromCtx(ctx, "devops")
}

func assembleEvent(id uint64, instanceID uint64, operator string, typ event.EventType) *event.ClusterMaintenanceEvent {
	ev := &event.ClusterMaintenanceEvent{
		BaseEvent: event.BaseEvent{
			ID:            id,
			OperatorEmail: operator,
			OperateAt:     pointer.Pointer(time.Now()),
			EventType:     typ,
		},
		Details: event.ClusterMaintenanceEventDetail{
			InstanceID: instanceID,
		},
	}
	return ev
}

func maintenanceLink(id uint64) string {
	return fmt.Sprintf("https://%s/maintenance/%d", consts.ConsoleAddr(), id)
}
