package main

import (
	"bytes"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"strconv"
	"strings"
	"syscall"
	"time"
	"unicode"

	rspec "github.com/opencontainers/runtime-spec/specs-go"
	"github.com/sirupsen/logrus"
	"github.com/spf13/cobra"
	"go.podman.io/storage/pkg/ioutils"
	"go.podman.io/storage/pkg/reexec"
)

// use defined names for our various commands. we absolutely don't support
// everything that an actual functional runtime would, and have no intention of
// expanding to do so
type modeType string

const (
	modeCreate  = modeType("create")
	modeStart   = modeType("start")
	modeState   = modeType("state")
	modeKill    = modeType("kill")
	modeDelete  = modeType("delete")
	subprocName = "dumpspec-subproc"
)

// signalsByName is a guess at which signals we'd be asked to send to a child
// process, currently restricted to the subset defined across all of our
// targets
var signalsByName = map[string]syscall.Signal{
	"SIGABRT": syscall.SIGABRT,
	"SIGALRM": syscall.SIGALRM,
	"SIGBUS":  syscall.SIGBUS,
	"SIGFPE":  syscall.SIGFPE,
	"SIGHUP":  syscall.SIGHUP,
	"SIGILL":  syscall.SIGILL,
	"SIGINT":  syscall.SIGINT,
	"SIGKILL": syscall.SIGKILL,
	"SIGPIPE": syscall.SIGPIPE,
	"SIGQUIT": syscall.SIGQUIT,
	"SIGSEGV": syscall.SIGSEGV,
	"SIGTERM": syscall.SIGTERM,
	"SIGTRAP": syscall.SIGTRAP,
}

var (
	globalArgs struct {
		debug         bool
		cgroupManager string
		log           string
		logFormat     string
		logLevel      string
		root          string
		systemdCgroup bool
		rootless      bool
	}
	createArgs struct {
		bundleDir     string
		configFile    string
		consoleSocket string
		pidFile       string
		noPivot       bool
		noNewKeyring  bool
		preserveFds   int
	}
	stateArgs struct {
		all   bool
		pid   int
		regex string
	}
	killArgs struct {
		all    bool
		pid    int
		regex  string
		signal int
	}
	deleteArgs struct {
		force bool
		regex string
	}
)

