package notification

import (
	"errors"
	"fmt"
	"os"
	"strings"
	"time"

	"github.com/spf13/viper"
	"gopkg.in/yaml.v3"
	"gorm.io/gorm"
	"gorm.io/gorm/clause"

	"jiraport/pkg/log"
	"jiraport/pkg/queue"
	"jiraport/service/jira"
	"jiraport/service/lark"
)

const (
	InnerUpdatedAssigneeEvent = "InnerUpdatedAssigneeEvent"
)

type TransitionError struct {
	s string
}

func (te *TransitionError) Error() string {
	return te.s
}

type TransitionErrorList struct {
	ErrorList []error
}

func (tel *TransitionErrorList) Error() string {
	ss := make([]string, 0, len(tel.ErrorList))
	for i := range tel.ErrorList {
		errStr := tel.ErrorList[i].Error()
		ss = append(ss, errStr)
	}
	return strings.Join(ss, ";")
}

func HandleTransitionError(err error, f func(*TransitionError) error) error {
	switch err := err.(type) {
	case *TransitionError:
		return f(err)
	case *TransitionErrorList:
		errList := make([]error, 0)
		for _, innerErr := range err.ErrorList {
			outErr := HandleTransitionError(innerErr, f)
			if outErr != nil {
				errList = append(errList, outErr)
			}
		}
		if len(errList) != 0 {
			return &TransitionErrorList{ErrorList: errList}
		}
		return nil
	}
	return err
}

// notifications' data manager
type Notifications struct {
	queue                chan<- queue.Event
	db                   *gorm.DB
	notificationPolicies map[string]NotificationPolicy
}

// create Notifications and migrate types to database
func New(queue chan<- queue.Event, dataDB *gorm.DB) (*Notifications, error) {
	policiesPath := viper.GetString("policy-file")

	notificationPolicies := make(map[string]NotificationPolicy)
	policiesStr, err := os.ReadFile(policiesPath)
	if err != nil {
		return nil, err
	}
	err = yaml.Unmarshal(policiesStr, &notificationPolicies)
	if err != nil {
		return nil, err
	}
	log.Debug("load policy", notificationPolicies)

	err = dataDB.AutoMigrate(&jira.Issue{}, &Notification{}, &NotificationActionExecInfo{}, &Message{})
	if err != nil {
		return nil, err
	}

	return &Notifications{
		queue:                queue,
		db:                   dataDB,
		notificationPolicies: notificationPolicies,
	}, nil
}

func (ns *Notifications) Insert(issue *jira.Issue) error {
	result := ns.db.Select("Key").Where(&jira.Issue{Key: issue.Key}).Find(&jira.Issue{})
	err := result.Error
	if err != nil {
		return err
	}
	if result.RowsAffected > 0 {
		return nil
	}
	return ns.db.Clauses(clause.OnConflict{DoNothing: true}).Create(issue).Error
}

func (ns *Notifications) NewNotification(policy string, issue *jira.Issue) (*Notification, error) {
	if _, ok := ns.notificationPolicies[policy]; !ok {
		return nil, errors.New("no policy found")
	}
	notification := &Notification{
		Policy:     policy,
		State:      Inited,
		UpdateTime: time.Now(),
		IssueKey:   issue.Key,
		Issue:      issue,
	}
	result := ns.db.Clauses(clause.OnConflict{DoNothing: true}).Omit("Issue").Create(notification)
	err := result.Error
	if err != nil {
		return nil, err
	}
	// check if duplicate notification
	if result.RowsAffected == 0 {
		// get exist notification
		err = ns.db.Where(&Notification{Policy: policy, IssueKey: issue.Key}).First(notification).Error
		if err != nil {
			return nil, err
		}
	} else {
		log.Debug("msg", "create notification", "notification", notification)
	}
	return notification, nil
}

func (ns *Notifications) Get(notificationID uint) (*Notification, error) {
	notification := &Notification{ID: notificationID}
	err := ns.db.Joins("Issue").First(notification).Error
	if err != nil {
		return nil, err
	}
	return notification, nil
}

