package action

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"log/slog"
	"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/db"
	"github.com/tidbcloud/jiraport/pkg/entity"
	"github.com/tidbcloud/jiraport/pkg/matcher"
	ntf "github.com/tidbcloud/jiraport/pkg/resolver/notification"
)

type AssignCommand struct {
	logger              *slog.Logger
	jiraClient          jira.Interface
	engineerClient      engineer.Interface
	notificationSvc     *ntf.Notifications
	assignMatcher       *matcher.Matcher
	oncallAssignMatcher *matcher.Matcher // optional: team-name routing for the new oncall assign
	oncallClient        oncall.Interface // optional: new oncall service
	needAssign          bool
	dbClient            db.Interface
}

func isInternalAlert(issue *entity.RawIssue) bool {
	if issue == nil || issue.Fields == nil {
		return false
	}

	kind, ok := issue.Fields.Unknowns["customfield_10159"].(string)
	return ok && strings.EqualFold(strings.TrimSpace(kind), "internal")
}

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

	var assignMatcher *matcher.Matcher
	if needAssign {
		var err error
		assignMatcher, err = matcher.NewMatcher(
			logger, viper.GetString("assign-matcher-file"))
		if err != nil {
			return nil, err
		}
	}

	// Optional: new oncall assign matcher (team-name routing) for the oncall
	// service. Currently used for log-only validation; will drive assignment at
	// cutover.
	var oncallAssignMatcher *matcher.Matcher
	if oncallFile := viper.GetString("oncall-assign-matcher-file"); oncallFile != "" {
		var err error
		oncallAssignMatcher, err = matcher.NewMatcher(logger, oncallFile)
		if err != nil {
			logger.Warn("failed to load oncall assign config, skipping", "file", oncallFile, "err", err)
		} else {
			logger.Info("oncall assign config loaded", "file", oncallFile)
		}
	}

	return &AssignCommand{
		logger:              logger,
		jiraClient:          jiraClient,
		engineerClient:      engineerClient,
		notificationSvc:     notificationSvc,
		assignMatcher:       assignMatcher,
		oncallAssignMatcher: oncallAssignMatcher,
		oncallClient:        oncallClient,
		needAssign:          needAssign,
		dbClient:            dbClient,
	}, nil
}

// func (cmd *AssignCommand) filterSeesByComponents(sees []engineer.Engineer, issue *entity.RawIssue) ([]engineer.Engineer, error) {
// 	candidates := make([]engineer.Engineer, 0)
// 	for _, see := range sees {
// 		components, err := cmd.engineerClient.GetComponentsByUserEmail(see.Email)
// 		if err != nil {
// 			return nil, err
// 		}
// 		contain := false
// 		for _, aimComponent := range components {
// 			for _, component := range issue.Fields.Components {
// 				if component.Name == aimComponent {
// 					contain = true
// 					break
// 				}
// 			}
// 		}
// 		if contain {
// 			candidates = append(candidates, see)
// 		}
// 	}
// 	return candidates, nil
// }

