package workers

import (
	"time"

	"github.com/pingcap/dbaas/billing2/model/payment"
	"github.com/pingcap/dbaas/billing2/utils/log"
	"go.uber.org/zap"
)

// janitorStaleThreshold is the cutoff between "worker is alive" and
// "worker has crashed". Must be larger than payingHeartbeatInterval
// (defined in stripe_invoice_paying_worker.go) so a healthy long-running
// task always has time to send another heartbeat before the janitor
// declares it stuck.
const janitorStaleThreshold = 10 * time.Minute

// janitorMessage is written to error_message on every force-failed paying
// so finance can grep for the auto-reset cause in the dashboard.
const janitorMessage = "paying marked failed by janitor: no heartbeat update for 10 minutes"

// StripeInvoicePayingJanitorWorker finds payings stuck in status='running'
// with no updated_at heartbeat for janitorStaleThreshold, and force-fails
// them. Without this worker, a crashed paying-worker leaves the row in
// 'running' forever because ClaimNextPendingPaying only picks rows where
// status='pending'.
//
// The janitor itself never flips a paying back to 'pending' (which would
// race with a healthy worker that is mid-run); it only flips to 'failed'.
// A future month's paying with the same month string is blocked by
// uk_active_paying while status='failed' is a terminal state, so the row
// does not need an explicit retry — finance can POST a fresh paying for
// the same month once the row has settled.
//
// Idempotent: re-marking an already-failed row returns ObjectNotExistsError
// (RowsAffected=0) which we swallow. Multiple janitor pods running at the
// same tick produce at most one update per stuck row.
func StripeInvoicePayingJanitorWorker() {
	payingModel := &payment.PayingModel{}

	stuck, err := payingModel.ListStuckRunning(janitorStaleThreshold)
	if err != nil {
		log.Warn("janitor failed to list stuck payings", zap.Error(err))
		return
	}
	if len(stuck) == 0 {
		return
	}
	log.Info("janitor found stuck-running payings",
		zap.Int("count", len(stuck)),
		zap.Duration("stale_threshold", janitorStaleThreshold),
	)
	for _, p := range stuck {
		if err := payingModel.FailPaying(p, janitorMessage); err != nil {
			log.Warn("janitor failed to mark stuck paying as failed",
				zap.Uint("payingID", p.ID),
				zap.String("month", p.Month),
				zap.Error(err),
			)
			continue
		}
		log.Warn("janitor marked stuck paying as failed",
			zap.Uint("payingID", p.ID),
			zap.String("month", p.Month),
			zap.Time("picked_up_at", derefTime(p.PickedUpAt)),
			zap.Time("updated_at", p.UpdatedAt),
		)
	}
}

func derefTime(t *time.Time) time.Time {
	if t == nil {
		return time.Time{}
	}
	return *t
}
