package action

import (
	"context"
	"fmt"
	"log/slog"

	"github.com/spf13/viper"

	"github.com/tidbcloud/jiraport/pkg/clients/jira"
	"github.com/tidbcloud/jiraport/pkg/clients/oncall"
	"github.com/tidbcloud/jiraport/pkg/db"
	"github.com/tidbcloud/jiraport/pkg/entity"
	"github.com/tidbcloud/jiraport/pkg/matcher"
)

const l2RetryMsg = "fail to find L2 assignee, retry of choosing L2 assignee will be triggered 2 hours later.\n if the issue is urgent, please escalate to L3 directly"

// CustomCommand does the L2 / SEE assignment: it maps the issue's component to
// one of the SEE teams (support_compute / support_storage / support_tools) via
// the see matcher, asks the oncall service who is on-call for that team, and
// writes the result into a Jira custom field (e.g. customfield_10053). Night
// shift / no-one-on-call is handled natively by the oncall schedule.
type CustomCommand struct {
	logger       *slog.Logger
	oncallClient oncall.Interface
	seeMatcher   *matcher.Matcher // component -> SEE team name
	jiraClient   jira.Interface
	dbClient     db.Interface
}

func NewCustomCommand(
	logger *slog.Logger,
	oncallClient oncall.Interface,
	jiraClient jira.Interface,
	dbClient db.Interface,
) (*CustomCommand, error) {
	var seeMatcher *matcher.Matcher
	if f := viper.GetString("l2-matcher-file"); f != "" {
		m, err := matcher.NewMatcher(logger, f)
		if err != nil {
			logger.Warn("failed to load L2/SEE matcher config, skipping", "file", f, "err", err)
		} else {
			seeMatcher = m
			logger.Info("L2/SEE matcher config loaded", "file", f)
		}
	}

	return &CustomCommand{
		logger:       logger,
		oncallClient: oncallClient,
		seeMatcher:   seeMatcher,
		jiraClient:   jiraClient,
		dbClient:     dbClient,
	}, nil
}

// seeTeam maps the issue to a SEE team name via the see matcher, or "" if none.
func (cmd *CustomCommand) seeTeam(issue *entity.RawIssue) string {
	if cmd.seeMatcher == nil {
		return ""
	}
	receivers, err := cmd.seeMatcher.Match(issue)
	if err != nil {
		cmd.logger.Warn("see matcher error", "issue", issue.Key, "err", err)
		return ""
	}
	teams, err := receivers.TryToStringSlice()
	if err != nil || len(teams) == 0 {
		return ""
	}
	return teams[0]
}

func (cmd *CustomCommand) Exec(ctx context.Context,
	actionExecInfo *entity.NotificationActionExecInfo,
	args ...string,
) (bool, string, error) {
	issueKey := actionExecInfo.Notification.IssueKey
	rawIssue := &actionExecInfo.Notification.Issue.RawIssue

	if len(args) == 0 {
		return false, "", fmt.Errorf("custom action param is missed")
	}
	customField := args[0]

	if cmd.oncallClient == nil {
		return false, l2RetryMsg, fmt.Errorf("oncall client not configured for L2 assign")
	}

	team := cmd.seeTeam(rawIssue)
	if team == "" {
		cmd.logger.Info("L2 assign: no SEE team for component", "issue", issueKey)
		return false, l2RetryMsg, fmt.Errorf("no SEE team for issue component")
	}

	emails, err := cmd.oncallClient.GetCurrentOnCall(team)
	if err != nil {
		cmd.logger.Warn("L2 assign: oncall lookup failed", "issue", issueKey, "team", team, "err", err)
		return false, l2RetryMsg, err
	}
	candidates := make([]string, 0, len(emails))
	for _, e := range emails {
		if e != "" {
			candidates = append(candidates, e)
		}
	}
	if len(candidates) == 0 {
		cmd.logger.Info("L2 assign: no SEE on-call", "issue", issueKey, "team", team)
		return false, l2RetryMsg, fmt.Errorf("no SEE on-call for team %s", team)
	}

	// One SEE on-call is the norm; if several, spread load fairly.
	selected, err := cmd.dbClient.SelectAssigneeFromCandidate(candidates)
	if err != nil || selected == "" {
		selected = candidates[0]
	}

	if err := cmd.jiraClient.UpdateCustomSEE2(issueKey, selected, customField); err != nil {
		return false, l2RetryMsg, fmt.Errorf("update custom SEE field error, %w", err)
	}

	cmd.logger.Info("assigned L2 SEE via oncall", "issue", issueKey, "team", team, "see", selected)
	return true, fmt.Sprintf("assign L2 SEE to %s (team %s)", selected, team), nil
}
