package config

import (
	"fmt"
	"log/slog"
	"os"
	"strings"
	"time"
)

// Config holds all runtime configuration for an Oikos role.
// Hierarchy: compiled defaults → config file → env vars → Infisical (secrets only).
type Config struct {
	// Database
	DatabaseURL string // postgres://user:pass@host:5432/oikos?sslmode=disable

	// API
	APIListen string // :8090
	APIEnv    string // dev, prod

	// Auth (Phase 2: static bearer tokens + OIDC JWT)
	APIToken         string // operator/CI bearer token for the REST API
	MCPBearerToken   string // shared secret for Nomos→API MCP calls
	OIDCIssuer       string // OIDC issuer URL for JWT validation (e.g. https://authentik.example.com/application/o/oikos/)
	OIDCClientID     string // OIDC client ID (aud claim expected in JWT)
	OIDCClientSecret string // optional client secret for token endpoint proxy (confidential clients)

	// CORS (client/server split — see plans/2026-07-12-wails-desktop-app.md
	// 0.3). Needed for the Wails webview and local dev (Vite on a different
	// port than the API); a no-op when the SPA and API share an origin.
	CORSAllowedOrigin string

	// Rate limiting (plan D3). APIRateLimit is the per-IP requests/sec cap;
	// APIRateBurst is the token-bucket burst (defaults to 2x the limit when
	// unset). A limit of 0 disables rate limiting entirely.
	APIRateLimit int
	APIRateBurst int

	// Health probe HTTP listener (plan D5). Background-loop roles (scheduler,
	// execution-worker) expose a staleness-aware /healthz here. Empty disables the
	// health server (local/non-docker runs).
	HealthListen string

	// Observability
	Debug bool // verbose logging, probe payloads, SQL

	// Seeds directory (for ingest/export)
	SeedsDir string

	// Migrations directory (embedded at build time, but path for fallback)
	MigrationsDir string

	// Scheduler (Phase 3)
	SchedulerInterval time.Duration // check loop interval (default 30s)

	// Actuator (Phase 3)
	SSHKeyPath       string // path to the restricted SSH key
	SSHUser          string // SSH user on targets (default "oikos")
	CircuitThreshold int    // N consecutive failures before opening circuit (default 3)
	CircuitSeconds   int    // circuit breaker cooldown seconds (default 300)

	// Learning (Phase 3)
	LearningInterval time.Duration // pattern extraction interval (default 3600s)

	// Nomos agent entity ID (Phase 4)
	NomosAgentID   string
	NomosAgentSlug string

	// Infisical (Phase 5)
	InfisicalSiteURL      string
	InfisicalClientID     string
	InfisicalClientSecret string
	InfisicalProjectID    string
	InfisicalEnv          string
	SecretsDir            string
}

// Default returns a Config with compiled defaults.
func Default() Config {
	return Config{
		DatabaseURL:       "postgres://oikos:***@localhost:5432/oikos?sslmode=disable",
		APIListen:         ":8090",
		APIEnv:            "dev",
		CORSAllowedOrigin: "*",
		SeedsDir:          "seeds",
		MigrationsDir:     "migrations",
		SchedulerInterval: 30 * time.Second,
		SSHUser:           "oikos",
		CircuitThreshold:  3,
		CircuitSeconds:    300,
		LearningInterval:  3600 * time.Second,
	}
}

