package alertbyoc

import (
	"context"
	"fmt"
	"time"

	"github.com/pingcap/log"
	"github.com/robfig/cron/v3"
	"go.uber.org/zap"
	"gorm.io/gorm"

	"github.com/pingcap-inc/clinic_cloud_server/pkg/alertbyoc/repo"
	"github.com/pingcap-inc/clinic_cloud_server/pkg/client/o11y"
	"github.com/pingcap-inc/clinic_cloud_server/pkg/config"
	"github.com/pingcap-inc/clinic_cloud_server/pkg/db"
)

var expiredDay = 30

type RefreshTokenJob struct {
	Repo       *repo.AlertmanagerTokenRepo
	O11Y       *o11y.Client
	ExpiredDay int
}

func (j *RefreshTokenJob) Run() {
	log.Info("corn job running", zap.String("name", "RefreshTokenJob"))
	defer func() {
		if err := recover(); err != nil {
			log.Error("RefreshTokenJob failed", zap.String("err", fmt.Sprintf("%v", err)))
		}
	}()

	err := j.scanAndHandleTokens()
	if err != nil {
		log.Error("scanAndHandleTokens failed", zap.Error(err))
	}
}

func (j *RefreshTokenJob) scanAndHandleTokens() error {
	expiredTokens, err := GetExpiredTokens(j.ExpiredDay)
	if err != nil {
		log.Error("get expired token failed", zap.Error(err))
		return err
	}
	log.Info("get expiredTokens", zap.Int("len", len(expiredTokens)))
	for _, token := range expiredTokens {
		if token.ExpiredAt == nil {
			if err := j.handleExpiredTokenWithDownstream(&token); err != nil {
				log.Error("handle expired token failed", zap.Error(err))
				continue
			}
		} else {
			logger := log.L().With(
				zap.Uint64("tenant_id", token.TenantID),
				zap.Time("created_at", token.CreatedAt),
				zap.Time("expired_at", *token.ExpiredAt))
			sevenDaysAgo := time.Now().AddDate(0, 0, -7)
			if token.ExpiredAt.Before(sevenDaysAgo) {
				err := db.Transaction(func(tx *gorm.DB) error {
					var validTokenCount int64
					err := tx.Model(&db.AlertmanagerToken{}).
						Where("tenant_id = ? AND expired_at IS NULL", token.TenantID).
						Count(&validTokenCount).Error
					if err != nil {
						return fmt.Errorf("count valid token failed: %v", err)
					}

					if validTokenCount == 0 {
						return fmt.Errorf("deletion is prohibited: tenant %d does not have any other valid tokens", token.TenantID)
					}
					return tx.Where("tenant_id = ? AND created_at = ?", token.TenantID, token.CreatedAt).Delete(&db.AlertmanagerToken{}).Error
				})
				if err != nil {
					logger.Warn("delete expired token failed", zap.Error(err))
				} else {
					logger.Info("delete expired token successfully")
				}
			}
		}
	}
	return nil
}

func (j *RefreshTokenJob) handleExpiredTokenWithDownstream(oldToken *db.AlertmanagerToken) error {
	return db.Transaction(func(tx *gorm.DB) error {
		newPwd, err := generateSecureToken(16)
		if err != nil {
			return fmt.Errorf("generate new password failed: %v", err)
		}

		newToken := db.AlertmanagerToken{
			TenantID: oldToken.TenantID,
			Username: oldToken.Username,
			Password: newPwd,
		}

		if err := tx.Create(&newToken).Error; err != nil {
			return fmt.Errorf("save new token failed: %v", err)
		}

		now := time.Now()
		if err := tx.Model(&oldToken).
			Where("tenant_id = ? AND created_at = ?", oldToken.TenantID, oldToken.CreatedAt).
			Update("expired_at", now).Error; err != nil {
			return fmt.Errorf("update expired_at for token failed: %v", err)
		}

		if err := j.O11Y.UpdateCustomerAlertAuth(
			context.TODO(),
			fmt.Sprintf("%v", oldToken.TenantID),
			o11y.UpdateCustomerAlertAuthRequest{
				Username: oldToken.Username,
				Password: newPwd,
			},
		); err != nil {
			return fmt.Errorf("update customer token failed: %v", err)
		}

		log.Info("update customer token successfully")
		return nil
	})
}

type CronJob struct {
	cron *cron.Cron
	Jobs map[cron.EntryID]cron.Job
}

func NewCron(cfg *config.Config) *CronJob {
	// the cron should run in 02:00:00 UTC+8
	tzone, err := time.LoadLocation("Asia/Shanghai")
	if err != nil {
		log.Warn("load time location failed", zap.Error(err))
		tzone = time.Local
	}
	log.Info("new cron", zap.String("timezone", tzone.String()))

	o11y, err := o11y.New(
		cfg.O11YConfig.GlobalServiceEndpoint, cfg.O11YConfig.Auth0ClientID, cfg.O11YConfig.Auth0ClientSecret,
	)
	if err != nil {
		log.Warn("new o11y client failed", zap.Error(err))
		return nil
	}

	c := &CronJob{
		cron: cron.New(cron.WithLocation(tzone)),
		Jobs: make(map[cron.EntryID]cron.Job),
	}
	_ = c.AddJob("0 2 * * ?", &RefreshTokenJob{
		Repo:       &repo.AlertmanagerTokenRepo{},
		ExpiredDay: expiredDay,
		O11Y:       o11y,
	})
	return c
}

func (c *CronJob) AddJob(cronplan string, job cron.Job) error {
	entryid, err := c.cron.AddJob(cronplan, cron.NewChain(cron.Recover(cron.DefaultLogger)).Then(job))
	if err != nil {
		return err
	}
	c.Jobs[entryid] = job
	return nil
}

func (c *CronJob) Start() {
	c.cron.Start()
}

func GetExpiredTokens(expiredDay int) ([]db.AlertmanagerToken, error) {
	tokens := &[]db.AlertmanagerToken{}
	err := db.Transaction(func(tx *gorm.DB) error {
		return tx.Where("created_at < ? AND username != 'deployment'", time.Now().Add(-24*time.Duration(expiredDay)*time.Hour)).Find(tokens).Error
	})
	return *tokens, err
}
