// 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 caddycmd

import (
	"bytes"
	"context"
	"crypto/rand"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"io/fs"
	"log"
	"maps"
	"net"
	"net/http"
	"os"
	"os/exec"
	"runtime"
	"runtime/debug"
	"strings"

	"github.com/aryann/difflib"
	"go.uber.org/zap"

	"github.com/caddyserver/caddy/v2"
	"github.com/caddyserver/caddy/v2/caddyconfig"
	"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
	"github.com/caddyserver/caddy/v2/internal"
)

func cmdStart(fl Flags) (int, error) {
	configFlag := fl.String("config")
	configAdapterFlag := fl.String("adapter")
	pidfileFlag := fl.String("pidfile")
	watchFlag := fl.Bool("watch")

	var err error
	var envfileFlag []string
	envfileFlag, err = fl.GetStringSlice("envfile")
	if err != nil {
		return caddy.ExitCodeFailedStartup,
			fmt.Errorf("reading envfile flag: %v", err)
	}

	// open a listener to which the child process will connect when
	// it is ready to confirm that it has successfully started
	ln, err := net.Listen("tcp", "127.0.0.1:0")
	if err != nil {
		return caddy.ExitCodeFailedStartup,
			fmt.Errorf("opening listener for success confirmation: %v", err)
	}
	defer ln.Close()

	// craft the command with a pingback address and with a
	// pipe for its stdin, so we can tell it our confirmation
	// code that we expect so that some random port scan at
	// the most unfortunate time won't fool us into thinking
	// the child succeeded (i.e. the alternative is to just
	// wait for any connection on our listener, but better to
	// ensure it's the process we're expecting - we can be
	// sure by giving it some random bytes and having it echo
	// them back to us)
	cmd := exec.Command(os.Args[0], "run", "--pingback", ln.Addr().String()) //nolint:gosec // no command injection that I can determine...
	// we should be able to run caddy in relative paths
	if errors.Is(cmd.Err, exec.ErrDot) {
		cmd.Err = nil
	}
	if configFlag != "" {
		cmd.Args = append(cmd.Args, "--config", configFlag)
	}

	for _, envfile := range envfileFlag {
		cmd.Args = append(cmd.Args, "--envfile", envfile)
	}
	if configAdapterFlag != "" {
		cmd.Args = append(cmd.Args, "--adapter", configAdapterFlag)
	}
	if watchFlag {
		cmd.Args = append(cmd.Args, "--watch")
	}
	if pidfileFlag != "" {
		cmd.Args = append(cmd.Args, "--pidfile", pidfileFlag)
	}
	stdinPipe, err := cmd.StdinPipe()
	if err != nil {
		return caddy.ExitCodeFailedStartup,
			fmt.Errorf("creating stdin pipe: %v", err)
	}
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr

	// generate the random bytes we'll send to the child process
	expect := make([]byte, 32)
	_, err = rand.Read(expect)
	if err != nil {
		return caddy.ExitCodeFailedStartup,
			fmt.Errorf("generating random confirmation bytes: %v", err)
	}

	// begin writing the confirmation bytes to the child's
	// stdin; use a goroutine since the child hasn't been
	// started yet, and writing synchronously would result
	// in a deadlock
	go func() {
		_, _ = stdinPipe.Write(expect)
		stdinPipe.Close()
	}()

	// start the process
	err = cmd.Start()
	if err != nil {
		return caddy.ExitCodeFailedStartup,
			fmt.Errorf("starting caddy process: %v", err)
	}

	// there are two ways we know we're done: either
	// the process will connect to our listener, or
	// it will exit with an error
	success, exit := make(chan struct{}), make(chan error)

	// in one goroutine, we await the success of the child process
	go func() {
		for {
			conn, err := ln.Accept()
			if err != nil {
				if !errors.Is(err, net.ErrClosed) {
					log.Println(err)
				}
				break
			}
			err = handlePingbackConn(conn, expect)
			if err == nil {
				close(success)
				break
			}
			log.Println(err)
		}
	}()

	// in another goroutine, we await the failure of the child process
	go func() {
		err := cmd.Wait() // don't send on this line! Wait blocks, but send starts before it unblocks
		exit <- err       // sending on separate line ensures select won't trigger until after Wait unblocks
	}()

	// when one of the goroutines unblocks, we're done and can exit
	select {
	case <-success:
		fmt.Printf("Successfully started Caddy (pid=%d) - Caddy is running in the background\n", cmd.Process.Pid)
	case err := <-exit:
		return caddy.ExitCodeFailedStartup,
			fmt.Errorf("caddy process exited with error: %v", err)
	}

	return caddy.ExitCodeSuccess, nil
}