func (ns *Notifications) getByIssueKeyWithTxn(tx *gorm.DB, issueKey string) ([]Notification, error) {
	notifications := make([]Notification, 0)
	err := tx.
		Model(&Notification{}).
		Joins("Issue").
		Where(&Notification{IssueKey: issueKey}).
		Find(&notifications).Error
	if err != nil {
		return nil, err
	}
	return notifications, nil
}

func (ns *Notifications) GetByIssueKey(issueKey string) ([]Notification, error) {
	return ns.getByIssueKeyWithTxn(ns.db, issueKey)
}

// do global transition
func (ns *Notifications) GlobalTransition(issueKey string, transEvent NotificationTransitionEvent) error {
	notifications, err := ns.GetByIssueKey(issueKey)
	if err != nil {
		return err
	}
	if len(notifications) == 0 {
		return nil
	}
	errList := make([]error, 0)
	for _, nf := range notifications {
		err := ns.Transition(nf.ID, transEvent)
		if err != nil {
			errList = append(errList, err)
		}
	}
	if len(errList) != 0 {
		return &TransitionErrorList{ErrorList: errList}
	}
	return nil
}

// do global transition with update
func (ns *Notifications) GlobalTransitionWithUpdate(issue *jira.Issue, transEvent NotificationTransitionEvent) error {
	actionExecInfos := make([]*NotificationActionExecInfo, 0)
	errList := make([]error, 0)
	err := ns.db.Transaction(func(txn *gorm.DB) error {
		// only for lock, discard result
		log.Debug("msg", "lock issue", "issue", issue.Key)
		err := txn.Clauses(clause.Locking{Strength: "UPDATE"}).Select("Key").First(&jira.Issue{Key: issue.Key}).Error
		if err != nil {
			return err
		}

		// update issue
		err = txn.Model(issue).Updates(issue).Error
		if err != nil {
			return err
		}

		notifications, err := ns.getByIssueKeyWithTxn(txn, issue.Key)
		if err != nil {
			return err
		}
		if len(notifications) == 0 {
			return nil
		}

		for _, nf := range notifications {
			actionExecInfo, err := ns.transitionWithTxn(txn, nf.ID, transEvent)
			if err != nil {
				errList = append(errList, err)
				continue
			}
			if actionExecInfo != nil {
				actionExecInfos = append(actionExecInfos, actionExecInfo)
			}
		}
		return nil
	})
	if err != nil {
		return err
	}

	// do actions
	for _, actionExecInfo := range actionExecInfos {
		err = ns.doAction(actionExecInfo)
		if err != nil {
			errList = append(errList, err)
		}
	}

	if len(errList) != 0 {
		return &TransitionErrorList{ErrorList: errList}
	}
	return nil
}

// do transition with lock
func (ns *Notifications) Transition(notificationID uint, transEvent NotificationTransitionEvent) error {
	actionExecInfo, err := ns.transitionWithTxn(ns.db, notificationID, transEvent)
	if err != nil {
		return err
	}
	if actionExecInfo != nil {
		return ns.doAction(actionExecInfo)
	}
	return nil
}

