package creditriskcontrol

import (
	"errors"
	"fmt"
	"time"

	"github.com/google/uuid"
	"github.com/pingcap/dbaas/billing2/billing_errors"
	"github.com/pingcap/dbaas/billing2/db"
	"github.com/pingcap/dbaas/billing2/db/entity"
	"github.com/pingcap/dbaas/billing2/forms"
	"github.com/pingcap/dbaas/billing2/utils/errorutils"
	"github.com/pingcap/dbaas/billing2/utils/log"
	"github.com/pingcap/dbaas/billing2/utils/timeutils"
	"go.uber.org/zap"
	"gorm.io/gorm"
	"gorm.io/gorm/clause"
)

type TenantCloseModel struct{}

func (m *TenantCloseModel) ListCloseReasons() ([]string, error) {
	var ret []string
	err := db.GetDB().Model(&entity.TenantClosePolicy{}).Distinct("reason").Select("reason").Order("reason asc").Scan(&ret).Error
	return ret, err
}

func (m *TenantCloseModel) CreateTenantRiskClosePolicy(creditRiskLevelID uint, policy *forms.TenantClosePolicy) error {
	policy.Reason = entity.TenantCloseReason_Risky
	policy.CreditRiskLevelID = creditRiskLevelID
	_, err := m.CreateTenantClosePolicy(policy)
	return err
}

func (m *TenantCloseModel) CreateTenantClosePolicy(policy *forms.TenantClosePolicy) (*forms.TenantClosePolicy, error) {
	err := validatePolicy(&policy.ClosePolicy)
	if err != nil {
		return nil, err
	}

	if policy.CreditRiskLevelID != 0 {
		_, err = (&CreditRiskLevelModel{}).GetCreditRiskLevel(policy.CreditRiskLevelID)
		if err != nil {
			return nil, err
		}
		_, err = m.GetTenantClosePolicyByCreditRiskLevel(policy.CreditRiskLevelID)
		if err == nil || (!errors.Is(err, billing_errors.ObjectNotExistsError)) {
			return nil, billing_errors.NewObjectAlreadyExistsError(fmt.Errorf("tenant credit close policy, risk level id: %d", policy.CreditRiskLevelID))
		}
	}

	if policy.ClosePolicy.Actions == nil {
		policy.ClosePolicy.Actions = []*forms.CloseAction{}
	}

	policyEntity := entity.TenantClosePolicy{
		Reason:            policy.Reason,
		CreditRiskLevelID: policy.CreditRiskLevelID,
		ClosePolicy:       entity.ClosePolicy(policy.ClosePolicy),
	}
	err = policyEntity.Create(db.GetDB())
	if err != nil {
		return nil, err
	}
	return m.GetTenantClosePolicy(policy.Reason, policy.CreditRiskLevelID)
}

func validatePolicy(policy *forms.ClosePolicy) error {
	return nil
}

func (*TenantCloseModel) UpdateTenantRiskClosePolicy(creditRiskLevelID uint, policy *forms.TenantClosePolicy) error {
	err := validatePolicy(&policy.ClosePolicy)
	if err != nil {
		return err
	}
	err = entity.NewTenantClosePolicyQuerySet(db.GetDB()).CreditRiskLevelIDEq(creditRiskLevelID).ReasonEq(entity.TenantCloseReason_Risky).GetUpdater().SetClosePolicy(entity.ClosePolicy(policy.ClosePolicy)).Update()
	if err != nil {
		return err
	}

	log.Info("updated credit risk level policy", zap.Uint("creditRiskLevelID", creditRiskLevelID))

	return nil
}

func policyFromEntity(entity *entity.TenantClosePolicy) *forms.TenantClosePolicy {
	return &forms.TenantClosePolicy{
		PolicyID:          entity.ID,
		Reason:            entity.Reason,
		CreditRiskLevelID: entity.CreditRiskLevelID,
		ClosePolicy:       forms.ClosePolicy(entity.ClosePolicy),
	}
}

func (m *TenantCloseModel) GetTenantClosePolicyByCreditRiskLevel(creditRiskLevelID uint) (*forms.TenantClosePolicy, error) {
	policyEntity := &entity.TenantClosePolicy{}
	err := errorutils.WrapSQLError(entity.NewTenantClosePolicyQuerySet(db.GetDB()).ReasonEq(entity.TenantCloseReason_Risky).CreditRiskLevelIDEq(creditRiskLevelID).One(policyEntity), fmt.Sprintf("tenant credit close policy, risk level id: %d", creditRiskLevelID))
	if err != nil {
		return nil, err
	}
	return policyFromEntity(policyEntity), nil
}

