package redis

import (
	
	
	
	
	
	
	
	
	
	
	

	
)

// Limiter is the interface of a rate limiter or a circuit breaker.
type Limiter interface {
	// Allow returns nil if operation is allowed or an error otherwise.
	// If operation is allowed client must ReportResult of the operation
	// whether it is a success or a failure.
	Allow() error
	// ReportResult reports the result of the previously allowed operation.
	// nil indicates a success, non-nil error usually indicates a failure.
	ReportResult(result error)
}

// Options keeps the settings to set up redis connection.
type Options struct {
	// The network type, either tcp or unix.
	// Default is tcp.
	Network string
	// host:port address.
	Addr string

	// ClientName will execute the `CLIENT SETNAME ClientName` command for each conn.
	ClientName string

	// Dialer creates new network connection and has priority over
	// Network and Addr options.
	Dialer func(ctx context.Context, network, addr string) (net.Conn, error)

	// Hook that is called when new connection is established.
	OnConnect func(ctx context.Context, cn *Conn) error

	// Use the specified Username to authenticate the current connection
	// with one of the connections defined in the ACL list when connecting
	// to a Redis 6.0 instance, or greater, that is using the Redis ACL system.
	Username string
	// Optional password. Must match the password specified in the
	// requirepass server configuration option (if connecting to a Redis 5.0 instance, or lower),
	// or the User Password when connecting to a Redis 6.0 instance, or greater,
	// that is using the Redis ACL system.
	Password string
	// CredentialsProvider allows the username and password to be updated
	// before reconnecting. It should return the current username and password.
	CredentialsProvider func() (username string, password string)

	// Database to be selected after connecting to the server.
	DB int

	// Maximum number of retries before giving up.
	// Default is 3 retries; -1 (not 0) disables retries.
	MaxRetries int
	// Minimum backoff between each retry.
	// Default is 8 milliseconds; -1 disables backoff.
	MinRetryBackoff time.Duration
	// Maximum backoff between each retry.
	// Default is 512 milliseconds; -1 disables backoff.
	MaxRetryBackoff time.Duration

	// Dial timeout for establishing new connections.
	// Default is 5 seconds.
	DialTimeout time.Duration
	// Timeout for socket reads. If reached, commands will fail
	// with a timeout instead of blocking. Supported values:
	//   - `0` - default timeout (3 seconds).
	//   - `-1` - no timeout (block indefinitely).
	//   - `-2` - disables SetReadDeadline calls completely.
	ReadTimeout time.Duration
	// Timeout for socket writes. If reached, commands will fail
	// with a timeout instead of blocking.  Supported values:
	//   - `0` - default timeout (3 seconds).
	//   - `-1` - no timeout (block indefinitely).
	//   - `-2` - disables SetWriteDeadline calls completely.
	WriteTimeout time.Duration
	// ContextTimeoutEnabled controls whether the client respects context timeouts and deadlines.
	// See https://redis.uptrace.dev/guide/go-redis-debugging.html#timeouts
	ContextTimeoutEnabled bool

	// Type of connection pool.
	// true for FIFO pool, false for LIFO pool.
	// Note that FIFO has slightly higher overhead compared to LIFO,
	// but it helps closing idle connections faster reducing the pool size.
	PoolFIFO bool
	// Maximum number of socket connections.
	// Default is 10 connections per every available CPU as reported by runtime.GOMAXPROCS.
	PoolSize int
	// Amount of time client waits for connection if all connections
	// are busy before returning an error.
	// Default is ReadTimeout + 1 second.
	PoolTimeout time.Duration
	// Minimum number of idle connections which is useful when establishing
	// new connection is slow.
	MinIdleConns int
	// Maximum number of idle connections.
	MaxIdleConns int
	// ConnMaxIdleTime is the maximum amount of time a connection may be idle.
	// Should be less than server's timeout.
	//
	// Expired connections may be closed lazily before reuse.
	// If d <= 0, connections are not closed due to a connection's idle time.
	//
	// Default is 30 minutes. -1 disables idle timeout check.
	ConnMaxIdleTime time.Duration
	// ConnMaxLifetime is the maximum amount of time a connection may be reused.
	//
	// Expired connections may be closed lazily before reuse.
	// If <= 0, connections are not closed due to a connection's age.
	//
	// Default is to not close idle connections.
	ConnMaxLifetime time.Duration

	// TLS Config to use. When set, TLS will be negotiated.
	TLSConfig *tls.Config

	// Limiter interface used to implement circuit breaker or rate limiter.
	Limiter Limiter

	// Enables read only queries on slave/follower nodes.
	readOnly bool
}