func main() {
	if reexec.Init() {
		return
	}

	if len(os.Args) < 2 {
		return
	}

	var container, containerID, containerDir string

	mainCommand := cobra.Command{
		Use:   "dumpspec",
		Short: "fake OCI runtime",
		PersistentPreRunE: func(_ *cobra.Command, args []string) error {
			tmpdir, ok := os.LookupEnv("XDG_RUNTIME_DIR")
			if !ok {
				tmpdir = filepath.Join(os.TempDir(), strconv.Itoa(os.Getuid()))
			}
			if globalArgs.root != "" {
				tmpdir = globalArgs.root
			}
			tmpdir = filepath.Join(tmpdir, "dumpspec")
			if err := os.MkdirAll(tmpdir, 0o700); err != nil && !errors.Is(err, os.ErrExist) {
				return fmt.Errorf("ensuring that %q exists: %w", tmpdir, err)
			}
			if len(args) > 0 {
				// this is the first arg for all of the commands that we care about
				container = args[0]
			}
			containerID = mapToContainerID(container)
			containerDir = filepath.Join(tmpdir, containerID)
			return nil
		},
	}
	mainFlags := mainCommand.PersistentFlags()
	mainFlags.BoolVar(&globalArgs.debug, "debug", false, "log for debugging")
	mainFlags.BoolVar(&globalArgs.systemdCgroup, "systemd-cgroup", false, "use systemd for handling cgroups")
	mainFlags.BoolVar(&globalArgs.rootless, "rootless", false, "ignore some settings to that conflict with rootless operation")
	mainFlags.StringVar(&globalArgs.cgroupManager, "cgroup-manager", "cgroupfs", "method for managing cgroups")
	mainFlags.StringVar(&globalArgs.log, "log", "", "logging destination")
	mainFlags.StringVar(&globalArgs.logFormat, "log-format", "", "logging format specifier")
	mainFlags.StringVar(&globalArgs.logLevel, "log-level", "", "logging level")
	rootUsage := "root `directory` of runtime data"
	rootDefault := ""
	if xdgRuntimeDir, ok := os.LookupEnv("XDG_RUNTIME_DIR"); ok {
		rootUsage += " (default $XDG_RUNTIME_DIR)"
		rootDefault = xdgRuntimeDir
	}
	mainFlags.StringVar(&globalArgs.root, "root", rootDefault, rootUsage)

	createCommand := &cobra.Command{
		Use:   string(modeCreate),
		Args:  cobra.ExactArgs(1),
		Short: "create a ready-to-start container process",
		RunE: func(_ *cobra.Command, _ []string) error {
			if err := os.MkdirAll(containerDir, 0o700); err != nil {
				return fmt.Errorf("creating container directory: %w", err)
			}
			configFile := createArgs.configFile
			if configFile == "" {
				configFile = filepath.Join(createArgs.bundleDir, "config.json")
			}
			config, err := os.ReadFile(configFile)
			if err != nil {
				return fmt.Errorf("reading runtime configuration: %w", err)
			}
			var spec rspec.Spec
			if err := json.Unmarshal(config, &spec); err != nil {
				return fmt.Errorf("parsing runtime configuration: %w", err)
			}
			if err := os.WriteFile(filepath.Join(containerDir, "config.json"), config, 0o600); err != nil {
				return fmt.Errorf("saving copy of runtime configuration: %w", err)
			}
			state := rspec.State{
				Version: rspec.Version,
				ID:      container,
				Bundle:  createArgs.bundleDir,
			}
			stateBytes, err := json.Marshal(state)
			if err != nil {
				return fmt.Errorf("encoding initial runtime state: %w", err)
			}
			if err := os.WriteFile(filepath.Join(containerDir, "state"), stateBytes, 0o600); err != nil {
				return fmt.Errorf("writing initial runtime state: %w", err)
			}
			pr, pw, err := os.Pipe()
			if err != nil {
				return fmt.Errorf("internal error: %w", err)
			}
			defer pr.Close()
			cmd := getStarter(containerDir, createArgs.consoleSocket, createArgs.pidFile, spec, pw)
			if err := cmd.Start(); err != nil {
				return fmt.Errorf("internal error: %w", err)
			}
			pw.Close()
			ready, err := io.ReadAll(pr)
			if err != nil {
				return fmt.Errorf("waiting for child to start: %w", err)
			}
			if strings.TrimSpace(string(ready)) != "OK" {
				return fmt.Errorf("unexpected child status %q", string(ready))
			}
			return nil
		},
	}
	createFlags := createCommand.Flags()
	createFlags.StringVarP(&createArgs.bundleDir, "bundle", "b", "", "`directory` containing config.json")
	createFlags.StringVarP(&createArgs.configFile, "config", "f", "", "`path` to config.json")
	createFlags.StringVar(&createArgs.consoleSocket, "console-socket", "", "socket `path` for passing PTY")
	createFlags.StringVar(&createArgs.pidFile, "pid-file", "", "`path` in which to store child PID")
	createFlags.BoolVar(&createArgs.noPivot, "no-pivot", false, "use chroot() instead of pivot_root()")
	createFlags.BoolVar(&createArgs.noNewKeyring, "no-new-keyring", false, "don't create a new keyring")
	mainCommand.AddCommand(createCommand)

	startCommand := &cobra.Command{
		Use:   string(modeStart),
		Args:  cobra.ExactArgs(1),
		Short: "start a previously-created container process",
		RunE: func(_ *cobra.Command, _ []string) error {
			if err := ioutils.AtomicWriteFile(filepath.Join(containerDir, "start"), []byte("start"), 0o600); err != nil {
				return fmt.Errorf("writing start file: %w", err)
			}
			return nil
		},
	}
	mainCommand.AddCommand(startCommand)

	stateCommand := &cobra.Command{
		Use:   string(modeState),
		Args:  cobra.ExactArgs(1),
		Short: "poll the state of a container process",
		RunE: func(_ *cobra.Command, _ []string) error {
			stateFile, err := os.Open(filepath.Join(containerDir, "state"))
			if err != nil {
				return err
			}
			defer stateFile.Close()
			if _, err := io.Copy(os.Stdout, stateFile); err != nil {
				return fmt.Errorf("copying state file: %w", err)
			}
			return nil
		},
	}
	stateFlags := stateCommand.Flags()
	stateFlags.BoolVarP(&stateArgs.all, "all", "a", false, "start all containers")
	stateFlags.IntVar(&stateArgs.pid, "pid", 0, "start container by `pid`")
	stateFlags.StringVarP(&stateArgs.regex, "regex", "r", "", "start containers with IDs matching a `regex`")
	mainCommand.AddCommand(stateCommand)

	killCommand := &cobra.Command{
		Use:   string(modeKill),
		Args:  cobra.RangeArgs(1, 2),
		Short: "signal/kill a container process",
		RunE: func(_ *cobra.Command, args []string) error {
			if len(args) > 1 {
				signalString := args[1]
				signalNumber, err := strconv.Atoi(signalString)
				if err != nil {
					n, ok := signalsByName[signalString]
					if !ok {
						n, ok = signalsByName["SIG"+signalString]
						if !ok {
							return fmt.Errorf("%v: unrecognized signal %q", os.Args, signalString)
						}
					}
					signalNumber = int(n)
				}
				killArgs.signal = signalNumber
			}
			if err := ioutils.AtomicWriteFile(filepath.Join(containerDir, "kill"), []byte(strconv.Itoa(killArgs.signal)), 0o600); err != nil {
				return fmt.Errorf("writing exit status file: %w", err)
			}
			return nil
		},
	}
	killFlags := killCommand.Flags()
	killFlags.BoolVarP(&killArgs.all, "all", "a", false, "signal/kill all containers")
	killFlags.IntVar(&killArgs.pid, "pid", 0, "signal/kill container by `pid`")
	killFlags.StringVarP(&killArgs.regex, "regex", "r", "", "signal/kill containers with IDs matching a `regex`")
	mainCommand.AddCommand(killCommand)

	deleteCommand := &cobra.Command{
		Use:   string(modeDelete),
		Args:  cobra.ExactArgs(1),
		Short: "delete a container process",
		RunE: func(_ *cobra.Command, _ []string) error {
			if err := os.RemoveAll(containerDir); err != nil {
				return fmt.Errorf("removing container directory: %w", err)
			}
			return nil
		},
	}
	deleteFlags := deleteCommand.Flags()
	deleteFlags.StringVarP(&deleteArgs.regex, "regex", "r", "", "delete containers with IDs matching a `regex`")
	deleteFlags.BoolVarP(&deleteArgs.force, "force", "f", false, "forcibly stop containers which are not stopped")
	mainCommand.AddCommand(deleteCommand)

	err := mainCommand.Execute()
	if err != nil {
		logrus.Fatal(err)
		os.Exit(1)
	}
	os.Exit(0)
}

