package bus

import (
	"context"
	"log"
	"time"

	"github.com/redis/go-redis/v9"
)

const (
	leaderTTL   = 30 * time.Second
	leaderRenew = 10 * time.Second
)

// RunAsLeader runs job on exactly one process at a time, cluster-wide.
//
// The background work this guards is not merely wasteful when duplicated. Every
// replica running the monitor scheduler means every check fires N times, every
// incident notification is delivered N times to the customer, and every hourly
// rollup is written N times. The Free-instance reaper is worse: it deletes
// whole instances, and two processes deleting the same one concurrently is not
// a race anyone wins.
//
// Redis rather than a Kubernetes Lease so that Docker Compose, which has no
// API server, takes the identical code path — one implementation to reason
// about, not two.
//
// job is given a context cancelled the moment leadership is lost, and must
// return when it is cancelled. Losing the lock (a paused process, a Redis
// blip) is treated as fatal to that run of the job: the successor may already
// have started, and two schedulers overlapping is the exact thing being
// prevented.
func RunAsLeader(ctx context.Context, name string, job func(context.Context)) {
	go func() {
		key := leaderKey + name
		for {
			if ctx.Err() != nil {
				return
			}
			ok, err := rdb.SetNX(ctx, key, nodeID, leaderTTL).Result()
			if err != nil {
				log.Printf("leader %s: acquire failed: %v", name, err)
			}
			if !ok {
				select {
				case <-ctx.Done():
					return
				case <-time.After(leaderRenew):
				}
				continue
			}

			log.Printf("leader %s: acquired by %s", name, nodeID)
			jobCtx, cancel := context.WithCancel(ctx)
			go job(jobCtx)
			holdLeadership(ctx, key, name)
			cancel()
			log.Printf("leader %s: released by %s", name, nodeID)

			// Give up the key on a clean shutdown so a successor takes over in
			// milliseconds rather than waiting out the TTL.
			if ctx.Err() != nil {
				relCtx, relCancel := context.WithTimeout(context.Background(), 2*time.Second)
				_ = releaseIfOwner.Run(relCtx, rdb, []string{key}, nodeID).Err()
				relCancel()
				return
			}
		}
	}()
}

// holdLeadership renews the lock until it is lost or ctx ends.
func holdLeadership(ctx context.Context, key, name string) {
	t := time.NewTicker(leaderRenew)
	defer t.Stop()
	for {
		select {
		case <-ctx.Done():
			return
		case <-t.C:
			n, err := renewIfOwner.Run(ctx, rdb, []string{key}, nodeID, int(leaderTTL/time.Millisecond)).Int()
			if err != nil && err != redis.Nil {
				log.Printf("leader %s: renew failed, standing down: %v", name, err)
				return
			}
			if n == 0 {
				log.Printf("leader %s: lock lost, standing down", name)
				return
			}
		}
	}
}

var renewIfOwner = redis.NewScript(`
if redis.call("GET", KEYS[1]) == ARGV[1] then
	return redis.call("PEXPIRE", KEYS[1], ARGV[2])
end
return 0
`)
