package incident

import (
	"context"
	"errors"
	"fmt"
	"net/url"
	"strconv"
	"strings"
	"time"

	userv1 "github.com/tidbcloud/aisupport/packages/sdk-go/contracts/api/core-server/user/v1"
	coreuser "github.com/tidbcloud/aisupport/packages/sdk-go/contracts/entity/core-server/user/v1"
	incidentapi "github.com/tidbcloud/devops/api/incident"
	"github.com/tidbcloud/devops/entity"
	"github.com/tidbcloud/devops/entity/validation"
	"github.com/tidbcloud/devops/models"
	"github.com/tidbcloud/devops/pkg/infra/clients/jiracli"
	"github.com/tidbcloud/devops/pkg/infra/clients/larkcli"
	"github.com/tidbcloud/devops/pkg/infra/clients/larkcli/notify"
	"github.com/tidbcloud/devops/pkg/infra/clients/llm_cli"
	"github.com/tidbcloud/devops/pkg/infra/config"
	"github.com/tidbcloud/devops/pkg/infra/consts"
	"github.com/tidbcloud/devops/pkg/infra/utils/repoopt"
	"github.com/tidbcloud/devops/pkg/service"
	"github.com/tidbcloud/devops/pkg/service/incident/repo/cvt"
	jirasvcapi "github.com/tidbcloud/devops/pkg/service/jira/api"

	"github.com/go-logr/logr"
	"github.com/tidbcloud/pkgv2/db"
	"go.uber.org/dig"
)

const defaultOverdueToDoActionReminderCronSpec = "0 10 * * *"

type ServiceParam struct {
	dig.In

	Conf       *config.RootConfig
	Logger     logr.Logger
	DBProvider db.TransProvider
	UserAPI    userv1.UserServiceClient
	JiraAPI    jirasvcapi.JiraServiceAPI
	JiraCli    jiracli.JiraClient
	LarkCli    *larkcli.LarkClient
	LLMCli     *llm_cli.LLMCli
}

type ServiceResult struct {
	dig.Out

	API     IncidentService
	Service service.Service `group:"service"`
}

type incidentService struct {
	*service.JobService

	conf config.IncidentServiceConfig

	cvter      *cvt.Converter
	dbProvider db.TransProvider
	userSvc    userv1.UserServiceClient
	jiraSvc    jirasvcapi.JiraServiceAPI
	jiraCli    jiracli.JiraClient
	larkclient *larkcli.LarkClient
	llmCli     *llm_cli.LLMCli
}

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

	svc := &incidentService{
		JobService: base,
		conf:       p.Conf.Services.Incident,
		dbProvider: p.DBProvider,
		userSvc:    p.UserAPI,
		jiraSvc:    p.JiraAPI,
		jiraCli:    p.JiraCli,
		larkclient: p.LarkCli,
		llmCli:     p.LLMCli,
		cvter:      cvt.NewConverter(),
	}

	if !svc.conf.OverdueToDoActionReminderCronDisable {
		spec := strings.TrimSpace(svc.conf.OverdueToDoActionReminderCronSpec)
		if spec == "" {
			spec = defaultOverdueToDoActionReminderCronSpec
		}
		svc.AddJob(service.MustNewCronJob("NotifyOverdueIncidentToDoActions", spec, svc.NotifyOverdueToDoActions))
	}

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

func (s *incidentService) ListIncidents(ctx context.Context, param ListIncidentsParam) ([]*entity.Incident, int64, error) {
	param = param.Normalize()
	db := s.dbProvider.ReadDB(ctx).Model(&models.Incident{})
	if param.WarRoomID != nil {
		db = db.Where("war_room_id = ?", *param.WarRoomID)
	}
	if param.StatusIs != nil {
		db = db.Where("status = ?", *param.StatusIs)
	}
	if param.TierIs != nil {
		db = db.Where("tier = ?", *param.TierIs)
	}
	for _, component := range param.ComponentsContain {
		db = db.Where("components LIKE ?", "%"+component+"%")
	}
	for _, bizRegion := range param.BizRegionsContain {
		db = db.Where("biz_region LIKE ?", "%"+bizRegion+"%")
	}

	var total int64
	if err := db.Count(&total).Error; err != nil {
		s.Logger(ctx).Error(err, "count incidents error when accessing database")
		return nil, 0, err
	}

	db = repoopt.Aggregate(
		&repoopt.Order{Column: "created_at", Desc: true},
		&repoopt.Order{Column: "id", Desc: true},
		&repoopt.Pager{Page: param.Page, PerPage: param.PerPage},
	).Apply(db)
	var incidents []models.Incident
	result := db.Find(&incidents)
	if result.Error != nil {
		s.Logger(ctx).Error(result.Error, "list incidents error when accessing database")
		return nil, 0, result.Error
	}
	incidentEntities := make([]*entity.Incident, 0, len(incidents))
	for i := range incidents {
		incidentEntities = append(incidentEntities, s.incidentModel2Entity(ctx, &incidents[i], false))
	}
	return incidentEntities, total, nil
}

func (s *incidentService) GetIncident(ctx context.Context, id uint64) (*entity.Incident, error) {
	db := s.dbProvider.ReadDB(ctx)
	var incidentModel models.Incident
	err := db.Where("id = ?", id).First(&incidentModel).Error
	if err != nil {
		s.Logger(ctx).Error(err, "get incident error when accessing database")
		return nil, err
	}
	return s.incidentModel2Entity(ctx, &incidentModel, true), nil
}

func (s *incidentService) CreateIncident(ctx context.Context, incidentCreation *entity.Incident) (uint64, error) {
	err := validation.Incident.IncidentCreation(incidentCreation)
	if err != nil {
		return 0, err
	}
	incidentModel := s.incidentEntity2Model(ctx, incidentCreation)
	db := s.dbProvider.WriteDB(ctx)
	err = db.Create(incidentModel).Error
	if err != nil {
		s.Logger(ctx).Error(err, "create incident error when accessing database")
		return 0, err
	}
	return incidentModel.GetID().Uint64(), nil
}