func mapToContainerID(container string) string {
	var encoder strings.Builder
	for _, c := range container {
		if unicode.IsLetter(c) || unicode.IsNumber(c) {
			if _, err := encoder.WriteRune(c); err != nil {
				logrus.Fatalf("%v: encoding container ID: %q: %v", os.Args, c, err)
			}
		} else {
			if _, err := encoder.WriteString(strconv.Itoa(int(c))); err != nil {
				logrus.Fatalf("%v: encoding container ID: %q: %v", os.Args, c, err)
			}
		}
	}
	return encoder.String()
}

func waitForFile(dirname, basename string) string {
	waitedFile := filepath.Join(dirname, basename)
	for {
		if _, err := os.Stat(dirname); err != nil {
			logrus.Fatalf("%v: %v", os.Args, err)
		}
		st, err := os.Stat(waitedFile)
		if err != nil && !errors.Is(err, os.ErrNotExist) {
			logrus.Fatalf("%v: %v", os.Args, err)
		}
		if err != nil || st.Size() == 0 {
			time.Sleep(100 * time.Millisecond)
			continue
		}
		contents, err := os.ReadFile(waitedFile)
		if err != nil {
			logrus.Fatalf("%v: %v", os.Args, err)
		}
		text := strings.TrimSpace(string(contents))
		return text
	}
}

func init() {
	reexec.Register(subprocName, subproc)
}

