package config

import (
	"fmt"
	"log/slog"
	"net/url"
	"os"
	"path"
	"path/filepath"
	"strconv"
	"strings"
	"time"
)

const appName = "Update Server"

type Config struct {
	AppName                  string
	HTTPAddr                 string
	BaseURL                  string
	DataDir                  string
	SQLitePath               string
	ArtifactsDir             string
	MigrationsDir            string
	TemplatesDir             string
	StaticDir                string
	MaxUploadBytes           int64
	AdminEmail               string
	AdminPassword            string
	SessionCookieName        string
	CSRFCookieName           string
	SessionTTL               time.Duration
	SecureCookies            bool
	TrustProxyHeaders        bool
	ReadTimeout              time.Duration
	ReadHeaderTimeout        time.Duration
	WriteTimeout             time.Duration
	IdleTimeout              time.Duration
	ShutdownTimeout          time.Duration
	MaxHeaderBytes           int
	LoginRateLimitPerMinute  int
	LoginRateLimitBurst      int
	ClientRateLimitPerMinute int
	ClientRateLimitBurst     int
	LogLevel                 slog.Level
}

func Load() (Config, error) {
	baseURL := getenv("APP_BASE_URL", "http://127.0.0.1:8080")
	parsedBaseURL, err := validateBaseURL(baseURL)
	if err != nil {
		return Config{}, err
	}

	dataDir, err := filepath.Abs(getenv("DATA_DIR", "data-dev"))
	if err != nil {
		return Config{}, fmt.Errorf("resolve DATA_DIR: %w", err)
	}

	sqlitePath, err := resolvePath("SQLITE_PATH", filepath.Join(dataDir, "db.sqlite"))
	if err != nil {
		return Config{}, err
	}

	artifactsDir, err := resolvePath("ARTIFACTS_DIR", filepath.Join(dataDir, "artifacts"))
	if err != nil {
		return Config{}, err
	}

	migrationsDir, err := resolvePath("MIGRATIONS_DIR", "migrations")
	if err != nil {
		return Config{}, err
	}

	templatesDir, err := resolvePath("TEMPLATES_DIR", "web/templates")
	if err != nil {
		return Config{}, err
	}

	staticDir, err := resolvePath("STATIC_DIR", "web/static")
	if err != nil {
		return Config{}, err
	}

	adminEmail := strings.TrimSpace(os.Getenv("ADMIN_EMAIL"))
	adminPassword := os.Getenv("ADMIN_PASSWORD")
	if adminEmail == "" && strings.TrimSpace(adminPassword) != "" {
		return Config{}, fmt.Errorf("ADMIN_PASSWORD requires ADMIN_EMAIL")
	}

	if adminEmail != "" && strings.TrimSpace(adminPassword) == "" {
		return Config{}, fmt.Errorf("ADMIN_EMAIL requires ADMIN_PASSWORD")
	}

	sessionCookieName := getenv("SESSION_COOKIE_NAME", "update_server_session")
	if strings.TrimSpace(sessionCookieName) == "" {
		return Config{}, fmt.Errorf("SESSION_COOKIE_NAME must not be empty")
	}

	csrfCookieName := getenv("CSRF_COOKIE_NAME", "update_server_csrf")
	if strings.TrimSpace(csrfCookieName) == "" {
		return Config{}, fmt.Errorf("CSRF_COOKIE_NAME must not be empty")
	}

	sessionTTL, err := parseDuration("SESSION_TTL", "24h")
	if err != nil {
		return Config{}, err
	}

	if sessionTTL <= 0 {
		return Config{}, fmt.Errorf("SESSION_TTL must be greater than zero")
	}

	maxUploadBytes, err := parseInt64("MAX_UPLOAD_BYTES", 1<<30)
	if err != nil {
		return Config{}, err
	}

	if maxUploadBytes <= 0 {
		return Config{}, fmt.Errorf("MAX_UPLOAD_BYTES must be greater than zero")
	}

	readTimeout, err := parseDuration("APP_READ_TIMEOUT", "10s")
	if err != nil {
		return Config{}, err
	}

	readHeaderTimeout, err := parseDuration("APP_READ_HEADER_TIMEOUT", "5s")
	if err != nil {
		return Config{}, err
	}

	writeTimeout, err := parseDuration("APP_WRITE_TIMEOUT", "60s")
	if err != nil {
		return Config{}, err
	}

	idleTimeout, err := parseDuration("APP_IDLE_TIMEOUT", "60s")
	if err != nil {
		return Config{}, err
	}

	shutdownTimeout, err := parseDuration("APP_SHUTDOWN_TIMEOUT", "10s")
	if err != nil {
		return Config{}, err
	}

	maxHeaderBytes, err := parseInt("APP_MAX_HEADER_BYTES", 1<<20)
	if err != nil {
		return Config{}, err
	}

	if maxHeaderBytes <= 0 {
		return Config{}, fmt.Errorf("APP_MAX_HEADER_BYTES must be greater than zero")
	}

	trustProxyHeaders, err := parseBool("TRUST_PROXY_HEADERS", false)
	if err != nil {
		return Config{}, err
	}

	loginRateLimitPerMinute, err := parseInt("APP_LOGIN_RATE_LIMIT_PER_MINUTE", 10)
	if err != nil {
		return Config{}, err
	}

	loginRateLimitBurst, err := parseInt("APP_LOGIN_RATE_LIMIT_BURST", 5)
	if err != nil {
		return Config{}, err
	}

	clientRateLimitPerMinute, err := parseInt("APP_CLIENT_RATE_LIMIT_PER_MINUTE", 120)
	if err != nil {
		return Config{}, err
	}

	clientRateLimitBurst, err := parseInt("APP_CLIENT_RATE_LIMIT_BURST", 60)
	if err != nil {
		return Config{}, err
	}

	for key, value := range map[string]int{
		"APP_LOGIN_RATE_LIMIT_PER_MINUTE":  loginRateLimitPerMinute,
		"APP_LOGIN_RATE_LIMIT_BURST":       loginRateLimitBurst,
		"APP_CLIENT_RATE_LIMIT_PER_MINUTE": clientRateLimitPerMinute,
		"APP_CLIENT_RATE_LIMIT_BURST":      clientRateLimitBurst,
	} {
		if value <= 0 {
			return Config{}, fmt.Errorf("%s must be greater than zero", key)
		}
	}

	logLevel, err := parseLogLevel(getenv("APP_LOG_LEVEL", "INFO"))
	if err != nil {
		return Config{}, err
	}

	if pathWithin(artifactsDir, staticDir) {
		return Config{}, fmt.Errorf("ARTIFACTS_DIR must be outside STATIC_DIR")
	}

	return Config{
		AppName:                  appName,
		HTTPAddr:                 getenv("APP_ADDR", ":8080"),
		BaseURL:                  baseURL,
		DataDir:                  dataDir,
		SQLitePath:               sqlitePath,
		ArtifactsDir:             artifactsDir,
		MigrationsDir:            migrationsDir,
		TemplatesDir:             templatesDir,
		StaticDir:                staticDir,
		MaxUploadBytes:           maxUploadBytes,
		AdminEmail:               adminEmail,
		AdminPassword:            adminPassword,
		SessionCookieName:        sessionCookieName,
		CSRFCookieName:           csrfCookieName,
		SessionTTL:               sessionTTL,
		SecureCookies:            strings.EqualFold(parsedBaseURL.Scheme, "https"),
		TrustProxyHeaders:        trustProxyHeaders,
		ReadTimeout:              readTimeout,
		ReadHeaderTimeout:        readHeaderTimeout,
		WriteTimeout:             writeTimeout,
		IdleTimeout:              idleTimeout,
		ShutdownTimeout:          shutdownTimeout,
		MaxHeaderBytes:           maxHeaderBytes,
		LoginRateLimitPerMinute:  loginRateLimitPerMinute,
		LoginRateLimitBurst:      loginRateLimitBurst,
		ClientRateLimitPerMinute: clientRateLimitPerMinute,
		ClientRateLimitBurst:     clientRateLimitBurst,
		LogLevel:                 logLevel,
	}, nil
}

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

	return fallback
}

