package billing

import (
	"context"
	"fmt"
	"strconv"
	"time"

	"github.com/pingcap/dbaas/central/config"
	"github.com/pingcap/dbaas/central/db"
	"github.com/pingcap/dbaas/central/db/dbutil"
	"github.com/pingcap/dbaas/central/db/entity"
	"github.com/pingcap/dbaas/central/handler/billing2"
	"github.com/pingcap/dbaas/central/model/util"
	"github.com/pingcap/dbaas/central/utils"
	"github.com/pingcap/errcode"
	"github.com/pingcap/errors"
	"github.com/tidbcloud/idl/pbgen/proto/central"
	"github.com/tidbcloud/pkgv2/log"
	"go.uber.org/zap"
	"gorm.io/gorm"
)

var (
	PocReminderFreePoints1000       int64 = 1000 * 100 // since our unit is penny, the same below
	PoCReminderFreePoints750        int64 = 750 * 100
	PoCReminderFreePoints500        int64 = 500 * 100
	PoCReminderFreePoints250        int64 = 250 * 100
	PoCReminderFreePointsThresholds       = []int64{PoCReminderFreePoints250, PoCReminderFreePoints500, PoCReminderFreePoints750, PocReminderFreePoints1000}
)

func (c Context) bindPOCPackage(t *entity.Tenant) error {
	bindedPackageID, err := billing2.GetHandler().BindPackage(t.ID.Uint64(), config.Global().BillingConfig.BillingPackageIDs.Poc)
	if err != nil {
		c.Log().Error("failed bind poc package", zap.String("email", t.Email), zap.Error(err))
		return err
	}
	c.Log().Info("successfully bind poc package", zap.String("email", t.Email))
	t.Plan = entity.TenantPlanPOC
	t.POCBindedPackageID = bindedPackageID

	err = db.Transaction(func(tx *gorm.DB) error {
		err = t.Update(tx, entity.TenantDBSchema.Plan, entity.TenantDBSchema.POCBindedPackageID)
		if err != nil {
			return err
		}
		if err := dbutil.AddPOCTrialRecord(context.Background(), tx, *t); err != nil {
			return errors.Annotate(err, "failed to add trial record for tenant")
		}
		return nil
	})
	if err != nil {
		c.Log().Error("failed update entity poc", zap.String("email", t.Email), zap.Error(err))
		return err
	}
	c.Log().Info("successfully update entity poc", zap.String("email", t.Email))

	return nil
}

func (c Context) resetPOCTrialEndedAt(t *entity.Tenant) error {
	ctx := context.Background()
	dbProvider := db.NewProvider()
	tx := dbProvider.WriteDB(ctx)
	if err := dbutil.ResetPOCTrialEndedAt(ctx, tx, *t); err != nil {
		log.Error(ctx, "failed to reset PoC trial endedAt", zap.Uint64("tenantID", t.ID.Uint64()), zap.Error(err))
		return err
	}
	c.Log().Info("PoC Trial endedAt reset, done")
	return nil
}

func (c Context) EnableOrgPOCPlan() error {
	t, err := c.GetTenantEntity()
	if err != nil {
		return err
	}

	if t.Plan != entity.TenantPlanNewUser && t.Plan != entity.TenantPlanFreeTrial && t.Plan != entity.TenantPlanPOC {
		return errcode.NewInvalidInputErr(errors.New("only allow this API to update POC status of NewUser or FreeTrial organization"))
	}

	if t.Plan != entity.TenantPlanPOC {
		err = c.bindPOCPackage(t)
		if err != nil {
			return err
		}
	} else {
		err = c.resetPOCTrialEndedAt(t)
		if err != nil {
			return err
		}
	}

	err = c.RefreshPoCReminders()
	if err != nil {
		return errors.Annotate(err, "failed to refresh poc will end reminders")
	}
	return nil
}

func (c Context) DisableOrgPOCPlan() error {
	ctx := context.Background()
	enable, err := c.IsPOCEnabled()
	if err != nil {
		return err
	}
	if !enable {
		c.Log().Info("POC already disabled")
		return nil
	}

	t, err := c.GetTenantEntity()
	if err != nil {
		return err
	}
	dbProvider := db.NewProvider()
	tx := dbProvider.WriteDB(ctx)
	err = dbutil.MarkPOCTrialAsEnd(ctx, tx, *t)
	if err != nil {
		c.Log().Error("failed to mark poc trial as end", zap.Uint64("tenantID", t.ID.Uint64()), zap.Error(err))
		return errors.Annotate(err, "failed to mark poc trial as end")
	}
	err = c.sendPOCEndEmail(t)
	if err != nil {
		c.Log().Error("failed to send poc end email", zap.Uint64("tenantID", t.ID.Uint64()), zap.Error(err))
		return err
	}
	return nil
}

