// Package redisconn provides shared helpers for opening, instrumenting and
// pinging Redis connections used by Galaxy services.
//
// The package codifies the steady-state rules captured in `ARCHITECTURE.md`
// `§Persistence Backends`: each service connects to one master plus
// zero-or-more replicas with a mandatory password, no TLS, and no
// `USERNAME`/ACL. The deprecated env vars `*_REDIS_TLS_ENABLED` and
// `*_REDIS_USERNAME` are rejected by LoadFromEnv with a clear startup error.
package redisconn

import (
	"errors"
	"fmt"
	"os"
	"strconv"
	"strings"
	"time"
)

// Default configuration values applied by DefaultConfig and LoadFromEnv when
// the corresponding environment variable is absent.
const (
	DefaultDB               = 0
	DefaultOperationTimeout = 250 * time.Millisecond
)

// Config stores the connection settings for one master plus zero-or-more
// replica Redis instances. The replica list is preserved so future read-routing
// is a non-breaking change.
type Config struct {
	// MasterAddr stores the Redis network address in host:port form. Required.
	MasterAddr string

	// ReplicaAddrs stores zero-or-more read-only replica addresses.
	ReplicaAddrs []string

	// Password is the mandatory connection password. Empty values are rejected
	// by Validate to enforce the architectural rule that Redis traffic is
	// password-protected even on the trusted segment.
	Password string

	// DB selects the logical Redis database index.
	DB int

	// OperationTimeout bounds individual Redis round trips.
	OperationTimeout time.Duration
}

// DefaultConfig returns the default tuning. MasterAddr and Password remain
// zero-valued and must be supplied by callers (or by LoadFromEnv).
func DefaultConfig() Config {
	return Config{
		DB:               DefaultDB,
		OperationTimeout: DefaultOperationTimeout,
	}
}

// Validate reports whether cfg is usable.
func (cfg Config) Validate() error {
	if strings.TrimSpace(cfg.MasterAddr) == "" {
		return errors.New("redis master addr must not be empty")
	}
	if strings.TrimSpace(cfg.Password) == "" {
		return errors.New("redis password must not be empty")
	}
	for index, addr := range cfg.ReplicaAddrs {
		if strings.TrimSpace(addr) == "" {
			return fmt.Errorf("redis replica addr at index %d must not be empty", index)
		}
	}
	if cfg.DB < 0 {
		return errors.New("redis db must not be negative")
	}
	if cfg.OperationTimeout <= 0 {
		return errors.New("redis operation timeout must be positive")
	}
	return nil
}

// LoadFromEnv populates Config from environment variables prefixed with
// `<prefix>_REDIS_`. The required variables are
// `<prefix>_REDIS_MASTER_ADDR` and `<prefix>_REDIS_PASSWORD`; every other
// variable falls back to DefaultConfig values.
//
// LoadFromEnv hard-fails when either of the deprecated variables
// `<prefix>_REDIS_TLS_ENABLED` or `<prefix>_REDIS_USERNAME` is set in the
// environment, with an error pointing to ARCHITECTURE.md.
func LoadFromEnv(prefix string) (Config, error) {
	if strings.TrimSpace(prefix) == "" {
		return Config{}, errors.New("redis env prefix must not be empty")
	}

	if err := rejectDeprecatedEnv(prefix); err != nil {
		return Config{}, err
	}

	cfg := DefaultConfig()

	masterName := envName(prefix, "MASTER_ADDR")
	master, ok := os.LookupEnv(masterName)
	if !ok || strings.TrimSpace(master) == "" {
		return Config{}, fmt.Errorf("%s must be set", masterName)
	}
	cfg.MasterAddr = strings.TrimSpace(master)

	passwordName := envName(prefix, "PASSWORD")
	password, ok := os.LookupEnv(passwordName)
	if !ok || strings.TrimSpace(password) == "" {
		return Config{}, fmt.Errorf("%s must be set", passwordName)
	}
	cfg.Password = strings.TrimSpace(password)

	if raw, ok := os.LookupEnv(envName(prefix, "REPLICA_ADDRS")); ok {
		cfg.ReplicaAddrs = splitCSV(raw)
	}

	db, err := loadInt(envName(prefix, "DB"), cfg.DB)
	if err != nil {
		return Config{}, err
	}
	cfg.DB = db

	timeout, err := loadDuration(envName(prefix, "OPERATION_TIMEOUT"), cfg.OperationTimeout)
	if err != nil {
		return Config{}, err
	}
	cfg.OperationTimeout = timeout

	if err := cfg.Validate(); err != nil {
		return Config{}, err
	}
	return cfg, nil
}

func rejectDeprecatedEnv(prefix string) error {
	for _, suffix := range []string{"TLS_ENABLED", "USERNAME"} {
		name := envName(prefix, suffix)
		if _, ok := os.LookupEnv(name); ok {
			return fmt.Errorf("%s is no longer supported (see ARCHITECTURE.md §Persistence Backends); unset it before starting the service", name)
		}
	}
	return nil
}

func envName(prefix, suffix string) string {
	return strings.ToUpper(strings.TrimSpace(prefix)) + "_REDIS_" + suffix
}

func splitCSV(raw string) []string {
	parts := strings.Split(raw, ",")
	out := make([]string, 0, len(parts))
	for _, part := range parts {
		trimmed := strings.TrimSpace(part)
		if trimmed == "" {
			continue
		}
		out = append(out, trimmed)
	}
	if len(out) == 0 {
		return nil
	}
	return out
}

func loadDuration(name string, fallback time.Duration) (time.Duration, error) {
	raw, ok := os.LookupEnv(name)
	if !ok {
		return fallback, nil
	}
	parsed, err := time.ParseDuration(strings.TrimSpace(raw))
	if err != nil {
		return 0, fmt.Errorf("%s: %w", name, err)
	}
	return parsed, nil
}

func loadInt(name string, fallback int) (int, error) {
	raw, ok := os.LookupEnv(name)
	if !ok {
		return fallback, nil
	}
	parsed, err := strconv.Atoi(strings.TrimSpace(raw))
	if err != nil {
		return 0, fmt.Errorf("%s: %w", name, err)
	}
	return parsed, nil
}