func (s *incidentService) UpdateIncident(ctx context.Context, incidentId uint64, incidentUpdate *entity.Incident) error {
	err := validation.Incident.IncidentUpdate(incidentUpdate)
	if err != nil {
		return err
	}
	incidentModel := s.incidentEntity2Model(ctx, incidentUpdate)
	db := s.dbProvider.WriteDB(ctx)
	err = db.Model(&models.Incident{}).Where("id = ?", incidentId).Updates(incidentModel).Error
	if err != nil {
		s.Logger(ctx).Error(err, "update incident error when accessing database")
		return err
	}
	return nil
}

func (s *incidentService) UpdatePostMortemToDoActionImportStatus(ctx context.Context, incidentId uint64, status string) error {
	db := s.dbProvider.WriteDB(ctx)
	err := db.Model(&models.Incident{}).
		Where("id = ?", incidentId).
		Update("postmortem_todo_action_import_status", status).Error
	if err != nil {
		s.Logger(ctx).Error(err, "update incident postmortem todo action import status error when accessing database")
		return err
	}
	return nil
}

// TODO: To implement delete serivce for incidents
func (s *incidentService) DeleteIncident(ctx context.Context, id uint64) error {
	return nil
}

func (s *incidentService) NotifyOverdueToDoActions(ctx context.Context) error {
	if s.larkclient == nil || s.larkclient.Notify == nil {
		return fmt.Errorf("lark notify client is not configured")
	}
	if err := s.SyncAllToDoActionStatusFromTCOCP(ctx); err != nil {
		return fmt.Errorf("sync todo action status from TCOCP before overdue reminder failed: %w", err)
	}

	db := s.dbProvider.ReadDB(ctx)
	var actionModels []*models.IncidentPostMortemToDoAction
	err := db.
		Where("deadline IS NOT NULL AND deadline <= ?", time.Now().UTC()).
		Where("(status IS NULL OR LOWER(status) NOT IN ?)", []string{
			ToDoActionStatusDone,
			ToDoActionStatusNoNeed,
			ToDoActionStatusCanceled,
			"cancelled",
		}).
		Find(&actionModels).Error
	if err != nil {
		s.Logger(ctx).Error(err, "list overdue incident todo actions error when accessing database")
		return err
	}

	actionsByIncident := groupToDoActionsByIncident(actionModels)
	incidentByID, err := s.listIncidentsByID(ctx, actionsByIncident)
	if err != nil {
		return err
	}

	var errs []error
	for _, action := range actionModels {
		if action == nil {
			continue
		}
		if action.Persons == nil {
			continue
		}
		ownerEmails := uniqueNonEmptyStrings(strings.Fields(*action.Persons))
		if len(ownerEmails) == 0 {
			continue
		}
		incident := incidentByID[toDoActionIncidentID(action)]
		for _, email := range ownerEmails {
			param := s.buildOverdueToDoActionReminderParam(action, incident, email)
			if _, err := s.larkclient.Notify.IncidentToDoActionOverdue(ctx, email, param); err != nil {
				s.Logger(ctx).Error(err, "send overdue incident todo action reminder failed", "todo_action_id", action.ID.Uint64(), "owner", email)
				errs = append(errs, fmt.Errorf("send reminder to %s for todo action %d: %w", email, action.ID.Uint64(), err))
			}
		}
	}
	if err := s.notifyIncidentFBOwnersForOverdueActions(ctx, actionsByIncident, incidentByID); err != nil {
		errs = append(errs, err)
	}
	return errors.Join(errs...)
}

func (s *incidentService) listIncidentsByID(ctx context.Context, actionsByIncident map[uint64][]*models.IncidentPostMortemToDoAction) (map[uint64]*models.Incident, error) {
	if len(actionsByIncident) == 0 {
		return nil, nil
	}

	incidentIDs := make([]uint64, 0, len(actionsByIncident))
	for incidentID := range actionsByIncident {
		incidentIDs = append(incidentIDs, incidentID)
	}

	var incidents []*models.Incident
	err := s.dbProvider.ReadDB(ctx).Where("id IN ?", incidentIDs).Find(&incidents).Error
	if err != nil {
		s.Logger(ctx).Error(err, "list incidents for overdue todo action summary failed")
		return nil, err
	}

	incidentByID := make(map[uint64]*models.Incident, len(incidents))
	for _, incident := range incidents {
		if incident == nil {
			continue
		}
		incidentByID[incident.ID.Uint64()] = incident
	}
	return incidentByID, nil
}

func (s *incidentService) notifyIncidentFBOwnersForOverdueActions(ctx context.Context, actionsByIncident map[uint64][]*models.IncidentPostMortemToDoAction, incidentByID map[uint64]*models.Incident) error {
	var errs []error
	for incidentID, actions := range actionsByIncident {
		incident := incidentByID[incidentID]
		if incident == nil {
			continue
		}
		if len(actions) == 0 {
			continue
		}
		fbOwners := uniqueNonEmptyStrings(strings.Fields(incident.FBOwners))
		if len(fbOwners) == 0 {
			continue
		}
		param := s.buildIncidentOverdueToDoActionSummaryParam(incident, actions)
		for _, email := range fbOwners {
			if _, err := s.larkclient.Notify.IncidentToDoActionOverdueSummary(ctx, email, param); err != nil {
				s.Logger(ctx).Error(err, "send incident overdue todo action summary failed", "incident_id", incidentID, "fbowner", email)
				errs = append(errs, fmt.Errorf("send incident %d overdue summary to %s: %w", incidentID, email, err))
			}
		}
	}
	return errors.Join(errs...)
}

func (s *incidentService) buildOverdueToDoActionReminderParam(action *models.IncidentPostMortemToDoAction, incident *models.Incident, ownerEmail string) notify.IncidentToDoActionOverdueParam {
	title := "(untitled)"
	if action.Title != nil && strings.TrimSpace(*action.Title) != "" {
		title = strings.TrimSpace(*action.Title)
	}

	return notify.IncidentToDoActionOverdueParam{
		ActionTitle:  title,
		Status:       toDoActionStatusForReminder(action),
		DueTime:      toDoActionDueTimeForReminder(action),
		IncidentName: incidentNameForReminder(incident),
		OwnerEmail:   ownerEmail,
		IncidentLink: s.incidentLinkForReminder(action, incident),
		TCOCPKey:     firstTCOCPIssueKey(action.LinkedJiraIssue),
		TCOCPLink:    s.tcocpLinkForReminder(action),
	}
}

