// Copyright 2015 Matthew Holt and The Caddy Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package caddy

import (
	"bytes"
	"context"
	"encoding/hex"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"log"
	"net/http"
	"os"
	"path"
	"path/filepath"
	"runtime/debug"
	"strconv"
	"strings"
	"sync"
	"sync/atomic"
	"time"

	"github.com/caddyserver/caddy/v2/notify"
	"github.com/caddyserver/certmagic"
	"github.com/google/uuid"
	"go.uber.org/zap"
)

// Config is the top (or beginning) of the Caddy configuration structure.
// Caddy config is expressed natively as a JSON document. If you prefer
// not to work with JSON directly, there are [many config adapters](/docs/config-adapters)
// available that can convert various inputs into Caddy JSON.
//
// Many parts of this config are extensible through the use of Caddy modules.
// Fields which have a json.RawMessage type and which appear as dots (•••) in
// the online docs can be fulfilled by modules in a certain module
// namespace. The docs show which modules can be used in a given place.
//
// Whenever a module is used, its name must be given either inline as part of
// the module, or as the key to the module's value. The docs will make it clear
// which to use.
//
// Generally, all config settings are optional, as it is Caddy convention to
// have good, documented default values. If a parameter is required, the docs
// should say so.
//
// Go programs which are directly building a Config struct value should take
// care to populate the JSON-encodable fields of the struct (i.e. the fields
// with `json` struct tags) if employing the module lifecycle (e.g. Provision
// method calls).
type Config struct {
	Admin   *AdminConfig `json:"admin,omitempty"`
	Logging *Logging     `json:"logging,omitempty"`

	// StorageRaw is a storage module that defines how/where Caddy
	// stores assets (such as TLS certificates). The default storage
	// module is `caddy.storage.file_system` (the local file system),
	// and the default path
	// [depends on the OS and environment](/docs/conventions#data-directory).
	StorageRaw json.RawMessage `json:"storage,omitempty" caddy:"namespace=caddy.storage inline_key=module"`

	// AppsRaw are the apps that Caddy will load and run. The
	// app module name is the key, and the app's config is the
	// associated value.
	AppsRaw ModuleMap `json:"apps,omitempty" caddy:"namespace="`

	apps    map[string]App
	storage certmagic.Storage

	cancelFunc context.CancelFunc
}

// App is a thing that Caddy runs.
type App interface {
	Start() error
	Stop() error
}

// Run runs the given config, replacing any existing config.
func Run(cfg *Config) error {
	cfgJSON, err := json.Marshal(cfg)
	if err != nil {
		return err
	}
	return Load(cfgJSON, true)
}

// Load loads the given config JSON and runs it only
// if it is different from the current config or
// forceReload is true.
func Load(cfgJSON []byte, forceReload bool) error {
	if err := notify.Reloading(); err != nil {
		Log().Error("unable to notify service manager of reloading state", zap.Error(err))
	}

	// after reload, notify system of success or, if
	// failure, update with status (error message)
	var err error
	defer func() {
		if err != nil {
			if notifyErr := notify.Error(err, 0); notifyErr != nil {
				Log().Error("unable to notify to service manager of reload error",
					zap.Error(notifyErr),
					zap.String("reload_err", err.Error()))
			}
			return
		}
		if err := notify.Ready(); err != nil {
			Log().Error("unable to notify to service manager of ready state", zap.Error(err))
		}
	}()

	err = changeConfig(http.MethodPost, "/"+rawConfigKey, cfgJSON, "", forceReload)
	if errors.Is(err, errSameConfig) {
		err = nil // not really an error
	}

	return err
}

