// Package config loads all service configuration from environment variables.
// Both the runner and backend binaries call config.Load() at startup; each
// uses only the sub-struct relevant to it.
//
// Every field has a documented default so the service starts sensibly without
// any environment configuration (useful for local development).
package config

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

// PocketBase holds connection settings for the remote PocketBase instance.
type PocketBase struct {
	// URL is the base URL of the PocketBase instance, e.g. https://pb.libnovel.cc
	URL string
	// AdminEmail is the admin account email used for API authentication.
	AdminEmail string
	// AdminPassword is the admin account password.
	AdminPassword string
}

// MinIO holds connection settings for the remote MinIO / S3-compatible store.
type MinIO struct {
	// Endpoint is the host:port of the MinIO S3 API, e.g. storage.libnovel.cc:443
	Endpoint string
	// PublicEndpoint is the browser-visible endpoint used for presigned URLs.
	// Falls back to Endpoint when empty.
	PublicEndpoint string
	// AccessKey is the MinIO access key.
	AccessKey string
	// SecretKey is the MinIO secret key.
	SecretKey string
	// UseSSL enables TLS for the internal MinIO connection.
	UseSSL bool
	// PublicUseSSL enables TLS for presigned URL generation.
	PublicUseSSL bool
	// BucketChapters is the bucket that holds chapter markdown objects.
	BucketChapters string
	// BucketAudio is the bucket that holds generated audio MP3 objects.
	BucketAudio string
	// BucketAvatars is the bucket that holds user avatar images.
	BucketAvatars string
	// BucketBrowse is the bucket that holds cached browse page snapshots (JSON).
	BucketBrowse string
	// BucketTranslations is the bucket that holds machine-translated chapter markdown.
	BucketTranslations string
}

// Kokoro holds connection settings for the Kokoro-FastAPI TTS service.
type Kokoro struct {
	// URL is the base URL of the Kokoro service, e.g. https://tts.libnovel.cc
	// An empty string disables Kokoro TTS generation.
	URL string
	// DefaultVoice is the voice used when none is specified.
	DefaultVoice string
}

// PocketTTS holds connection settings for the kyutai-labs/pocket-tts service.
type PocketTTS struct {
	// URL is the base URL of the pocket-tts service, e.g. https://pocket-tts.libnovel.cc
	// An empty string disables pocket-tts generation.
	URL string
}

// CFAI holds credentials for Cloudflare Workers AI TTS.
type CFAI struct {
	// AccountID is the Cloudflare account ID.
	// An empty string disables CF AI generation.
	AccountID string
	// APIToken is a Workers AI API token with Workers AI Read+Edit permissions.
	APIToken string
	// Model is the Workers AI TTS model ID.
	// Defaults to "@cf/deepgram/aura-2-en" when empty.
	Model string
}

// LibreTranslate holds connection settings for a self-hosted LibreTranslate instance.
type LibreTranslate struct {
	// URL is the base URL of the LibreTranslate instance, e.g. https://translate.libnovel.cc
	// An empty string disables machine translation entirely.
	URL string
	// APIKey is the optional API key for the LibreTranslate instance.
	// Leave empty if the instance runs without authentication.
	APIKey string
}

// HTTP holds settings for the HTTP server (backend only).
type HTTP struct {
	// Addr is the listen address, e.g. ":8080"
	Addr string
}

// Meilisearch holds connection settings for the Meilisearch full-text search service.
type Meilisearch struct {
	// URL is the base URL of the Meilisearch instance, e.g. http://localhost:7700
	// An empty string disables Meilisearch indexing and search.
	URL string
	// APIKey is the Meilisearch master/search API key.
	APIKey string
}

// Valkey holds connection settings for the Valkey/Redis presign URL cache.
type Valkey struct {
	// Addr is the host:port of the Valkey instance, e.g. localhost:6379
	// An empty string disables the Valkey cache (falls through to MinIO directly).
	Addr string
}

