// Package config loads the gateway's settings from the environment.
package config

import (
	"encoding/json"
	"fmt"
	"os"
	"strconv"
	"strings"
	"time"
	_ "time/tzdata"
)

type Config struct {
	ListenAddr string

	// EmbyURL is how the gateway itself reaches Emby (may be a private/docker address).
	EmbyURL string
	// EmbyPublicURL is the address handed to TV clients for direct video playback.
	// Defaults to EmbyURL; set it when the gateway talks to Emby over a network the
	// TVs cannot reach.
	EmbyPublicURL string

	DatabaseURL string
	RedisURL    string

	// ClientName is reported to Emby in the X-Emby-Authorization header, so sessions
	// show up as this in Emby's dashboard. It is deliberately not the app's own name:
	// the header travels to whatever Emby does with its logs, and the product name has
	// no business being the thing that identifies a client to a third party.
	ClientName string

	HomeTTL        time.Duration
	ItemTTL        time.Duration
	SearchTTL      time.Duration
	ScreensaverTTL time.Duration
	SessionTTL     time.Duration
	// SessionIdleExpiry retires gateway tokens that go unused for this long.
	SessionIdleExpiry time.Duration
	// RecommendTTL is how long computed recommendation rows stay warm. Long, because
	// taste moves slowly and each rebuild costs several Emby queries.
	RecommendTTL time.Duration
	// RecommendTimeout bounds a background rebuild, which fans out further than a
	// normal request and so needs more headroom than UpstreamTimeout.
	RecommendTimeout time.Duration
	// RecommendationWeights is an optional JSON overlay on the weighted defaults.
	RecommendationWeights string

	UpstreamTimeout time.Duration

	// EmbyHealthInterval paces the reachability probe behind the "server not responding"
	// and "back online" banners. One probe per gateway, not per TV. Zero disables it.
	EmbyHealthInterval time.Duration

	// AdminToken guards the operator interface. Empty disables /admin entirely, so an
	// unconfigured deployment cannot leave it exposed.
	AdminToken string

	// PublicURL is the externally reachable Memby gateway address used in update links.
	PublicURL string
	// ReleaseDir persists signed APKs published by CI.
	ReleaseDir string
	// ReleasePublishToken authorizes the CI-only release upload endpoint. It is separate
	// from AdminToken so a compromised build runner cannot change maintenance settings.
	ReleasePublishToken string

	// SyncInterval is how often the library import runs. Zero disables the schedule.
	SyncInterval time.Duration
	// SyncTimeout bounds one import; a full pass over a large library is slow.
	SyncTimeout time.Duration
	// SyncOnStart triggers an incremental import at boot.
	SyncOnStart bool
	// SyncUserID / SyncAPIKey are an optional Emby service account for imports. Without
	// them the newest TV session is borrowed instead.
	SyncUserID string
	SyncAPIKey string

	// AnalyticsRetention is how long raw row events are kept before being pruned.
	AnalyticsRetention time.Duration

	// Sonarr is optional. When configured, its local calendar supplies the informational
	// Five-day "Shows airing" home row. The API key never leaves this server.
	SonarrURL      string
	SonarrAPIKey   string
	SonarrTTL      time.Duration
	SonarrLocation *time.Location

	// SonarrAlertWindow is how long after an episode airs the "aired, coming soon"
	// banner keeps being offered to clients. Zero turns the banners off without
	// touching the five-day schedule row.
	SonarrAlertWindow time.Duration

	// Radarr is optional. Its calendar supplies the five-day digital movie release row.
	RadarrURL      string
	RadarrAPIKey   string
	RadarrTTL      time.Duration
	RadarrLocation *time.Location

	// RadarrWebhookToken guards the Radarr "on import" webhook. Empty means the hook
	// 404s, so an unconfigured deployment cannot be fed alerts by anyone who finds it.
	RadarrWebhookToken string
	// RadarrAlertWindow is how long an imported movie keeps being announced to clients.
	// It is a window rather than a one-shot push because the gateway has no connection
	// to a TV that is switched off: a set turned on inside the window still gets the
	// news, and each TV dedupes by alert id. Zero turns the banners off.
	RadarrAlertWindow time.Duration

	// Bazarr is optional and is what lets a viewer fetch a subtitle a title does not have.
	// It writes the file beside the media, so Emby serves the result and the gateway needs
	// no storage of its own. Unset means the player simply never offers the option.
	BazarrURL    string
	BazarrAPIKey string
	// BazarrTTL is how long the movie/series/episode listings are cached. They exist only
	// to turn an Emby item into the *arr id Bazarr keys on, and a household's library does
	// not change between one subtitle search and the next.
	BazarrTTL time.Duration
	// BazarrTimeout bounds a manual search, which queries live subtitle providers and is
	// legitimately slow. It is separate from BazarrTTL because a short HTTP timeout here
	// shows up as "no subtitles found" rather than as an error.
	BazarrTimeout time.Duration

	// Tracearr is an optional, read-only source of completion, session-length and
	// direct-play signals for the per-user For You area. The public API key stays in
	// the gateway and is never returned to a TV.
	TracearrURL      string
	TracearrAPIKey   string
	TracearrServerID string
	// TracearrSyncInterval imports recent changed sessions. FullInterval reconciles
	// late/out-of-order updates and deletions without needing a source cursor.
	TracearrSyncInterval time.Duration
	TracearrFullInterval time.Duration
	// Prepared pools rebuild daily at RebuildHour in the configured timezone. The age
	// settings are safety bounds for manual/background fallback paths.
	ForYouMinRebuildAge   time.Duration
	ForYouRefreshInterval time.Duration
	ForYouRebuildHour     int
}