// changeConfig changes the current config (rawCfg) according to the
// method, traversed via the given path, and uses the given input as
// the new value (if applicable; i.e. "DELETE" doesn't have an input).
// If the resulting config is the same as the previous, no reload will
// occur unless forceReload is true. If the config is unchanged and not
// forcefully reloaded, then errConfigUnchanged This function is safe for
// concurrent use.
// The ifMatchHeader can optionally be given a string of the format:
//
//	"<path> <hash>"
//
// where <path> is the absolute path in the config and <hash> is the expected hash of
// the config at that path. If the hash in the ifMatchHeader doesn't match
// the hash of the config, then an APIError with status 412 will be returned.
func changeConfig(method, path string, input []byte, ifMatchHeader string, forceReload bool) error {
	switch method {
	case http.MethodGet,
		http.MethodHead,
		http.MethodOptions,
		http.MethodConnect,
		http.MethodTrace:
		return fmt.Errorf("method not allowed")
	}

	currentCtxMu.Lock()
	defer currentCtxMu.Unlock()

	if ifMatchHeader != "" {
		// expect the first and last character to be quotes
		if len(ifMatchHeader) < 2 || ifMatchHeader[0] != '"' || ifMatchHeader[len(ifMatchHeader)-1] != '"' {
			return APIError{
				HTTPStatus: http.StatusBadRequest,
				Err:        fmt.Errorf("malformed If-Match header; expect quoted string"),
			}
		}

		// read out the parts
		parts := strings.Fields(ifMatchHeader[1 : len(ifMatchHeader)-1])
		if len(parts) != 2 {
			return APIError{
				HTTPStatus: http.StatusBadRequest,
				Err:        fmt.Errorf("malformed If-Match header; expect format \"<path> <hash>\""),
			}
		}

		// get the current hash of the config
		// at the given path
		hash := etagHasher()
		err := unsyncedConfigAccess(http.MethodGet, parts[0], nil, hash)
		if err != nil {
			return err
		}

		if hex.EncodeToString(hash.Sum(nil)) != parts[1] {
			return APIError{
				HTTPStatus: http.StatusPreconditionFailed,
				Err:        fmt.Errorf("If-Match header did not match current config hash"),
			}
		}
	}

	err := unsyncedConfigAccess(method, path, input, nil)
	if err != nil {
		return err
	}

	// the mutation is complete, so encode the entire config as JSON
	newCfg, err := json.Marshal(rawCfg[rawConfigKey])
	if err != nil {
		return APIError{
			HTTPStatus: http.StatusBadRequest,
			Err:        fmt.Errorf("encoding new config: %v", err),
		}
	}

	// if nothing changed, no need to do a whole reload unless the client forces it
	if !forceReload && bytes.Equal(rawCfgJSON, newCfg) {
		Log().Info("config is unchanged")
		return errSameConfig
	}

	// find any IDs in this config and index them
	idx := make(map[string]string)
	err = indexConfigObjects(rawCfg[rawConfigKey], "/"+rawConfigKey, idx)
	if err != nil {
		return APIError{
			HTTPStatus: http.StatusInternalServerError,
			Err:        fmt.Errorf("indexing config: %v", err),
		}
	}

	// load this new config; if it fails, we need to revert to
	// our old representation of caddy's actual config
	err = unsyncedDecodeAndRun(newCfg, true)
	if err != nil {
		if len(rawCfgJSON) > 0 {
			// restore old config state to keep it consistent
			// with what caddy is still running; we need to
			// unmarshal it again because it's likely that
			// pointers deep in our rawCfg map were modified
			var oldCfg any
			err2 := json.Unmarshal(rawCfgJSON, &oldCfg)
			if err2 != nil {
				err = fmt.Errorf("%v; additionally, restoring old config: %v", err, err2)
			}
			rawCfg[rawConfigKey] = oldCfg
		}

		return fmt.Errorf("loading new config: %v", err)
	}

	// success, so update our stored copy of the encoded
	// config to keep it consistent with what caddy is now
	// running (storing an encoded copy is not strictly
	// necessary, but avoids an extra json.Marshal for
	// each config change)
	rawCfgJSON = newCfg
	rawCfgIndex = idx

	return nil
}