func (s *incidentService) buildIncidentOverdueToDoActionSummaryParam(incident *models.Incident, actions []*models.IncidentPostMortemToDoAction) notify.IncidentToDoActionOverdueSummaryParam {
	incidentID := incident.ID.Uint64()
	incidentURL := s.withOpsPortalBaseURL(fmt.Sprintf("/incidents/%d", incidentID))
	actionItems := make([]string, 0, len(actions))
	for i, action := range actions {
		if action == nil {
			continue
		}
		title := "(untitled)"
		if action.Title != nil && strings.TrimSpace(*action.Title) != "" {
			title = strings.TrimSpace(*action.Title)
		}
		owners := ""
		if action.Persons != nil {
			owners = strings.Join(uniqueNonEmptyStrings(strings.Fields(*action.Persons)), ", ")
		}
		if owners == "" {
			owners = "N/A"
		}
		tcocpKey := firstTCOCPIssueKey(action.LinkedJiraIssue)
		tcocpLink := s.tcocpLinkForReminder(action)
		tcocpText := "N/A"
		if tcocpKey != "" && tcocpLink != "" {
			tcocpText = fmt.Sprintf("[%s](%s)", tcocpKey, tcocpLink)
		}
		actionItems = append(actionItems, fmt.Sprintf("**%d. %s**  \nStatus: %s &nbsp;|&nbsp; Due: %s\nTCOCP: %s\n%s",
			i+1,
			title,
			toDoActionStatusForReminder(action),
			toDoActionDueTimeForReminder(action),
			tcocpText,
			owners,
		))
	}

	return notify.IncidentToDoActionOverdueSummaryParam{
		IncidentName: incidentNameForReminder(incident),
		IncidentLink: incidentURL,
		ActionCount:  strconv.Itoa(len(actions)),
		ActionItems:  strings.Join(actionItems, "\n"),
	}
}

func groupToDoActionsByIncident(actions []*models.IncidentPostMortemToDoAction) map[uint64][]*models.IncidentPostMortemToDoAction {
	grouped := make(map[uint64][]*models.IncidentPostMortemToDoAction)
	for _, action := range actions {
		if action == nil || action.IncidentId == nil {
			continue
		}
		grouped[*action.IncidentId] = append(grouped[*action.IncidentId], action)
	}
	return grouped
}

func toDoActionIncidentID(action *models.IncidentPostMortemToDoAction) uint64 {
	if action == nil || action.IncidentId == nil {
		return 0
	}
	return *action.IncidentId
}

func toDoActionStatusForReminder(action *models.IncidentPostMortemToDoAction) string {
	if action == nil || action.Status == nil || strings.TrimSpace(*action.Status) == "" {
		return ToDoActionStatusOpen
	}
	return strings.TrimSpace(*action.Status)
}

func toDoActionDueTimeForReminder(action *models.IncidentPostMortemToDoAction) string {
	if action == nil || action.Deadline == nil {
		return ""
	}
	return action.Deadline.In(time.FixedZone("CST", 8*3600)).Format("2006-01-02 15:04")
}

func incidentNameForReminder(incident *models.Incident) string {
	if incident == nil {
		return "(unknown incident)"
	}
	incidentID := incident.ID.Uint64()
	name := strings.TrimSpace(incident.Name)
	if incidentID == 0 {
		if name == "" {
			return "(unknown incident)"
		}
		return name
	}
	if name == "" {
		return fmt.Sprintf("Incident %d", incidentID)
	}
	return fmt.Sprintf("%d - %s", incidentID, name)
}

func (s *incidentService) incidentLinkForReminder(action *models.IncidentPostMortemToDoAction, incident *models.Incident) string {
	incidentID := uint64(0)
	if incident != nil {
		incidentID = incident.ID.Uint64()
	}
	if incidentID == 0 {
		incidentID = toDoActionIncidentID(action)
	}
	if incidentID == 0 {
		return ""
	}
	return s.withOpsPortalBaseURL(fmt.Sprintf("/incidents/%d", incidentID))
}

func uniqueNonEmptyStrings(items []string) []string {
	seen := make(map[string]struct{}, len(items))
	ret := make([]string, 0, len(items))
	for _, item := range items {
		item = strings.TrimSpace(item)
		if item == "" {
			continue
		}
		if _, ok := seen[item]; ok {
			continue
		}
		seen[item] = struct{}{}
		ret = append(ret, item)
	}
	return ret
}

func (s *incidentService) ListIncidentLogs(ctx context.Context, incidentId uint64) ([]*entity.IncidentLogItem, error) {
	db := s.dbProvider.ReadDB(ctx)
	var incidentLogModels []models.IncidentLogItem
	result := db.Where("incident_id = ?", incidentId).Order("created_at").Find(&incidentLogModels)
	if result.Error != nil {
		s.Logger(ctx).Error(result.Error, "list incident log items error when accessing database for incident id", "incident id", incidentId)
		return nil, result.Error
	}
	incidentLogEntities := make([]*entity.IncidentLogItem, 0)
	for _, incidentModel := range incidentLogModels {
		incidentEntity := &entity.IncidentLogItem{}
		if incidentModel.EventTime != nil {
			incidentEntity.EventTime = incidentModel.EventTime
		} else {
			incidentEntity.EventTime = &incidentModel.CreatedAt
		}
		incidentEntity.CreatedAt = &incidentModel.CreatedAt
		incidentEntity.Id = incidentModel.GetID().Uint64()
		incidentEntity.IncidentId = incidentModel.IncidentId
		incidentEntity.ItemType = entity.LogItemType(incidentModel.LogItemType)
		incidentEntity.Description = incidentModel.Content
		incidentEntity.PostMortemUrl = incidentModel.PostMortemLink
		incidentEntity.Status = incidentModel.Status
		incidentEntity.MilestoneType = incidentModel.MilestoneType
		incidentEntity.From = incidentModel.From
		incidentLogEntities = append(incidentLogEntities, incidentEntity)
	}
	return incidentLogEntities, nil
}