func (cmd *AssignCommand) assign(issue *entity.RawIssue) (*engineer.Engineer, error) {
	var assignee *engineer.Engineer
	//Check whether issue is ascended directly. If no, assign it to L2
	// MIGRATE RELATED
	// if (issue.Fields.Project.Key == "TCOC" || issue.Fields.Project.Key == "GTOC") && (issue.Fields.Unknowns["customfield_10152"] == nil || issue.Fields.Unknowns["customfield_10152"] == "None") {
	// 	// issue is not ascended directly,assign to SEE
	// 	seeAssignees, err := cmd.engineerClient.GetSEEs(true)
	// 	if err != nil && err != gorm.ErrRecordNotFound {
	// 		return nil, err
	// 	}
	// 	seeWathcers, err := cmd.engineerClient.GetSEEs(false)
	// 	if err != nil && err != gorm.ErrRecordNotFound {
	// 		return nil, err
	// 	}
	// 	assignCandidates, err := cmd.filterSeesByComponents(seeAssignees, issue)
	// 	if err != nil {
	// 		return nil, err
	// 	}
	// 	watcherCandicate, err := cmd.filterSeesByComponents(seeWathcers, issue)
	// 	if err != nil {
	// 		return nil, err
	// 	}
	// 	if len(assignCandidates) > 0 {
	// 		assignee, err = cmd.selectAssignee(assignCandidates)
	// 		if err != nil {
	// 			return nil, fmt.Errorf("select candidates error %w", err)
	// 		}
	// 		err = cmd.jiraClient.Assign(issue.Key, assignee.Email)
	// 		if err != nil {
	// 			return nil, fmt.Errorf("assign error %w", err)
	// 		} else {
	// 			return assignee, nil
	// 		}
	// 	} else {
	// 		for _, watcherCandicate := range watcherCandicate {
	// 			err = cmd.jiraClient.AddWatchers(issue.ID, watcherCandicate.Email)
	// 			if err != nil {
	// 				cmd.logger.Error("fail to add watcher", "msg", err)
	// 			}
	// 		}
	// 	}
	// }
	// If assign label is an email address, assign directly without matcher lookup
	if alertData, ok := issue.Fields.Unknowns["customfield_10160"].(string); ok {
		var alertLabels struct {
			Labels struct {
				Assign string `json:"assign"`
			} `json:"labels"`
		}
		if err := json.Unmarshal([]byte(alertData), &alertLabels); err == nil {
			assignVal := alertLabels.Labels.Assign
			// Explicit "none" skips assignment entirely, leaving whatever assignee
			// the issue already has (including none). A pre-existing assignee is
			// assumed to be set manually and must not be overwritten.
			if strings.EqualFold(strings.TrimSpace(assignVal), "none") {
				cmd.logger.Info("assign label is 'none', skipping assignment", "issue", issue.Key)
				return nil, nil
			}
			if strings.Contains(assignVal, "@") {
				eng, err := cmd.engineerClient.GetEngineerByEmail(assignVal)
				if err == nil && eng != nil {
					cmd.logger.Info("assign by email in assign label", "email", assignVal, "issue", issue.Key)
					if err := cmd.jiraClient.Assign(issue.Key, eng.Email); err != nil {
						return nil, fmt.Errorf("assign by email error %w", err)
					}
					return eng, nil
				}
				cmd.logger.Warn("assign email not found in engineer DB, falling back to matcher", "email", assignVal)
			}
		}
	}

	// Prefer the oncall-service-based assignment. Only fall back to the
	// matcher-based path when the oncall path is not configured or does not
	// apply (no team / schedule gap / engineer not in DB / Jira API error).
	// Hard errors (matcher crash, oncall service failure) propagate.
	if eng, fallback, err := cmd.assignByOncall(issue); !fallback {
		if err != nil {
			cmd.logger.Error("assign oncall error", "issue", issue.Key, "err", err)
			return nil, err
		}
		return eng, nil
	}

	//issue is ascended directly or SEE is not found,assign to L3
	assignReceivers, err := cmd.assignMatcher.Match(issue)
	if err != nil {
		return nil, fmt.Errorf("assign matching error, %w", err)
	}
	assigneeCandi := make([]engineer.Engineer, 0)
	categoryIds, err := assignReceivers.TryToIntSlice()
	if err == matcher.ErrorReceiverType {
		emails, err := assignReceivers.TryToStringSlice()
		if err != nil {
			return nil, fmt.Errorf("inconsistency receiver type")
		} else {
			if len(emails) == 0 {
				return nil, errors.New("empty email assignee")
			}
			for _, email := range emails {
				if email == "" {
					continue
				}
				emailCancidate, err := cmd.engineerClient.GetEngineerByEmail(email)
				if err != nil {
					cmd.logger.Error("fail to find assignee by email", "email", email, "err", err)
					continue
				}
				if emailCancidate != nil {
					assigneeCandi = append(assigneeCandi, *emailCancidate)
				}
			}
		}
	} else {
		if len(categoryIds) == 0 {
			return nil, errors.New("no matched category")
		}
		assigneeCandi, err = cmd.engineerClient.GetEngineersByCategoryIDs(categoryIds)
		if err != nil {
			return nil, fmt.Errorf("finding engineer by category_id error, %w", err)
		}
	}

	if len(assigneeCandi) == 0 {
		return nil, errors.New("no candidate found")
	}

	assignee, err = cmd.selectAssignee(assigneeCandi)
	if err != nil {
		return nil, fmt.Errorf("select assignee error %w", err)
	}
	err = cmd.jiraClient.Assign(issue.Key, assignee.Email)
	if err != nil {
		return nil, fmt.Errorf("assign error %w", err)
	}

	return assignee, nil
}