// Redis holds connection settings for the Asynq task queue Redis instance.
// This is separate from Valkey (presign cache) — it may point to the same
// Redis or a dedicated one. An empty Addr falls back to PocketBase polling.
type Redis struct {
	// Addr is the host:port (or rediss://... URL) of the Redis instance.
	// Use rediss:// scheme for TLS (e.g. rediss://:password@redis.libnovel.cc:6380).
	// An empty string disables Asynq and falls back to PocketBase polling.
	Addr string
	// Password is the Redis AUTH password.
	// Not needed when Addr is a full rediss:// URL that includes the password.
	Password string
}

// Runner holds settings specific to the runner/worker binary.
type Runner struct {
	// PollInterval is how often the runner checks PocketBase for pending tasks.
	PollInterval time.Duration
	// MaxConcurrentScrape limits simultaneous book-scrape goroutines.
	MaxConcurrentScrape int
	// MaxConcurrentAudio limits simultaneous audio-generation goroutines.
	MaxConcurrentAudio int
	// MaxConcurrentTranslation limits simultaneous translation goroutines.
	MaxConcurrentTranslation int
	// WorkerID is a unique identifier for this runner instance.
	// Defaults to the system hostname.
	WorkerID string
	// Workers is the number of chapter-scraping goroutines per book.
	Workers int
	// Timeout is the per-request HTTP timeout for scraping.
	Timeout time.Duration
	// MetricsAddr is the listen address for the runner /metrics HTTP endpoint.
	// Defaults to ":9091". Set to "" to disable.
	MetricsAddr string
	// CatalogueRefreshInterval is how often the runner walks the full catalogue,
	// scrapes per-book metadata, downloads covers, and re-indexes in Meilisearch.
	// Defaults to 24h. Set to 0 to use the default.
	CatalogueRefreshInterval time.Duration
	// SkipInitialCatalogueRefresh prevents the runner from running a full
	// catalogue walk on startup. Useful for quick restarts where the catalogue
	// is already indexed and a 24h walk would be wasteful.
	// Controlled by RUNNER_SKIP_INITIAL_CATALOGUE_REFRESH=true.
	SkipInitialCatalogueRefresh bool
	// CatalogueRequestDelay is the base delay inserted between per-book metadata
	// requests during a catalogue refresh. A random jitter of up to 50% is added
	// on top. Defaults to 2s. Increase to reduce 429 pressure on novelfire.net.
	// Controlled by RUNNER_CATALOGUE_REQUEST_DELAY (e.g. "3s", "500ms").
	CatalogueRequestDelay time.Duration
}

// Config is the top-level configuration struct consumed by both binaries.
type Config struct {
	PocketBase     PocketBase
	MinIO          MinIO
	Kokoro         Kokoro
	PocketTTS      PocketTTS
	CFAI           CFAI
	LibreTranslate LibreTranslate
	HTTP           HTTP
	Runner         Runner
	Meilisearch    Meilisearch
	Valkey         Valkey
	Redis          Redis
	// LogLevel is one of "debug", "info", "warn", "error".
	LogLevel string
}

