package database

// SettingsRegistry provides a typed, cached view over the system_settings table.
// It is the single source of truth for tunable runtime values that used to be
// hardcoded as Go literals.
//
// Consumers call the domain-specific getters (SessionDuration, OpdsPageSize,
// etc.) which read from an in-memory cache. The cache is populated by Load at
// startup and refreshed by Reload whenever a setting is written. Getters always
// fall back to a compiled-in default if the DB value is missing or unparsable,
// so a corrupt or deleted row can never break the app.
//
// SettingsRegistry lives in the database package (rather than its own package)
// so that every consumer already imports database and does not need to take on
// a new package import.

import (
	"context"
	"log"
	"strconv"
	"sync"
	"time"
)

// SettingType enumerates the value types stored in system_settings.setting_type.
const (
	SettingTypeInt       = "int"
	SettingTypeBool      = "bool"
	SettingTypeString    = "string"
	SettingTypeStringList = "string_list"
)

// SecondsPerDay / SecondsPerHour are conversion helpers used by defaults.
const (
	SecondsPerMinute = 60
	SecondsPerHour   = 3600
	SecondsPerDay    = 86400
)

// SettingDefault holds the fallback value for a key. These mirror the literals that
// were previously hardcoded in the source so an empty/corrupt DB row preserves
// prior behavior exactly.
type SettingDefault struct {
	Key             string
	Value           string
	Type            string
	Min             string
	Max             string
	RequiresRestart bool
	Category        string
	Group           string
	Description     string
}