// readConfig traverses the current config to path
// and writes its JSON encoding to out.
func readConfig(path string, out io.Writer) error {
	currentCtxMu.RLock()
	defer currentCtxMu.RUnlock()
	return unsyncedConfigAccess(http.MethodGet, path, nil, out)
}

// indexConfigObjects recursively searches ptr for object fields named
// "@id" and maps that ID value to the full configPath in the index.
// This function is NOT safe for concurrent access; obtain a write lock
// on currentCtxMu.
func indexConfigObjects(ptr any, configPath string, index map[string]string) error {
	switch val := ptr.(type) {
	case map[string]any:
		for k, v := range val {
			if k == idKey {
				switch idVal := v.(type) {
				case string:
					index[idVal] = configPath
				case float64: // all JSON numbers decode as float64
					index[fmt.Sprintf("%v", idVal)] = configPath
				default:
					return fmt.Errorf("%s: %s field must be a string or number", configPath, idKey)
				}
				continue
			}
			// traverse this object property recursively
			err := indexConfigObjects(val[k], path.Join(configPath, k), index)
			if err != nil {
				return err
			}
		}
	case []any:
		// traverse each element of the array recursively
		for i := range val {
			err := indexConfigObjects(val[i], path.Join(configPath, strconv.Itoa(i)), index)
			if err != nil {
				return err
			}
		}
	}

	return nil
}

// unsyncedDecodeAndRun removes any meta fields (like @id tags)
// from cfgJSON, decodes the result into a *Config, and runs
// it as the new config, replacing any other current config.
// It does NOT update the raw config state, as this is a
// lower-level function; most callers will want to use Load
// instead. A write lock on currentCtxMu is required! If
// allowPersist is false, it will not be persisted to disk,
// even if it is configured to.
func unsyncedDecodeAndRun(cfgJSON []byte, allowPersist bool) error {
	// remove any @id fields from the JSON, which would cause
	// loading to break since the field wouldn't be recognized
	strippedCfgJSON := RemoveMetaFields(cfgJSON)

	var newCfg *Config
	err := StrictUnmarshalJSON(strippedCfgJSON, &newCfg)
	if err != nil {
		return err
	}

	// prevent recursive config loads; that is a user error, and
	// although frequent config loads should be safe, we cannot
	// guarantee that in the presence of third party plugins, nor
	// do we want this error to go unnoticed (we assume it was a
	// pulled config if we're not allowed to persist it)
	if !allowPersist &&
		newCfg != nil &&
		newCfg.Admin != nil &&
		newCfg.Admin.Config != nil &&
		newCfg.Admin.Config.LoadRaw != nil &&
		newCfg.Admin.Config.LoadDelay <= 0 {
		return fmt.Errorf("recursive config loading detected: pulled configs cannot pull other configs without positive load_delay")
	}

	// run the new config and start all its apps
	ctx, err := run(newCfg, true)
	if err != nil {
		return err
	}

	// swap old context (including its config) with the new one
	oldCtx := currentCtx
	currentCtx = ctx

	// Stop, Cleanup each old app
	unsyncedStop(oldCtx)

	// autosave a non-nil config, if not disabled
	if allowPersist &&
		newCfg != nil &&
		(newCfg.Admin == nil ||
			newCfg.Admin.Config == nil ||
			newCfg.Admin.Config.Persist == nil ||
			*newCfg.Admin.Config.Persist) {
		dir := filepath.Dir(ConfigAutosavePath)
		err := os.MkdirAll(dir, 0700)
		if err != nil {
			Log().Error("unable to create folder for config autosave",
				zap.String("dir", dir),
				zap.Error(err))
		} else {
			err := os.WriteFile(ConfigAutosavePath, cfgJSON, 0600)
			if err == nil {
				Log().Info("autosaved config (load with --resume flag)", zap.String("file", ConfigAutosavePath))
			} else {
				Log().Error("unable to autosave config",
					zap.String("file", ConfigAutosavePath),
					zap.Error(err))
			}
		}
	}

	return nil
}