// Load reads all configuration from environment variables and returns a
// populated Config. Missing variables fall back to documented defaults.
func Load() Config {
	workerID, _ := os.Hostname()
	if workerID == "" {
		workerID = "runner-default"
	}

	return Config{
		LogLevel: envOr("LOG_LEVEL", "info"),

		PocketBase: PocketBase{
			URL:           envOr("POCKETBASE_URL", "http://localhost:8090"),
			AdminEmail:    envOr("POCKETBASE_ADMIN_EMAIL", "admin@libnovel.local"),
			AdminPassword: envOr("POCKETBASE_ADMIN_PASSWORD", "changeme123"),
		},

		MinIO: MinIO{
			Endpoint:           envOr("MINIO_ENDPOINT", "localhost:9000"),
			PublicEndpoint:     envOr("MINIO_PUBLIC_ENDPOINT", ""),
			AccessKey:          envOr("MINIO_ACCESS_KEY", "admin"),
			SecretKey:          envOr("MINIO_SECRET_KEY", "changeme123"),
			UseSSL:             envBool("MINIO_USE_SSL", false),
			PublicUseSSL:       envBool("MINIO_PUBLIC_USE_SSL", true),
			BucketChapters:     envOr("MINIO_BUCKET_CHAPTERS", "chapters"),
			BucketAudio:        envOr("MINIO_BUCKET_AUDIO", "audio"),
			BucketAvatars:      envOr("MINIO_BUCKET_AVATARS", "avatars"),
			BucketBrowse:       envOr("MINIO_BUCKET_BROWSE", "catalogue"),
			BucketTranslations: envOr("MINIO_BUCKET_TRANSLATIONS", "translations"),
		},

		Kokoro: Kokoro{
			URL:          envOr("KOKORO_URL", ""),
			DefaultVoice: envOr("KOKORO_VOICE", "af_bella"),
		},

		PocketTTS: PocketTTS{
			URL: envOr("POCKET_TTS_URL", ""),
		},

		CFAI: CFAI{
			AccountID: envOr("CFAI_ACCOUNT_ID", ""),
			APIToken:  envOr("CFAI_API_TOKEN", ""),
			Model:     envOr("CFAI_TTS_MODEL", ""),
		},

		LibreTranslate: LibreTranslate{
			URL:    envOr("LIBRETRANSLATE_URL", ""),
			APIKey: envOr("LIBRETRANSLATE_API_KEY", ""),
		},

		HTTP: HTTP{
			Addr: envOr("BACKEND_HTTP_ADDR", ":8080"),
		},

		Runner: Runner{
			PollInterval:                envDuration("RUNNER_POLL_INTERVAL", 30*time.Second),
			MaxConcurrentScrape:         envInt("RUNNER_MAX_CONCURRENT_SCRAPE", 1),
			MaxConcurrentAudio:          envInt("RUNNER_MAX_CONCURRENT_AUDIO", 1),
			MaxConcurrentTranslation:    envInt("RUNNER_MAX_CONCURRENT_TRANSLATION", 1),
			WorkerID:                    envOr("RUNNER_WORKER_ID", workerID),
			Workers:                     envInt("RUNNER_WORKERS", 0), // 0 → runtime.NumCPU()
			Timeout:                     envDuration("RUNNER_TIMEOUT", 90*time.Second),
			MetricsAddr:                 envOr("RUNNER_METRICS_ADDR", ":9091"),
			CatalogueRefreshInterval:    envDuration("RUNNER_CATALOGUE_REFRESH_INTERVAL", 0),
			SkipInitialCatalogueRefresh: envBool("RUNNER_SKIP_INITIAL_CATALOGUE_REFRESH", false),
			CatalogueRequestDelay:       envDuration("RUNNER_CATALOGUE_REQUEST_DELAY", 2*time.Second),
		},

		Meilisearch: Meilisearch{
			URL:    envOr("MEILI_URL", ""),
			APIKey: envOr("MEILI_API_KEY", ""),
		},

		Valkey: Valkey{
			Addr: envOr("VALKEY_ADDR", ""),
		},

		Redis: Redis{
			Addr:     envOr("REDIS_ADDR", ""),
			Password: envOr("REDIS_PASSWORD", ""),
		},
	}
}

// ── helpers ───────────────────────────────────────────────────────────────────

func envOr(key, fallback string) string {
	if v := os.Getenv(key); v != "" {
		return v
	}
	return fallback
}

func envBool(key string, fallback bool) bool {
	v := os.Getenv(key)
	if v == "" {
		return fallback
	}
	return strings.ToLower(v) == "true"
}

func envInt(key string, fallback int) int {
	v := os.Getenv(key)
	if v == "" {
		return fallback
	}
	n, err := strconv.Atoi(v)
	if err != nil || n < 0 {
		return fallback
	}
	return n
}

func envDuration(key string, fallback time.Duration) time.Duration {
	v := os.Getenv(key)
	if v == "" {
		return fallback
	}
	d, err := time.ParseDuration(v)
	if err != nil {
		return fallback
	}
	return d
}
