package detect

import (
	"context"
	"encoding/json"
	"fmt"
	"time"

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

const (
	// streakTTL is the Redis TTL for a per-node streak key.
	//
	// Must be comfortably wider than the longest streak window, which is the
	// confirmed threshold: 6 cycles × 5 min = 30 min.  Two hours provides
	// enough margin to survive a process restart or a short leader-election gap
	// without zeroing out a valid streak mid-observation.
	streakTTL = 2 * time.Hour
)

// Streak holds the consecutive-cycle counters for a single node.
// All three counters are stored together under one Redis key to make the
// read-modify-write loop atomic with a single GET + SET round-trip.
type Streak struct {
	// FailStreak is the number of consecutive detection cycles in which the
	// node's domestic probes failed while the node was in "up" state.
	// It is reset to zero once the suspect threshold is reached (transition
	// attempted) or when domestic probes recover.
	FailStreak int `json:"fail_streak"`

	// SuspectStreak counts consecutive cycles spent in "blocked_suspect" where
	// the GFW-block condition (domestic fail + overseas OK) still holds.
	// Reaching ConfirmedStreakMin triggers the confirmed transition.
	SuspectStreak int `json:"suspect_streak"`

	// RecoverStreak counts consecutive cycles in "blocked_suspect" where
	// domestic probes pass.  Reaching RecoverStreakMin triggers recovery.
	RecoverStreak int `json:"recover_streak"`
}

// StreakStore persists per-node Streak values in Redis.
//
// Key format:   detect:streak:{nodeID}   (JSON-encoded Streak, TTL 2 h)
//
// Missing-key semantics: a missing key means "no streak data" and is treated
// as a zero Streak.  After a process restart, the first Tick loads whichever
// streaks survived in Redis and continues from that point — streaks are NOT
// re-derived from probe history on restart, which keeps the restart path
// simple at the cost of a potential single-cycle window of uncertainty.
type StreakStore struct {
	rdb *redis.Client
}

// NewStreakStore creates a StreakStore backed by the given Redis client.
func NewStreakStore(rdb *redis.Client) *StreakStore {
	return &StreakStore{rdb: rdb}
}

// streakKey returns the Redis key for nodeID's streak data.
func streakKey(nodeID string) string {
	return "detect:streak:" + nodeID
}

// Load reads the current Streak for nodeID.
// A missing or corrupted key returns a zero Streak without error.
func (s *StreakStore) Load(ctx context.Context, nodeID string) (Streak, error) {
	val, err := s.rdb.Get(ctx, streakKey(nodeID)).Result()
	if err == redis.Nil {
		return Streak{}, nil
	}
	if err != nil {
		return Streak{}, fmt.Errorf("detect: streak get %s: %w", nodeID, err)
	}
	var sk Streak
	if err := json.Unmarshal([]byte(val), &sk); err != nil {
		// Corrupted entry: treat as zero (non-fatal; we overwrite on the next Save).
		return Streak{}, nil
	}
	return sk, nil
}

// Save persists sk for nodeID with the standard streakTTL.
func (s *StreakStore) Save(ctx context.Context, nodeID string, sk Streak) error {
	data, err := json.Marshal(sk)
	if err != nil {
		return fmt.Errorf("detect: streak marshal %s: %w", nodeID, err)
	}
	if err := s.rdb.Set(ctx, streakKey(nodeID), data, streakTTL).Err(); err != nil {
		return fmt.Errorf("detect: streak set %s: %w", nodeID, err)
	}
	return nil
}

// Reset deletes the streak key for nodeID (e.g. after a confirmed transition).
func (s *StreakStore) Reset(ctx context.Context, nodeID string) error {
	if err := s.rdb.Del(ctx, streakKey(nodeID)).Err(); err != nil {
		return fmt.Errorf("detect: streak del %s: %w", nodeID, err)
	}
	return nil
}
