// Package config loads server configuration from env vars (the
// canonical source) with optional YAML overlay. Documented vars are
// listed in spec.md §4.1.
package config

import (
	"fmt"
	"net"
	"net/netip"
	"os"
	"strings"

	"gopkg.in/yaml.v3"
)

// Config holds runtime parameters resolved from env + (optionally) a
// YAML file. Env wins over YAML so operators can tweak a single var
// without rewriting the file.
//
// The server is HTTP-only by design: the expected deployment fronts it
// with a TLS-terminating reverse proxy (Caddy/Traefik/nginx). See
// spec.md §11 for the rationale.
type Config struct {
	Listen         string   `yaml:"listen"`
	DataDir        string   `yaml:"data_dir"`
	BaseURL        string   `yaml:"base_url"`
	SecretKeyFile  string   `yaml:"secret_key_file"`
	TrustedProxies []string `yaml:"trusted_proxies"`
	// CookieSecure controls the Secure attribute on session cookies.
	// Defaults to true. Set RM_COOKIE_SECURE=false only for local HTTP
	// testing — production deployments are always behind a TLS proxy
	// and the cookie must be Secure.
	CookieSecure bool        `yaml:"cookie_secure"`
	OIDCRaw      *OIDCConfig `yaml:"oidc"`
	OIDC         *OIDCConfig `yaml:"-"`

	// BundledAssetsDir is the read-only path inside the image that
	// holds agent binaries (under agent-binaries/) and install
	// scripts (under install/). The /agent/binary and /install/*
	// handlers fall back here when the file is not present in
	// DataDir. Source-build deployments can override via
	// RM_BUNDLED_ASSETS_DIR.
	BundledAssetsDir string `yaml:"bundled_assets_dir"`

	// MetricsToken, if set, gates the /metrics scrape endpoint
	// behind a `Authorization: Bearer <token>` check (constant-time
	// compare). When neither this nor MetricsTrustedCIDRs is set,
	// the route is not mounted at all (the endpoint is opt-in).
	MetricsToken string `yaml:"metrics_token"`

	// MetricsTrustedCIDRs, if non-empty, gates /metrics so only
	// callers from these networks may scrape. ANDed with
	// MetricsToken when both are set.
	MetricsTrustedCIDRs []string `yaml:"metrics_trusted_cidrs"`
}

// MetricsAuthEnabled reports whether the operator has opted into
// exposing the Prometheus scrape endpoint by configuring at least
// one auth gate.
func (c Config) MetricsAuthEnabled() bool {
	return c.MetricsToken != "" || len(c.MetricsTrustedCIDRs) > 0
}

// Load resolves config in this order:
//  1. defaults
//  2. YAML at the given path (if non-empty and exists)
//  3. environment variables (RM_LISTEN, RM_DATA_DIR, …)
//
// The result is validated; a zero-error return means the server is
// safe to start.
func Load(yamlPath string) (Config, error) {
	c := Config{
		Listen:           ":8080",
		DataDir:          "/data",
		CookieSecure:     true,
		BundledAssetsDir: "/opt/restic-manager/dist",
	}

	if yamlPath != "" {
		body, err := os.ReadFile(yamlPath)
		if err != nil && !os.IsNotExist(err) {
			return c, fmt.Errorf("config: read %q: %w", yamlPath, err)
		}
		if err == nil {
			if err := yaml.Unmarshal(body, &c); err != nil {
				return c, fmt.Errorf("config: parse %q: %w", yamlPath, err)
			}
		}
	}

	if v, ok := os.LookupEnv("RM_LISTEN"); ok {
		c.Listen = v
	}
	if v, ok := os.LookupEnv("RM_DATA_DIR"); ok {
		c.DataDir = v
	}
	if v, ok := os.LookupEnv("RM_BASE_URL"); ok {
		c.BaseURL = v
	}
	if v, ok := os.LookupEnv("RM_SECRET_KEY_FILE"); ok {
		c.SecretKeyFile = v
	}
	if v, ok := os.LookupEnv("RM_COOKIE_SECURE"); ok {
		// Anything other than "false"/"0" leaves the safe default.
		if v == "false" || v == "0" {
			c.CookieSecure = false
		} else {
			c.CookieSecure = true
		}
	}
	if v, ok := os.LookupEnv("RM_BUNDLED_ASSETS_DIR"); ok {
		c.BundledAssetsDir = v
	}
	if v, ok := os.LookupEnv("RM_METRICS_TOKEN"); ok {
		c.MetricsToken = v
	}
	if v, ok := os.LookupEnv("RM_METRICS_TRUSTED_CIDR"); ok {
		parts := strings.Split(v, ",")
		c.MetricsTrustedCIDRs = c.MetricsTrustedCIDRs[:0]
		for _, p := range parts {
			p = strings.TrimSpace(p)
			if p != "" {
				c.MetricsTrustedCIDRs = append(c.MetricsTrustedCIDRs, p)
			}
		}
	}
	if v, ok := os.LookupEnv("RM_TRUSTED_PROXY"); ok {
		// Comma-separated CIDRs; allow whitespace for readability.
		parts := strings.Split(v, ",")
		c.TrustedProxies = c.TrustedProxies[:0]
		for _, p := range parts {
			p = strings.TrimSpace(p)
			if p != "" {
				c.TrustedProxies = append(c.TrustedProxies, p)
			}
		}
	}

	var rawOIDC OIDCConfig
	if c.OIDCRaw != nil {
		rawOIDC = *c.OIDCRaw
	}
	oidc, err := loadOIDC(envSnapshot(), rawOIDC)
	if err != nil {
		return c, err
	}
	c.OIDC = oidc

	return c, c.validate()
}

func (c *Config) validate() error {
	if c.Listen == "" {
		return fmt.Errorf("config: RM_LISTEN must be set")
	}
	if _, _, err := net.SplitHostPort(c.Listen); err != nil {
		return fmt.Errorf("config: RM_LISTEN %q invalid: %w", c.Listen, err)
	}
	if c.DataDir == "" {
		return fmt.Errorf("config: RM_DATA_DIR must be set")
	}
	if c.SecretKeyFile == "" {
		// Default to data dir.
		c.SecretKeyFile = c.DataDir + "/secret.key"
	}
	for _, cidr := range c.TrustedProxies {
		if _, err := netip.ParsePrefix(cidr); err != nil {
			return fmt.Errorf("config: RM_TRUSTED_PROXY entry %q is not a valid CIDR: %w", cidr, err)
		}
	}
	for _, cidr := range c.MetricsTrustedCIDRs {
		if _, err := netip.ParsePrefix(cidr); err != nil {
			return fmt.Errorf("config: RM_METRICS_TRUSTED_CIDR entry %q is not a valid CIDR: %w", cidr, err)
		}
	}
	return nil
}
