// Package postgres provides shared helpers for opening, instrumenting and
// migrating PostgreSQL backends used by Galaxy services.
//
// The package codifies the steady-state rules captured in `ARCHITECTURE.md`
// `§Persistence Backends`: services connect through `database/sql` driven by
// the pgx driver, apply embedded goose migrations at startup, and expose
// statement spans plus pool metrics via OpenTelemetry.
package postgres

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

// Default configuration values applied by DefaultConfig and LoadFromEnv when
// the corresponding environment variable is absent.
const (
	DefaultOperationTimeout = 1 * time.Second
	DefaultMaxOpenConns     = 25
	DefaultMaxIdleConns     = 5
	DefaultConnMaxLifetime  = 30 * time.Minute
)

// Config stores the connection and pool tuning used to open a primary plus
// zero-or-more replica `*sql.DB` instances. The replica list is preserved
// so future read-routing is a non-breaking change.
type Config struct {
	// PrimaryDSN stores the DSN used by the primary connection. Required.
	PrimaryDSN string

	// ReplicaDSNs stores zero-or-more read-only replica DSNs.
	ReplicaDSNs []string

	// OperationTimeout bounds startup operations such as Ping and individual
	// pgx connect attempts.
	OperationTimeout time.Duration

	// MaxOpenConns caps the maximum number of open connections per pool.
	MaxOpenConns int

	// MaxIdleConns caps the maximum number of idle connections per pool.
	MaxIdleConns int

	// ConnMaxLifetime bounds the lifetime of an individual connection.
	ConnMaxLifetime time.Duration
}

// DefaultConfig returns the default tuning. PrimaryDSN and ReplicaDSNs remain
// zero-valued and must be supplied by callers (or by LoadFromEnv).
func DefaultConfig() Config {
	return Config{
		OperationTimeout: DefaultOperationTimeout,
		MaxOpenConns:     DefaultMaxOpenConns,
		MaxIdleConns:     DefaultMaxIdleConns,
		ConnMaxLifetime:  DefaultConnMaxLifetime,
	}
}

// Validate reports whether cfg is usable. DSN strings are checked for
// non-emptiness only; full pgx parsing happens at OpenPrimary/OpenReplicas
// time so callers see a single failure point.
func (cfg Config) Validate() error {
	if strings.TrimSpace(cfg.PrimaryDSN) == "" {
		return errors.New("postgres primary DSN must not be empty")
	}
	for index, dsn := range cfg.ReplicaDSNs {
		if strings.TrimSpace(dsn) == "" {
			return fmt.Errorf("postgres replica DSN at index %d must not be empty", index)
		}
	}
	if cfg.OperationTimeout <= 0 {
		return errors.New("postgres operation timeout must be positive")
	}
	if cfg.MaxOpenConns <= 0 {
		return errors.New("postgres max open conns must be positive")
	}
	if cfg.MaxIdleConns < 0 {
		return errors.New("postgres max idle conns must not be negative")
	}
	if cfg.MaxIdleConns > cfg.MaxOpenConns {
		return errors.New("postgres max idle conns must not exceed max open conns")
	}
	if cfg.ConnMaxLifetime <= 0 {
		return errors.New("postgres conn max lifetime must be positive")
	}
	return nil
}

// LoadFromEnv populates Config from environment variables prefixed with
// `<prefix>_POSTGRES_`. The required variable is `<prefix>_POSTGRES_PRIMARY_DSN`;
// every other variable falls back to DefaultConfig values.
//
// Example variable set for prefix "USERSERVICE":
//
//	USERSERVICE_POSTGRES_PRIMARY_DSN=postgres://userservice:secret@host:5432/galaxy?search_path=user&sslmode=disable
//	USERSERVICE_POSTGRES_REPLICA_DSNS=postgres://...,postgres://...
//	USERSERVICE_POSTGRES_OPERATION_TIMEOUT=1s
//	USERSERVICE_POSTGRES_MAX_OPEN_CONNS=25
//	USERSERVICE_POSTGRES_MAX_IDLE_CONNS=5
//	USERSERVICE_POSTGRES_CONN_MAX_LIFETIME=30m
func LoadFromEnv(prefix string) (Config, error) {
	if strings.TrimSpace(prefix) == "" {
		return Config{}, errors.New("postgres env prefix must not be empty")
	}

	cfg := DefaultConfig()

	primaryName := envName(prefix, "PRIMARY_DSN")
	primary, ok := os.LookupEnv(primaryName)
	if !ok || strings.TrimSpace(primary) == "" {
		return Config{}, fmt.Errorf("%s must be set", primaryName)
	}
	cfg.PrimaryDSN = strings.TrimSpace(primary)

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

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

	maxOpen, err := loadInt(envName(prefix, "MAX_OPEN_CONNS"), cfg.MaxOpenConns)
	if err != nil {
		return Config{}, err
	}
	cfg.MaxOpenConns = maxOpen

	maxIdle, err := loadInt(envName(prefix, "MAX_IDLE_CONNS"), cfg.MaxIdleConns)
	if err != nil {
		return Config{}, err
	}
	cfg.MaxIdleConns = maxIdle

	connLifetime, err := loadDuration(envName(prefix, "CONN_MAX_LIFETIME"), cfg.ConnMaxLifetime)
	if err != nil {
		return Config{}, err
	}
	cfg.ConnMaxLifetime = connLifetime

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

func envName(prefix, suffix string) string {
	return strings.ToUpper(strings.TrimSpace(prefix)) + "_POSTGRES_" + 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
}