// run runs newCfg and starts all its apps if
// start is true. If any errors happen, cleanup
// is performed if any modules were provisioned;
// apps that were started already will be stopped,
// so this function should not leak resources if
// an error is returned. However, if no error is
// returned and start == false, you should cancel
// the config if you are not going to start it,
// so that each provisioned module will be
// cleaned up.
//
// This is a low-level function; most callers
// will want to use Run instead, which also
// updates the config's raw state.
func run(newCfg *Config, start bool) (Context, error) {
	// because we will need to roll back any state
	// modifications if this function errors, we
	// keep a single error value and scope all
	// sub-operations to their own functions to
	// ensure this error value does not get
	// overridden or missed when it should have
	// been set by a short assignment
	var err error

	if newCfg == nil {
		newCfg = new(Config)
	}

	// create a context within which to load
	// modules - essentially our new config's
	// execution environment; be sure that
	// cleanup occurs when we return if there
	// was an error; if no error, it will get
	// cleaned up on next config cycle
	ctx, cancel := NewContext(Context{Context: context.Background(), cfg: newCfg})
	defer func() {
		if err != nil {
			// if there were any errors during startup,
			// we should cancel the new context we created
			// since the associated config won't be used;
			// this will cause all modules that were newly
			// provisioned to clean themselves up
			cancel()

			// also undo any other state changes we made
			if currentCtx.cfg != nil {
				certmagic.Default.Storage = currentCtx.cfg.storage
			}
		}
	}()
	newCfg.cancelFunc = cancel // clean up later

	// set up logging before anything bad happens
	if newCfg.Logging == nil {
		newCfg.Logging = new(Logging)
	}
	err = newCfg.Logging.openLogs(ctx)
	if err != nil {
		return ctx, err
	}

	// start the admin endpoint (and stop any prior one)
	if start {
		err = replaceLocalAdminServer(newCfg)
		if err != nil {
			return ctx, fmt.Errorf("starting caddy administration endpoint: %v", err)
		}
	}

	// prepare the new config for use
	newCfg.apps = make(map[string]App)

	// set up global storage and make it CertMagic's default storage, too
	err = func() error {
		if newCfg.StorageRaw != nil {
			val, err := ctx.LoadModule(newCfg, "StorageRaw")
			if err != nil {
				return fmt.Errorf("loading storage module: %v", err)
			}
			stor, err := val.(StorageConverter).CertMagicStorage()
			if err != nil {
				return fmt.Errorf("creating storage value: %v", err)
			}
			newCfg.storage = stor
		}

		if newCfg.storage == nil {
			newCfg.storage = DefaultStorage
		}
		certmagic.Default.Storage = newCfg.storage

		return nil
	}()
	if err != nil {
		return ctx, err
	}

	// Load and Provision each app and their submodules
	err = func() error {
		for appName := range newCfg.AppsRaw {
			if _, err := ctx.App(appName); err != nil {
				return err
			}
		}
		return nil
	}()
	if err != nil {
		return ctx, err
	}

	if !start {
		return ctx, nil
	}

	// Provision any admin routers which may need to access
	// some of the other apps at runtime
	err = newCfg.Admin.provisionAdminRouters(ctx)
	if err != nil {
		return ctx, err
	}

	// Start
	err = func() error {
		started := make([]string, 0, len(newCfg.apps))
		for name, a := range newCfg.apps {
			err := a.Start()
			if err != nil {
				// an app failed to start, so we need to stop
				// all other apps that were already started
				for _, otherAppName := range started {
					err2 := newCfg.apps[otherAppName].Stop()
					if err2 != nil {
						err = fmt.Errorf("%v; additionally, aborting app %s: %v",
							err, otherAppName, err2)
					}
				}
				return fmt.Errorf("%s app module: start: %v", name, err)
			}
			started = append(started, name)
		}
		return nil
	}()
	if err != nil {
		return ctx, err
	}

	// now that the user's config is running, finish setting up anything else,
	// such as remote admin endpoint, config loader, etc.
	return ctx, finishSettingUp(ctx, newCfg)
}