func ( *Options) () {
	if .Addr == "" {
		.Addr = "localhost:6379"
	}
	if .Network == "" {
		if strings.HasPrefix(.Addr, "/") {
			.Network = "unix"
		} else {
			.Network = "tcp"
		}
	}
	if .DialTimeout == 0 {
		.DialTimeout = 5 * time.Second
	}
	if .Dialer == nil {
		.Dialer = NewDialer()
	}
	if .PoolSize == 0 {
		.PoolSize = 10 * runtime.GOMAXPROCS(0)
	}
	switch .ReadTimeout {
	case -2:
		.ReadTimeout = -1
	case -1:
		.ReadTimeout = 0
	case 0:
		.ReadTimeout = 3 * time.Second
	}
	switch .WriteTimeout {
	case -2:
		.WriteTimeout = -1
	case -1:
		.WriteTimeout = 0
	case 0:
		.WriteTimeout = .ReadTimeout
	}
	if .PoolTimeout == 0 {
		if .ReadTimeout > 0 {
			.PoolTimeout = .ReadTimeout + time.Second
		} else {
			.PoolTimeout = 30 * time.Second
		}
	}
	if .ConnMaxIdleTime == 0 {
		.ConnMaxIdleTime = 30 * time.Minute
	}

	if .MaxRetries == -1 {
		.MaxRetries = 0
	} else if .MaxRetries == 0 {
		.MaxRetries = 3
	}
	switch .MinRetryBackoff {
	case -1:
		.MinRetryBackoff = 0
	case 0:
		.MinRetryBackoff = 8 * time.Millisecond
	}
	switch .MaxRetryBackoff {
	case -1:
		.MaxRetryBackoff = 0
	case 0:
		.MaxRetryBackoff = 512 * time.Millisecond
	}
}

func ( *Options) () *Options {
	 := *
	return &
}

// NewDialer returns a function that will be used as the default dialer
// when none is specified in Options.Dialer.
func ( *Options) func(context.Context, string, string) (net.Conn, error) {
	return func( context.Context, ,  string) (net.Conn, error) {
		 := &net.Dialer{
			Timeout:   .DialTimeout,
			KeepAlive: 5 * time.Minute,
		}
		if .TLSConfig == nil {
			return .DialContext(, , )
		}
		return tls.DialWithDialer(, , , .TLSConfig)
	}
}

// ParseURL parses an URL into Options that can be used to connect to Redis.
// Scheme is required.
// There are two connection types: by tcp socket and by unix socket.
// Tcp connection:
//
//	redis://<user>:<password>@<host>:<port>/<db_number>
//
// Unix connection:
//
//	unix://<user>:<password>@</path/to/redis.sock>?db=<db_number>
//
// Most Option fields can be set using query parameters, with the following restrictions:
//   - field names are mapped using snake-case conversion: to set MaxRetries, use max_retries
//   - only scalar type fields are supported (bool, int, time.Duration)
//   - for time.Duration fields, values must be a valid input for time.ParseDuration();
//     additionally a plain integer as value (i.e. without unit) is intepreted as seconds
//   - to disable a duration field, use value less than or equal to 0; to use the default
//     value, leave the value blank or remove the parameter
//   - only the last value is interpreted if a parameter is given multiple times
//   - fields "network", "addr", "username" and "password" can only be set using other
//     URL attributes (scheme, host, userinfo, resp.), query paremeters using these
//     names will be treated as unknown parameters
//   - unknown parameter names will result in an error
//
// Examples:
//
//	redis://user:password@localhost:6789/3?dial_timeout=3&db=1&read_timeout=6s&max_retries=2
//	is equivalent to:
//	&Options{
//		Network:     "tcp",
//		Addr:        "localhost:6789",
//		DB:          1,               // path "/3" was overridden by "&db=1"
//		DialTimeout: 3 * time.Second, // no time unit = seconds
//		ReadTimeout: 6 * time.Second,
//		MaxRetries:  2,
//	}
func ( string) (*Options, error) {
	,  := url.Parse()
	if  != nil {
		return nil, 
	}

	switch .Scheme {
	case "redis", "rediss":
		return setupTCPConn()
	case "unix":
		return setupUnixConn()
	default:
		return nil, fmt.Errorf("redis: invalid URL scheme: %s", .Scheme)
	}
}

func setupTCPConn( *url.URL) (*Options, error) {
	 := &Options{Network: "tcp"}

	.Username, .Password = getUserPassword()

	,  := getHostPortWithDefaults()
	.Addr = net.JoinHostPort(, )

	 := strings.FieldsFunc(.Path, func( rune) bool {
		return  == '/'
	})
	switch len() {
	case 0:
		.DB = 0
	case 1:
		var  error
		if .DB,  = strconv.Atoi([0]);  != nil {
			return nil, fmt.Errorf("redis: invalid database number: %q", [0])
		}
	default:
		return nil, fmt.Errorf("redis: invalid URL path: %s", .Path)
	}

	if .Scheme == "rediss" {
		.TLSConfig = &tls.Config{
			ServerName: ,
			MinVersion: tls.VersionTLS12,
		}
	}

	return setupConnParams(, )
}

