// Package instanceconfig describes the environment the running backend
// resolved, so an operator can see what a variable actually became.
//
// It never reads a second source of truth: every resolver calls the same
// config helper the runtime uses, so the page cannot drift from behaviour.
// Nothing here writes: the environment is authoritative.
package instanceconfig

import (
	"os"
	"strings"

	"github.com/warmbly/warmbly/internal/config"
)

// Groups the admin panel renders as sections.
const (
	GroupDeployment    = "deployment"
	GroupAddresses     = "addresses"
	GroupDatabase      = "database"
	GroupCache         = "cache"
	GroupMail          = "mail"
	GroupAuth          = "auth"
	GroupEncryption    = "encryption"
	GroupStorage       = "storage"
	GroupEventBus      = "eventbus"
	GroupWorkers       = "workers"
	GroupTracking      = "tracking"
	GroupCaptcha       = "captcha"
	GroupObservability = "observability"
	GroupUpdates       = "updates"
)

// Where a resolved value came from.
const (
	SourceEnv     = "env"
	SourceDefault = "default"
	SourceDerived = "derived"
	SourceUnset   = "unset"
)

// Whether a change needs a restart.
const (
	ChangeBootOnly   = "boot-only"
	ChangePerRequest = "per-request"
)

// PublishedDefaults are the working secret values shipped in docker-compose.yml
// and the Makefile. They are public in this repository, so a deployment still
// running on one is not protected by it at all.
//
// This mirrors insecureDefaults in cmd/backend/boot.go; the two must stay
// identical or the health check and the boot refusal will disagree.
var PublishedDefaults = map[string]string{
	"AUTH_SECRET":                "local-dev-auth-secret-minimum-32-characters-long",
	"KMS_LOCAL_MASTER_KEY":       "Xr0JA7gqF2POy29a7MRByyqddivTNt8WOyKsOXklazk=",
	"CREDENTIALS_ENCRYPTION_KEY": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
	"INTERNAL_API_TOKEN":         "local-dev-internal-token",
	"SECRET_KEY_BASE":            "local-development-secret-key-base-minimum-64-characters-for-phoenix",
}

// Runtime carries the facts only cmd/backend knows after boot: values it
// derived rather than read, and the outcome of its own preflight. A nil
// Runtime is valid; every resolver falls back to the environment.
type Runtime struct {
	CORSOrigins       []string
	WebAuthnRPID      string
	WebAuthnRPOrigins []string
	OIDCRedirectURL   string
	OIDCConfigured    bool
	// OIDCDiscoveryErr is the boot-time discovery failure, empty when discovery
	// succeeded or was never attempted.
	OIDCDiscoveryErr  string
	MailTransportKind string
	MailTransportDesc string
	MailDelivers      bool
	PasskeysUsable    bool
	// InsecureDefaults are the variables boot found still holding a published
	// default value.
	InsecureDefaults []string
	Policy           *config.AuthPolicy
	WebsocketURL     string
}

// Entry describes one self-host-relevant environment variable.
type Entry struct {
	Key string
	// Aliases are older names for the same setting. Any of them being set
	// makes the source env.
	Aliases []string
	Group   string
	// RuntimeChangeable is ChangeBootOnly or ChangePerRequest.
	RuntimeChangeable string
	// Effect is one sentence saying what the resolved value does.
	Effect     string
	DocsAnchor string
	// WhenUnset is the source reported when no variable is set but a value
	// still resolves. SourceDefault when empty.
	WhenUnset string
	Resolve   func(rt *Runtime) string
}

// Resolved is one row of GET /admin/instance/config.
type Resolved struct {
	Key   string `json:"key"`
	Value string `json:"value"`
	// Source is one of the Source* constants.
	Source    string `json:"source"`
	Sensitive bool   `json:"sensitive"`
	Set       bool   `json:"set"`
	// Fingerprint is the first 4 hex characters of SHA-256 of a sensitive
	// value, so two services can be compared without either being disclosed.
	Fingerprint string `json:"fingerprint"`
	Group       string `json:"group"`
	Effect      string `json:"effect"`
	Docs        string `json:"docs"`
	// RuntimeChangeable is one of the Change* constants.
	RuntimeChangeable string `json:"runtime_changeable"`
}

// Entries resolves the whole table in declaration order.
func Entries(rt *Runtime) []Resolved {
	out := make([]Resolved, 0, len(table))
	for _, e := range table {
		out = append(out, e.resolve(rt))
	}
	return out
}

func (e Entry) resolve(rt *Runtime) Resolved {
	value := ""
	if e.Resolve != nil {
		value = strings.TrimSpace(e.Resolve(rt))
	}

	r := Resolved{
		Key:               e.Key,
		Group:             e.Group,
		Effect:            e.Effect,
		Docs:              e.DocsAnchor,
		RuntimeChangeable: e.RuntimeChangeable,
		Sensitive:         Sensitive(e.Key),
		Set:               e.envSet(),
	}
	r.Source = e.source(r.Set, value)

	// A sensitive value never leaves the process; the fingerprint is what an
	// operator compares across services.
	if r.Sensitive {
		r.Fingerprint = Fingerprint(value)
		return r
	}
	r.Value = value
	return r
}

func (e Entry) envSet() bool {
	if strings.TrimSpace(os.Getenv(e.Key)) != "" {
		return true
	}
	for _, alias := range e.Aliases {
		if strings.TrimSpace(os.Getenv(alias)) != "" {
			return true
		}
	}
	return false
}

func (e Entry) source(set bool, value string) string {
	if set {
		return SourceEnv
	}
	if value == "" {
		return SourceUnset
	}
	if e.WhenUnset != "" {
		return e.WhenUnset
	}
	return SourceDefault
}