// finishSettingUp should be run after all apps have successfully started.
func finishSettingUp(ctx Context, cfg *Config) error {
	// establish this server's identity (only after apps are loaded
	// so that cert management of this endpoint doesn't prevent user's
	// servers from starting which likely also use HTTP/HTTPS ports;
	// but before remote management which may depend on these creds)
	err := manageIdentity(ctx, cfg)
	if err != nil {
		return fmt.Errorf("provisioning remote admin endpoint: %v", err)
	}

	// replace any remote admin endpoint
	err = replaceRemoteAdminServer(ctx, cfg)
	if err != nil {
		return fmt.Errorf("provisioning remote admin endpoint: %v", err)
	}

	// if dynamic config is requested, set that up and run it
	if cfg != nil && cfg.Admin != nil && cfg.Admin.Config != nil && cfg.Admin.Config.LoadRaw != nil {
		val, err := ctx.LoadModule(cfg.Admin.Config, "LoadRaw")
		if err != nil {
			return fmt.Errorf("loading config loader module: %s", err)
		}

		logger := Log().Named("config_loader").With(
			zap.String("module", val.(Module).CaddyModule().ID.Name()),
			zap.Int("load_delay", int(cfg.Admin.Config.LoadDelay)))

		runLoadedConfig := func(config []byte) error {
			logger.Info("applying dynamically-loaded config")
			err := changeConfig(http.MethodPost, "/"+rawConfigKey, config, "", false)
			if errors.Is(err, errSameConfig) {
				return err
			}
			if err != nil {
				logger.Error("failed to run dynamically-loaded config", zap.Error(err))
				return err
			}
			logger.Info("successfully applied dynamically-loaded config")
			return nil
		}

		if cfg.Admin.Config.LoadDelay > 0 {
			go func() {
				// the loop is here to iterate ONLY if there is an error, a no-op config load,
				// or an unchanged config; in which case we simply wait the delay and try again
				for {
					timer := time.NewTimer(time.Duration(cfg.Admin.Config.LoadDelay))
					select {
					case <-timer.C:
						loadedConfig, err := val.(ConfigLoader).LoadConfig(ctx)
						if err != nil {
							logger.Error("failed loading dynamic config; will retry", zap.Error(err))
							continue
						}
						if loadedConfig == nil {
							logger.Info("dynamically-loaded config was nil; will retry")
							continue
						}
						err = runLoadedConfig(loadedConfig)
						if errors.Is(err, errSameConfig) {
							logger.Info("dynamically-loaded config was unchanged; will retry")
							continue
						}
					case <-ctx.Done():
						if !timer.Stop() {
							<-timer.C
						}
						logger.Info("stopping dynamic config loading")
					}
					break
				}
			}()
		} else {
			// if no LoadDelay is provided, will load config synchronously
			loadedConfig, err := val.(ConfigLoader).LoadConfig(ctx)
			if err != nil {
				return fmt.Errorf("loading dynamic config from %T: %v", val, err)
			}
			// do this in a goroutine so current config can finish being loaded; otherwise deadlock
			go func() { _ = runLoadedConfig(loadedConfig) }()
		}
	}

	return nil
}

// ConfigLoader is a type that can load a Caddy config. If
// the return value is non-nil, it must be valid Caddy JSON;
// if nil or with non-nil error, it is considered to be a
// no-op load and may be retried later.
type ConfigLoader interface {
	LoadConfig(Context) ([]byte, error)
}