// getHostPortWithDefaults is a helper function that splits the url into
// a host and a port. If the host is missing, it defaults to localhost
// and if the port is missing, it defaults to 6379.
func getHostPortWithDefaults( *url.URL) (string, string) {
	, ,  := net.SplitHostPort(.Host)
	if  != nil {
		 = .Host
	}
	if  == "" {
		 = "localhost"
	}
	if  == "" {
		 = "6379"
	}
	return , 
}

func setupUnixConn( *url.URL) (*Options, error) {
	 := &Options{
		Network: "unix",
	}

	if strings.TrimSpace(.Path) == "" { // path is required with unix connection
		return nil, errors.New("redis: empty unix socket path")
	}
	.Addr = .Path
	.Username, .Password = getUserPassword()
	return setupConnParams(, )
}

type queryOptions struct {
	q   url.Values
	err error
}

func ( *queryOptions) ( string) bool {
	return len(.q[]) > 0
}

func ( *queryOptions) ( string) string {
	 := .q[]
	if len() == 0 {
		return ""
	}
	delete(.q, ) // enable detection of unknown parameters
	return [len()-1]
}

func ( *queryOptions) ( string) []string {
	 := .q[]
	delete(.q, )
	return 
}

func ( *queryOptions) ( string) int {
	 := .string()
	if  == "" {
		return 0
	}
	,  := strconv.Atoi()
	if  == nil {
		return 
	}
	if .err == nil {
		.err = fmt.Errorf("redis: invalid %s number: %s", , )
	}
	return 0
}

func ( *queryOptions) ( string) time.Duration {
	 := .string()
	if  == "" {
		return 0
	}
	// try plain number first
	if ,  := strconv.Atoi();  == nil {
		if  <= 0 {
			// disable timeouts
			return -1
		}
		return time.Duration() * time.Second
	}
	,  := time.ParseDuration()
	if  == nil {
		return 
	}
	if .err == nil {
		.err = fmt.Errorf("redis: invalid %s duration: %w", , )
	}
	return 0
}

func ( *queryOptions) ( string) bool {
	switch  := .string();  {
	case "true", "1":
		return true
	case "false", "0", "":
		return false
	default:
		if .err == nil {
			.err = fmt.Errorf("redis: invalid %s boolean: expected true/false/1/0 or an empty string, got %q", , )
		}
		return false
	}
}

func ( *queryOptions) () []string {
	if len(.q) == 0 {
		return nil
	}
	 := make([]string, 0, len(.q))
	for  := range .q {
		 = append(, )
	}
	sort.Strings()
	return 
}

// setupConnParams converts query parameters in u to option value in o.
func setupConnParams( *url.URL,  *Options) (*Options, error) {
	 := queryOptions{q: .Query()}

	// compat: a future major release may use q.int("db")
	if  := .string("db");  != "" {
		,  := strconv.Atoi()
		if  != nil {
			return nil, fmt.Errorf("redis: invalid database number: %w", )
		}
		.DB = 
	}

	.ClientName = .string("client_name")
	.MaxRetries = .int("max_retries")
	.MinRetryBackoff = .duration("min_retry_backoff")
	.MaxRetryBackoff = .duration("max_retry_backoff")
	.DialTimeout = .duration("dial_timeout")
	.ReadTimeout = .duration("read_timeout")
	.WriteTimeout = .duration("write_timeout")
	.PoolFIFO = .bool("pool_fifo")
	.PoolSize = .int("pool_size")
	.PoolTimeout = .duration("pool_timeout")
	.MinIdleConns = .int("min_idle_conns")
	.MaxIdleConns = .int("max_idle_conns")
	if .has("conn_max_idle_time") {
		.ConnMaxIdleTime = .duration("conn_max_idle_time")
	} else {
		.ConnMaxIdleTime = .duration("idle_timeout")
	}
	if .has("conn_max_lifetime") {
		.ConnMaxLifetime = .duration("conn_max_lifetime")
	} else {
		.ConnMaxLifetime = .duration("max_conn_age")
	}
	if .err != nil {
		return nil, .err
	}

	// any parameters left?
	if  := .remaining(); len() > 0 {
		return nil, fmt.Errorf("redis: unexpected option: %s", strings.Join(, ", "))
	}

	return , nil
}

func getUserPassword( *url.URL) (string, string) {
	var ,  string
	if .User != nil {
		 = .User.Username()
		if ,  := .User.Password();  {
			 = 
		}
	}
	return , 
}

func newConnPool(
	 *Options,
	 func( context.Context, ,  string) (net.Conn, error),
) *pool.ConnPool {
	return pool.NewConnPool(&pool.Options{
		Dialer: func( context.Context) (net.Conn, error) {
			return (, .Network, .Addr)
		},
		PoolFIFO:        .PoolFIFO,
		PoolSize:        .PoolSize,
		PoolTimeout:     .PoolTimeout,
		MinIdleConns:    .MinIdleConns,
		MaxIdleConns:    .MaxIdleConns,
		ConnMaxIdleTime: .ConnMaxIdleTime,
		ConnMaxLifetime: .ConnMaxLifetime,
	})
}