func (s *incidentService) GetIncidentLogItem(ctx context.Context, id uint64) (*entity.IncidentLogItem, error) {
	db := s.dbProvider.ReadDB(ctx)
	var logItemModel models.IncidentLogItem
	err := db.Where("id = ?", id).First(&logItemModel).Error
	if err != nil {
		s.Logger(ctx).Error(err, "get incident log item error when accessing database", "log item id", id)
		return nil, err
	}
	logItemEntity := &entity.IncidentLogItem{}
	if logItemModel.EventTime != nil {
		logItemEntity.EventTime = logItemModel.EventTime
	} else {
		logItemEntity.EventTime = &logItemModel.CreatedAt
	}
	logItemEntity.CreatedAt = &logItemModel.CreatedAt
	logItemEntity.Id = logItemModel.GetID().Uint64()
	logItemEntity.IncidentId = logItemModel.IncidentId
	logItemEntity.ItemType = entity.LogItemType(logItemModel.LogItemType)
	logItemEntity.Description = logItemModel.Content
	logItemEntity.PostMortemUrl = logItemModel.PostMortemLink
	logItemEntity.Status = logItemModel.Status
	logItemEntity.MilestoneType = logItemModel.MilestoneType
	logItemEntity.From = logItemModel.From
	return logItemEntity, nil
}

// TODO: Currently not implemented. The incident log item is going to be refactored.
func (s *incidentService) CreateIncidentLogItem(ctx context.Context, incidentId uint64, logCreation *entity.IncidentLogItem) (uint64, error) {
	err := validation.Incident.IncidentLogCreate(logCreation)
	if err != nil {
		return 0, err
	}
	logItemModel := s.cvter.EntityToModel.IncidentLogItem(logCreation)
	logItemModel.IncidentId = incidentId
	db := s.dbProvider.WriteDB(ctx)
	err = db.Create(logItemModel).Error
	if err != nil {
		s.Logger(ctx).Error(err, "create incident error when accessing database")
		return 0, err
	}
	return logItemModel.GetID().Uint64(), nil
}

// TODO: Currently not implemented. The incident log item is going to be refactored.
func (s *incidentService) UpdateIncidentLogItem(ctx context.Context, logItemId uint64, logUpdate *entity.IncidentLogItem) error {
	err := validation.Incident.IncidentLogUpdate(logUpdate)
	if err != nil {
		return err
	}
	incidentModel := s.cvter.EntityToModel.IncidentLogItem(logUpdate)
	db := s.dbProvider.WriteDB(ctx)
	err = db.Model(&models.IncidentLogItem{}).Where("id = ?", logItemId).Updates(incidentModel).Error
	if err != nil {
		s.Logger(ctx).Error(err, "update incident error when accessing database")
		return err
	}
	return nil
}

// TODO: Currently not implemented. The incident log item is going to be refactored.
func (s *incidentService) DeleteIncidentLogItem(ctx context.Context, id uint64) error {
	return nil
}

func (s *incidentService) GenerateReportInFeishuChannel(ctx context.Context, id uint64) error {
	return s.dbProvider.Transaction(ctx, func(ctx context.Context) error {
		entityIncident, err := s.GetIncident(ctx, id)
		if err != nil {
			return fmt.Errorf("failed when get incident: %w", err)
		}
		params := generateFeishuParams(entityIncident)
		chatId := s.conf.ReportChatId
		// use global lark now.
		msgId, err := s.larkclient.Notify.IncidentTopic(ctx, chatId, *params)
		if err != nil {
			return fmt.Errorf("failed when generating feishu report message: %w", err)
		}
		s.UpdateIncident(ctx, id, &entity.Incident{FeishuChannelMessage: *msgId})
		return nil
	})
}

func (s *incidentService) ListIncidentPostMortemItems(ctx context.Context, incidentId uint64) ([]*entity.IncidentPostMortem, error) {
	db := s.dbProvider.ReadDB(ctx)
	var postMortemModels []*models.IncidentPostMortem
	result := db.Where("incident_id = ?", incidentId).Order("created_at").Find(&postMortemModels)
	if result.Error != nil {
		s.Logger(ctx).Error(result.Error, "list incident postmortem items error when accessing database for incident id", "incident id", incidentId)
		return nil, result.Error
	}
	postMortemEntities := make([]*entity.IncidentPostMortem, 0)
	for _, postmortemModel := range postMortemModels {
		postmortemEntity := s.cvter.ModelToEntity.IncidentPostMortem(postmortemModel)
		postmortemEntity.InvolvedEngineers = s.handleUserString(ctx, postmortemModel.InvolvedEngineers, true)
		postMortemEntities = append(postMortemEntities, postmortemEntity)
	}

	return postMortemEntities, nil
}

func (s *incidentService) GetIncidentPostMortem(ctx context.Context, id uint64) (*entity.IncidentPostMortem, error) {
	db := s.dbProvider.ReadDB(ctx)
	var incidentPostMortemModel *models.IncidentPostMortem
	err := db.Where("id = ?", id).First(incidentPostMortemModel).Error
	if err != nil {
		s.Logger(ctx).Error(err, "get incident postmortem error when accessing database for id", "postmortem id", id)
		return nil, err
	}

	incidentPostMortemEntity := s.cvter.ModelToEntity.IncidentPostMortem(incidentPostMortemModel)
	incidentPostMortemEntity.InvolvedEngineers = s.handleUserString(ctx, incidentPostMortemModel.InvolvedEngineers, true)

	return incidentPostMortemEntity, nil
}

func (s *incidentService) CreateIncidentPostMortem(ctx context.Context, postMortemCreation *entity.IncidentPostMortem) (*entity.IncidentPostMortem, error) {
	var err error
	wdb := s.dbProvider.WriteDB(ctx)

	itemModel := s.cvter.EntityToModel.IncidentPostMortem(postMortemCreation)
	err = wdb.Save(itemModel).Error
	if err != nil {
		return nil, fmt.Errorf("save postmortem failed, error: %s", err)
	}
	postMortemCreation.Id = itemModel.GetID().Uint64()

	return postMortemCreation, nil
}

func (s *incidentService) UpdateIncidentPostMortem(ctx context.Context, id uint64, postMortemUpdate *entity.IncidentPostMortem) error {
	_, err := s.GetIncidentPostMortem(ctx, id)
	if err != nil {
		return fmt.Errorf("postmortem not exist, %w", err)
	}
	wdb := s.dbProvider.WriteDB(ctx)

	postMortemModel := s.cvter.EntityToModel.IncidentPostMortem(postMortemUpdate)
	err = wdb.Save(postMortemModel).Error
	if err != nil {
		return fmt.Errorf("save postmortem failed, error: %w", err)
	}
	return nil
}