// SettingDefaults is the source of truth for fallback values and metadata. New keys
// must be added here AND seeded in database/schema/schema.sql. Entries are ordered
// by (RequiresRestart, Group) so the admin UI renders coherent sub-sections.
var SettingDefaults = []SettingDefault{
	{Key: "scan_poll_interval_seconds", Value: "60", Type: SettingTypeInt, Min: "1", Max: "3600", Category: "scanner", Group: "Scanning", Description: "How often to scan all libraries (seconds)"},
	{Key: "auto_scan_enabled", Value: "true", Type: SettingTypeBool, Category: "scanner", Group: "Scanning", Description: "Whether auto-scanning is enabled system-wide"},
	{Key: "default_timezone", Value: "UTC", Type: SettingTypeString, Category: "general", Group: "System Defaults", Description: "System default timezone"},
	{Key: "session_duration_seconds", Value: "604800", Type: SettingTypeInt, Min: "300", Max: "31536000", Category: "security", Group: "Session", Description: "How long a login session stays valid"},
	{Key: "password_min_length", Value: "8", Type: SettingTypeInt, Min: "1", Max: "128", Category: "security", Group: "Password Quality", Description: "Minimum password length"},
	{Key: "password_require_upper", Value: "true", Type: SettingTypeBool, Category: "security", Group: "Password Quality", Description: "Require at least one uppercase letter (A-Z)"},
	{Key: "password_require_lower", Value: "true", Type: SettingTypeBool, Category: "security", Group: "Password Quality", Description: "Require at least one lowercase letter (a-z)"},
	{Key: "password_require_number", Value: "true", Type: SettingTypeBool, Category: "security", Group: "Password Quality", Description: "Require at least one number (0-9)"},
	{Key: "password_require_special", Value: "true", Type: SettingTypeBool, Category: "security", Group: "Password Quality", Description: "Require at least one special character"},
	{Key: "opds_default_page_size", Value: "50", Type: SettingTypeInt, Min: "1", Max: "500", Category: "api", Group: "OPDS Catalog", Description: "Default OPDS page size"},
	{Key: "opds_max_page_size", Value: "200", Type: SettingTypeInt, Min: "1", Max: "1000", Category: "api", Group: "OPDS Catalog", Description: "Maximum OPDS page size"},
	{Key: "device_rate_sync_per_min", Value: "60", Type: SettingTypeInt, Min: "1", Max: "10000", Category: "api", Group: "Device Rate Limits", Description: "Device sync requests per minute"},
	{Key: "device_rate_progress_per_min", Value: "120", Type: SettingTypeInt, Min: "1", Max: "10000", Category: "api", Group: "Device Rate Limits", Description: "Device progress requests per minute"},
	{Key: "device_rate_metadata_per_min", Value: "30", Type: SettingTypeInt, Min: "1", Max: "10000", Category: "api", Group: "Device Rate Limits", Description: "Device metadata requests per minute"},
	{Key: "annotation_tombstone_ttl_days", Value: "30", Type: SettingTypeInt, Min: "1", Max: "3650", Category: "sync", Group: "Annotation Retention", Description: "How long deleted annotations are kept before purge"},
	{Key: "conversion_cache_ttl_hours", Value: "24", Type: SettingTypeInt, Min: "1", Max: "720", Category: "performance", Group: "Conversion Cache", Description: "How long converted (kepub) files are cached"},
	{Key: "auth_rate_limit_per_min", Value: "10", Type: SettingTypeInt, Min: "1", Max: "10000", RequiresRestart: true, Category: "security", Group: "Auth Rate Limiting", Description: "Global auth API rate limit (requests per minute)"},
	{Key: "login_max_attempts", Value: "5", Type: SettingTypeInt, Min: "1", Max: "100", RequiresRestart: true, Category: "security", Group: "Login Lockout", Description: "Failed login attempts before lockout"},
	{Key: "login_lockout_minutes", Value: "15", Type: SettingTypeInt, Min: "1", Max: "10080", RequiresRestart: true, Category: "security", Group: "Login Lockout", Description: "Lockout duration after too many failed logins"},
	{Key: "sync_queue_interval_seconds", Value: "5", Type: SettingTypeInt, Min: "1", Max: "3600", RequiresRestart: true, Category: "sync", Group: "Sync Queue", Description: "How often the sync queue flushes"},
	{Key: "sync_queue_batch_size", Value: "50", Type: SettingTypeInt, Min: "1", Max: "10000", RequiresRestart: true, Category: "sync", Group: "Sync Queue", Description: "Maximum items processed per sync queue flush"},
	{Key: "worker_pool_size", Value: "3", Type: SettingTypeInt, Min: "1", Max: "100", RequiresRestart: true, Category: "performance", Group: "Worker Pool", Description: "Number of background worker goroutines"},
	{Key: "worker_queue_cap", Value: "100", Type: SettingTypeInt, Min: "1", Max: "10000", RequiresRestart: true, Category: "performance", Group: "Worker Pool", Description: "Background worker job queue capacity"},
}

// defaultBy indexes SettingDefaults by key for O(1) lookup.
var defaultBy = func() map[string]SettingDefault {
	m := make(map[string]SettingDefault, len(SettingDefaults))
	for _, d := range SettingDefaults {
		m[d.Key] = d
	}
	return m
}()

// Registry caches system_settings values in memory. The zero value is not
// usable; construct with New.
type SettingsRegistry struct {
	q *Queries

	mu       sync.RWMutex
	values   map[string]string
	loadedAt time.Time
}

// New returns a Registry backed by the given queries. The cache is empty
// until Load is called.
func NewSettingsRegistry(q *Queries) *SettingsRegistry {
	return &SettingsRegistry{q: q, values: make(map[string]string)}
}

// Load populates the cache from the database. Missing rows fall back to the
// compiled defaults. Safe to call multiple times.
func (r *SettingsRegistry) Load(ctx context.Context) error {
	rows, err := r.q.GetAllSystemSettings(ctx)
	if err != nil {
		return err
	}
	fresh := make(map[string]string, len(SettingDefaults))
	for _, d := range SettingDefaults {
		fresh[d.Key] = d.Value
	}
	for _, row := range rows {
		if _, ok := fresh[row.SettingKey]; ok {
			fresh[row.SettingKey] = row.SettingValue
		}
	}
	r.mu.Lock()
	r.values = fresh
	r.loadedAt = time.Now()
	r.mu.Unlock()
	return nil
}

