// 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"
	"log"
	"net"
	"net/http"
	"os"
	"os/exec"
	"runtime"
	"runtime/debug"
	"strings"

	"github.com/aryann/difflib"
	"github.com/caddyserver/caddy/v2"
	"github.com/caddyserver/caddy/v2/caddyconfig"
	"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
	"go.uber.org/zap"
)

func cmdStart(fl Flags) (int, error) {
	startCmdConfigFlag := fl.String("config")
	startCmdConfigAdapterFlag := fl.String("adapter")
	startCmdPidfileFlag := fl.String("pidfile")
	startCmdWatchFlag := fl.Bool("watch")
	startCmdEnvfileFlag := fl.String("envfile")

	// 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())
	if startCmdConfigFlag != "" {
		cmd.Args = append(cmd.Args, "--config", startCmdConfigFlag)
	}
	if startCmdEnvfileFlag != "" {
		cmd.Args = append(cmd.Args, "--envfile", startCmdEnvfileFlag)
	}
	if startCmdConfigAdapterFlag != "" {
		cmd.Args = append(cmd.Args, "--adapter", startCmdConfigAdapterFlag)
	}
	if startCmdWatchFlag {
		cmd.Args = append(cmd.Args, "--watch")
	}
	if startCmdPidfileFlag != "" {
		cmd.Args = append(cmd.Args, "--pidfile", startCmdPidfileFlag)
	}
	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()

	runCmdConfigFlag := fl.String("config")
	runCmdConfigAdapterFlag := fl.String("adapter")
	runCmdResumeFlag := fl.Bool("resume")
	runCmdLoadEnvfileFlag := fl.String("envfile")
	runCmdPrintEnvFlag := fl.Bool("environ")
	runCmdWatchFlag := fl.Bool("watch")
	runCmdPidfileFlag := fl.String("pidfile")
	runCmdPingbackFlag := fl.String("pingback")

	// load all additional envs as soon as possible
	if runCmdLoadEnvfileFlag != "" {
		if err := loadEnvFromFile(runCmdLoadEnvfileFlag); err != nil {
			return caddy.ExitCodeFailedStartup,
				fmt.Errorf("loading additional environment variables: %v", err)
		}
	}

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

	// load the config, depending on flags
	var config []byte
	var err error
	if runCmdResumeFlag {
		config, err = os.ReadFile(caddy.ConfigAutosavePath)
		if os.IsNotExist(err) {
			// not a bad error; just can't resume if autosave file doesn't exist
			caddy.Log().Info("no autosave file exists", zap.String("autosave_file", caddy.ConfigAutosavePath))
			runCmdResumeFlag = false
		} else if err != nil {
			return caddy.ExitCodeFailedStartup, err
		} else {
			if runCmdConfigFlag == "" {
				caddy.Log().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!)
				caddy.Log().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
	if !runCmdResumeFlag {
		config, configFile, err = LoadConfig(runCmdConfigFlag, runCmdConfigAdapterFlag)
		if err != nil {
			return caddy.ExitCodeFailedStartup, err
		}
	}

	// run the initial config
	err = caddy.Load(config, true)
	if err != nil {
		return caddy.ExitCodeFailedStartup, fmt.Errorf("loading initial config: %v", err)
	}
	caddy.Log().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 runCmdPingbackFlag != "" {
		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", runCmdPingbackFlag)
		if err != nil {
			return caddy.ExitCodeFailedStartup,
				fmt.Errorf("dialing confirmation address: %v", err)
		}
		defer conn.Close()
		_, err = conn.Write(confirmationBytes)
		if err != nil {
			return caddy.ExitCodeFailedStartup,
				fmt.Errorf("writing confirmation bytes to %s: %v", runCmdPingbackFlag, err)
		}
	}

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

	// create pidfile
	if runCmdPidfileFlag != "" {
		err := caddy.PIDFile(runCmdPidfileFlag)
		if err != nil {
			caddy.Log().Error("unable to write PID file",
				zap.String("pidfile", runCmdPidfileFlag),
				zap.Error(err))
		}
	}

	// 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 {
			caddy.Log().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 {
			caddy.Log().Warn("$home environment variable is empty - please fix; some assets might be stored in ./caddy")
		}
	default:
		if os.Getenv("HOME") == "" && !hasXDG {
			caddy.Log().Warn("$HOME environment variable is empty - please fix; some assets might be stored in ./caddy")
		}
	}

	select {}
}

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

	adminAddr, err := DetermineAdminAPIAddress(addrFlag, 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")
	addrFlag := fl.String("address")
	forceFlag := fl.Bool("force")

	// get the config in caddy's native format
	config, configFile, 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(addrFlag, config, configFlag, 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")
	}

	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
}

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

	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()
	}

	// organize modules by whether they come with the standard distribution
	standard, nonstandard, unknown, err := getModules()
	if err != nil {
		// oh well, just print the module IDs and exit
		for _, m := range caddy.Modules() {
			fmt.Println(m)
		}
		return caddy.ExitCodeSuccess, nil
	}

	// 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(_ Flags) (int, error) {
	printEnvironment()
	return caddy.ExitCodeSuccess, nil
}