func validateBaseURL(raw string) (*url.URL, error) {
	parsed, err := url.Parse(raw)
	if err != nil {
		return nil, fmt.Errorf("parse APP_BASE_URL: %w", err)
	}

	if parsed.Scheme == "" || parsed.Host == "" {
		return nil, fmt.Errorf("APP_BASE_URL must include scheme and host, got %q", raw)
	}

	if !strings.EqualFold(parsed.Scheme, "http") && !strings.EqualFold(parsed.Scheme, "https") {
		return nil, fmt.Errorf("APP_BASE_URL scheme must be http or https, got %q", parsed.Scheme)
	}

	return parsed, nil
}

func resolvePath(key, fallback string) (string, error) {
	path, err := filepath.Abs(getenv(key, fallback))
	if err != nil {
		return "", fmt.Errorf("resolve %s: %w", key, err)
	}

	return path, nil
}

func parseDuration(key, fallback string) (time.Duration, error) {
	raw := getenv(key, fallback)
	value, err := time.ParseDuration(raw)
	if err != nil {
		return 0, fmt.Errorf("parse %s: %w", key, err)
	}

	return value, nil
}

func parseInt64(key string, fallback int64) (int64, error) {
	raw := strings.TrimSpace(os.Getenv(key))
	if raw == "" {
		return fallback, nil
	}

	value, err := strconv.ParseInt(raw, 10, 64)
	if err != nil {
		return 0, fmt.Errorf("parse %s: %w", key, err)
	}

	return value, nil
}

func parseInt(key string, fallback int) (int, error) {
	raw := strings.TrimSpace(os.Getenv(key))
	if raw == "" {
		return fallback, nil
	}

	value, err := strconv.Atoi(raw)
	if err != nil {
		return 0, fmt.Errorf("parse %s: %w", key, err)
	}

	return value, nil
}

func parseBool(key string, fallback bool) (bool, error) {
	raw := strings.TrimSpace(os.Getenv(key))
	if raw == "" {
		return fallback, nil
	}

	value, err := strconv.ParseBool(raw)
	if err != nil {
		return false, fmt.Errorf("parse %s: %w", key, err)
	}

	return value, nil
}

func parseLogLevel(raw string) (slog.Level, error) {
	switch strings.ToUpper(strings.TrimSpace(raw)) {
	case "DEBUG":
		return slog.LevelDebug, nil
	case "INFO":
		return slog.LevelInfo, nil
	case "WARN", "WARNING":
		return slog.LevelWarn, nil
	case "ERROR":
		return slog.LevelError, nil
	default:
		return 0, fmt.Errorf("APP_LOG_LEVEL must be one of DEBUG, INFO, WARN, ERROR")
	}
}

func pathWithin(candidate, parent string) bool {
	rel, err := filepath.Rel(filepath.Clean(parent), filepath.Clean(candidate))
	if err != nil {
		return false
	}

	if rel == "." {
		return true
	}

	rel = filepath.ToSlash(rel)
	return rel != ".." && !strings.HasPrefix(rel, "../") && path.Clean(rel) != ".."
}