func (s *incidentService) DeleteIncidentPostMortem(ctx context.Context, id uint64) error {
	item, err := s.GetIncidentPostMortem(ctx, id)
	if err != nil {
		return fmt.Errorf("postmortem %v not exist, %w", id, err)
	}
	wdb := s.dbProvider.WriteDB(ctx)

	itemModel := s.cvter.EntityToModel.IncidentPostMortem(item)
	err = wdb.Delete(itemModel).Error
	if err != nil {
		return fmt.Errorf("delete postmortem failed: %w", err)
	}
	return nil
}

func (s *incidentService) GetTopicReplies(ctx context.Context, topicId string, startTime string) ([]*entity.MessageReply, error) {
	chatId := s.conf.ReportChatId
	endTime := strconv.FormatInt(time.Now().Unix(), 10)
	resp, err := s.larkclient.Notify.GetTopicReplies(ctx, chatId, topicId, startTime, endTime)
	if err != nil {
		s.Logger(ctx).Error(err, "failed to get incident topic replies")
		return nil, err
	}
	replies := make([]*entity.MessageReply, 0)
	for _, r := range resp {
		t, _ := strconv.ParseInt(*r.CreateTime, 10, 64)
		replies = append(replies, &entity.MessageReply{
			Time:    time.Unix(t/1000, 0),
			Content: *r.Body.Content,
		})
	}
	return replies, nil
}

func (s *incidentService) CreateFeishuTask(ctx context.Context, task *entity.FeishuTask) (string, error) {
	executors := toLarkIdArray(s.emailsToUsers(ctx, task.Executors))
	followers := toLarkIdArray(s.emailsToUsers(ctx, task.Followers))
	dueTime := ""
	if task.DueTime != nil {
		dueTime = strconv.FormatInt(task.DueTime.Unix(), 10)
	}
	task.IncidentUrl = s.withOpsPortalBaseURL(task.IncidentUrl)
	taskId, err := s.larkclient.Task.CreateTask(ctx, task.Title, task.Description, dueTime, task.IncidentUrl, executors, followers)
	if err != nil {
		s.Logger(ctx).Error(err, "failed to create task")
		return "", err
	}
	return *taskId, nil
}

func (s *incidentService) UpdateFeishuTask(ctx context.Context, id string, task *entity.FeishuTask) error {
	executors := toLarkIdArray(s.emailsToUsers(ctx, task.Executors))
	followers := toLarkIdArray(s.emailsToUsers(ctx, task.Followers))
	dueTime := ""
	if task.DueTime != nil {
		dueTime = strconv.FormatInt(task.DueTime.Unix(), 10)
	}
	task.IncidentUrl = s.withOpsPortalBaseURL(task.IncidentUrl)
	err := s.larkclient.Task.UpdateTask(ctx, id, task.Title, task.Description, dueTime, task.IncidentUrl, executors, followers)
	if err != nil {
		s.Logger(ctx).Error(err, "failed to update task")
		return err
	}
	return nil
}

func (s *incidentService) CompleteFeishuTask(ctx context.Context, id string) error {
	err := s.larkclient.Task.CompleteTask(ctx, id)
	if err != nil {
		s.Logger(ctx).Error(err, "failed to complete task")
		return err
	}
	return nil
}

func (s *incidentService) DeleteFeishuTask(ctx context.Context, id string) error {
	err := s.larkclient.Task.DeleteTask(ctx, id)
	if err != nil {
		s.Logger(ctx).Error(err, "failed to delete task")
		return err
	}
	return nil
}

func (s *incidentService) GetFeishuTask(ctx context.Context, id string) (*entity.FeishuTask, error) {
	larkTask, err := s.larkclient.Task.GetTask(ctx, id)
	if err != nil {
		return nil, fmt.Errorf("get task for task id %s failed: %s", id, err)
	}
	if larkTask == nil {
		return nil, fmt.Errorf("cannot get feishu task for task id %s", id)
	}

	ent := &entity.FeishuTask{
		Title:        *larkTask.RichSummary,
		Description:  *larkTask.Description,
		CompleteTime: larkTask.CompleteTime,
	}

	// TODO: add sync executors
	if larkTask.Due != nil && larkTask.Due.Time != nil {
		ts, err := strconv.ParseInt(*larkTask.Due.Time, 10, 64)
		if err != nil {
			return nil, fmt.Errorf("parse due time for task id %s failed, due time: %s, error: %w", id, *larkTask.Due.Time, err)
		}
		tm := time.Unix(ts, 0)
		ent.DueTime = &tm
	}

	return ent, nil
}

func (s *incidentService) withOpsPortalBaseURL(rawURL string) string {
	if rawURL == "" {
		return ""
	}
	if parsed, err := url.Parse(rawURL); err == nil && parsed.IsAbs() {
		return rawURL
	}
	return strings.TrimRight(s.opsPortalBaseURL(), "/") + "/" + strings.TrimLeft(rawURL, "/")
}

func (s *incidentService) opsPortalBaseURL() (baseURL string) {
	if strings.TrimSpace(s.conf.OpsPortalBaseURL) != "" {
		return strings.TrimSpace(s.conf.OpsPortalBaseURL)
	}
	defer func() {
		if recover() != nil {
			baseURL = "https://ops.tidbcloud.com"
		}
	}()
	switch consts.GetEnv() {
	case consts.EnvironmentDev:
		return "https://ops-dev.tidbcloud.com"
	case consts.EnvironmentStaging:
		return "https://ops-staging.tidbcloud.com"
	default:
		return "https://ops.tidbcloud.com"
	}
}

