// Package config loads server configuration from environment variables,
// optionally seeded from a .env file in the working directory. The PocketBase
// connection is also editable at runtime from the panel, which persists the
// change back into the same .env via UpdateEnvFile.
package config

import (
	"os"
	"strings"
)

// Config holds all runtime configuration for the API Server.
type Config struct {
	Addr          string
	PocketBaseURL string
	WebAppURL     string
	AllowOrigins  []string

	// ServerName is this server's display name. It is what clients that can be
	// pointed at more than one DriverVault call this one — the Web App's server
	// switcher labels an added server with it — so it is reported by /api/health
	// rather than kept for the panel alone.
	ServerName string

	// UsersCollection is the PocketBase auth collection holding app users.
	UsersCollection string

	// Superuser service account. Every privileged flow (user/organization
	// management, all car-domain database access) runs through it. Optional at
	// startup: when unset those endpoints return 503 and a superadmin can still
	// log in to the panel to configure it.
	PocketBaseAdminEmail    string
	PocketBaseAdminPassword string

	// OCPP control endpoint (Anker Solix charger control). OCPPRequireTLS rejects
	// charger connections that did not arrive over TLS (a plaintext ws:// carries
	// the charger's Basic-auth token in the clear); disable only for local dev.
	// OCPPPublicURL, when set, is the canonical ws(s):// base an operator points
	// the charger at, instead of deriving it from request headers.
	OCPPRequireTLS bool
	OCPPPublicURL  string

	// Bootstrap controls the on-startup PocketBase schema setup: when true (the
	// default) the server creates any missing collections and reconciles existing
	// ones against the desired schema, then ensures the super-admin below exists.
	// It requires a configured service account; without one it is a no-op.
	Bootstrap bool

	// Super-admin account created during bootstrap when both fields are set and no
	// user with that email exists yet. Existing accounts are left untouched.
	SuperAdminEmail    string
	SuperAdminPassword string
	SuperAdminName     string
}

// EnvFile is the .env path (relative to the working directory) that Load reads
// and that runtime settings changes persist back into.
const EnvFile = ".env"

// DefaultServerName labels a server whose operator has not named one, so
// /api/health always carries something a client can display.
const DefaultServerName = "DriverVault API Server"

// AdminConfigured reports whether a service account has been supplied.
func (c Config) AdminConfigured() bool {
	return c.PocketBaseAdminEmail != "" && c.PocketBaseAdminPassword != ""
}

// Load reads configuration from environment variables, applying sensible
// defaults. A .env file, if present in the working directory, is loaded first.
func Load() Config {
	loadDotEnv(EnvFile)

	return Config{
		Addr:                    normalizeAddr(firstEnv("API_ADDR", "PORT"), ":8080"),
		PocketBaseURL:           strings.TrimRight(firstEnvOr("http://10.2.1.10:8027", "POCKETBASE_URL", "PB_URL"), "/"),
		WebAppURL:               strings.TrimRight(getenv("WEBAPP_URL", "http://localhost:8090"), "/"),
		AllowOrigins:            splitCSV(firstEnvOr("*", "CORS_ALLOW_ORIGINS", "CORS_ORIGINS")),
		ServerName:              strings.TrimSpace(getenv("SERVER_NAME", DefaultServerName)),
		UsersCollection:         getenv("AUTH_USERS_COLLECTION", "users"),
		PocketBaseAdminEmail:    firstEnv("POCKETBASE_ADMIN_EMAIL", "PB_ADMIN_EMAIL"),
		PocketBaseAdminPassword: firstEnv("POCKETBASE_ADMIN_PASSWORD", "PB_ADMIN_PASSWORD"),
		OCPPRequireTLS:          boolEnv("OCPP_REQUIRE_TLS", true),
		OCPPPublicURL:           strings.TrimRight(getenv("OCPP_PUBLIC_URL", ""), "/"),
		Bootstrap:               boolEnv("PB_BOOTSTRAP", true),
		SuperAdminEmail:         firstEnv("DRIVERVAULT_SUPERADMIN_EMAIL", "SUPERADMIN_EMAIL"),
		SuperAdminPassword:      firstEnv("DRIVERVAULT_SUPERADMIN_PASSWORD", "SUPERADMIN_PASSWORD"),
		SuperAdminName:          getenv("DRIVERVAULT_SUPERADMIN_NAME", "Administrator"),
	}
}