// oncallTeamLabel returns the alert's labels.oncall_team value, or "".
func oncallTeamLabel(issue *entity.RawIssue) string {
	alertData, ok := issue.Fields.Unknowns["customfield_10160"].(string)
	if !ok {
		return ""
	}
	var parsed struct {
		Labels struct {
			OncallTeam string `json:"oncall_team"`
		} `json:"labels"`
	}
	if err := json.Unmarshal([]byte(alertData), &parsed); err != nil {
		return ""
	}
	return strings.TrimSpace(parsed.Labels.OncallTeam)
}

// assignByOncall performs the oncall-service-based assignment. It returns:
//   - (eng, false, nil)  — assigned successfully via the oncall path
//   - (nil, true,  nil)  — oncall path does not apply, caller should fallback
//   - (nil, false, err)  — oncall path applies but failed, do NOT fallback
//
// Fallback is appropriate only when the oncall path is not configured, no team
// can be determined, or there is a schedule gap. All other failures (service
// errors, data inconsistencies, Jira API errors) are hard errors.
func (cmd *AssignCommand) assignByOncall(issue *entity.RawIssue) (*engineer.Engineer, bool, error) {
	if cmd.oncallAssignMatcher == nil || cmd.oncallClient == nil {
		return nil, true, nil
	}

	emails, team, reason, err := cmd.oncallCandidateEmails(issue)
	if err != nil {
		return nil, false, fmt.Errorf("oncall assign for %s: %w", issue.Key, err)
	}
	if len(emails) == 0 {
		cmd.logger.Info("oncall assign fallback", "issue", issue.Key, "team", team, "reason", reason)
		return nil, true, nil
	}

	candidates := make([]engineer.Engineer, 0, len(emails))
	for _, e := range emails {
		eng, err := cmd.engineerClient.GetEngineerByEmail(e)
		if err != nil || eng == nil || eng.Email == "" {
			cmd.logger.Warn("oncall assign: engineer not found by email", "issue", issue.Key, "team", team, "email", e, "err", err)
			continue
		}
		candidates = append(candidates, *eng)
	}
	if len(candidates) == 0 {
		cmd.logger.Warn("oncall assign fallback", "issue", issue.Key, "team", team, "reason", "engineer_not_found", "emails", fmt.Sprintf("%v", emails))
		return nil, true, nil
	}

	assignee, err := cmd.selectAssignee(candidates)
	if err != nil {
		return nil, false, fmt.Errorf("oncall assign for %s: select assignee error (team %s): %w", issue.Key, team, err)
	}
	if err := cmd.jiraClient.Assign(issue.Key, assignee.Email); err != nil {
		cmd.logger.Warn("oncall assign fallback", "issue", issue.Key, "team", team, "reason", fmt.Sprintf("jira_assign_error: %v", err), "assignee", assignee.Email)
		return nil, true, nil
	}
	cmd.logger.Info("assigned via oncall service", "issue", issue.Key, "team", team, "candidates", fmt.Sprintf("%v", emails), "assignee", assignee.Email)
	return assignee, false, nil
}

// oncallCandidateEmails resolves the candidate engineer emails the oncall path
// would assign among. Returns:
//   - (emails, team, "", nil)    with len(emails)>0 — candidates found
//   - (nil,    team, reason, nil) — soft fallback: no team / schedule gap
//   - (nil,    team, "", err)     — hard error: matcher crash or oncall service failure
func (cmd *AssignCommand) oncallCandidateEmails(issue *entity.RawIssue) (emails []string, team string, fallbackReason string, err error) {
	receivers, err := cmd.oncallAssignMatcher.Match(issue)
	if err != nil {
		return nil, "", "", fmt.Errorf("oncall assign matcher error: %w", err)
	}

	strs, _ := receivers.TryToStringSlice()
	if len(strs) > 0 && strings.Contains(strs[0], "@") {
		out := make([]string, 0, len(strs))
		for _, s := range strs {
			if s = strings.TrimSpace(s); strings.Contains(s, "@") {
				out = append(out, s)
			}
		}
		return out, "", "", nil
	}

	// Team routing: matched team name, else fall back to the oncall_team label.
	team = oncallTeamLabel(issue)
	if len(strs) > 0 {
		if r := strings.TrimSpace(strs[0]); r != "" {
			team = r
		}
	}
	if team == "" {
		return nil, "", "no_team", nil
	}

	current, err := cmd.oncallClient.GetCurrentOnCall(team)
	if err != nil {
		return nil, team, "", fmt.Errorf("oncall service lookup error for team %s: %w", team, err)
	}
	out := make([]string, 0, len(current))
	for _, e := range current {
		if e = strings.TrimSpace(e); e != "" {
			out = append(out, e)
		}
	}
	if len(out) == 0 {
		return nil, team, "empty_schedule_gap", nil
	}
	return out, team, "", nil
}