func cmdAdaptConfig(fl Flags) (int, error) {
	adaptCmdInputFlag := fl.String("config")
	adaptCmdAdapterFlag := fl.String("adapter")
	adaptCmdPrettyFlag := fl.Bool("pretty")
	adaptCmdValidateFlag := fl.Bool("validate")

	// if no input file was specified, try a default
	// Caddyfile if the Caddyfile adapter is plugged in
	if adaptCmdInputFlag == "" && caddyconfig.GetAdapter("caddyfile") != nil {
		_, err := os.Stat("Caddyfile")
		if err == nil {
			// default Caddyfile exists
			adaptCmdInputFlag = "Caddyfile"
			caddy.Log().Info("using adjacent Caddyfile")
		} else if !os.IsNotExist(err) {
			// default Caddyfile exists, but error accessing it
			return caddy.ExitCodeFailedStartup, fmt.Errorf("accessing default Caddyfile: %v", err)
		}
	}

	if adaptCmdInputFlag == "" {
		return caddy.ExitCodeFailedStartup,
			fmt.Errorf("input file required when there is no Caddyfile in current directory (use --config flag)")
	}
	if adaptCmdAdapterFlag == "" {
		return caddy.ExitCodeFailedStartup,
			fmt.Errorf("adapter name is required (use --adapt flag or leave unspecified for default)")
	}

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

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

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

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

	if adaptCmdPrettyFlag {
		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(adaptCmdAdapterFlag).Warn(msg,
			zap.String("file", warn.File),
			zap.Int("line", warn.Line))
	}

	// validate output if requested
	if adaptCmdValidateFlag {
		var cfg *caddy.Config
		err = json.Unmarshal(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) {
	validateCmdConfigFlag := fl.String("config")
	validateCmdAdapterFlag := fl.String("adapter")

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

	var cfg *caddy.Config
	err = json.Unmarshal(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) {
	formatCmdConfigFile := fl.Arg(0)
	if formatCmdConfigFile == "" {
		formatCmdConfigFile = "Caddyfile"
	}

	// as a special case, read from stdin if the file name is "-"
	if formatCmdConfigFile == "-" {
		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(formatCmdConfigFile)
	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(formatCmdConfigFile, output, 0600); err != nil {
			return caddy.ExitCodeFailedStartup, fmt.Errorf("overwriting formatted file: %v", err)
		}
	} else 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))
	}

	return caddy.ExitCodeSuccess, 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://unixsocket" // hack so that http.NewRequest() is happy
	}

	// 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() {
		// When listening on a unix socket, the admin endpoint doesn't
		// accept any Host header because there is no host:port for
		// a unix socket's address. The server's host check is fairly
		// strict for security reasons, so we don't allow just any
		// Host header. For unix sockets, the Host header must be
		// empty. Unfortunately, Go makes it impossible to make HTTP
		// requests with an empty Host header... except with this one
		// weird trick. (Hopefully they don't fix it. It's already
		// hard enough to use HTTP over unix sockets.)
		//
		// An equivalent curl command would be something like:
		// $ curl --unix-socket caddy.sock http:/:$REQUEST_URI
		req.URL.Host = " "
		req.Host = ""
	} else {
		req.Header.Set("Origin", origin)
	}
	if body != nil {
		req.Header.Set("Content-Type", "application/json")
	}
	for k, v := range headers {
		req.Header[k] = v
	}

	// 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)
	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*10))
		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
}

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