func cmdRun(fl Flags) (int, error) {
	caddy.TrapSignals()

	// set up buffered logging for early startup
	// so that we can hold onto logs until after
	// the config is loaded (or fails to load)
	// so that we can write the logs to the user's
	// configured output. we must be sure to flush
	// on any error before the config is loaded.
	logger, defaultLogger, logBuffer := caddy.BufferedLog()

	undoMaxProcs := setResourceLimits(logger)
	defer undoMaxProcs()
	// release the local reference to the undo function so it can be GC'd;
	// the deferred call above has already captured the actual function value.
	undoMaxProcs = nil //nolint:ineffassign,wastedassign

	configFlag := fl.String("config")
	configAdapterFlag := fl.String("adapter")
	resumeFlag := fl.Bool("resume")
	printEnvFlag := fl.Bool("environ")
	watchFlag := fl.Bool("watch")
	pidfileFlag := fl.String("pidfile")
	pingbackFlag := fl.String("pingback")

	// load all additional envs as soon as possible
	err := handleEnvFileFlag(fl)
	if err != nil {
		logBuffer.FlushTo(defaultLogger)
		return caddy.ExitCodeFailedStartup, err
	}

	// if we are supposed to print the environment, do that first
	if printEnvFlag {
		printEnvironment()
	}

	// load the config, depending on flags
	var config []byte
	if resumeFlag {
		config, err = os.ReadFile(caddy.ConfigAutosavePath)
		if errors.Is(err, fs.ErrNotExist) {
			// not a bad error; just can't resume if autosave file doesn't exist
			logger.Info("no autosave file exists", zap.String("autosave_file", caddy.ConfigAutosavePath))
			resumeFlag = false
		} else if err != nil {
			logBuffer.FlushTo(defaultLogger)
			return caddy.ExitCodeFailedStartup, err
		} else {
			if configFlag == "" {
				logger.Info("resuming from last configuration",
					zap.String("autosave_file", caddy.ConfigAutosavePath))
			} else {
				// if they also specified a config file, user should be aware that we're not
				// using it (doing so could lead to data/config loss by overwriting!)
				logger.Warn("--config and --resume flags were used together; ignoring --config and resuming from last configuration",
					zap.String("autosave_file", caddy.ConfigAutosavePath))
			}
		}
	}
	// we don't use 'else' here since this value might have been changed in 'if' block; i.e. not mutually exclusive
	var configFile string
	var adapterUsed string
	if !resumeFlag {
		config, configFile, adapterUsed, err = LoadConfig(configFlag, configAdapterFlag)
		if err != nil {
			logBuffer.FlushTo(defaultLogger)
			return caddy.ExitCodeFailedStartup, err
		}
	}

	// create pidfile now, in case loading config takes a while (issue #5477)
	if pidfileFlag != "" {
		err := caddy.PIDFile(pidfileFlag)
		if err != nil {
			logger.Error("unable to write PID file",
				zap.String("pidfile", pidfileFlag),
				zap.Error(err))
		}
	}

	// If we have a source config file (we're running via 'caddy run --config ...'),
	// record it so SIGUSR1 can reload from the same file. Also provide a callback
	// that knows how to load/adapt that source when requested by the main process.
	if configFile != "" {
		caddy.SetLastConfig(configFile, adapterUsed, func(file, adapter string) error {
			cfg, _, _, err := LoadConfig(file, adapter)
			if err != nil {
				return err
			}
			return caddy.Load(cfg, true)
		})
	}

	// run the initial config
	err = caddy.Load(config, true)
	if err != nil {
		logBuffer.FlushTo(defaultLogger)
		return caddy.ExitCodeFailedStartup, fmt.Errorf("loading initial config: %v", err)
	}
	// release the reference to the config so it can be GC'd
	config = nil //nolint:ineffassign,wastedassign

	// at this stage the config will have replaced the
	// default logger to the configured one, so we can
	// log normally, now that the config is running.
	// also clear our ref to the buffer so it can get GC'd
	logger = caddy.Log()
	defaultLogger = nil //nolint:ineffassign,wastedassign
	logBuffer = nil     //nolint:wastedassign,ineffassign
	logger.Info("serving initial configuration")

	// if we are to report to another process the successful start
	// of the server, do so now by echoing back contents of stdin
	if pingbackFlag != "" {
		confirmationBytes, err := io.ReadAll(os.Stdin)
		if err != nil {
			return caddy.ExitCodeFailedStartup,
				fmt.Errorf("reading confirmation bytes from stdin: %v", err)
		}
		conn, err := net.Dial("tcp", pingbackFlag)
		if err != nil {
			return caddy.ExitCodeFailedStartup,
				fmt.Errorf("dialing confirmation address: %v", err)
		}
		_, err = conn.Write(confirmationBytes)
		if err != nil {
			return caddy.ExitCodeFailedStartup,
				fmt.Errorf("writing confirmation bytes to %s: %v", pingbackFlag, err)
		}
		// close (non-defer because we `select {}` below)
		// and release references so they can be GC'd
		conn.Close()
		confirmationBytes = nil //nolint:ineffassign,wastedassign
		conn = nil              //nolint:wastedassign,ineffassign
	}

	// if enabled, reload config file automatically on changes
	// (this better only be used in dev!)
	if watchFlag {
		go watchConfigFile(configFile, adapterUsed)
	}

	// warn if the environment does not provide enough information about the disk
	hasXDG := os.Getenv("XDG_DATA_HOME") != "" &&
		os.Getenv("XDG_CONFIG_HOME") != "" &&
		os.Getenv("XDG_CACHE_HOME") != ""
	switch runtime.GOOS {
	case "windows":
		if os.Getenv("HOME") == "" && os.Getenv("USERPROFILE") == "" && !hasXDG {
			logger.Warn("neither HOME nor USERPROFILE environment variables are set - please fix; some assets might be stored in ./caddy")
		}
	case "plan9":
		if os.Getenv("home") == "" && !hasXDG {
			logger.Warn("$home environment variable is empty - please fix; some assets might be stored in ./caddy")
		}
	default:
		if os.Getenv("HOME") == "" && !hasXDG {
			logger.Warn("$HOME environment variable is empty - please fix; some assets might be stored in ./caddy")
		}
	}

	// release the last local logger reference
	logger = nil //nolint:wastedassign,ineffassign

	select {}
}

