package action

import (
	"context"
	"errors"
	"fmt"
	"math/rand"
	"sync"
	"time"

	"github.com/spf13/viper"
	"gorm.io/gorm"

	"jiraport/pkg/log"
	"jiraport/pkg/matcher"
	eng "jiraport/resolver/engineer"
	"jiraport/service/jira"
	ntf "jiraport/service/notification"
)

type AssignCommand struct {
	db                    *gorm.DB
	jira                  *jira.Jira
	ns                    *ntf.Notifications
	assignMatcher         matcher.Matcher
	needAssign            bool
	lastAssignedTime      map[string]time.Time
	lastAssignedTimeMutex sync.Mutex
}

func NewAssignCommand(
	db *gorm.DB,
	jira *jira.Jira,
	ns *ntf.Notifications) (*AssignCommand, error) {

	needAssign := viper.GetBool("need-assign")

	var assignMatcher matcher.Matcher
	if needAssign {
		err := assignMatcher.LoadFile(viper.GetString("assign-matcher-file"))
		if err != nil {
			return nil, err
		}
		log.Debug("load assign matcher", assignMatcher)
	}

	return &AssignCommand{
		db:               db,
		jira:             jira,
		ns:               ns,
		assignMatcher:    assignMatcher,
		needAssign:       needAssign,
		lastAssignedTime: make(map[string]time.Time),
	}, nil
}

func (cmd *AssignCommand) assign(issue *jira.RawIssue) (*eng.Engineer, error) {
	assignReceivers, err := cmd.assignMatcher.Match(issue)
	if err != nil {
		return nil, fmt.Errorf("assign matching error, %w", err)
	}
	categoryIds, err := assignReceivers.TryToIntSlice()
	if err != nil {
		return nil, err
	}
	if len(categoryIds) == 0 {
		return nil, errors.New("no matched category")
	}

	assigneeCandi, err := eng.GetEngineersByCategoryIDs(cmd.db, 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")
	}

	assignee, err := cmd.selectAssignee(assigneeCandi)
	if err != nil {
		return nil, fmt.Errorf("select assignee error %w", err)
	}
	err = cmd.jira.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 []eng.Engineer) (*eng.Engineer, error) {
	log.Debug("select assignee candi", candi)
	if len(candi) == 0 {
		return nil, errors.New("empty candidate")
	}
	var selected *eng.Engineer
	noExperience := make([]*eng.Engineer, 0)

	cmd.lastAssignedTimeMutex.Lock()
	defer cmd.lastAssignedTimeMutex.Unlock()
	for i := range candi {
		lastAssigneeTime, ok := cmd.lastAssignedTime[candi[i].Email]
		if ok {
			if selected == nil || cmd.lastAssignedTime[selected.Email].After(lastAssigneeTime) {
				selected = &candi[i]
			}
		} else {
			noExperience = append(noExperience, &candi[i])
		}
	}
	if len(noExperience) != 0 {
		selected = noExperience[rand.Intn(len(noExperience))]
	}
	cmd.lastAssignedTime[selected.Email] = time.Now()
	log.Debug("select assignee", selected)
	return selected, nil
}

func (cmd *AssignCommand) Exec(ctx context.Context, actionExecInfo *ntf.NotificationActionExecInfo, args ...string) (bool, 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

	assignee, err := cmd.assign(rawIssue)
	if err != nil {
		return false, err
	}

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

	return true, nil
}
