package redis

import (
	"context"
	"fmt"
	"log/slog"
	"strconv"
	"time"

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

// Client wraps go-redis with a simple key-value interface.
type Client struct {
	rdb *redis.Client
}

// Config holds Redis connection parameters.
// Either URL or Host must be provided.
type Config struct {
	URL      string // e.g. "redis://:password@host:6379/2" (takes precedence)
	Host     string // e.g. "10.47.208.3"
	Port     string // e.g. "6379" (default "6379")
	Password string
	DB       string // e.g. "2" (default "0")
}

// New creates a Redis client from Config.
// Supports both URL format and separate Host/Port/Password/DB fields.
func New(cfg Config) (*Client, error) {
	var opts *redis.Options

	if cfg.URL != "" {
		var err error
		opts, err = redis.ParseURL(cfg.URL)
		if err != nil {
			return nil, fmt.Errorf("parse redis url: %w", err)
		}
	} else if cfg.Host != "" {
		port := cfg.Port
		if port == "" {
			port = "6379"
		}
		db := 0
		if cfg.DB != "" {
			var err error
			db, err = strconv.Atoi(cfg.DB)
			if err != nil {
				return nil, fmt.Errorf("invalid REDIS_DB %q: %w", cfg.DB, err)
			}
		}
		opts = &redis.Options{
			Addr:     cfg.Host + ":" + port,
			Password: cfg.Password,
			DB:       db,
		}
	} else {
		return nil, fmt.Errorf("redis: either REDIS_URL or REDIS_HOST is required")
	}

	rdb := redis.NewClient(opts)

	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	if err := rdb.Ping(ctx).Err(); err != nil {
		rdb.Close()
		return nil, fmt.Errorf("redis ping failed: %w", err)
	}

	slog.Info("redis: connected", "addr", opts.Addr, "db", opts.DB)
	return &Client{rdb: rdb}, nil
}

func (c *Client) Get(ctx context.Context, key string) (string, error) {
	val, err := c.rdb.Get(ctx, key).Result()
	if err == redis.Nil {
		return "", nil
	}
	return val, err
}

func (c *Client) Set(ctx context.Context, key, value string, ttl time.Duration) error {
	return c.rdb.Set(ctx, key, value, ttl).Err()
}

func (c *Client) Del(ctx context.Context, key string) error {
	return c.rdb.Del(ctx, key).Err()
}

// GetDel atomically gets and deletes a key.
// Uses a Lua script for compatibility with Redis < 6.2 (where native GETDEL is unavailable).
var getDelScript = redis.NewScript(`
local v = redis.call("GET", KEYS[1])
if v then
	redis.call("DEL", KEYS[1])
	return v
end
return false
`)

func (c *Client) GetDel(ctx context.Context, key string) (string, error) {
	res, err := getDelScript.Run(ctx, c.rdb, []string{key}).Result()
	if err == redis.Nil || res == nil {
		return "", nil
	}
	if err != nil {
		return "", err
	}
	return fmt.Sprintf("%v", res), nil
}

// SetNX sets a key only if it does not exist (CAS). Returns true if set.
func (c *Client) SetNX(ctx context.Context, key, value string, ttl time.Duration) (bool, error) {
	return c.rdb.SetNX(ctx, key, value, ttl).Result()
}

// Expire sets a TTL on an existing key.
func (c *Client) Expire(ctx context.Context, key string, ttl time.Duration) error {
	return c.rdb.Expire(ctx, key, ttl).Err()
}

// RPush appends one or more values to a list.
func (c *Client) RPush(ctx context.Context, key string, values ...string) error {
	args := make([]interface{}, len(values))
	for i, v := range values {
		args[i] = v
	}
	return c.rdb.RPush(ctx, key, args...).Err()
}

// LRange returns elements from a list between start and stop (inclusive).
func (c *Client) LRange(ctx context.Context, key string, start, stop int64) ([]string, error) {
	return c.rdb.LRange(ctx, key, start, stop).Result()
}

// LTrim trims a list to only contain elements between start and stop (inclusive).
func (c *Client) LTrim(ctx context.Context, key string, start, stop int64) error {
	return c.rdb.LTrim(ctx, key, start, stop).Err()
}

// LLen returns the length of a list.
func (c *Client) LLen(ctx context.Context, key string) (int64, error) {
	return c.rdb.LLen(ctx, key).Result()
}

// DelIfEqual atomically deletes a key only if its value matches expected.
// Used for safe distributed lock release (prevents deleting another owner's lock).
var delIfEqualScript = redis.NewScript(`
if redis.call("GET", KEYS[1]) == ARGV[1] then
	return redis.call("DEL", KEYS[1])
end
return 0
`)

func (c *Client) DelIfEqual(ctx context.Context, key, expected string) (bool, error) {
	res, err := delIfEqualScript.Run(ctx, c.rdb, []string{key}, expected).Int64()
	if err != nil {
		return false, err
	}
	return res == 1, nil
}

// Scan iterates keys matching a pattern in batches.
// Returns all matching keys. Safe for production (non-blocking cursor scan).
func (c *Client) Scan(ctx context.Context, pattern string, batchSize int64) ([]string, error) {
	var all []string
	var cursor uint64
	for {
		keys, next, err := c.rdb.Scan(ctx, cursor, pattern, batchSize).Result()
		if err != nil {
			return all, fmt.Errorf("redis scan: %w", err)
		}
		all = append(all, keys...)
		cursor = next
		if cursor == 0 {
			break
		}
	}
	return all, nil
}

func (c *Client) Close() error {
	return c.rdb.Close()
}