func cmdStop(fl Flags) (int, error) {
	addressFlag := fl.String("address")
	configFlag := fl.String("config")
	configAdapterFlag := fl.String("adapter")

	adminAddr, err := DetermineAdminAPIAddress(addressFlag, nil, configFlag, configAdapterFlag)
	if err != nil {
		return caddy.ExitCodeFailedStartup, fmt.Errorf("couldn't determine admin API address: %v", err)
	}

	resp, err := AdminAPIRequest(adminAddr, http.MethodPost, "/stop", nil, nil)
	if err != nil {
		caddy.Log().Warn("failed using API to stop instance", zap.Error(err))
		return caddy.ExitCodeFailedStartup, err
	}
	defer resp.Body.Close()

	return caddy.ExitCodeSuccess, nil
}

func cmdReload(fl Flags) (int, error) {
	configFlag := fl.String("config")
	configAdapterFlag := fl.String("adapter")
	addressFlag := fl.String("address")
	forceFlag := fl.Bool("force")

	// get the config in caddy's native format
	config, configFile, adapterUsed, err := LoadConfig(configFlag, configAdapterFlag)
	if err != nil {
		return caddy.ExitCodeFailedStartup, err
	}
	if configFile == "" {
		return caddy.ExitCodeFailedStartup, fmt.Errorf("no config file to load")
	}

	adminAddr, err := DetermineAdminAPIAddress(addressFlag, config, configFile, configAdapterFlag)
	if err != nil {
		return caddy.ExitCodeFailedStartup, fmt.Errorf("couldn't determine admin API address: %v", err)
	}

	// optionally force a config reload
	headers := make(http.Header)
	if forceFlag {
		headers.Set("Cache-Control", "must-revalidate")
	}
	// Provide the source file/adapter to the running process so it can
	// preserve its last-config knowledge if this reload came from the same source.
	headers.Set("Caddy-Config-Source-File", configFile)
	headers.Set("Caddy-Config-Source-Adapter", adapterUsed)

	resp, err := AdminAPIRequest(adminAddr, http.MethodPost, "/load", headers, bytes.NewReader(config))
	if err != nil {
		return caddy.ExitCodeFailedStartup, fmt.Errorf("sending configuration to instance: %v", err)
	}
	defer resp.Body.Close()

	return caddy.ExitCodeSuccess, nil
}

