package license

import (
	"context"
	"log/slog"
	"math/rand/v2"
	"time"

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

const (
	heartbeatJitterRatio = 0.10
	maxHeartbeatBackoff  = 30 * time.Minute
	leaderLockKey        = "qycw:license:heartbeat:leader"
	minLeaderLockTTL     = 10 * time.Second
	leaderLockTTLMargin  = 30 * time.Second
)

// redisSetNXClient is the minimal Redis surface Reporter needs for leader
// election. Satisfied by *redisstore.Client (which embeds *redis.Client).
type redisSetNXClient interface {
	SetNX(ctx context.Context, key string, value any, expiration time.Duration) *redis.BoolCmd
}

type Reporter struct {
	manager   *Manager
	interval  time.Duration
	logger    *slog.Logger
	randFloat func() float64
	redis     redisSetNXClient
	nodeID    string
}

func NewReporter(manager *Manager, interval time.Duration, logger *slog.Logger) *Reporter {
	if interval <= 0 {
		interval = 5 * time.Minute
	}
	return &Reporter{manager: manager, interval: interval, logger: logger, randFloat: rand.Float64}
}

// WithRedis attaches a Redis client for heartbeat leader election across
// multiple replicas. nodeID should be stable per-process (e.g. instance ID
// plus a short random suffix) so repeated SetNX calls from the same replica
// do not self-conflict. When redis is nil, the Reporter always acts as
// leader (single-replica behavior, unchanged).
func (r *Reporter) WithRedis(client redisSetNXClient, nodeID string) *Reporter {
	r.redis = client
	r.nodeID = nodeID
	return r
}

func (r *Reporter) Run(ctx context.Context) error {
	if r == nil || r.manager == nil {
		return nil
	}
	failures := 0
	for {
		if err := r.process(ctx); err != nil {
			failures++
		} else {
			failures = 0
		}
		timer := time.NewTimer(r.nextDelay(failures))
		select {
		case <-ctx.Done():
			if !timer.Stop() {
				<-timer.C
			}
			return nil
		case <-timer.C:
		}
	}
}

func (r *Reporter) ProcessOnce(ctx context.Context) error {
	if r == nil || r.manager == nil {
		return nil
	}
	return r.manager.Report(ctx)
}

func (r *Reporter) process(ctx context.Context) error {
	if r.redis != nil {
		ttl := r.interval - leaderLockTTLMargin
		if ttl < minLeaderLockTTL {
			ttl = minLeaderLockTTL
		}
		ok, err := r.redis.SetNX(ctx, leaderLockKey, r.nodeID, ttl).Result()
		if err == nil && !ok {
			// Not leader this cycle: refresh L1 from the DB instead of
			// sending a duplicate heartbeat.
			if rehydrateErr := r.rehydrateFromDB(ctx); rehydrateErr != nil && r.logger != nil {
				r.logger.Warn("authorization state rehydrate failed", "error", rehydrateErr)
			}
			return nil
		}
		// err != nil means Redis is unavailable; degrade to acting as
		// leader (every replica heartbeats independently — a fail-toward-
		// grace degradation rather than a correctness issue).
	}
	err := r.ProcessOnce(ctx)
	if err != nil && r.logger != nil {
		r.logger.Warn("authorization heartbeat failed", "error", err)
	}
	return err
}

// rehydrateFromDB refreshes the in-process L1 evaluator from the last
// signed lease persisted by the current leader, without sending a network
// heartbeat. Used by non-leader replicas so their L1 state does not go
// stale between the leader's successful heartbeats.
func (r *Reporter) rehydrateFromDB(ctx context.Context) error {
	if r.manager == nil || r.manager.store == nil || r.manager.eval == nil {
		return nil
	}
	state, err := r.manager.store.Load(ctx)
	if err != nil || len(state.LeaseRaw) == 0 {
		return err
	}
	if loadErr := r.manager.eval.LoadFromQuery(state.LeaseRaw, state.LeaseSignature); loadErr != nil {
		elapsed := r.manager.hwcNow().Sub(state.LastHeartbeatAt)
		if elapsed <= r.manager.graceWindow() {
			expiresAt := persistedLeaseExpiresAt(state)
			r.manager.eval.SetGrace(expiresAt, state.LastHeartbeatAt.Add(r.manager.graceWindow()))
		}
	}
	return nil
}

func (r *Reporter) nextDelay(failures int) time.Duration {
	delay := r.interval
	if failures > 1 {
		shift := min(failures-1, 3)
		capDelay := max(maxHeartbeatBackoff, r.interval)
		for range shift {
			if delay >= capDelay || delay > capDelay/2 {
				delay = capDelay
				break
			}
			delay *= 2
		}
	}
	random := 0.5
	if r.randFloat != nil {
		random = r.randFloat()
	}
	factor := 1 - heartbeatJitterRatio + 2*heartbeatJitterRatio*random
	return time.Duration(float64(delay) * factor)
}