// do transition with transaction and lock
// nil value of *NotificationActionExecInfo is acceptable
func (ns *Notifications) transitionWithTxn(txn *gorm.DB, notificationID uint, transEvent NotificationTransitionEvent) (*NotificationActionExecInfo, error) {
	var action NotificationAction
	nf := &Notification{ID: notificationID}

	// must lock entry to prevent wrong state
	err := txn.Transaction(func(txnInner *gorm.DB) error {
		err := txnInner.Select("IssueKey").First(nf).Error
		if err != nil {
			return err
		}
		log.Debug("msg", "lock issue", "issue", nf.IssueKey)
		err = txnInner.Clauses(clause.Locking{Strength: "UPDATE"}).Select("Key").First(&jira.Issue{Key: nf.IssueKey}).Error
		if err != nil {
			return err
		}
		err = txnInner.Clauses(clause.Locking{Strength: "UPDATE"}).Joins("Issue").First(nf).Error
		if err != nil {
			return err
		}
		policy, ok := ns.notificationPolicies[nf.Policy]
		if !ok {
			return &TransitionError{s: fmt.Sprintf("policy %s not found", nf.Policy)}
		}
		curState, ok := policy[nf.State]
		if !ok {
			return &TransitionError{s: fmt.Sprintf("invalid current state '%s'", nf.State)}
		}
		transEndpoint, ok := curState[transEvent]
		if !ok {
			return &TransitionError{s: fmt.Sprintf(
				"invalid event %s send to policy of '%s' and state of '%s'", transEvent, nf.Policy, nf.State)}
		}
		nf.UpdateTime = time.Now()
		nf.State = transEndpoint.State
		action = transEndpoint.Action

		err = txnInner.
			Model(nf).
			Select("UpdateTime", "State").
			Updates(Notification{UpdateTime: nf.UpdateTime, State: nf.State}).
			Error
		if err != nil {
			return err
		}

		return nil
	})
	if err != nil {
		return nil, err
	}

	if len(action.Cmd) == 0 {
		return nil, nil
	}

	// do action in transition endpoint

	actionExecInfo := &NotificationActionExecInfo{
		Notification:   nf,
		NotificationID: nf.ID,
		Action:         action,
		Done:           0,
		ErrorCount:     0,
	}
	// must omit Notification to prevent creating duplicate entry
	err = txn.Omit("Notification").Create(actionExecInfo).Error
	if err != nil {
		return nil, err
	}
	return actionExecInfo, nil
}

func (ns *Notifications) doAction(actionExecInfo *NotificationActionExecInfo) error {
	if actionExecInfo == nil {
		return nil
	}
	// action will be executed by resolver
	ev := queue.Event{
		Name:    "NotificationAction",
		Payload: actionExecInfo,
	}
	ns.queue <- ev
	return nil
}

// set action finished
func (ns *Notifications) SetFinished(actionExecInfoID uint, status uint) error {
	actionExecInfo := NotificationActionExecInfo{ID: actionExecInfoID, Done: status}
	return ns.db.
		Model(&actionExecInfo).
		Select("Done").
		Updates(&actionExecInfo).
		Error
}

func (ns *Notifications) RetryUnfinishedActions() error {
	actions := make([]NotificationActionExecInfo, 0)
	err := ns.db.
		Where("done = ?", 0).
		Joins("Notification").
		Joins("Issue").
		First(&actions).
		Error
	if err != nil {
		return fmt.Errorf("get unfinished actions error %w", err)
	}
	for _, action := range actions {
		log.Debug("msg", "retry action", "action", action)
		ev := queue.Event{
			Name:    "NotificationAction",
			Payload: action,
		}
		ns.queue <- ev
	}
	return nil
}

func (ns *Notifications) UpdateAssignee(issueKey string, assigneeEmail string) error {
	issue := jira.Issue{Key: issueKey, AssigneeEmail: assigneeEmail}
	err := ns.GlobalTransitionWithUpdate(&issue, InnerUpdatedAssigneeEvent)
	err = HandleTransitionError(err, func(te *TransitionError) error {
		log.Warn("msg", "do global transition error part", "err", te)
		return nil
	})
	return err
}

func (ns *Notifications) AddMessage(notificationID uint, msgIndex *lark.MessageIndex) error {
	msg := Message{
		NotificationID: notificationID,
		MessageIndex:   msgIndex,
	}
	err := ns.db.Omit("Notification").Create(&msg).Error
	if err != nil {
		return err
	}
	return nil
}

func (ns *Notifications) GetMessageShallow(msgIndex *lark.MessageIndex) (*Message, error) {
	var msg Message
	err := ns.db.
		Where(&Message{MessageIndex: msgIndex}).
		Order("id DESC").
		First(&msg).
		Error
	if err != nil {
		return nil, err
	}
	return &msg, nil
}