func cmdVersion(_ Flags) (int, error) {
	_, full := caddy.Version()
	fmt.Println(full)
	return caddy.ExitCodeSuccess, nil
}

func cmdBuildInfo(_ Flags) (int, error) {
	bi, ok := debug.ReadBuildInfo()
	if !ok {
		return caddy.ExitCodeFailedStartup, fmt.Errorf("no build information")
	}
	fmt.Println(bi)
	return caddy.ExitCodeSuccess, nil
}

// jsonModuleInfo holds metadata about a Caddy module for JSON output.
type jsonModuleInfo struct {
	ModuleName string `json:"module_name"`
	ModuleType string `json:"module_type"`
	Version    string `json:"version,omitempty"`
	PackageURL string `json:"package_url,omitempty"`
}

func cmdListModules(fl Flags) (int, error) {
	packages := fl.Bool("packages")
	versions := fl.Bool("versions")
	skipStandard := fl.Bool("skip-standard")
	jsonOutput := fl.Bool("json")

	// Organize modules by whether they come with the standard distribution
	standard, nonstandard, unknown, err := getModules()
	if err != nil {
		// If module info can't be fetched, just print the IDs and exit
		for _, m := range caddy.Modules() {
			fmt.Println(m)
		}
		return caddy.ExitCodeSuccess, nil
	}

	// Logic for JSON output
	if jsonOutput {
		output := []jsonModuleInfo{}

		// addToOutput is a helper to convert internal module info to the JSON-serializable struct
		addToOutput := func(list []moduleInfo, moduleType string) {
			for _, mi := range list {
				item := jsonModuleInfo{
					ModuleName: mi.caddyModuleID,
					ModuleType: moduleType, // Mapping the type here
				}
				if mi.goModule != nil {
					item.Version = mi.goModule.Version
					item.PackageURL = mi.goModule.Path
				}
				output = append(output, item)
			}
		}

		// Pass the respective type for each category
		if !skipStandard {
			addToOutput(standard, "standard")
		}
		addToOutput(nonstandard, "non-standard")
		addToOutput(unknown, "unknown")

		jsonBytes, err := json.MarshalIndent(output, "", "  ")
		if err != nil {
			return caddy.ExitCodeFailedQuit, err
		}
		fmt.Println(string(jsonBytes))
		return caddy.ExitCodeSuccess, nil
	}

	// Logic for Text output (Fallback)
	printModuleInfo := func(mi moduleInfo) {
		fmt.Print(mi.caddyModuleID)
		if versions && mi.goModule != nil {
			fmt.Print(" " + mi.goModule.Version)
		}
		if packages && mi.goModule != nil {
			fmt.Print(" " + mi.goModule.Path)
			if mi.goModule.Replace != nil {
				fmt.Print(" => " + mi.goModule.Replace.Path)
			}
		}
		if mi.err != nil {
			fmt.Printf(" [%v]", mi.err)
		}
		fmt.Println()
	}

	// Standard modules (always shipped with Caddy)
	if !skipStandard {
		if len(standard) > 0 {
			for _, mod := range standard {
				printModuleInfo(mod)
			}
		}
		fmt.Printf("\n  Standard modules: %d\n", len(standard))
	}

	// Non-standard modules (third party plugins)
	if len(nonstandard) > 0 {
		if len(standard) > 0 && !skipStandard {
			fmt.Println()
		}
		for _, mod := range nonstandard {
			printModuleInfo(mod)
		}
		fmt.Printf("\n  Non-standard modules: %d\n", len(nonstandard))
	}

	// Unknown modules (couldn't get Caddy module info)
	if len(unknown) > 0 {
		if (len(standard) > 0 && !skipStandard) || len(nonstandard) > 0 {
			fmt.Println()
		}
		for _, mod := range unknown {
			printModuleInfo(mod)
		}
		fmt.Printf("\n  Unknown modules: %d\n", len(unknown))
	}

	return caddy.ExitCodeSuccess, nil
}