// FromEnv loads config from environment variables, overlaying defaults.
func FromEnv() Config {
	c := Default()

	if v := os.Getenv("OIKOS_DATABASE_URL"); v != "" {
		c.DatabaseURL = v
	}
	if v := os.Getenv("OIKOS_API_LISTEN"); v != "" {
		c.APIListen = v
	}
	if v := os.Getenv("OIKOS_ENV"); v != "" {
		c.APIEnv = v
	}
	if v := os.Getenv("OIKOS_OIDC_ISSUER"); v != "" {
		c.OIDCIssuer = v
	}
	if v := os.Getenv("OIKOS_OIDC_CLIENT_ID"); v != "" {
		c.OIDCClientID = v
	}
	if v := os.Getenv("OIKOS_OIDC_CLIENT_SECRET"); v != "" {
		c.OIDCClientSecret = v
	}
	if v := os.Getenv("OIKOS_API_TOKEN"); v != "" {
		c.APIToken = v
	}
	if v := os.Getenv("OIKOS_MCP_BEARER_TOKEN"); v != "" {
		c.MCPBearerToken = v
	}
	if v := os.Getenv("OIKOS_CORS_ORIGIN"); v != "" {
		c.CORSAllowedOrigin = v
	}
	c.APIRateLimit = parseInt(os.Getenv("OIKOS_API_RATE_LIMIT"))
	c.APIRateBurst = parseInt(os.Getenv("OIKOS_API_RATE_BURST"))
	if v := os.Getenv("OIKOS_HEALTH_LISTEN"); v != "" {
		c.HealthListen = v
	}
	if v := os.Getenv("OIKOS_SEEDS_DIR"); v != "" {
		c.SeedsDir = v
	}
	c.Debug = os.Getenv("OIKOS_DEBUG") == "true" || os.Getenv("OIKOS_DEBUG") == "1"

	// Phase 3 config
	if v := os.Getenv("OIKOS_SCHEDULER_INTERVAL"); v != "" {
		if d, err := time.ParseDuration(v); err == nil {
			c.SchedulerInterval = d
		}
	}
	if v := os.Getenv("OIKOS_SSH_KEY_PATH"); v != "" {
		c.SSHKeyPath = v
	}
	if v := os.Getenv("OIKOS_SSH_USER"); v != "" {
		c.SSHUser = v
	}
	if v := os.Getenv("OIKOS_CIRCUIT_THRESHOLD"); v != "" {
		c.CircuitThreshold = parseInt(v)
	}
	if v := os.Getenv("OIKOS_CIRCUIT_SECONDS"); v != "" {
		c.CircuitSeconds = parseInt(v)
	}
	if v := os.Getenv("OIKOS_LEARNING_INTERVAL"); v != "" {
		if d, err := time.ParseDuration(v); err == nil {
			c.LearningInterval = d
		}
	}
	if v := os.Getenv("OIKOS_NOMOS_AGENT_ID"); v != "" {
		c.NomosAgentID = v
	}
	if v := os.Getenv("OIKOS_NOMOS_AGENT_SLUG"); v != "" {
		c.NomosAgentSlug = v
	}

	// Phase 5: Infisical secrets
	if v := os.Getenv("OIKOS_INFISICAL_SITE_URL"); v != "" {
		c.InfisicalSiteURL = v
	}
	if v := os.Getenv("OIKOS_INFISICAL_CLIENT_ID"); v != "" {
		c.InfisicalClientID = v
	}
	if v := os.Getenv("OIKOS_INFISICAL_CLIENT_SECRET"); v != "" {
		c.InfisicalClientSecret = v
	}
	if v := os.Getenv("OIKOS_INFISICAL_PROJECT_ID"); v != "" {
		c.InfisicalProjectID = v
	}
	if v := os.Getenv("OIKOS_INFISICAL_ENV"); v != "" {
		c.InfisicalEnv = v
	}
	if v := os.Getenv("OIKOS_SECRETS_DIR"); v != "" {
		c.SecretsDir = v
	}

	return c
}

// parseInt parses a decimal integer from an env var string. Returns 0 on error.
func parseInt(s string) int {
	var n int
	fmt.Sscanf(s, "%d", &n)
	return n
}

// redactedDBURL masks credentials in a postgres:// URL.
func (c Config) redactedDBURL() string {
	dbURL := c.DatabaseURL
	if i := strings.Index(dbURL, "@"); i >= 0 {
		if j := strings.Index(dbURL, "://"); j >= 0 && j < i {
			dbURL = dbURL[:j+3] + "***" + dbURL[i:]
		}
	}
	return dbURL
}

// String returns a human-safe representation (secrets redacted).
func (c Config) String() string {
	token := ""
	if c.MCPBearerToken != "" {
		token = "***"
	}
	return fmt.Sprintf("Config{DB=%s Listen=%s Env=%s Debug=%v MCPToken=%s SeedsDir=%s OIDCIssuer=%s OIDCClientID=%s}",
		c.redactedDBURL(), c.APIListen, c.APIEnv, c.Debug, token, c.SeedsDir, c.OIDCIssuer, c.OIDCClientID)
}

// LogValue implements slog.LogValuer so structured handlers (JSON) never
// serialize raw secrets — without this, slog marshals struct fields
// directly and String() is bypassed.
func (c Config) LogValue() slog.Value {
	token := ""
	if c.MCPBearerToken != "" {
		token = "***"
	}
	return slog.GroupValue(
		slog.String("db", c.redactedDBURL()),
		slog.String("listen", c.APIListen),
		slog.String("env", c.APIEnv),
		slog.Bool("debug", c.Debug),
		slog.String("mcp_token", token),
		slog.String("seeds_dir", c.SeedsDir),
		slog.String("oidc_issuer", c.OIDCIssuer),
		slog.String("oidc_client_id", c.OIDCClientID),
	)
}