// Reload refreshes the cache from the database. Should be called after any
// setting write. On error the cache is left untouched and the error is logged.
func (r *SettingsRegistry) Reload(ctx context.Context) {
	if err := r.Load(ctx); err != nil {
		log.Printf("settings: reload failed: %v", err)
	}
}

// raw returns the cached string value for a key (or the default), clamped to
// [min, max] for int-typed keys.
func (r *SettingsRegistry) raw(key string) string {
	r.mu.RLock()
	v, ok := r.values[key]
	r.mu.RUnlock()
	if !ok || v == "" {
		v = defaultBy[key].Value
	}
	return v
}

func (r *SettingsRegistry) getInt(key string) int {
	d := defaultBy[key]
	v := r.raw(key)
	n, err := strconv.Atoi(v)
	if err != nil {
		n, _ = strconv.Atoi(d.Value)
	}
	if d.Min != "" {
		if mn, err := strconv.Atoi(d.Min); err == nil && n < mn {
			n = mn
		}
	}
	if d.Max != "" {
		if mx, err := strconv.Atoi(d.Max); err == nil && n > mx {
			n = mx
		}
	}
	return n
}

func (r *SettingsRegistry) getBool(key string) bool {
	v := r.raw(key)
	b, err := strconv.ParseBool(v)
	if err != nil {
		b, _ = strconv.ParseBool(defaultBy[key].Value)
	}
	return b
}

// ---- Domain-specific getters (call sites use these) ----

// ScanPollInterval is how often the scanner polls, as a duration.
func (r *SettingsRegistry) ScanPollInterval() time.Duration {
	return time.Duration(r.getInt("scan_poll_interval_seconds")) * time.Second
}

// AutoScanEnabled reports whether auto-scanning is on.
func (r *SettingsRegistry) AutoScanEnabled() bool { return r.getBool("auto_scan_enabled") }

// DefaultTimezone returns the configured default timezone name.
func (r *SettingsRegistry) DefaultTimezone() string { return r.raw("default_timezone") }

// SessionDuration is how long a login session / refresh token stays valid.
func (r *SettingsRegistry) SessionDuration() time.Duration {
	return time.Duration(r.getInt("session_duration_seconds")) * time.Second
}

// PasswordMinLength is the minimum password length.
func (r *SettingsRegistry) PasswordMinLength() int { return r.getInt("password_min_length") }

// PasswordRules bundles the active complexity requirements.
type PasswordRules struct {
	MinLength int
	Upper     bool
	Lower     bool
	Number    bool
	Special   bool
}

// PasswordRules returns the active password complexity configuration.
func (r *SettingsRegistry) PasswordRules() PasswordRules {
	return PasswordRules{
		MinLength: r.PasswordMinLength(),
		Upper:     r.getBool("password_require_upper"),
		Lower:     r.getBool("password_require_lower"),
		Number:    r.getBool("password_require_number"),
		Special:   r.getBool("password_require_special"),
	}
}

// AuthRateLimit is the global auth endpoint rate limit (requests/minute). Read
// once at startup.
func (r *SettingsRegistry) AuthRateLimit() int { return r.getInt("auth_rate_limit_per_min") }

// LoginLockout returns (max attempts, lockout duration). Read once at startup.
func (r *SettingsRegistry) LoginLockout() (int, time.Duration) {
	return r.getInt("login_max_attempts"), time.Duration(r.getInt("login_lockout_minutes")) * time.Minute
}

// OpdsDefaultPageSize is the default OPDS items-per-page.
func (r *SettingsRegistry) OpdsDefaultPageSize() int { return r.getInt("opds_default_page_size") }

