package action

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

	"github.com/tidbcloud/jiraport/pkg/clients/clinic"
	"github.com/tidbcloud/jiraport/pkg/clients/engineer"
	"github.com/tidbcloud/jiraport/pkg/clients/jira"
	"github.com/tidbcloud/jiraport/pkg/clients/lark"
	"github.com/tidbcloud/jiraport/pkg/entity"
	"github.com/tidbcloud/jiraport/pkg/matcher"
	"github.com/tidbcloud/jiraport/pkg/resolver/handler/action/render"

	"github.com/spf13/viper"
)

type FillFBOwnerCommand struct {
	logger                *slog.Logger
	jiraCient             jira.Interface
	engineerClient        engineer.Interface
	larkClient            lark.Interface
	render                *render.Render
	fbOwnerMatcher        *matcher.Matcher
	lastAssignedTime      map[string]time.Time
	lastAssignedTimeMutex sync.Mutex
}

func NewFillFBOwnerCommand(
	logger *slog.Logger,
	jiraClient jira.Interface,
	larkClient lark.Interface,
	clinicClient clinic.Interface,
	engineerClient engineer.Interface,
) (*FillFBOwnerCommand, error) {
	fbOwnerMatcher, err := matcher.NewMatcher(
		logger, viper.GetString("fbowner-matcher-file"))
	if err != nil {
		return nil, err
	}

	r, err := render.New(clinicClient, engineerClient)
	if err != nil {
		return nil, err
	}

	return &FillFBOwnerCommand{
		logger:           logger,
		jiraCient:        jiraClient,
		larkClient:       larkClient,
		engineerClient:   engineerClient,
		render:           r,
		fbOwnerMatcher:   fbOwnerMatcher,
		lastAssignedTime: make(map[string]time.Time),
	}, nil
}

func (cmd *FillFBOwnerCommand) assignFBOwner(issue *entity.RawIssue) (*engineer.Engineer, error) {
	fbOwnerReceivers, err := cmd.fbOwnerMatcher.Match(issue)
	if err != nil {
		return nil, fmt.Errorf("assign matching error, %w", err)
	}
	fbOwners, err := fbOwnerReceivers.TryToStringSlice()
	if err != nil {
		return nil, err
	}
	if len(fbOwners) == 0 {
		return nil, errors.New("no matched category")
	}

	candidates := make([]engineer.Engineer, 0)
	for _, fbOwnerEmail := range fbOwners {
		if fbOwnerEmail == "firemaster" {
			candidateSet, err := cmd.engineerClient.GetFiremasters()
			if err != nil {
				cmd.logger.Error("get firemaster error", "err", err, "issue", issue.Key)
			} else {
				candidates = append(candidates, candidateSet...)
			}
		} else {
			candidate, err := cmd.engineerClient.GetEngineerByEmail(fbOwnerEmail)
			if err != nil {
				cmd.logger.Warn("get engineer by email error", "err", err, "email", fbOwnerEmail)
			} else {
				candidates = append(candidates, *candidate)
			}
		}
	}

	if len(candidates) == 0 {
		return nil, errors.New("no fb owner")
	}

	assignee, err := cmd.selectFBOwner(candidates)
	if err != nil {
		return nil, fmt.Errorf("select fb owner error %w", err)
	}
	err = cmd.jiraCient.AssignFBOwner(issue.Key, assignee.Email)
	if err != nil {
		return nil, fmt.Errorf("assign fb owner error %w", err)
	}
	return assignee, nil
}

// select assignee, will store last assigned time of engineer
func (cmd *FillFBOwnerCommand) selectFBOwner(candi []engineer.Engineer) (*engineer.Engineer, error) {
	if len(candi) == 0 {
		return nil, errors.New("empty candidate")
	}
	var selected *engineer.Engineer
	noExperience := make([]*engineer.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 {
		n, err := rand.Int(rand.Reader, big.NewInt(int64(len(noExperience))))
		if err != nil {
			return nil, err
		}
		selected = noExperience[n.Int64()]
	}
	cmd.lastAssignedTime[selected.Email] = time.Now()
	return selected, nil
}

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

	assignee, err := cmd.assignFBOwner(rawIssue)
	if err != nil {
		return false, "", err
	}
	msg := fmt.Sprintf("fb owner assign to %s(%s)", assignee.Name, assignee.Email)

	// TODO: Replace this with a more elegant implementation
	if len(args) > 0 {
		templateName := args[0]

		data := make(map[string]interface{})
		data["Issue"] = actionExecInfo.Notification.Issue

		// render message content
		title, text, color, err := cmd.render.Render(templateName, data)
		if err != nil {
			return false, "", fmt.Errorf("render error, %w", err)
		}
		message, err := newMessage("card", title, text, color, assignee, "default")
		if err != nil {
			return false, "", fmt.Errorf("generate message error for fb owner, %w", err)
		}
		if _, err := cmd.larkClient.Notify(message); err != nil {
			cmd.logger.Error("lark notify failed", "err", err, "msg", message, "issue", rawIssue.Key)
		}
	}
	return true, msg, nil
}