func cmdEnviron(fl Flags) (int, error) {
	// load all additional envs as soon as possible
	err := handleEnvFileFlag(fl)
	if err != nil {
		return caddy.ExitCodeFailedStartup, err
	}

	printEnvironment()
	return caddy.ExitCodeSuccess, nil
}

func cmdAdaptConfig(fl Flags) (int, error) {
	configFlag := fl.String("config")
	adapterFlag := fl.String("adapter")
	prettyFlag := fl.Bool("pretty")
	validateFlag := fl.Bool("validate")

	var err error
	configFlag, err = configFileWithRespectToDefault(caddy.Log(), configFlag)
	if err != nil {
		return caddy.ExitCodeFailedStartup, err
	}
	if configFlag == "" {
		return caddy.ExitCodeFailedStartup,
			fmt.Errorf("input file required when there is no Caddyfile in current directory (use --config flag)")
	}

	// load all additional envs as soon as possible
	err = handleEnvFileFlag(fl)
	if err != nil {
		return caddy.ExitCodeFailedStartup, err
	}

	if adapterFlag == "" {
		return caddy.ExitCodeFailedStartup,
			fmt.Errorf("adapter name is required (use --adapt flag or leave unspecified for default)")
	}

	cfgAdapter := caddyconfig.GetAdapter(adapterFlag)
	if cfgAdapter == nil {
		return caddy.ExitCodeFailedStartup,
			fmt.Errorf("unrecognized config adapter: %s", adapterFlag)
	}

	var input []byte
	// read from stdin if the file name is "-"
	if configFlag == "-" {
		input, err = io.ReadAll(os.Stdin)
	} else {
		input, err = os.ReadFile(configFlag)
	}
	if err != nil {
		return caddy.ExitCodeFailedStartup,
			fmt.Errorf("reading input file: %v", err)
	}

	opts := map[string]any{"filename": configFlag}

	adaptedConfig, warnings, err := cfgAdapter.Adapt(input, opts)
	if err != nil {
		return caddy.ExitCodeFailedStartup, err
	}

	if prettyFlag {
		var prettyBuf bytes.Buffer
		err = json.Indent(&prettyBuf, adaptedConfig, "", "\t")
		if err != nil {
			return caddy.ExitCodeFailedStartup, err
		}
		adaptedConfig = prettyBuf.Bytes()
	}

	// print result to stdout
	fmt.Println(string(adaptedConfig))

	// print warnings to stderr
	for _, warn := range warnings {
		msg := warn.Message
		if warn.Directive != "" {
			msg = fmt.Sprintf("%s: %s", warn.Directive, warn.Message)
		}
		caddy.Log().Named(adapterFlag).Warn(msg,
			zap.String("file", warn.File),
			zap.Int("line", warn.Line))
	}

	// validate output if requested
	if validateFlag {
		var cfg *caddy.Config
		err = caddy.StrictUnmarshalJSON(adaptedConfig, &cfg)
		if err != nil {
			return caddy.ExitCodeFailedStartup, fmt.Errorf("decoding config: %v", err)
		}
		err = caddy.Validate(cfg)
		if err != nil {
			return caddy.ExitCodeFailedStartup, fmt.Errorf("validation: %v", err)
		}
	}

	return caddy.ExitCodeSuccess, nil
}