func (m *TenantCloseModel) GetTenantRiskClosePolicyByTenantID(tenantID uint) (*forms.TenantClosePolicy, error) {
	level, err := (&CreditRiskLevelModel{}).GetTenantCreditRiskLevel(tenantID)
	if err != nil {
		return nil, err
	}
	return m.GetTenantClosePolicyByCreditRiskLevel(level.CreditRiskLevelID)
}

func (m *TenantCloseModel) GetTenantClosePolicyByTenantID(tenantID uint, reason string) (*forms.TenantClosePolicy, error) {
	level, err := (&CreditRiskLevelModel{}).GetTenantCreditRiskLevel(tenantID)
	if err != nil {
		return nil, err
	}
	return m.GetTenantClosePolicy(reason, level.CreditRiskLevelID)
}

func (m *TenantCloseModel) GetTenantClosePolicy(reason string, creditRiskLevelID uint) (*forms.TenantClosePolicy, error) {
	var entities []entity.TenantClosePolicy
	err := entity.NewTenantClosePolicyQuerySet(db.GetDB()).ReasonEq(reason).All(&entities)
	if err != nil {
		return nil, err
	}
	for _, ent := range entities {
		if ent.CreditRiskLevelID == 0 || ent.CreditRiskLevelID == creditRiskLevelID {
			return policyFromEntity(&ent), nil
		}
	}
	err = billing_errors.NewInvalidInputError(errors.New("unknown reason or unmatched credit level"))
	log.Error("failed to retrieve close policy", zap.Error(err), zap.String("reason", reason))
	return nil, err
}

func (m *TenantCloseModel) DeleteTenantRiskClosePolicy(creditRiskLevelID uint) error {
	policy, err := m.GetTenantClosePolicy(entity.TenantCloseReason_Risky, creditRiskLevelID)
	if err != nil {
		return err
	}
	return m.DeleteTenantClosePolicy(policy.CreditRiskLevelID)
}
func (*TenantCloseModel) DeleteTenantClosePolicy(policyID uint) error {
	err := db.GetDB().Transaction(func(tx *gorm.DB) error {
		var ent entity.TenantClosePolicy
		err := entity.NewTenantClosePolicyQuerySet(tx.Clauses(clause.Locking{Strength: "UPDATE"})).IDEq(policyID).One(&ent)
		if err != nil {
			return err
		}
		err = entity.NewTenantClosePolicyQuerySet(tx).IDEq(policyID).GetUpdater().SetReason(fmt.Sprintf("deleted-%s-%s", uuid.NewString(), ent.Reason)).Update()
		if err != nil {
			return err
		}
		err = entity.NewTenantClosePolicyQuerySet(tx).IDEq(policyID).Delete()
		if err != nil {
			return err
		}
		return nil
	})
	if err != nil {
		return err
	}
	return nil
}

func (m *TenantCloseModel) RunTenantClose(tenantID uint, reason string) error {
	info, err := m.GetTenantCloseInfo(tenantID)
	if err != nil {
		if !errors.Is(err, billing_errors.ObjectNotExistsError) {
			log.Error("failed to run tenant credit close", zap.Uint("tenantID", tenantID), zap.Error(err))
			return err
		}
	} else {
		if info.Status == entity.TenantCloseStatus_DONE || info.Status == entity.TenantCloseStatus_CANCELED {
			err = m.DeleteTenantCloseRecord(tenantID)
			if err != nil {
				log.Error("failed to run tenant credit close", zap.Uint("tenantID", tenantID), zap.Error(err))
				return err
			}
		} else {
			err = billing_errors.NewObjectAlreadyExistsError(fmt.Errorf("can not run tenant credit close when already has one actived"))
			log.Error("can not run tenant credit close when already has one actived", zap.Uint("tenantID", tenantID), zap.Error(err))
			return err
		}
	}

	policyInfo, err := m.GetTenantClosePolicyByTenantID(tenantID, reason)
	if err != nil {
		log.Error("failed to run tenant credit close", zap.Uint("tenantID", tenantID), zap.Error(err))
		return err
	}

	recordEntity := &entity.TenantCloseRecord{
		TenantID:          tenantID,
		Status:            entity.TenantCloseStatus_RUNNING,
		Reason:            reason,
		CreditRiskLevelID: policyInfo.CreditRiskLevelID,
		CloseActionsStatus: entity.CloseActionsStatus{
			Status: forms.CloseActionStatusInner{},
		},
		ClosePolicy: entity.ClosePolicy(policyInfo.ClosePolicy),
		StartedAt:   timeutils.GetNow(),
	}

	err = recordEntity.Create(db.GetDB())
	if err != nil {
		log.Error("failed to run tenant credit close", zap.Uint("tenantID", tenantID), zap.Error(err))
		return err
	}

	log.Info("success run tenant credit close", zap.Uint("tenantID", tenantID))

	return nil
}