func (c Context) IsPOCEnabled() (bool, error) {
	ctx := context.Background()
	dbProvider := db.NewProvider()
	t, err := c.GetTenantEntity()
	if err != nil {
		return false, err
	}

	if t.Plan != entity.TenantPlanPOC {
		return false, nil
	}

	tx := dbProvider.WriteDB(ctx)

	trialEndedAt, err := dbutil.GetPOCTrialEndedAt(ctx, tx, t.ID)
	if err != nil {
		return false, errors.Annotate(err, "failed to get poc trial end at")
	}
	return trialEndedAt == nil, nil
}

type AddPOCCreditReq struct {
	TenantID     uint64
	CreditAmount int64
	Description  string
}

func (c Context) AddPOCCredits(req *AddPOCCreditReq) error {
	// now action is if poc credits used and close the poc status, will be reactive after add credits
	t, err := c.GetTenantEntity()
	if err != nil {
		return err
	}

	if !(t.Plan == entity.TenantPlanPOC) {
		return errors.NotValidf("can not add credit from poc disabled organization, tenant is %s", t.Name)
	}

	chargeCount := utils.IntToDecimal(req.CreditAmount)

	err = billing2.GetHandler().ChargeCredits(t.ID.Uint64(), t.POCBindedPackageID, &billing2.CreditsChargeInfoInput{
		ChargeCount: chargeCount,
	})
	if err != nil {
		c.Log().Info("failed add credits to poc package", zap.String("email", t.Email), zap.Time("time", time.Now().UTC()), zap.Error(err))
		return err
	}
	c.Log().Info("success add credits to poc package", zap.String("email", t.Email), zap.Time("time", time.Now().UTC()))

	err = c.EnableOrgPOCPlan()
	if err != nil {
		return err
	}

	vars := map[string]string{
		"amount": chargeCount.StringFixed(2),
	}
	err = c.Model.mail.SendMailWithTemplate(context.Background(), []string{t.Email}, config.EmailSubjectNotification, config.TNCreditPointsAdded, vars, nil)
	if err != nil {
		c.Log().Error("failed to send add credit email", zap.String("email", t.Email), zap.Time("time", time.Now().UTC()), zap.Error(err))
		return err
	}

	c.Log().Info("send add credit email", zap.String("email", t.Email), zap.Time("time", time.Now().UTC()))
	return nil
}

func (c Context) GetPOCCredits() (int64, error) {
	t, err := c.GetTenantEntity()
	if err != nil {
		return 0, err
	}
	if !(t.Plan == entity.TenantPlanPOC) {
		return 0, errors.NotValidf("can not get credit from poc disabled organization, tenant is %s", t.Name)
	}

	credits, err := billing2.GetHandler().GetCredits(t.ID.Uint64(), t.POCBindedPackageID)

	if err != nil {
		return 0, err
	}

	return utils.DecimalToInt(*credits), nil
}

func (c Context) GetPOCCreditsHistory() (*central.CreditsHistory, error) {
	t, err := c.GetTenantEntity()
	if err != nil {
		return nil, err
	}
	if !(t.Plan == entity.TenantPlanPOC) {
		return &central.CreditsHistory{
			Amount:  0,
			History: []*central.Credits{},
		}, nil
	}

	credits, err := billing2.GetHandler().GetCreditsHistory(t.ID.Uint64(), t.POCBindedPackageID)
	if err != nil {
		return nil, err
	}

	output := &central.CreditsHistory{
		Amount:  utils.DecimalToInt(credits.TotalAmounts),
		History: make([]*central.Credits, len(credits.History)),
	}

	for i, history := range credits.History {
		output.History[i] = &central.Credits{}
		output.History[i].Amount = utils.DecimalToInt(history.ChargeCount)
		output.History[i].Created = history.ChargeTime.Unix()
	}

	return output, nil
}