// Stop stops running the current configuration.
// It is the antithesis of Run(). This function
// will log any errors that occur during the
// stopping of individual apps and continue to
// stop the others. Stop should only be called
// if not replacing with a new config.
func Stop() error {
	currentCtxMu.Lock()
	defer currentCtxMu.Unlock()
	unsyncedStop(currentCtx)
	currentCtx = Context{}
	rawCfgJSON = nil
	rawCfgIndex = nil
	rawCfg[rawConfigKey] = nil
	return nil
}

// unsyncedStop stops cfg from running, but has
// no locking around cfg. It is a no-op if cfg is
// nil. If any app returns an error when stopping,
// it is logged and the function continues stopping
// the next app. This function assumes all apps in
// cfg were successfully started first.
func unsyncedStop(ctx Context) {
	if ctx.cfg == nil {
		return
	}

	// stop each app
	for name, a := range ctx.cfg.apps {
		err := a.Stop()
		if err != nil {
			log.Printf("[ERROR] stop %s: %v", name, err)
		}
	}

	// clean up all modules
	ctx.cfg.cancelFunc()
}

// Validate loads, provisions, and validates
// cfg, but does not start running it.
func Validate(cfg *Config) error {
	_, err := run(cfg, false)
	if err == nil {
		cfg.cancelFunc() // call Cleanup on all modules
	}
	return err
}

// exitProcess exits the process as gracefully as possible,
// but it always exits, even if there are errors doing so.
// It stops all apps, cleans up external locks, removes any
// PID file, and shuts down admin endpoint(s) in a goroutine.
// Errors are logged along the way, and an appropriate exit
// code is emitted.
func exitProcess(ctx context.Context, logger *zap.Logger) {
	// let the rest of the program know we're quitting
	atomic.StoreInt32(exiting, 1)

	// give the OS or service/process manager our 2 weeks' notice: we quit
	if err := notify.Stopping(); err != nil {
		Log().Error("unable to notify service manager of stopping state", zap.Error(err))
	}

	if logger == nil {
		logger = Log()
	}
	logger.Warn("exiting; byeee!! 👋")

	exitCode := ExitCodeSuccess

	// stop all apps
	if err := Stop(); err != nil {
		logger.Error("failed to stop apps", zap.Error(err))
		exitCode = ExitCodeFailedQuit
	}

	// clean up certmagic locks
	certmagic.CleanUpOwnLocks(ctx, logger)

	// remove pidfile
	if pidfile != "" {
		err := os.Remove(pidfile)
		if err != nil {
			logger.Error("cleaning up PID file:",
				zap.String("pidfile", pidfile),
				zap.Error(err))
			exitCode = ExitCodeFailedQuit
		}
	}

	// shut down admin endpoint(s) in goroutines so that
	// if this function was called from an admin handler,
	// it has a chance to return gracefully
	// use goroutine so that we can finish responding to API request
	go func() {
		defer func() {
			logger = logger.With(zap.Int("exit_code", exitCode))
			if exitCode == ExitCodeSuccess {
				logger.Info("shutdown complete")
			} else {
				logger.Error("unclean shutdown")
			}
			os.Exit(exitCode)
		}()

		if remoteAdminServer != nil {
			err := stopAdminServer(remoteAdminServer)
			if err != nil {
				exitCode = ExitCodeFailedQuit
				logger.Error("failed to stop remote admin server gracefully", zap.Error(err))
			}
		}
		if localAdminServer != nil {
			err := stopAdminServer(localAdminServer)
			if err != nil {
				exitCode = ExitCodeFailedQuit
				logger.Error("failed to stop local admin server gracefully", zap.Error(err))
			}
		}
	}()
}

var exiting = new(int32) // accessed atomically

// Exiting returns true if the process is exiting.
// EXPERIMENTAL API: subject to change or removal.
func Exiting() bool { return atomic.LoadInt32(exiting) == 1 }

// Duration can be an integer or a string. An integer is
// interpreted as nanoseconds. If a string, it is a Go
// time.Duration value such as `300ms`, `1.5h`, or `2h45m`;
// valid units are `ns`, `us`/`µs`, `ms`, `s`, `m`, `h`, and `d`.
type Duration time.Duration