func cmdValidateConfig(fl Flags) (int, error) {
	configFlag := fl.String("config")
	adapterFlag := fl.String("adapter")

	// load all additional envs as soon as possible
	err := handleEnvFileFlag(fl)
	if err != nil {
		return caddy.ExitCodeFailedStartup, err
	}

	// use default config and ensure a config file is specified
	configFlag, err = configFileWithRespectToDefault(caddy.Log(), configFlag)
	if err != nil {
		return caddy.ExitCodeFailedStartup, err
	}
	if configFlag == "" {
		return caddy.ExitCodeFailedStartup,
			fmt.Errorf("input file required when there is no Caddyfile in current directory (use --config flag)")
	}

	input, _, _, err := LoadConfig(configFlag, adapterFlag)
	if err != nil {
		return caddy.ExitCodeFailedStartup, err
	}
	input = caddy.RemoveMetaFields(input)

	var cfg *caddy.Config
	err = caddy.StrictUnmarshalJSON(input, &cfg)
	if err != nil {
		return caddy.ExitCodeFailedStartup, fmt.Errorf("decoding config: %v", err)
	}

	err = caddy.Validate(cfg)
	if err != nil {
		return caddy.ExitCodeFailedStartup, err
	}

	fmt.Println("Valid configuration")

	return caddy.ExitCodeSuccess, nil
}

func cmdFmt(fl Flags) (int, error) {
	configFile := fl.Arg(0)
	configFlag := fl.String("config")
	if (len(fl.Args()) > 1) || (configFlag != "" && configFile != "") {
		return caddy.ExitCodeFailedStartup, fmt.Errorf("fmt does not support multiple files %s %s", configFlag, strings.Join(fl.Args(), " "))
	}
	if configFile == "" && configFlag == "" {
		configFile = "Caddyfile"
	} else if configFile == "" {
		configFile = configFlag
	}
	// as a special case, read from stdin if the file name is "-"
	if configFile == "-" {
		input, err := io.ReadAll(os.Stdin)
		if err != nil {
			return caddy.ExitCodeFailedStartup,
				fmt.Errorf("reading stdin: %v", err)
		}
		fmt.Print(string(caddyfile.Format(input)))
		return caddy.ExitCodeSuccess, nil
	}

	input, err := os.ReadFile(configFile)
	if err != nil {
		return caddy.ExitCodeFailedStartup,
			fmt.Errorf("reading input file: %v", err)
	}

	output := caddyfile.Format(input)

	if fl.Bool("overwrite") {
		if err := os.WriteFile(configFile, output, 0o600); err != nil {
			return caddy.ExitCodeFailedStartup, fmt.Errorf("overwriting formatted file: %v", err)
		}
		return caddy.ExitCodeSuccess, nil
	}

	if fl.Bool("diff") {
		diff := difflib.Diff(
			strings.Split(string(input), "\n"),
			strings.Split(string(output), "\n"))
		for _, d := range diff {
			switch d.Delta {
			case difflib.Common:
				fmt.Printf("  %s\n", d.Payload)
			case difflib.LeftOnly:
				fmt.Printf("- %s\n", d.Payload)
			case difflib.RightOnly:
				fmt.Printf("+ %s\n", d.Payload)
			}
		}
	} else {
		fmt.Print(string(output))
	}

	if warning, diff := caddyfile.FormattingDifference(configFile, input); diff {
		return caddy.ExitCodeFailedStartup, fmt.Errorf(`%s:%d: Caddyfile input is not formatted; Tip: use '--overwrite' to update your Caddyfile in-place instead of previewing it. Consult '--help' for more options`,
			warning.File,
			warning.Line,
		)
	}

	return caddy.ExitCodeSuccess, nil
}

