package approval

import (
	"context"
	"fmt"
	"slices"

	apprent "github.com/tidbcloud/devops/entity/approval"
	"github.com/tidbcloud/devops/entity/event"
	"github.com/tidbcloud/devops/pkg/infra/log"
	"github.com/tidbcloud/devops/pkg/service/approval/api"
	"github.com/tidbcloud/devops/pkg/service/approval/approval"
)

func (s *svc) GetApproval(ctx context.Context, id uint64) (*apprent.Approval, error) {
	return s.repo.GetApproval(ctx, id)
}

func (s *svc) CreateApproval(ctx context.Context, param api.CreateApprovalParam) (*uint64, error) {
	return s.createApproval(ctx, param, nil)
}

func (s *svc) CreateApprovalFromTemplate(ctx context.Context, param api.CreateApprovalFromTemplateParam, mutates ...func(param *api.CreateApprovalParam)) (*uint64, error) {
	template, exist := s.templates[param.FromTemplate]
	if !exist {
		return nil, fmt.Errorf("template %s is not found", param.FromTemplate)
	}

	cParam := api.CreateApprovalParam{
		CreateApprovalParamBase: param.CreateApprovalParamBase,

		Nodes:      template.Nodes,
		CCTasks:    template.CCTasks,
		AdminTasks: template.AdminTasks,
	}

	for _, mutate := range mutates {
		mutate(&cParam)
	}

	return s.createApproval(ctx, cParam, &param.FromTemplate)
}

func (s *svc) FinishApprovalTask(ctx context.Context, id uint64, param api.FinishApprovalTaskParam) error {
	appr, err := s.repo.GetApproval(ctx, id)
	if err != nil {
		return err
	}

	actor := approval.NewApprovalActor(ctx, s.apprComps(ctx), appr)

	err = s.dbp.Transaction(ctx, func(ctx context.Context) error {
		err := actor.FinishApprovalTask(param.User, param.Result)
		if err != nil {
			return err
		}
		return nil
	})
	if err != nil {
		return err
	}

	s.sendFinishEventIfNeed(ctx, actor.GetContext().Approval)
	return nil
}

func (s *svc) FinishApprovalAdminTask(ctx context.Context, id uint64, param api.FinishApprovalTaskParam) error {
	if s.conf.DisableAdminApprove {
		return fmt.Errorf("admin approve is disabled")
	}

	appr, err := s.repo.GetApproval(ctx, id)
	if err != nil {
		return err
	}
	actor := approval.NewApprovalActor(ctx, s.apprComps(ctx), appr)

	err = s.dbp.Transaction(ctx, func(ctx context.Context) error {
		err := actor.FinishApprovalAdminTask(param.User, param.Result)
		if err != nil {
			return err
		}
		return nil
	})
	if err != nil {
		return err
	}

	s.sendFinishEventIfNeed(ctx, actor.GetContext().Approval)
	return nil
}

func (s *svc) CancelApproval(ctx context.Context, id uint64) error {
	appr, err := s.repo.GetApproval(ctx, id)
	if err != nil {
		return err
	}

	actor := approval.NewApprovalActor(ctx, s.apprComps(ctx), appr)
	err = s.dbp.Transaction(ctx, func(ctx context.Context) error {
		err := actor.Cancel()
		if err != nil {
			return err
		}
		return nil
	})
	if err != nil {
		return err
	}

	s.sendFinishEventIfNeed(ctx, actor.GetContext().Approval)
	return nil
}

func (s *svc) CheckApprovalPermission(ctx context.Context, id uint64, userEmail string) (*api.ApprovalPermission, error) {
	appr, err := s.repo.GetApproval(ctx, id)
	if err != nil {
		return nil, err
	}

	perm := &api.ApprovalPermission{}

	if appr.Status != apprent.ApprovalStatusPending {
		return perm, nil
	}

	// check direct approver
	for _, level := range appr.Levels {
		if level.EvalLevelStatus() != apprent.ApprovalNodeStatusPending {
			continue
		}
		for _, node := range level.Nodes {
			if node.Status != apprent.ApprovalNodeStatusPending {
				continue
			}
			for _, task := range node.Tasks {
				if task.User == userEmail && task.Status == apprent.ApprovalTaskStatusPending {
					perm.CanApprove = true
				}
			}
		}
		break // only check the first pending level
	}

	// check backup approver
	if !perm.CanApprove {
		for _, level := range appr.Levels {
			if level.EvalLevelStatus() != apprent.ApprovalNodeStatusPending {
				continue
			}
			for _, node := range level.Nodes {
				if node.Status != apprent.ApprovalNodeStatusPending {
					continue
				}
				for _, bt := range node.BackupTasks {
					if bt.User == userEmail && bt.StartAt != nil && bt.EndAt == nil {
						perm.CanApprove = true
						break
					}
				}
				if perm.CanApprove {
					break
				}
			}
			break // only check the first pending level
		}
	}

	// check admin approver
	if !s.conf.DisableAdminApprove {
		for _, adminTask := range appr.AdminTasks {
			if adminTask.User == userEmail && adminTask.EndAt == nil {
				perm.CanAdminApprove = true
				break
			}
		}
	}

	return perm, nil
}

func (s *svc) RegisterTemplate(ctx context.Context, tmpl apprent.Template) error {
	_, exist := s.templates[tmpl.ID]
	if exist {
		return fmt.Errorf("template %s already exists", tmpl.ID)
	}

	s.templates[tmpl.ID] = tmpl
	return nil
}

func (s *svc) createApproval(ctx context.Context, param api.CreateApprovalParam, fromTemplate *string) (*uint64, error) {
	// inject fixed admin approvers if configured
	if s.conf.FixedAdminApproverUserGroup != "" {
		if param.AdminTasks == nil {
			param.AdminTasks = &apprent.Approver{}
		}
		param.AdminTasks.UserGroups = append(param.AdminTasks.UserGroups, s.conf.FixedAdminApproverUserGroup)
	}

	actor := approval.NewApprovalActor(ctx, s.apprComps(ctx), nil)
	err := s.dbp.Transaction(ctx, func(ctx context.Context) error {
		err := actor.Create(param, fromTemplate)
		if err != nil {
			log.FromContext(ctx).Info("created approval failed", "param", param, "error", err) // log for debug
			return err
		}

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

	appr := actor.GetContext().Approval
	e := newEvent(ctx, appr.ID, event.ApprovalEventTypeCreate)
	if err := s.eventbus.ApprovalEvent.Publish(e); err != nil {
		s.Logger(ctx).Error(err, "failed to publish approval event", "approval_id", appr.ID)
	}

	s.sendFinishEventIfNeed(ctx, actor.GetContext().Approval)

	return &appr.ID, nil
}

func (s *svc) sendFinishEventIfNeed(ctx context.Context, appr *apprent.Approval) {
	id := appr.ID
	status := appr.Status

	if slices.Contains(apprent.ApprovalStatusFinishedValues, status) {
		e := newEvent(ctx, id, event.ApprovalEventTypeFinish)
		e.Details.Finish = &event.ApprovalEventFinished{Result: status}
		eerr := s.eventbus.ApprovalEvent.Publish(e)
		if eerr != nil {
			s.Logger(ctx).Error(eerr, "failed to publish approval event", "approval_id", id)
		}
	}
}
