package action

import (
	"context"
	"crypto/rand"
	"errors"
	"fmt"
	"log/slog"
	"math/big"
	"strings"

	"github.com/spf13/viper"

	"github.com/tidbcloud/jiraport/pkg/clients/engineer"
	"github.com/tidbcloud/jiraport/pkg/clients/jira"
	"github.com/tidbcloud/jiraport/pkg/clients/oncall"
	"github.com/tidbcloud/jiraport/pkg/entity"
	ntf "github.com/tidbcloud/jiraport/pkg/resolver/notification"
)

type EscalateCommand struct {
	logger          *slog.Logger
	engineerClient  engineer.Interface
	oncallClient    oncall.Interface
	jiraClient      jira.Interface
	notificationSvc *ntf.Notifications
	needAssign      bool
}

func NewEscalateCommand(
	logger *slog.Logger,
	jiraClient jira.Interface,
	engineerClient engineer.Interface,
	oncallClient oncall.Interface,
	notificationSvc *ntf.Notifications,
) (*EscalateCommand, error) {
	needAssign := viper.GetBool("need-assign")

	return &EscalateCommand{
		logger:          logger,
		jiraClient:      jiraClient,
		notificationSvc: notificationSvc,
		needAssign:      needAssign,
		engineerClient:  engineerClient,
		oncallClient:    oncallClient,
	}, nil
}

// escalate moves the issue up one level to the oncall_team's managers. If the
// current assignee is already a team manager, there is no higher level in the
// team model, so it returns an error — the policy then escalates to firemaster.
func (cmd *EscalateCommand) escalate(issue *entity.Issue, specific string) (*engineer.Engineer, error) {
	// Explicit target override (cmd: ["escalate", "<email>"]).
	if specific != "" {
		eng, err := cmd.engineerClient.GetEngineerByEmail(specific)
		if err != nil {
			return nil, err
		}
		if eng == nil || eng.Email == "" {
			return nil, fmt.Errorf("escalate target %s not found", specific)
		}
		if err := cmd.jiraClient.Assign(issue.Key, eng.Email); err != nil {
			return nil, fmt.Errorf("assign error %w", err)
		}
		return eng, nil
	}

	if cmd.oncallClient == nil {
		return nil, errors.New("oncall client not configured for escalate")
	}
	team := issue.OncallTeam()
	if team == "" {
		return nil, errors.New("no oncall_team to escalate")
	}
	managers, err := cmd.oncallClient.GetTeamManagers(team)
	if err != nil {
		return nil, err
	}
	if len(managers) == 0 {
		return nil, fmt.Errorf("no managers for team %s to escalate", team)
	}

	cur := strings.ToLower(strings.TrimSpace(issue.AssigneeEmail))
	for _, m := range managers {
		if strings.ToLower(strings.TrimSpace(m)) == cur && cur != "" {
			return nil, fmt.Errorf("assignee %s already a manager of %s, no further escalation", issue.AssigneeEmail, team)
		}
	}

	pick := managers[0]
	if len(managers) > 1 {
		if n, err := rand.Int(rand.Reader, big.NewInt(int64(len(managers)))); err == nil {
			pick = managers[n.Int64()]
		}
	}
	eng, err := cmd.engineerClient.GetEngineerByEmail(pick)
	if err != nil {
		return nil, err
	}
	if eng == nil || eng.Email == "" {
		return nil, fmt.Errorf("manager %s not found in engineer DB", pick)
	}
	if err := cmd.jiraClient.Assign(issue.Key, eng.Email); err != nil {
		return nil, fmt.Errorf("assign error %w", err)
	}
	return eng, nil
}

func (cmd *EscalateCommand) Exec(ctx context.Context, actionExecInfo *entity.NotificationActionExecInfo, args ...string) (bool, string, error) {
	if !cmd.needAssign {
		return false, "", errors.New("\"need-assign\" is not set, action is not allowed")
	}

	toAssign := ""
	if len(args) > 0 {
		toAssign = args[0]
	}

	issue := actionExecInfo.Notification.Issue
	assignee, err := cmd.escalate(issue, toAssign)
	if err != nil {
		return false, "", err
	}
	err = cmd.notificationSvc.UpdateAssignee(issue.Key, assignee.Email)
	if err != nil {
		return false, "", err
	}

	cmd.logger.Debug("escalate successfully", "issue", actionExecInfo.Notification.IssueKey, "assignee", assignee)
	msg := fmt.Sprintf("escalate to %s(%s)", assignee.Name, assignee.Email)

	return true, msg, nil
}
