package db

import (
	"errors"
	"fmt"
	"log"
	"time"

	"github.com/tidbcloud/jiraport/pkg/config"
	"github.com/tidbcloud/jiraport/pkg/entity"

	"gorm.io/gorm"
	"gorm.io/gorm/clause"
)

func (c *Client) CreateNotification(policy string, issue *entity.Issue) error {
	if _, ok := c.notificationPolicies[policy]; !ok {
		return errors.New("no policy found")
	}
	notification := &entity.Notification{
		Policy:     policy,
		State:      entity.Inited,
		UpdateTime: time.Now(),
		IssueKey:   issue.Key,
		Issue:      issue,
	}
	result := c.dataDB.Clauses(clause.OnConflict{DoNothing: true}).Omit("Issue").Create(notification)
	err := result.Error
	if err != nil {
		return err
	}
	return nil
}

func (c *Client) GetNotification(notificationID uint) (*entity.Notification, error) {
	notification := &entity.Notification{ID: notificationID}
	err := c.dataDB.Joins("Issue").First(notification).Error
	if err != nil {
		return nil, err
	}
	return notification, nil
}

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

func (c *Client) ListNotificationsByIssueKey(issueKey string) ([]entity.Notification, error) {
	return c.getByIssueKeyWithTxn(c.dataDB, issueKey)
}

// do global transition with update
func (c *Client) GlobalTransitionWithUpdate(
	issue *entity.Issue,
	transEvent config.NotificationTransitionEvent,
) ([]*entity.NotificationActionExecInfo, error) {
	actionExecInfos := make([]*entity.NotificationActionExecInfo, 0)
	errList := make([]error, 0)
	err := c.dataDB.Transaction(func(txn *gorm.DB) error {
		// only for lock, discard result
		err := txn.Clauses(clause.Locking{Strength: "UPDATE"}).Select("Key").First(&entity.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 := c.getByIssueKeyWithTxn(txn, issue.Key)
		if err != nil {
			return err
		}
		if len(notifications) == 0 {
			return nil
		}

		for _, nf := range notifications {
			actionExecInfo, err := c.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 {
		errList = append(errList, err)
		return nil, &TransitionErrorList{ErrorList: errList}
	}
	return actionExecInfos, nil
}

// do transition with lock
func (c *Client) Transition(
	notificationID uint,
	transEvent config.NotificationTransitionEvent,
) (*entity.NotificationActionExecInfo, error) {
	actionExecInfo, err := c.transitionWithTxn(c.dataDB, notificationID, transEvent)
	if err != nil {
		return nil, err
	}
	return actionExecInfo, nil
}

// do transition with transaction and lock
// nil value of *NotificationActionExecInfo is acceptable
func (c *Client) transitionWithTxn(
	txn *gorm.DB,
	notificationID uint,
	transEvent config.NotificationTransitionEvent,
) (*entity.NotificationActionExecInfo, error) {
	var action entity.NotificationAction
	nf := &entity.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
		}
		err = txnInner.Clauses(clause.Locking{Strength: "UPDATE"}).Select("Key").First(&entity.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 := c.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)}
		}
		// *: all events received
		valid := false
		general, ok := policy["General"]
		if ok {
			generalEndpoint, ok := general[transEvent]
			if ok {
				valid = true
				nf.UpdateTime = time.Now()
				nf.State = generalEndpoint.State
				action = generalEndpoint.Action
			}
		}
		transEndpoint, ok := curState[transEvent]
		if ok {
			valid = true
			nf.UpdateTime = time.Now()
			nf.State = transEndpoint.State
			action = transEndpoint.Action
		}
		if !valid {
			return &TransitionError{s: fmt.Sprintf(
				"invalid event %s send to policy of '%s' and state of '%s'", transEvent, nf.Policy, nf.State)}
		}
		log.Printf("notification %d transition to state '%s' with event '%s'", nf.ID, nf.State, transEvent)

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

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

	// do action in transition endpoint
	actionExecInfo := &entity.NotificationActionExecInfo{
		Notification:   nf,
		NotificationID: nf.ID,
		Action:         action,
		Done:           entity.ActionStatusPending,
		CreatedTime:    time.Now(),
	}
	// must omit Notification to prevent creating duplicate entry
	err = txn.Omit("Notification", "FinishedTime").Create(actionExecInfo).Error
	if err != nil {
		return nil, err
	}
	return actionExecInfo, nil
}