// UnmarshalJSON satisfies json.Unmarshaler.
func (d *Duration) UnmarshalJSON(b []byte) error {
	if len(b) == 0 {
		return io.EOF
	}
	var dur time.Duration
	var err error
	if b[0] == byte('"') && b[len(b)-1] == byte('"') {
		dur, err = ParseDuration(strings.Trim(string(b), `"`))
	} else {
		err = json.Unmarshal(b, &dur)
	}
	*d = Duration(dur)
	return err
}

// ParseDuration parses a duration string, adding
// support for the "d" unit meaning number of days,
// where a day is assumed to be 24h. The maximum
// input string length is 1024.
func ParseDuration(s string) (time.Duration, error) {
	if len(s) > 1024 {
		return 0, fmt.Errorf("parsing duration: input string too long")
	}
	var inNumber bool
	var numStart int
	for i := 0; i < len(s); i++ {
		ch := s[i]
		if ch == 'd' {
			daysStr := s[numStart:i]
			days, err := strconv.ParseFloat(daysStr, 64)
			if err != nil {
				return 0, err
			}
			hours := days * 24.0
			hoursStr := strconv.FormatFloat(hours, 'f', -1, 64)
			s = s[:numStart] + hoursStr + "h" + s[i+1:]
			i--
			continue
		}
		if !inNumber {
			numStart = i
		}
		inNumber = (ch >= '0' && ch <= '9') || ch == '.' || ch == '-' || ch == '+'
	}
	return time.ParseDuration(s)
}

// InstanceID returns the UUID for this instance, and generates one if it
// does not already exist. The UUID is stored in the local data directory,
// regardless of storage configuration, since each instance is intended to
// have its own unique ID.
func InstanceID() (uuid.UUID, error) {
	uuidFilePath := filepath.Join(AppDataDir(), "instance.uuid")
	uuidFileBytes, err := os.ReadFile(uuidFilePath)
	if os.IsNotExist(err) {
		uuid, err := uuid.NewRandom()
		if err != nil {
			return uuid, err
		}
		err = os.WriteFile(uuidFilePath, []byte(uuid.String()), 0600)
		return uuid, err
	} else if err != nil {
		return [16]byte{}, err
	}
	return uuid.ParseBytes(uuidFileBytes)
}

// CustomVersion is an optional string that overrides Caddy's
// reported version. It can be helpful when downstream packagers
// need to manually set Caddy's version. If no other version
// information is available, the short form version (see
// Version()) will be set to CustomVersion, and the full version
// will include CustomVersion at the beginning.
//
// Set this variable during `go build` with `-ldflags`:
//
//	-ldflags '-X github.com/caddyserver/caddy/v2.CustomVersion=v2.6.2'
//
// for example.
var CustomVersion string