func subproc() {
	mainCommand := cobra.Command{
		Use:   "dumpspec",
		Short: "fake OCI runtime",
		Long:  "dumpspec containerDir consoleSocket pidFile [spec ...]",
		Args:  cobra.ExactArgs(3),
		RunE: func(_ *cobra.Command, args []string) error {
			dir := args[0]
			consoleSocket := args[1]
			pidFile := args[2]

			config, err := os.ReadFile(filepath.Join(dir, "config.json"))
			if err != nil {
				return fmt.Errorf("reading runtime configuration: %w", err)
			}
			var spec rspec.Spec
			if err := json.Unmarshal(config, &spec); err != nil {
				return fmt.Errorf("parsing runtime configuration: %w", err)
			}

			stateBytes, err := os.ReadFile(filepath.Join(dir, "state"))
			if err != nil {
				return fmt.Errorf("reading initial state : %w", err)
			}
			var state rspec.State
			if err := json.Unmarshal(stateBytes, &state); err != nil {
				return fmt.Errorf("parsing initial state: %w", err)
			}

			saveState := func() error {
				stateBytes, err := json.Marshal(state)
				if err != nil {
					return fmt.Errorf("encoding updated state: %w", err)
				}
				err = ioutils.AtomicWriteFile(filepath.Join(dir, "state"), stateBytes, 0o600)
				if err != nil {
					return fmt.Errorf("writing updated state: %w", err)
				}
				return nil
			}

			output := io.Writer(os.Stdout)

			if pidFile != "" {
				if err := ioutils.AtomicWriteFile(pidFile, []byte(strconv.Itoa(os.Getpid())), 0o600); err != nil {
					return fmt.Errorf("writing pid file %q: %w", pidFile, err)
				}
			}

			state.Pid = os.Getpid()
			state.Status = rspec.StateCreated
			if err := saveState(); err != nil {
				return err
			}

			if consoleSocket != "" {
				if output, err = sendConsoleDescriptor(consoleSocket); err != nil {
					return fmt.Errorf("sending terminal control fd to parent process: %w", err)
				}
			}

			ok := os.NewFile(3, "startup status pipe")
			fmt.Fprintf(ok, "OK")
			ok.Close()

			start := waitForFile(dir, "start")
			if start != "start" {
				return fmt.Errorf("unexpected start indicator %q", start)
			}

			state.Status = rspec.StateRunning
			if err := saveState(); err != nil {
				return err
			}

			if spec.Process == nil || len(spec.Process.Args) == 0 {
				if _, err := io.Copy(output, bytes.NewReader(config)); err != nil {
					return fmt.Errorf("writing configuration: %w", err)
				}
			} else {
				for _, query := range spec.Process.Args {
					var data any
					if err := json.Unmarshal(config, &data); err != nil {
						return fmt.Errorf("parsing runtime configuration: %w", err)
					}
					path := strings.Split(query, ".")
					for i, component := range path {
						if component == "" {
							continue
						}
						pathSoFar := strings.Join(path[:i], ".")
						if data == nil {
							return fmt.Errorf("unable to descend into %q after %q", component, pathSoFar)
						}
						if m, ok := data.(map[string]any); ok {
							data = m[component]
						} else if s, ok := data.([]any); ok {
							i, err := strconv.Atoi(component)
							if err != nil {
								return fmt.Errorf("%q is not numeric while indexing slice at %q", component, pathSoFar)
							}
							data = s[i]
						} else {
							return fmt.Errorf("unable to descend into %q after %q", component, pathSoFar)
						}
					}
					final, err := json.Marshal(data)
					if err != nil {
						return fmt.Errorf("encoding query result: %w", err)
					}
					if len(final) == 0 || final[len(final)-1] != '\n' {
						final = append(final, byte('\n'))
					}
					if _, err := io.Copy(output, bytes.NewReader(final)); err != nil {
						return fmt.Errorf("writing configuration subset %q: %w", query, err)
					}
				}
			}

			state.Status = rspec.StateStopped
			if err := saveState(); err != nil {
				return err
			}
			return nil
		},
	}
	err := mainCommand.Execute()
	if err != nil {
		logrus.Fatal(err)
		os.Exit(1)
	}
	os.Exit(0)
}