func (s *incidentService) incidentModel2Entity(ctx context.Context, modelIncident *models.Incident, needParsePerson bool) *entity.Incident {
	var feishuUrl, jiraIssues string

	if modelIncident.FeishuLinks == nil {
		feishuUrl = ""
	} else {
		feishuUrl = *modelIncident.FeishuLinks
	}

	if modelIncident.JiraIssues == nil {
		jiraIssues = ""
	} else {
		jiraIssues = *modelIncident.JiraIssues
	}

	entityIncident := entity.Incident{
		Id:   uint64(modelIncident.ID),
		Name: modelIncident.Name,

		CreatedAt: &modelIncident.CreatedAt,

		Status: entity.IncidentStatus(modelIncident.Status),
		Tier:   entity.IncidentTier(modelIncident.IncidentTier),

		FeishuUrl:      feishuUrl,
		JiraIssues:     jiraIssues,
		JiraIssueLinks: incidentapi.ParseIncidentJiraIssueLinks(jiraIssues),

		Oncallers: s.handleUserString(ctx, modelIncident.Oncallers, needParsePerson),
		Leaders:   s.handleUserString(ctx, modelIncident.TeamLeaders, needParsePerson),
		FBOwners:  s.handleUserString(ctx, modelIncident.FBOwners, needParsePerson),

		BizRegions: modelIncident.BizRegion,
		Components: modelIncident.Components,

		Description:                      modelIncident.Description,
		IncidentStartTime:                incidentapi.NormalizeIncidentTimeForService(modelIncident.IncidentStartTime),
		IncidentResolvedTime:             incidentapi.NormalizeIncidentTimeForService(modelIncident.IncidentResolvedTime),
		WarRoomID:                        modelIncident.WarRoomID,
		PostMortemToDoActionImportStatus: modelIncident.ToDoActionImportStatus,
	}

	return &entityIncident

}

func (s *incidentService) ListIncidentPostMortemTimelines(ctx context.Context, postMortemId uint64) ([]*entity.IncidentPostMortemTimelineItem, error) {
	db := s.dbProvider.ReadDB(ctx)
	var timelineModels []*models.IncidentPostMortemTimelineItem
	result := db.Where("postmortem_id = ?", postMortemId).Order("happened_at").Find(&timelineModels)
	if result.Error != nil {
		s.Logger(ctx).Error(result.Error, "get postmortem timeline items error when accessing database for postmortem id", "post mortem id", postMortemId)
		return nil, result.Error
	}
	timelineEntities := make([]*entity.IncidentPostMortemTimelineItem, 0)
	for _, timelineModel := range timelineModels {
		timelineEntity := s.cvter.ModelToEntity.IncidentPostMortemTimelineItem(timelineModel)
		timelineEntities = append(timelineEntities, timelineEntity)
	}
	return timelineEntities, nil
}

func (s *incidentService) GetIncidentPostMortemTimelineItem(ctx context.Context, id uint64) (*entity.IncidentPostMortemTimelineItem, error) {
	db := s.dbProvider.ReadDB(ctx)
	var timelineItemModel *models.IncidentPostMortemTimelineItem
	err := db.Where("id = ?", id).First(&timelineItemModel).Error
	if err != nil {
		s.Logger(ctx).Error(err, "get incident postmortem timeline item error when accessing database for id", "timeline item id", id)
		return nil, err
	}
	timelineItemEntity := s.cvter.ModelToEntity.IncidentPostMortemTimelineItem(timelineItemModel)
	return timelineItemEntity, nil
}

func (s *incidentService) CreateIncidentPostMortemTimelineItem(ctx context.Context, timelineItemCreation *entity.IncidentPostMortemTimelineItem) (*entity.IncidentPostMortemTimelineItem, error) {
	wdb := s.dbProvider.WriteDB(ctx)

	timelineModel := s.cvter.EntityToModel.IncidentPostMortemTimelineItem(timelineItemCreation)
	err := wdb.Save(timelineModel).Error
	if err != nil {
		return nil, fmt.Errorf("save postmortem timeline item failed, error: %s", err)
	}
	timelineItemCreation.Id = timelineModel.Id
	return timelineItemCreation, nil
}

func (s *incidentService) UpdateIncidentPostMortemTimelineItem(ctx context.Context, id uint64, timelineUpdate *entity.IncidentPostMortemTimelineItem) error {
	_, err := s.GetIncidentPostMortemTimelineItem(ctx, id)
	if err != nil {
		return fmt.Errorf("timeline %v not exist, %w", id, err)
	}
	wdb := s.dbProvider.WriteDB(ctx)

	timelineModel := s.cvter.EntityToModel.IncidentPostMortemTimelineItem(timelineUpdate)
	err = wdb.Save(timelineModel).Error
	if err != nil {
		return fmt.Errorf("save postmortem timeline item failed, error: %w", err)
	}
	return nil
}

func (s *incidentService) DeleteIncidentPostMortemTimelineItem(ctx context.Context, id uint64) error {
	item, err := s.GetIncidentPostMortemTimelineItem(ctx, id)
	if err != nil {
		return fmt.Errorf("timeline %v not exist, %w", id, err)
	}
	wdb := s.dbProvider.WriteDB(ctx)

	itemModel := s.cvter.EntityToModel.IncidentPostMortemTimelineItem(item)
	err = wdb.Delete(itemModel).Error
	if err != nil {
		return fmt.Errorf("delete timeline item failed: %w", err)
	}
	return nil
}

func (s *incidentService) ListIncidentPostMortemToDoActions(ctx context.Context, incidentId uint64, postMortemId *uint64) ([]*entity.IncidentPostMortemToDoAction, error) {
	db := s.dbProvider.ReadDB(ctx)
	var actionModels []*models.IncidentPostMortemToDoAction
	query := db.Where("incident_id = ?", incidentId)
	if realPostMortemId := s.resolvePostMortemID(ctx, incidentId, postMortemId); realPostMortemId != nil {
		query = query.Where("postmortem_id = ?", *realPostMortemId)
	}
	result := query.Find(&actionModels)
	if result.Error != nil {
		s.Logger(ctx).Error(result.Error, "get postmortem todo actions error when accessing database for postmortem id", "todo action id", incidentId)
		return nil, result.Error
	}
	actionEntities := make([]*entity.IncidentPostMortemToDoAction, 0)
	for _, actionModel := range actionModels {
		actionEntity := s.cvter.ModelToEntity.IncidentPostMortemToDoAction(actionModel)
		if actionModel.Persons != nil {
			actionEntity.Persons = s.handleUserString(ctx, *actionModel.Persons, true)
		}
		actionEntities = append(actionEntities, actionEntity)
	}
	return actionEntities, nil
}