// handleEnvFileFlag loads the environment variables from the given --envfile
// flag if specified. This should be called as early in the command function.
func handleEnvFileFlag(fl Flags) error {
	var err error
	var envfileFlag []string
	envfileFlag, err = fl.GetStringSlice("envfile")
	if err != nil {
		return fmt.Errorf("reading envfile flag: %v", err)
	}

	for _, envfile := range envfileFlag {
		if err := loadEnvFromFile(envfile); err != nil {
			return fmt.Errorf("loading additional environment variables: %v", err)
		}
	}

	return nil
}

// AdminAPIRequest makes an API request according to the CLI flags given,
// with the given HTTP method and request URI. If body is non-nil, it will
// be assumed to be Content-Type application/json. The caller should close
// the response body. Should only be used by Caddy CLI commands which
// need to interact with a running instance of Caddy via the admin API.
func AdminAPIRequest(adminAddr, method, uri string, headers http.Header, body io.Reader) (*http.Response, error) {
	parsedAddr, err := caddy.ParseNetworkAddress(adminAddr)
	if err != nil || parsedAddr.PortRangeSize() > 1 {
		return nil, fmt.Errorf("invalid admin address %s: %v", adminAddr, err)
	}
	origin := "http://" + parsedAddr.JoinHostPort(0)
	if parsedAddr.IsUnixNetwork() {
		origin = "http://127.0.0.1" // bogus host is a hack so that http.NewRequest() is happy

		// the unix address at this point might still contain the optional
		// unix socket permissions, which are part of the address/host.
		// those need to be removed first, as they aren't part of the
		// resulting unix file path
		addr, _, err := internal.SplitUnixSocketPermissionsBits(parsedAddr.Host)
		if err != nil {
			return nil, err
		}
		parsedAddr.Host = addr
	} else if parsedAddr.IsFdNetwork() {
		origin = "http://127.0.0.1"
	}

	// form the request
	req, err := http.NewRequest(method, origin+uri, body)
	if err != nil {
		return nil, fmt.Errorf("making request: %v", err)
	}
	if parsedAddr.IsUnixNetwork() || parsedAddr.IsFdNetwork() {
		// We used to conform to RFC 2616 Section 14.26 which requires
		// an empty host header when there is no host, as is the case
		// with unix sockets and socket fds. However, Go required a
		// Host value so we used a hack of a space character as the host
		// (it would see the Host was non-empty, then trim the space later).
		// As of Go 1.20.6 (July 2023), this hack no longer works. See:
		// https://github.com/golang/go/issues/60374
		// See also the discussion here:
		// https://github.com/golang/go/issues/61431
		//
		// After that, we now require a Host value of either 127.0.0.1
		// or ::1 if one is set. Above I choose to use 127.0.0.1. Even
		// though the value should be completely irrelevant (it could be
		// "srldkjfsd"), if for some reason the Host *is* used, at least
		// we can have some reasonable assurance it will stay on the local
		// machine and that browsers, if they ever allow access to unix
		// sockets, can still enforce CORS, ensuring it is still coming
		// from the local machine.
	} else {
		req.Header.Set("Origin", origin)
	}
	if body != nil {
		req.Header.Set("Content-Type", "application/json")
	}
	maps.Copy(req.Header, headers)

	// make an HTTP client that dials our network type, since admin
	// endpoints aren't always TCP, which is what the default transport
	// expects; reuse is not of particular concern here
	client := http.Client{
		Transport: &http.Transport{
			DialContext: func(_ context.Context, _, _ string) (net.Conn, error) {
				return net.Dial(parsedAddr.Network, parsedAddr.JoinHostPort(0))
			},
		},
	}

	resp, err := client.Do(req) //nolint:gosec // the only SSRF here would be self-sabatoge I think
	if err != nil {
		return nil, fmt.Errorf("performing request: %v", err)
	}

	// if it didn't work, let the user know
	if resp.StatusCode >= 400 {
		respBody, err := io.ReadAll(io.LimitReader(resp.Body, 1024*1024*2))
		if err != nil {
			return nil, fmt.Errorf("HTTP %d: reading error message: %v", resp.StatusCode, err)
		}
		return nil, fmt.Errorf("caddy responded with error: HTTP %d: %s", resp.StatusCode, respBody)
	}

	return resp, nil
}