// boolEnv reads a boolean environment variable, accepting the common truthy and
// falsey spellings and falling back to def when unset or unrecognized.
func boolEnv(key string, def bool) bool {
	switch strings.ToLower(strings.TrimSpace(os.Getenv(key))) {
	case "":
		return def
	case "1", "true", "yes", "on":
		return true
	case "0", "false", "no", "off":
		return false
	default:
		return def
	}
}

// normalizeAddr accepts either a full listen address (":8080") or a bare port
// ("8080", which is what the legacy PORT variable held) and returns a listen
// address.
func normalizeAddr(v, def string) string {
	if v == "" {
		return def
	}
	if strings.Contains(v, ":") {
		return v
	}
	return ":" + v
}

// UpdateEnvFile persists the given KEY=VALUE pairs into the .env file at path,
// replacing existing keys in place and appending new ones, while preserving all
// other lines (comments, ordering, unrelated keys). The file is created if it
// does not exist. Written with 0600 perms since it holds secrets.
func UpdateEnvFile(path string, updates map[string]string) error {
	existing, _ := os.ReadFile(path) // missing file → start empty

	remaining := make(map[string]string, len(updates))
	for k, v := range updates {
		remaining[k] = v
	}

	var out []string
	for _, line := range strings.Split(string(existing), "\n") {
		trimmed := strings.TrimSpace(line)
		if trimmed == "" || strings.HasPrefix(trimmed, "#") {
			out = append(out, line)
			continue
		}
		key, _, ok := strings.Cut(trimmed, "=")
		key = strings.TrimSpace(key)
		if ok {
			if v, found := remaining[key]; found {
				out = append(out, key+"="+v)
				delete(remaining, key)
				continue
			}
		}
		out = append(out, line)
	}
	// Append any keys that weren't already present.
	for k, v := range remaining {
		out = append(out, k+"="+v)
	}

	content := strings.Join(out, "\n")
	if !strings.HasSuffix(content, "\n") {
		content += "\n"
	}
	return os.WriteFile(path, []byte(content), 0o600)
}

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

// firstEnv returns the first of keys that is set to a non-empty value. It lets
// the modern POCKETBASE_* names take precedence while the legacy PB_* names from
// older deployments keep working.
func firstEnv(keys ...string) string {
	for _, k := range keys {
		if v := os.Getenv(k); v != "" {
			return v
		}
	}
	return ""
}

// firstEnvOr is firstEnv with a fallback when none of the keys are set.
func firstEnvOr(def string, keys ...string) string {
	if v := firstEnv(keys...); v != "" {
		return v
	}
	return def
}

func splitCSV(s string) []string {
	parts := strings.Split(s, ",")
	out := make([]string, 0, len(parts))
	for _, p := range parts {
		if p = strings.TrimSpace(p); p != "" {
			out = append(out, p)
		}
	}
	return out
}

// loadDotEnv loads KEY=VALUE pairs from a .env file into the process env if they
// are not already set. It is intentionally minimal (no quoting rules beyond
// trimming surrounding quotes).
func loadDotEnv(path string) {
	data, err := os.ReadFile(path)
	if err != nil {
		return // .env is optional
	}
	for _, line := range strings.Split(string(data), "\n") {
		line = strings.TrimSpace(line)
		if line == "" || strings.HasPrefix(line, "#") {
			continue
		}
		key, val, ok := strings.Cut(line, "=")
		if !ok {
			continue
		}
		key = strings.TrimSpace(key)
		val = strings.Trim(strings.TrimSpace(val), `"'`)
		if _, exists := os.LookupEnv(key); !exists {
			_ = os.Setenv(key, val)
		}
	}
}