func (s *incidentService) GetIncidentPostMortemToDoActionItem(ctx context.Context, incidentId uint64, postMortemId *uint64, id uint64) (*entity.IncidentPostMortemToDoAction, error) {
	db := s.dbProvider.ReadDB(ctx)
	var actionModel *models.IncidentPostMortemToDoAction
	err := db.Where("id = ? AND incident_id = ?", id, incidentId).First(&actionModel).Error
	if err != nil {
		s.Logger(ctx).Error(err, "get incident postmortem todo action item error when accessing database for id", "todo action id", id)
		return nil, err
	}
	if err := s.validateActionPostMortem(ctx, incidentId, postMortemId, actionModel); err != nil {
		return nil, err
	}
	actionEntity := s.cvter.ModelToEntity.IncidentPostMortemToDoAction(actionModel)
	if actionModel.Persons != nil {
		actionEntity.Persons = s.handleUserString(ctx, *actionModel.Persons, true)
	}
	return actionEntity, nil
}

func (s *incidentService) CreateIncidentPostMortemToDoActionItem(ctx context.Context, param *CreateIncidentPostMortemActionParams) (*entity.IncidentPostMortemToDoAction, error) {
	var err error
	wdb := s.dbProvider.WriteDB(ctx)

	// Transform and validate the existence of users.
	persons := s.emailsToUsers(ctx, param.Persons)
	status, err := normalizeToDoActionStatus(param.Status, true)
	if err != nil {
		return nil, err
	}

	actionCreation := &entity.IncidentPostMortemToDoAction{
		IncidentId:      &param.IncidentId,
		PostMortemId:    s.resolvePostMortemID(ctx, param.IncidentId, param.PostMortemId),
		Deadline:        param.Deadline,
		Title:           &param.Title,
		Type:            param.Type,
		Priority:        param.Priority,
		Description:     &param.Description,
		LinkedJiraIssue: param.LinkedJiraIssue,
		Persons:         persons,
		Status:          status,
	}

	if param.CreateAssociatedLarkTask {
		taskId, err := s.CreateFeishuTask(ctx, &entity.FeishuTask{
			Title:       param.Title,
			Description: param.Description,
			DueTime:     param.Deadline,
			IncidentUrl: fmt.Sprintf("/incidents/%d", param.IncidentId),
			Executors:   param.Persons,
		})
		if err != nil {
			return nil, fmt.Errorf("cannot create lark task: %s", err)
		}
		actionCreation.FeishuTaskId = &taskId
	}

	actionModel := s.cvter.EntityToModel.IncidentPostMortemToDoAction(actionCreation)
	err = wdb.Save(actionModel).Error
	if err != nil {
		return nil, fmt.Errorf("save postmortem todo action item failed, error: %s", err)
	}
	actionCreation.Id = actionModel.ID.Uint64()

	return actionCreation, nil
}

func (s *incidentService) UpdateIncidentPostMortemToDoActionItem(ctx context.Context, incidentId uint64, postMortemId *uint64, id uint64, param *EditIncidentPostMortemToDoActionParams) error {
	mod, err := s.GetIncidentPostMortemToDoActionItem(ctx, incidentId, postMortemId, id)
	if err != nil {
		return fmt.Errorf("todo action %v not exist, %w", id, err)
	}
	wdb := s.dbProvider.WriteDB(ctx)
	status, err := normalizeToDoActionStatus(param.Status, false)
	if err != nil {
		return err
	}

	actionUpdate := &entity.IncidentPostMortemToDoAction{
		Deadline:        mod.Deadline,
		Title:           mod.Title,
		Type:            mod.Type,
		Priority:        mod.Priority,
		Description:     mod.Description,
		LinkedJiraIssue: mod.LinkedJiraIssue,
		Status:          mod.Status,
		Persons:         mod.Persons,
	}
	if param.Deadline != nil {
		actionUpdate.Deadline = param.Deadline
	}
	if param.Title != nil {
		actionUpdate.Title = param.Title
	}
	if param.Type != nil {
		actionUpdate.Type = param.Type
	}
	if param.Priority != nil {
		actionUpdate.Priority = param.Priority
	}
	if param.Description != nil {
		actionUpdate.Description = param.Description
	}
	if param.LinkedJiraIssue != nil {
		actionUpdate.LinkedJiraIssue = param.LinkedJiraIssue
	}
	if status != nil {
		actionUpdate.Status = status
	}
	if param.Persons != nil {
		actionUpdate.Persons = s.emailsToUsers(ctx, param.Persons)
	}
	actionModel := s.cvter.EntityToModel.IncidentPostMortemToDoAction(actionUpdate)

	actionModel.ID = models.ID(mod.Id)
	actionModel.IncidentId = mod.IncidentId
	actionModel.PostMortemId = mod.PostMortemId
	actionModel.FeishuTaskId = mod.FeishuTaskId

	updateFields := []string{"Deadline", "Title", "Type", "Priority", "Description", "LinkedJiraIssue", "Status", "Persons"}
	err = wdb.Model(actionModel).Select(updateFields).Updates(actionModel).Error
	if err != nil {
		return fmt.Errorf("save postmortem todo action item failed, error: %w", err)
	}
	if mod.FeishuTaskId != nil && *mod.FeishuTaskId != "" {
		task := buildFeishuTaskForToDoAction(mod, param)
		if err := s.UpdateFeishuTask(ctx, *mod.FeishuTaskId, task); err != nil {
			return fmt.Errorf("update associated feishu task failed: %w", err)
		}
	}
	return nil
}

func (s *incidentService) DeleteIncidentPostMortemToDoActionItem(ctx context.Context, incidentId uint64, postMortemId *uint64, id uint64) error {
	item, err := s.GetIncidentPostMortemToDoActionItem(ctx, incidentId, postMortemId, id)
	if err != nil {
		return fmt.Errorf("todo action %v not exist, %w", id, err)
	}
	wdb := s.dbProvider.WriteDB(ctx)

	itemModel := s.cvter.EntityToModel.IncidentPostMortemToDoAction(item)
	err = wdb.Delete(itemModel, id).Error
	if err != nil {
		return fmt.Errorf("delete todo action item failed: %w", err)
	}
	if item.FeishuTaskId != nil && *item.FeishuTaskId != "" {
		if err := s.DeleteFeishuTask(ctx, *item.FeishuTaskId); err != nil {
			s.Logger(ctx).Error(err, "delete associated feishu task failed", "todo_action_id", id, "feishu_task_id", *item.FeishuTaskId)
		}
	}
	return nil
}