func (c Context) RefreshPoCReminders() error {
	ctx := context.Background()
	notificationMgr := util.NewNotificationMgr()
	var err error
	dbProvider := db.NewProvider()
	tx := dbProvider.WriteDB(ctx)

	tenantID := c.tenantAndUser.TenantID
	currentPoints, err := c.GetPOCCredits()
	if err != nil {
		return err
	}

	if err = notificationMgr.ClearNotificationIdentifier(ctx, tx, tenantID, entity.NotificationScenePocClusterEnd, string(entity.NotificationScenePocClusterEnd)); err != nil {
		log.Error(ctx, "failed to clear notification", zap.Uint64("tenantID", tenantID.Uint64()), zap.Error(err), zap.String("scene", string(entity.NotificationScenePocClusterEnd)))
		return err
	}
	if err = notificationMgr.ClearNotificationIdentifier(ctx, tx, tenantID, entity.NotificationScenePocEndBackupWillDelete, string(entity.NotificationScenePocEndBackupWillDelete)); err != nil {
		log.Error(ctx, "failed to clear notification", zap.Uint64("tenantID", tenantID.Uint64()), zap.Error(err), zap.String("scene", string(entity.NotificationScenePocEndBackupWillDelete)))
		return err
	}
	if err = notificationMgr.ClearNotificationIdentifier(ctx, tx, tenantID, entity.NotificationScenePocClusterWillEndIn3Days, string(entity.NotificationScenePocClusterWillEndIn3Days)); err != nil {
		log.Error(ctx, "failed to clear notification", zap.Uint64("tenantID", tenantID.Uint64()), zap.Error(err), zap.String("scene", string(entity.NotificationScenePocClusterWillEndIn3Days)))
		return err
	}

	for _, threshold := range PoCReminderFreePointsThresholds {
		identifier := fmt.Sprintf("%d-reatched", threshold)
		// for example, if the remaining points is 600, user should receive alert for 500, 250, but not 750, 1000
		// if the remain  ing points is 750, user should receive alert for 500, 250, but not 750, 1000, too.
		// so we fill the notification for threshold which is bigger, and clear the notification which is smaller

		if currentPoints <= threshold {
			if err = notificationMgr.FillNotificationIdentifier(ctx, tx, tenantID, entity.NotificationScenePocWillEnd, identifier); err != nil {
				log.Error(ctx, "fill notification", zap.Error(err), zap.String("scene", string(entity.NotificationScenePocWillEnd)), zap.String("identifier", identifier))
				return err
			}
		} else {
			if err = notificationMgr.ClearNotificationIdentifier(ctx, tx, tenantID, entity.NotificationScenePocWillEnd, identifier); err != nil {
				log.Error(ctx, "failed to clear notification", zap.Uint64("tenantID", tenantID.Uint64()), zap.Error(err), zap.String("identifier", identifier), zap.String("scene", string(entity.NotificationScenePocWillEnd)))
				return err
			}
		}
	}

	log.Info(ctx, "all types of poc will end email notification been refreshed")

	return nil
}

func (c Context) sendPOCEndEmail(t *entity.Tenant) error {
	ctx := context.Background()
	dbProvider := db.NewProvider()
	tx := dbProvider.WriteDB(ctx)
	tenantID := t.ID
	user, err := c.GetAdminUser(t.Email)
	if err != nil {
		return errors.Annotate(err, "failed to get admin user")
	}

	trialEndedAt, err := dbutil.GetPOCTrialEndedAt(ctx, tx, tenantID)
	if err != nil {
		return errors.Annotate(err, "failed to get poc trial end at")
	}

	notificationMgr := util.NewNotificationMgr()

	clusterWillDeletedAt := trialEndedAt.Add(time.Hour * 72)
	return notificationMgr.SendNotificationOnce(ctx, tx, tenantID, entity.NotificationScenePocClusterWillEndIn3Days, string(entity.NotificationScenePocClusterWillEndIn3Days), func() error {
		timezone, err := strconv.Atoi(t.TimeZone)
		if err != nil {
			return err
		}
		// notify user
		vars4user := map[string]string{
			"userName": user.FirstName,
			"endAt":    util.ConvertUtcTime(clusterWillDeletedAt, int32(timezone)),
		}
		if err := c.mail.SendMailWithTemplate(ctx, []string{t.Email}, config.EmailSubjectPocTrialWillEndIn3Days, config.TNPocClusterWillEndIn3Days, vars4user, nil); err != nil {
			return err
		}

		// notify ops/account-manager that customer's poc points run out
		vars := map[string]string{
			"now":        time.Now().Format(config.TimeFmtDTM),
			"orgName":    t.Name,
			"orgId":      fmt.Sprint(t.ID),
			"leftPoints": fmt.Sprintf("%.2f", 0.0), // org.RemainingCredit is 0
		}
		return c.mail.SendMailWithTemplate(ctx, util.GetPocStatusUpdateRelatedManagerEmails(ctx, t), config.CustomerStatusUpdateSubject, config.TNCustomerUpdatePocPointsChanged, vars, nil)
	})
}