// Version returns the Caddy version in a simple/short form, and
// a full version string. The short form will not have spaces and
// is intended for User-Agent strings and similar, but may be
// omitting valuable information. Note that Caddy must be compiled
// in a special way to properly embed complete version information.
// First this function tries to get the version from the embedded
// build info provided by go.mod dependencies; then it tries to
// get info from embedded VCS information, which requires having
// built Caddy from a git repository. If no version is available,
// this function returns "(devel)" because Go uses that, but for
// the simple form we change it to "unknown". If still no version
// is available (e.g. no VCS repo), then it will use CustomVersion;
// CustomVersion is always prepended to the full version string.
//
// See relevant Go issues: https://github.com/golang/go/issues/29228
// and https://github.com/golang/go/issues/50603.
//
// This function is experimental and subject to change or removal.
func Version() (simple, full string) {
	// the currently-recommended way to build Caddy involves
	// building it as a dependency so we can extract version
	// information from go.mod tooling; once the upstream
	// Go issues are fixed, we should just be able to use
	// bi.Main... hopefully.
	var module *debug.Module
	bi, ok := debug.ReadBuildInfo()
	if !ok {
		if CustomVersion != "" {
			full = CustomVersion
			simple = CustomVersion
			return
		}
		full = "unknown"
		simple = "unknown"
		return
	}
	// find the Caddy module in the dependency list
	for _, dep := range bi.Deps {
		if dep.Path == ImportPath {
			module = dep
			break
		}
	}
	if module != nil {
		simple, full = module.Version, module.Version
		if module.Sum != "" {
			full += " " + module.Sum
		}
		if module.Replace != nil {
			full += " => " + module.Replace.Path
			if module.Replace.Version != "" {
				simple = module.Replace.Version + "_custom"
				full += "@" + module.Replace.Version
			}
			if module.Replace.Sum != "" {
				full += " " + module.Replace.Sum
			}
		}
	}

	if full == "" {
		var vcsRevision string
		var vcsTime time.Time
		var vcsModified bool
		for _, setting := range bi.Settings {
			switch setting.Key {
			case "vcs.revision":
				vcsRevision = setting.Value
			case "vcs.time":
				vcsTime, _ = time.Parse(time.RFC3339, setting.Value)
			case "vcs.modified":
				vcsModified, _ = strconv.ParseBool(setting.Value)
			}
		}

		if vcsRevision != "" {
			var modified string
			if vcsModified {
				modified = "+modified"
			}
			full = fmt.Sprintf("%s%s (%s)", vcsRevision, modified, vcsTime.Format(time.RFC822))
			simple = vcsRevision

			// use short checksum for simple, if hex-only
			if _, err := hex.DecodeString(simple); err == nil {
				simple = simple[:8]
			}

			// append date to simple since it can be convenient
			// to know the commit date as part of the version
			if !vcsTime.IsZero() {
				simple += "-" + vcsTime.Format("20060102")
			}
		}
	}

	if full == "" {
		if CustomVersion != "" {
			full = CustomVersion
		} else {
			full = "unknown"
		}
	} else if CustomVersion != "" {
		full = CustomVersion + " " + full
	}

	if simple == "" || simple == "(devel)" {
		if CustomVersion != "" {
			simple = CustomVersion
		} else {
			simple = "unknown"
		}
	}

	return
}

// ActiveContext returns the currently-active context.
// This function is experimental and might be changed
// or removed in the future.
func ActiveContext() Context {
	currentCtxMu.RLock()
	defer currentCtxMu.RUnlock()
	return currentCtx
}

// CtxKey is a value type for use with context.WithValue.
type CtxKey string

// This group of variables pertains to the current configuration.
var (
	// currentCtxMu protects everything in this var block.
	currentCtxMu sync.RWMutex

	// currentCtx is the root context for the currently-running
	// configuration, which can be accessed through this value.
	// If the Config contained in this value is not nil, then
	// a config is currently active/running.
	currentCtx Context

	// rawCfg is the current, generic-decoded configuration;
	// we initialize it as a map with one field ("config")
	// to maintain parity with the API endpoint and to avoid
	// the special case of having to access/mutate the variable
	// directly without traversing into it.
	rawCfg = map[string]any{
		rawConfigKey: nil,
	}

	// rawCfgJSON is the JSON-encoded form of rawCfg. Keeping
	// this around avoids an extra Marshal call during changes.
	rawCfgJSON []byte

	// rawCfgIndex is the map of user-assigned ID to expanded
	// path, for converting /id/ paths to /config/ paths.
	rawCfgIndex map[string]string
)

// errSameConfig is returned if the new config is the same
// as the old one. This isn't usually an actual, actionable
// error; it's mostly a sentinel value.
var errSameConfig = errors.New("config is unchanged")

// ImportPath is the package import path for Caddy core.
// This identifier may be removed in the future.
const ImportPath = "github.com/caddyserver/caddy/v2"