func (cmd *AssignCommand) assignTSE(issue *entity.RawIssue) (*engineer.Engineer, error) {
	var assignee *engineer.Engineer
	var err error
	// MIGRATE RELATED
	if issue.Fields.Unknowns["customfield_10157"] != nil {
		tenantID := issue.Fields.Unknowns["customfield_10157"].(string)
		assignee, err = cmd.engineerClient.GetTSEByTenantID(tenantID)
		if err != nil {
			return assignee, err
		}
		err = cmd.jiraClient.Assign(issue.Key, assignee.Email)
		if err != nil {
			return nil, fmt.Errorf("assign error %w", err)
		}
	}
	return assignee, nil
}

func (cmd *AssignCommand) assignSpecific(issue *entity.RawIssue, specific string) (*engineer.Engineer, error) {
	if specific == "" {
		return nil, errors.New("empty specific assignee")
	}
	assignee, err := cmd.engineerClient.GetEngineerByEmail(specific)
	if err != nil {
		return nil, fmt.Errorf("find assignee by email error, %w", err)
	}
	if assignee == nil {
		return nil, fmt.Errorf("assignee %s not found", specific)
	}
	err = cmd.jiraClient.Assign(issue.Key, assignee.Email)
	if err != nil {
		return nil, fmt.Errorf("assign error %w", err)
	}
	return assignee, nil
}

// select assignee, will store last assigned time of engineer
func (cmd *AssignCommand) selectAssignee(candi []engineer.Engineer) (*engineer.Engineer, error) {
	if len(candi) == 0 {
		return nil, errors.New("empty candidate")
	}
	var selected *engineer.Engineer
	emails := make([]string, 0)
	for _, p := range candi {
		emails = append(emails, p.Email)
	}
	selectedEmail, err := cmd.dbClient.SelectAssigneeFromCandidate(emails)
	if err != nil {
		cmd.logger.Error("fail to select assignee", "err", err)
		return &candi[0], nil
	}
	for i := range candi {
		if candi[i].Email == selectedEmail {
			selected = &candi[i]
		}
	}
	return selected, nil
}

func (cmd *AssignCommand) 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")
	}

	issueKey := actionExecInfo.Notification.IssueKey
	rawIssue := &actionExecInfo.Notification.Issue.RawIssue
	if isInternalAlert(rawIssue) {
		cmd.logger.Info("internal alert, skipping assignment", "issue", issueKey)
		return true, "assignment skipped (internal alert)", nil
	}

	toAssign := ""
	var assignee *engineer.Engineer
	var err error
	if len(args) > 0 {
		toAssign = args[0]
	}
	if toAssign == "tse" {
		assignee, err = cmd.assignTSE(rawIssue)
		if err != nil {
			cmd.logger.Warn("error when assign tse", "err msg", err)
		}
	} else if toAssign != "" {
		assignee, err = cmd.assignSpecific(rawIssue, toAssign)
		if err != nil {
			return false, "", err
		}
	}
	if assignee == nil {
		assignee, err = cmd.assign(rawIssue)
		if err != nil {
			return false, "", err
		}
	}

	// assign() may intentionally skip assignment (assign: none), in which case
	// there is no assignee to record downstream.
	if assignee == nil {
		return true, "assignment skipped (assign: none)", nil
	}

	err = cmd.notificationSvc.UpdateAssignee(issueKey, assignee.Email)
	if err != nil {
		return false, "", err
	}

	cmd.logger.Debug("assign successfully", "issue", issueKey, "assignee", assignee.Name)
	msg := fmt.Sprintf("assign to %s(%s)", assignee.Name, assignee.Email)
	return true, msg, nil
}