func (m *TenantCloseModel) CancelTenantClose(tenantID uint) error {
	info, err := m.GetTenantCloseInfo(tenantID)
	if err != nil {
		log.Error("failed to run tenant credit close cancel", zap.Uint("tenantID", tenantID), zap.Error(err))
		return err
	}
	if info.Status == entity.TenantCloseStatus_DONE || info.Status == entity.TenantCloseStatus_CANCELED {
		err = billing_errors.NewInvalidStatusError(fmt.Errorf("can not cancel tenant credit close when status is not running"))
		log.Error("can not cancel tenant credit close when status is not running", zap.Uint("tenantID", tenantID), zap.Error(err))
		return err
	}

	err = entity.NewTenantCloseRecordQuerySet(db.GetDB()).TenantIDEq(tenantID).GetUpdater().SetStatus(entity.TenantCloseStatus_CANCELING).Update()
	if err != nil {
		log.Error("failed to run tenant credit close cancel", zap.Uint("tenantID", tenantID), zap.Error(err))
		return err
	}
	log.Info("success run tenant credit close cancel", zap.Uint("tenantID", tenantID))

	return nil
}

func (*TenantCloseModel) GetTenantCloseInfo(tenantID uint) (*forms.TenantCloseInfo, error) {
	recordEntity := entity.TenantCloseRecord{}
	err := errorutils.WrapSQLError(entity.NewTenantCloseRecordQuerySet(db.GetDB()).TenantIDEq(tenantID).One(&recordEntity), fmt.Sprintf("tenant credit close info %d", tenantID))
	if err != nil {
		return nil, err
	}
	return &forms.TenantCloseInfo{
		TenantID:           tenantID,
		CreditRiskLevelID:  recordEntity.CreditRiskLevelID,
		ClosePolicy:        forms.ClosePolicy(recordEntity.ClosePolicy),
		Status:             recordEntity.Status,
		CloseActionsStatus: forms.CloseActionsStatus(recordEntity.CloseActionsStatus),
		StartedAt:          recordEntity.StartedAt,
		ClosedAt:           recordEntity.ClosedAt,
		Reason:             recordEntity.Reason,
	}, nil
}

func (*TenantCloseModel) DeleteTenantCloseRecord(tenantID uint) error {
	err := entity.NewTenantCloseRecordQuerySet(db.GetDB()).TenantIDEq(tenantID).Delete()
	if err != nil {
		log.Error("failed delete tenant credit close record", zap.Uint("tenantID", tenantID), zap.Error(err))
		return err
	}
	log.Info("success delete tenant credit close record", zap.Uint("tenantID", tenantID))
	return nil
}

type ActionParams struct {
	CreditRiskClosedDate time.Time
}

type ActionRunner func(tenantID uint, params *ActionParams) error

var actionsRunner map[string]ActionRunner = map[string]ActionRunner{}

func RegisterCloseAction(action string, runner ActionRunner) {
	actionsRunner[action] = runner
}

func init() {

}