func (s *incidentService) resolvePostMortemID(ctx context.Context, incidentId uint64, postMortemId *uint64) *uint64 {
	if postMortemId == nil || *postMortemId == 0 {
		return nil
	}
	db := s.dbProvider.ReadDB(ctx)
	var count int64
	err := db.Model(&models.IncidentPostMortem{}).
		Where("id = ? AND incident_id = ?", *postMortemId, incidentId).
		Count(&count).Error
	if err != nil {
		s.Logger(ctx).Error(err, "check postmortem existence failed", "incident_id", incidentId, "postmortem_id", *postMortemId)
		return nil
	}
	if count == 0 {
		return nil
	}
	return postMortemId
}

func (s *incidentService) validateActionPostMortem(ctx context.Context, incidentId uint64, postMortemId *uint64, action *models.IncidentPostMortemToDoAction) error {
	if action.PostMortemId == nil || postMortemId == nil {
		return nil
	}
	realPostMortemId := s.resolvePostMortemID(ctx, incidentId, postMortemId)
	if realPostMortemId == nil || *realPostMortemId != *action.PostMortemId {
		return fmt.Errorf("todo action %d does not belong to postmortem %v", action.ID.Uint64(), postMortemId)
	}
	return nil
}

func normalizeToDoActionStatus(status *string, useDefault bool) (*string, error) {
	if status == nil {
		if !useDefault {
			return nil, nil
		}
		defaultStatus := ToDoActionStatusOpen
		return &defaultStatus, nil
	}

	normalized := strings.TrimSpace(strings.ToLower(*status))
	switch normalized {
	case "", ToDoActionStatusOpen:
		normalized = ToDoActionStatusOpen
	case "ongoing", "inprogress", "in_progress", "in progress":
		normalized = ToDoActionStatusInProgress
	case ToDoActionStatusDone:
		normalized = ToDoActionStatusDone
	case ToDoActionStatusNoNeed:
		normalized = ToDoActionStatusNoNeed
	default:
		return nil, fmt.Errorf("invalid todo action status %q", *status)
	}
	return &normalized, nil
}

func buildFeishuTaskForToDoAction(mod *entity.IncidentPostMortemToDoAction, param *EditIncidentPostMortemToDoActionParams) *entity.FeishuTask {
	task := &entity.FeishuTask{
		DueTime: mod.Deadline,
	}
	if mod.Title != nil {
		task.Title = *mod.Title
	}
	if mod.Description != nil {
		task.Description = *mod.Description
	}
	if mod.IncidentId != nil {
		task.IncidentUrl = fmt.Sprintf("/incidents/%d", *mod.IncidentId)
	}
	for _, person := range mod.Persons {
		task.Executors = append(task.Executors, person.Email)
	}

	if param.Title != nil {
		task.Title = *param.Title
	}
	if param.Description != nil {
		task.Description = *param.Description
	}
	if param.Deadline != nil {
		task.DueTime = param.Deadline
	}
	if param.Persons != nil {
		task.Executors = param.Persons
	}
	return task
}

func (s *incidentService) handleUserString(ctx context.Context, str string, needQueryUser bool) []*coreuser.User {
	users := make([]*coreuser.User, 0)
	emails := strings.Split(str, " ")
	if !needQueryUser {
		for _, email := range emails {
			if len(email) == 0 {
				continue
			}
			users = append(users, &coreuser.User{Email: email})
		}
	} else {
		for _, email := range emails {
			if len(email) == 0 {
				continue
			}
			userResp, err := s.userSvc.GetUser(ctx, &userv1.GetUserRequest{Email: email})
			if err != nil {
				s.Logger(ctx).Error(err, "cannot find user for email", "email", email)
			} else {
				users = append(users, userResp.User)
			}
		}
	}
	return users
}

func (s *incidentService) convert2UserString(users []*coreuser.User) string {
	ret := ""
	for _, user := range users {
		ret = ret + user.Email + " "
	}
	return ret
}

func (s *incidentService) incidentEntity2Model(ctx context.Context, entityIncident *entity.Incident) *models.Incident {
	modelIncident := models.Incident{
		Name: entityIncident.Name,

		Status:       string(entityIncident.Status),
		IncidentTier: models.IncidentTierTp(entityIncident.Tier),

		FeishuLinks: &entityIncident.FeishuUrl,
		JiraIssues:  &entityIncident.JiraIssues,

		Oncallers:   s.convert2UserString(entityIncident.Oncallers),
		TeamLeaders: s.convert2UserString(entityIncident.Leaders),
		FBOwners:    s.convert2UserString(entityIncident.FBOwners),

		BizRegion:  entityIncident.BizRegions,
		Components: entityIncident.Components,

		Description:            entityIncident.Description,
		IncidentStartTime:      entityIncident.IncidentStartTime,
		IncidentResolvedTime:   entityIncident.IncidentResolvedTime,
		WarRoomID:              entityIncident.WarRoomID,
		ToDoActionImportStatus: entityIncident.PostMortemToDoActionImportStatus,
	}

	return &modelIncident
}

func (s *incidentService) emailsToUsers(ctx context.Context, emails []string) []*coreuser.User {
	var users []*coreuser.User
	for _, email := range emails {
		if len(email) == 0 {
			continue
		}
		userResp, err := s.userSvc.GetUser(ctx, &userv1.GetUserRequest{Email: email})
		if err != nil {
			s.Logger(ctx).Error(err, "cannot find user for email", "email", email)
		} else {
			users = append(users, userResp.User)
		}
	}
	return users
}

func generateFeishuParams(incident *entity.Incident) *notify.IncidentTopicParam {
	tierText := strings.ToUpper(string(incident.Tier))
	regionText := strings.ToUpper(incident.BizRegions)
	return &notify.IncidentTopicParam{
		Name:        incident.Name,
		Tier:        tierText,
		Component:   incident.Components,
		Region:      regionText,
		Jira:        incident.JiraIssues,
		Description: incident.Description,
		OnCallers:   toLarkIdArray(incident.Oncallers),
		TeamLeaders: toLarkIdArray(incident.Leaders),
	}

}

func toLarkIdArray(users []*coreuser.User) []string {
	arr := []string{}
	for _, user := range users {
		// currently only support at lark global users
		if user.LarkType == coreuser.LarkType_LARK_TYPE_GLOBAL {
			arr = append(arr, user.LarkUserId)
		}
	}
	return arr
}