// OpdsMaxPageSize is the maximum items-per-page a client may request.
func (r *SettingsRegistry) OpdsMaxPageSize() int { return r.getInt("opds_max_page_size") }

// DeviceRateLimits bundles the per-route device rate limits (requests/minute).
type DeviceRateLimits struct {
	Sync     int
	Progress int
	Metadata int
}

// DeviceRateLimits returns the active device rate limits.
func (r *SettingsRegistry) DeviceRateLimits() DeviceRateLimits {
	return DeviceRateLimits{
		Sync:     r.getInt("device_rate_sync_per_min"),
		Progress: r.getInt("device_rate_progress_per_min"),
		Metadata: r.getInt("device_rate_metadata_per_min"),
	}
}

// TombstoneTTL is how long deleted annotations are retained before purge.
func (r *SettingsRegistry) TombstoneTTL() time.Duration {
	return time.Duration(r.getInt("annotation_tombstone_ttl_days")) * 24 * time.Hour
}

// ConversionCacheTTL is how long converted (kepub) files are served from cache.
func (r *SettingsRegistry) ConversionCacheTTL() time.Duration {
	return time.Duration(r.getInt("conversion_cache_ttl_hours")) * time.Hour
}

// SyncQueueConfig bundles the sync queue interval and batch size. Read at
// startup; changes require a restart.
type SyncQueueConfig struct {
	Interval  time.Duration
	BatchSize int
}

// SyncQueueConfig returns the active sync queue configuration.
func (r *SettingsRegistry) SyncQueueConfig() SyncQueueConfig {
	return SyncQueueConfig{
		Interval:  time.Duration(r.getInt("sync_queue_interval_seconds")) * time.Second,
		BatchSize: r.getInt("sync_queue_batch_size"),
	}
}

// WorkerPoolConfig bundles worker count and queue capacity. Read at startup;
// changes require a restart.
type WorkerPoolConfig struct {
	Size   int
	QueueCap int
}

// WorkerPoolConfig returns the active worker pool configuration.
func (r *SettingsRegistry) WorkerPoolConfig() WorkerPoolConfig {
	return WorkerPoolConfig{
		Size:     r.getInt("worker_pool_size"),
		QueueCap: r.getInt("worker_queue_cap"),
	}
}

// SettingEntry exposes one setting's metadata + current value, for the admin UI/API.
type SettingEntry struct {
	Key             string `json:"key"`
	Value           string `json:"value"`
	Type            string `json:"type"`
	Min             string `json:"min,omitempty"`
	Max             string `json:"max,omitempty"`
	RequiresRestart bool   `json:"requires_restart"`
	Category        string `json:"category"`
	Group           string `json:"group"`
	Description     string `json:"description"`
	IsDefault       bool   `json:"is_default"`
}

// All returns metadata + current values for every known setting, grouped by
// the in-memory cache (which reflects the DB after Load/Reload).
func (r *SettingsRegistry) All() []SettingEntry {
	r.mu.RLock()
	vals := make(map[string]string, len(r.values))
	for k, v := range r.values {
		vals[k] = v
	}
	r.mu.RUnlock()
	out := make([]SettingEntry, 0, len(SettingDefaults))
	for _, d := range SettingDefaults {
		v, ok := vals[d.Key]
		if !ok {
			v = d.Value
		}
		out = append(out, SettingEntry{
			Key:             d.Key,
			Value:           v,
			Type:            d.Type,
			Min:             d.Min,
			Max:             d.Max,
			RequiresRestart: d.RequiresRestart,
			Category:        d.Category,
			Group:           d.Group,
			Description:     d.Description,
			IsDefault:       v == d.Value,
		})
	}
	return out
}

// LookupDefault returns the compiled-in SettingDefault for a key (ok=false if unknown).
func LookupDefault(key string) (SettingDefault, bool) {
	d, ok := defaultBy[key]
	return d, ok
}