func (m *TenantCloseModel) RunAction(tenantID uint, action *forms.CloseAction, closeInfo *forms.TenantCloseInfo) error {
	if closeInfo.CloseActionsStatus.Status[action.Action] == entity.TenantCloseStatus_DONE {
		return nil
	}

	now := timeutils.GetNow()
	if closeInfo.StartedAt.Add(time.Duration(action.ActionTimeInHour) * time.Hour).After(now) {
		return nil
	}

	runner := actionsRunner[action.Action]
	if runner == nil {
		err := billing_errors.NewSystemError(fmt.Errorf("not found action runner %s", action.Action))
		log.Error("failed run action", zap.Uint("tenantID", tenantID), zap.Error(err))
		return err
	}

	lastActionInHour := uint(0)
	for _, action := range closeInfo.ClosePolicy.Actions {
		if action.ActionTimeInHour > lastActionInHour {
			lastActionInHour = action.ActionTimeInHour
		}
	}

	parmas := &ActionParams{
		CreditRiskClosedDate: closeInfo.StartedAt.Add(time.Duration(lastActionInHour) * time.Hour),
	}

	log.Info("start run action", zap.Uint("tenantID", tenantID), zap.String("action", action.Action))
	err := runner(tenantID, parmas)
	if err != nil {
		log.Error("failed run action", zap.Uint("tenantID", tenantID), zap.String("action", action.Action), zap.Error(err))
		return err
	}
	log.Info("success run action", zap.Uint("tenantID", tenantID), zap.String("action", action.Action))

	closeInfo.CloseActionsStatus.Status[action.Action] = entity.TenantCloseStatus_DONE
	err = entity.NewTenantCloseRecordQuerySet(db.GetDB()).TenantIDEq(tenantID).GetUpdater().SetCloseActionsStatus(entity.CloseActionsStatus(closeInfo.CloseActionsStatus)).Update()
	if err != nil {
		err := err
		log.Error("failed update record", zap.Uint("tenantID", tenantID), zap.Error(err))
		return err
	}

	log.Info("success run action", zap.Uint("tenantID", tenantID))

	return nil
}

func (m *TenantCloseModel) ProcessTenantClose(tenantID uint) error {
	info, err := m.GetTenantCloseInfo(tenantID)
	if err != nil {
		log.Error("failed to run tenant credit close progress", zap.Uint("tenantID", tenantID), zap.Error(err))
		return err
	}
	if info.Status == entity.TenantCloseStatus_DONE || info.Status == entity.TenantCloseStatus_CANCELED {
		return nil
	}

	now := timeutils.GetNow()

	if info.Status == entity.TenantCloseStatus_CANCELING {
		if info.ClosePolicy.CancelAction != nil {
			err = m.RunAction(tenantID, info.ClosePolicy.CancelAction, info)
			if err != nil {
				log.Error("failed to run tenant credit close progress", zap.Uint("tenantID", tenantID), zap.Error(err))
				return err
			}
		}
		err = entity.NewTenantCloseRecordQuerySet(db.GetDB()).TenantIDEq(tenantID).GetUpdater().SetStatus(entity.TenantCloseStatus_CANCELED).SetClosedAt(&now).Update()
		if err != nil {
			log.Error("failed to run tenant credit close progress", zap.Uint("tenantID", tenantID), zap.Error(err))
			return err
		}
		log.Info("success to run tenant credit close progress, status closed", zap.Uint("tenantID", tenantID))
		return nil
	}

	if info.Status == entity.TenantCloseStatus_RUNNING {
		for _, action := range info.ClosePolicy.Actions {
			if info.StartedAt.Add(time.Duration(action.ActionTimeInHour) * time.Hour).After(now) {
				log.Info("not match time action", zap.Uint("tenantID", tenantID), zap.String("action", action.Action))
				return nil
			}
			if info.CloseActionsStatus.Status[action.Action] == entity.TenantCloseStatus_DONE {
				continue
			}
			err = m.RunAction(tenantID, action, info)
			if err != nil {
				log.Error("failed to run action", zap.Uint("tenantID", tenantID), zap.Error(err))
				return err
			}
		}
		err = entity.NewTenantCloseRecordQuerySet(db.GetDB()).TenantIDEq(tenantID).GetUpdater().SetStatus(entity.TenantCloseStatus_DONE).SetClosedAt(&now).Update()
		if err != nil {
			log.Error("failed to run tenant credit close progress", zap.Uint("tenantID", tenantID), zap.Error(err))
			return err
		}
		log.Info("success to run tenant credit close progress, status done", zap.Uint("tenantID", tenantID))
		return nil
	}
	return nil
}

func (m *TenantCloseModel) GetAllCreditCloseRunningTenantIDs() ([]uint, error) {
	entities := []entity.TenantCloseRecord{}
	err := entity.NewTenantCloseRecordQuerySet(db.GetDB()).StatusIn(entity.TenantCloseStatus_RUNNING, entity.TenantCloseStatus_CANCELING).OrderAscByStartedAt().All(&entities)
	if err != nil {
		return nil, err
	}
	result := []uint{}
	for _, recordEntity := range entities {
		result = append(result, recordEntity.TenantID)
	}
	return result, nil
}