func Load() (Config, error) {
	c := Config{
		ListenAddr:        env("MEMBY_LISTEN_ADDR", ":8080"),
		EmbyURL:           strings.TrimRight(os.Getenv("MEMBY_EMBY_URL"), "/"),
		EmbyPublicURL:     strings.TrimRight(os.Getenv("MEMBY_EMBY_PUBLIC_URL"), "/"),
		DatabaseURL:       os.Getenv("MEMBY_DATABASE_URL"),
		RedisURL:          env("MEMBY_REDIS_URL", "redis://localhost:6379/0"),
		ClientName:        env("MEMBY_CLIENT_NAME", "MbyATV"),
		HomeTTL:           duration("MEMBY_HOME_TTL", 60*time.Second),
		ItemTTL:           duration("MEMBY_ITEM_TTL", 10*time.Minute),
		SearchTTL:         duration("MEMBY_SEARCH_TTL", 5*time.Minute),
		ScreensaverTTL:    duration("MEMBY_SCREENSAVER_TTL", 10*time.Minute),
		SessionTTL:        duration("MEMBY_SESSION_CACHE_TTL", 5*time.Minute),
		SessionIdleExpiry: duration("MEMBY_SESSION_IDLE_EXPIRY", 90*24*time.Hour),
		RecommendTTL:      duration("MEMBY_RECOMMEND_TTL", 24*time.Hour),
		RecommendTimeout:  duration("MEMBY_RECOMMEND_TIMEOUT", 60*time.Second),
		RecommendationWeights: strings.TrimSpace(
			os.Getenv("MEMBY_RECOMMENDATION_WEIGHTS"),
		),

		AdminToken: strings.TrimSpace(os.Getenv("MEMBY_ADMIN_TOKEN")),
		PublicURL:  strings.TrimRight(strings.TrimSpace(os.Getenv("MEMBY_PUBLIC_URL")), "/"),
		ReleaseDir: env("MEMBY_RELEASE_DIR", "/data/releases"),
		ReleasePublishToken: strings.TrimSpace(
			os.Getenv("MEMBY_RELEASE_PUBLISH_TOKEN"),
		),
		SyncInterval:          duration("MEMBY_SYNC_INTERVAL", time.Hour),
		SyncTimeout:           duration("MEMBY_SYNC_TIMEOUT", 30*time.Minute),
		SyncOnStart:           boolean("MEMBY_SYNC_ON_START", false),
		SyncUserID:            strings.TrimSpace(os.Getenv("MEMBY_SYNC_USER_ID")),
		SyncAPIKey:            strings.TrimSpace(os.Getenv("MEMBY_SYNC_API_KEY")),
		AnalyticsRetention:    duration("MEMBY_ANALYTICS_RETENTION", 90*24*time.Hour),
		UpstreamTimeout:       duration("MEMBY_UPSTREAM_TIMEOUT", 20*time.Second),
		EmbyHealthInterval:    duration("MEMBY_EMBY_HEALTH_INTERVAL", 60*time.Second),
		SonarrURL:             strings.TrimRight(strings.TrimSpace(os.Getenv("MEMBY_SONARR_URL")), "/"),
		SonarrAPIKey:          strings.TrimSpace(os.Getenv("MEMBY_SONARR_API_KEY")),
		SonarrTTL:             duration("MEMBY_SONARR_TTL", 5*time.Minute),
		SonarrAlertWindow:     duration("MEMBY_SONARR_ALERT_WINDOW", 3*time.Hour),
		RadarrURL:             strings.TrimRight(strings.TrimSpace(os.Getenv("MEMBY_RADARR_URL")), "/"),
		RadarrAPIKey:          strings.TrimSpace(os.Getenv("MEMBY_RADARR_API_KEY")),
		RadarrTTL:             duration("MEMBY_RADARR_TTL", 5*time.Minute),
		RadarrWebhookToken:    strings.TrimSpace(os.Getenv("MEMBY_RADARR_WEBHOOK_TOKEN")),
		RadarrAlertWindow:     duration("MEMBY_RADARR_ALERT_WINDOW", 3*time.Hour),
		BazarrURL:             strings.TrimRight(strings.TrimSpace(os.Getenv("MEMBY_BAZARR_URL")), "/"),
		BazarrAPIKey:          strings.TrimSpace(os.Getenv("MEMBY_BAZARR_API_KEY")),
		BazarrTTL:             duration("MEMBY_BAZARR_TTL", 5*time.Minute),
		BazarrTimeout:         duration("MEMBY_BAZARR_TIMEOUT", 45*time.Second),
		TracearrURL:           strings.TrimRight(strings.TrimSpace(os.Getenv("MEMBY_TRACEARR_URL")), "/"),
		TracearrAPIKey:        strings.TrimSpace(os.Getenv("MEMBY_TRACEARR_API_KEY")),
		TracearrServerID:      strings.TrimSpace(os.Getenv("MEMBY_TRACEARR_SERVER_ID")),
		TracearrSyncInterval:  duration("MEMBY_TRACEARR_SYNC_INTERVAL", 5*time.Minute),
		TracearrFullInterval:  duration("MEMBY_TRACEARR_FULL_INTERVAL", 24*time.Hour),
		ForYouMinRebuildAge:   duration("MEMBY_FOR_YOU_MIN_REBUILD_AGE", 24*time.Hour),
		ForYouRefreshInterval: duration("MEMBY_FOR_YOU_REFRESH_INTERVAL", 24*time.Hour),
		ForYouRebuildHour:     integer("MEMBY_FOR_YOU_REBUILD_HOUR", 4),
	}

	if c.EmbyURL == "" {
		return c, fmt.Errorf("MEMBY_EMBY_URL is required")
	}
	if c.DatabaseURL == "" {
		return c, fmt.Errorf("MEMBY_DATABASE_URL is required")
	}
	if c.EmbyPublicURL == "" {
		c.EmbyPublicURL = c.EmbyURL
	}
	if c.ReleasePublishToken != "" && c.PublicURL == "" {
		return c, fmt.Errorf("MEMBY_PUBLIC_URL is required when release publishing is enabled")
	}
	if (c.SonarrURL == "") != (c.SonarrAPIKey == "") {
		return c, fmt.Errorf("MEMBY_SONARR_URL and MEMBY_SONARR_API_KEY must be set together")
	}
	if (c.RadarrURL == "") != (c.RadarrAPIKey == "") {
		return c, fmt.Errorf("MEMBY_RADARR_URL and MEMBY_RADARR_API_KEY must be set together")
	}
	if (c.BazarrURL == "") != (c.BazarrAPIKey == "") {
		return c, fmt.Errorf("MEMBY_BAZARR_URL and MEMBY_BAZARR_API_KEY must be set together")
	}
	if (c.TracearrURL == "") != (c.TracearrAPIKey == "") {
		return c, fmt.Errorf("MEMBY_TRACEARR_URL and MEMBY_TRACEARR_API_KEY must be set together")
	}
	if c.ForYouRebuildHour < 0 || c.ForYouRebuildHour > 23 {
		return c, fmt.Errorf("MEMBY_FOR_YOU_REBUILD_HOUR must be between 0 and 23")
	}
	if c.RecommendationWeights != "" && !json.Valid([]byte(c.RecommendationWeights)) {
		return c, fmt.Errorf("MEMBY_RECOMMENDATION_WEIGHTS must be valid JSON")
	}
	location, err := time.LoadLocation(env("MEMBY_TIMEZONE", "Pacific/Auckland"))
	if err != nil {
		return c, fmt.Errorf("MEMBY_TIMEZONE: %w", err)
	}
	c.SonarrLocation = location
	c.RadarrLocation = location
	return c, nil
}

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

func boolean(key string, fallback bool) bool {
	raw := strings.TrimSpace(os.Getenv(key))
	if raw == "" {
		return fallback
	}
	value, err := strconv.ParseBool(raw)
	if err != nil {
		return fallback
	}
	return value
}

func duration(key string, fallback time.Duration) time.Duration {
	raw := strings.TrimSpace(os.Getenv(key))
	if raw == "" {
		return fallback
	}
	if d, err := time.ParseDuration(raw); err == nil {
		return d
	}
	// Bare numbers are read as seconds, which is friendlier in a compose file.
	if secs, err := strconv.Atoi(raw); err == nil {
		return time.Duration(secs) * time.Second
	}
	return fallback
}

func integer(key string, fallback int) int {
	raw := strings.TrimSpace(os.Getenv(key))
	if raw == "" {
		return fallback
	}
	value, err := strconv.Atoi(raw)
	if err != nil {
		return fallback
	}
	return value
}