// DetermineAdminAPIAddress determines which admin API endpoint address should
// be used based on the inputs. By priority: if `address` is specified, then
// it is returned; if `config` is specified, then that config will be used for
// finding the admin address; if `configFile` (and `configAdapter`) are specified,
// then that config will be loaded to find the admin address; otherwise, the
// default admin listen address will be returned.
func DetermineAdminAPIAddress(address string, config []byte, configFile, configAdapter string) (string, error) {
	// Prefer the address if specified and non-empty
	if address != "" {
		return address, nil
	}

	// Try to load the config from file if specified, with the given adapter name
	if configFile != "" {
		var loadedConfigFile string
		var err error

		// use the provided loaded config if non-empty
		// otherwise, load it from the specified file/adapter
		loadedConfig := config
		if len(loadedConfig) == 0 {
			// get the config in caddy's native format
			loadedConfig, loadedConfigFile, _, err = LoadConfig(configFile, configAdapter)
			if err != nil {
				return "", err
			}
			if loadedConfigFile == "" {
				return "", fmt.Errorf("no config file to load; either use --config flag or ensure Caddyfile exists in current directory")
			}
		}

		// get the address of the admin listener from the config
		if len(loadedConfig) > 0 {
			var tmpStruct struct {
				Admin caddy.AdminConfig `json:"admin"`
			}
			err := json.Unmarshal(loadedConfig, &tmpStruct)
			if err != nil {
				return "", fmt.Errorf("unmarshaling admin listener address from config: %v", err)
			}
			if tmpStruct.Admin.Listen != "" {
				return tmpStruct.Admin.Listen, nil
			}
		}
	}

	// Fallback to the default listen address otherwise
	return caddy.DefaultAdminListen, nil
}

// configFileWithRespectToDefault returns the filename to use for loading the config, based
// on whether a config file is already specified and a supported default config file exists.
func configFileWithRespectToDefault(logger *zap.Logger, configFile string) (string, error) {
	const defaultCaddyfile = "Caddyfile"

	// if no input file was specified, try a default Caddyfile if the Caddyfile adapter is plugged in
	if configFile == "" && caddyconfig.GetAdapter("caddyfile") != nil {
		_, err := os.Stat(defaultCaddyfile)
		if err == nil {
			// default Caddyfile exists
			if logger != nil {
				logger.Info("using adjacent Caddyfile")
			}
			return defaultCaddyfile, nil
		}
		if !errors.Is(err, fs.ErrNotExist) {
			// problem checking
			return configFile, fmt.Errorf("checking if default Caddyfile exists: %v", err)
		}
	}

	// default config file does not exist or is irrelevant
	return configFile, nil
}

type moduleInfo struct {
	caddyModuleID string
	goModule      *debug.Module
	err           error
}
