package main

import (
	"context"
	"errors"
	"fmt"
	"io"
	"log"
	"os"
	"os/exec"
	"os/user"
	"strconv"
	"sync"
	"syscall"
	"time"

	"hakurei.app/check"
	"hakurei.app/command"
	"hakurei.app/ext"
	"hakurei.app/fhs"
	"hakurei.app/hst"
	"hakurei.app/internal/dbus"
	"hakurei.app/internal/env"
	"hakurei.app/internal/info"
	"hakurei.app/internal/outcome"
	"hakurei.app/message"
)

// optionalErrorUnwrap calls [errors.Unwrap] and returns the resulting value
// if it is not nil, or the original value if it is.
func optionalErrorUnwrap(err error) error {
	if underlyingErr := errors.Unwrap(err); underlyingErr != nil {
		return underlyingErr
	}
	return err
}

var errSuccess = errors.New("success")

func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErrs, out io.Writer) command.Command {
	var (
		flagVerbose  bool
		flagInsecure bool
		flagJSON     bool
	)
	c := command.New(out, log.Printf, "hakurei", func([]string) error {
		msg.SwapVerbose(flagVerbose)

		if early.yamaLSM != nil {
			msg.Verbosef("cannot enable ptrace protection via Yama LSM: %v", early.yamaLSM)
			// not fatal
		}

		if early.dumpable != nil {
			log.Printf("cannot set SUID_DUMP_DISABLE: %s", early.dumpable)
			// not fatal
		}

		return nil
	}).
		Flag(&flagVerbose, "v", command.BoolFlag(false), "Increase log verbosity").
		Flag(&flagInsecure, "insecure", command.BoolFlag(false), "Allow use of insecure compatibility options").
		Flag(&flagJSON, "json", command.BoolFlag(false), "Serialise output in JSON when applicable")

	c.Command("shim", command.UsageInternal, func([]string) error { outcome.Shim(msg); return errSuccess })

	{
		var (
			flagIdentifierFile int
		)
		c.NewCommand("run", "Load and start container from configuration file", func(args []string) error {
			if len(args) < 1 {
				log.Fatal("run requires at least 1 argument")
			}

			config := tryPath(msg, args[0])
			if config != nil && config.Container != nil {
				config.Container.Args = append(config.Container.Args, args[1:]...)
			}

			var flags int
			if flagInsecure {
				flags |= hst.VAllowInsecure
			}

			outcome.Main(ctx, msg, config, flags, flagIdentifierFile)
			panic("unreachable")
		}).
			Flag(&flagIdentifierFile, "identifier-fd", command.IntFlag(-1),
				"Write identifier of current instance to fd after successful startup")
	}

	{
		var (
			flagDBusConfigSession string
			flagDBusConfigSystem  string
			flagDBusMpris         bool
			flagDBusVerbose       bool

			flagID       string
			flagIdentity int
			flagGroups   command.RepeatableFlag
			flagHomeDir  string
			flagUserName string

			flagSchedPolicy   string
			flagSchedPriority int

			flagPrivateRuntime, flagPrivateTmpdir bool

			flagWayland, flagX11, flagDBus, flagPipeWire, flagPulse bool
		)

		c.NewCommand("exec", "Configure and start a permissive container", func(args []string) error {
			if flagIdentity < hst.IdentityStart || flagIdentity > hst.IdentityEnd {
				log.Fatalf("identity %d out of range", flagIdentity)
			}

			// resolve home/username from os when flag is unset
			var (
				passwd     *user.User
				passwdOnce sync.Once
				passwdFunc = func() {
					us := strconv.Itoa(hst.ToUser(new(outcome.Hsu).MustID(msg), flagIdentity))
					if u, err := user.LookupId(us); err != nil {
						msg.Verbosef("cannot look up uid %s", us)
						passwd = &user.User{
							Uid:      us,
							Gid:      us,
							Username: "chronos",
							Name:     "Hakurei Permissive Default",
							HomeDir:  fhs.VarEmpty,
						}
					} else {
						passwd = u
					}
				}
			)

			// paths are identical, resolve inner shell and program path
			shell := fhs.AbsRoot.Append("bin", "sh")
			if a, err := check.NewAbs(os.Getenv("SHELL")); err == nil {
				shell = a
			}
			progPath := shell
			if len(args) > 0 {
				if p, err := exec.LookPath(args[0]); err != nil {
					log.Fatal(optionalErrorUnwrap(err))
					return err
				} else if progPath, err = check.NewAbs(p); err != nil {
					log.Fatal(err)
					return err
				}
			}

			var et hst.Enablements
			if flagWayland {
				et |= hst.EWayland
			}
			if flagX11 {
				et |= hst.EX11
			}
			if flagDBus {
				et |= hst.EDBus
			}
			if flagPipeWire || flagPulse {
				et |= hst.EPipeWire
			}

			config := hst.Config{
				ID:          flagID,
				Identity:    flagIdentity,
				Groups:      flagGroups,
				Enablements: &et,

				Container: &hst.ContainerConfig{
					Filesystem: []hst.FilesystemConfigJSON{
						// autoroot, includes the home directory
						{FilesystemConfig: &hst.FSBind{
							Target:  fhs.AbsRoot,
							Source:  fhs.AbsRoot,
							Write:   true,
							Special: true,
						}},
					},

					Username: flagUserName,
					Shell:    shell,

					Path: progPath,
					Args: args,

					Flags: hst.FUserns | hst.FHostNet | hst.FHostAbstract | hst.FTty,
				},
			}

			if err := config.SchedPolicy.UnmarshalText(
				[]byte(flagSchedPolicy),
			); err != nil {
				log.Fatal(err)
			}
			config.SchedPriority = ext.Int(flagSchedPriority)

			// bind GPU stuff
			if et&(hst.EX11|hst.EWayland) != 0 {
				config.Container.Filesystem = append(config.Container.Filesystem, hst.FilesystemConfigJSON{FilesystemConfig: &hst.FSBind{
					Source:   fhs.AbsDev.Append("dri"),
					Device:   true,
					Optional: true,
				}})
			}

			config.Container.Filesystem = append(config.Container.Filesystem,
				// opportunistically bind kvm
				hst.FilesystemConfigJSON{FilesystemConfig: &hst.FSBind{
					Source:   fhs.AbsDev.Append("kvm"),
					Device:   true,
					Optional: true,
				}},

				// do autoetc last
				hst.FilesystemConfigJSON{FilesystemConfig: &hst.FSBind{
					Target:  fhs.AbsEtc,
					Source:  fhs.AbsEtc,
					Special: true,
				}},
			)

			if config.Container.Username == "chronos" {
				passwdOnce.Do(passwdFunc)
				config.Container.Username = passwd.Username
			}

			{
				homeDir := flagHomeDir
				if homeDir == "os" {
					passwdOnce.Do(passwdFunc)
					homeDir = passwd.HomeDir
				}
				if a, err := check.NewAbs(homeDir); err != nil {
					log.Fatal(err)
					return err
				} else {
					config.Container.Home = a
				}
			}

			if !flagPrivateRuntime {
				config.Container.Flags |= hst.FShareRuntime
			}
			if !flagPrivateTmpdir {
				config.Container.Flags |= hst.FShareTmpdir
			}

			// parse D-Bus config file from flags if applicable
			if flagDBus {
				if flagDBusConfigSession == "builtin" {
					config.SessionBus = dbus.NewConfig(flagID, true, flagDBusMpris)
				} else {
					if f, err := os.Open(flagDBusConfigSession); err != nil {
						log.Fatal(err)
					} else {
						decodeJSON(log.Fatal, "load session bus proxy config", f, &config.SessionBus)
						if err = f.Close(); err != nil {
							log.Fatal(err)
						}
					}
				}

				// system bus proxy is optional
				if flagDBusConfigSystem != "nil" {
					if f, err := os.Open(flagDBusConfigSystem); err != nil {
						log.Fatal(err)
					} else {
						decodeJSON(log.Fatal, "load system bus proxy config", f, &config.SystemBus)
						if err = f.Close(); err != nil {
							log.Fatal(err)
						}
					}
				}

				// override log from configuration
				if flagDBusVerbose {
					if config.SessionBus != nil {
						config.SessionBus.Log = true
					}
					if config.SystemBus != nil {
						config.SystemBus.Log = true
					}
				}
			}

			outcome.Main(ctx, msg, &config, 0, -1)
			panic("unreachable")
		}).
			Flag(&flagDBusConfigSession, "dbus-config", command.StringFlag("builtin"),
				"Path to session bus proxy config file, or \"builtin\" for defaults").
			Flag(&flagDBusConfigSystem, "dbus-system", command.StringFlag("nil"),
				"Path to system bus proxy config file, or \"nil\" to disable").
			Flag(&flagDBusMpris, "mpris", command.BoolFlag(false),
				"Allow owning MPRIS D-Bus path, has no effect if custom config is available").
			Flag(&flagDBusVerbose, "dbus-log", command.BoolFlag(false),
				"Force buffered logging in the D-Bus proxy").
			Flag(&flagID, "id", command.StringFlag(""),
				"Reverse-DNS style Application identifier, leave empty to inherit instance identifier").
			Flag(&flagIdentity, "a", command.IntFlag(0),
				"Application identity").
			Flag(nil, "g", &flagGroups,
				"Groups inherited by all container processes").
			Flag(&flagHomeDir, "d", command.StringFlag("os"),
				"Container home directory").
			Flag(&flagUserName, "u", command.StringFlag("chronos"),
				"Passwd user name within sandbox").
			Flag(&flagSchedPolicy, "policy", command.StringFlag(""),
				"Scheduling policy to set for the container").
			Flag(&flagSchedPriority, "priority", command.IntFlag(0),
				"Scheduling priority to set for the container").
			Flag(&flagPrivateRuntime, "private-runtime", command.BoolFlag(false),
				"Do not share XDG_RUNTIME_DIR between containers under the same identity").
			Flag(&flagPrivateTmpdir, "private-tmpdir", command.BoolFlag(false),
				"Do not share TMPDIR between containers under the same identity").
			Flag(&flagWayland, "wayland", command.BoolFlag(false),
				"Enable connection to Wayland via security-context-v1").
			Flag(&flagX11, "X", command.BoolFlag(false),
				"Enable direct connection to X11").
			Flag(&flagDBus, "dbus", command.BoolFlag(false),
				"Enable proxied connection to D-Bus").
			Flag(&flagPipeWire, "pipewire", command.BoolFlag(false),
				"Enable connection to PipeWire via SecurityContext").
			Flag(&flagPulse, "pulse", command.BoolFlag(false),
				"Enable PulseAudio compatibility daemon")
	}

	{
		var (
			flagShort   bool
			flagNoStore bool
		)
		c.NewCommand("show", "Show live or local instance configuration", func(args []string) error {
			switch len(args) {
			case 0: // system
				printShowSystem(os.Stdout, flagShort, flagJSON)

			case 1: // instance
				name := args[0]

				var (
					config *hst.Config
					entry  *hst.State
				)
				if !flagNoStore {
					var sc hst.Paths
					env.CopyPaths().Copy(&sc, new(outcome.Hsu).MustID(nil))
					entry = tryIdentifier(msg, name, outcome.NewStore(&sc))
				}

				if entry == nil {
					config = tryPath(msg, name)
				} else {
					config = entry.Config
				}

				if !printShowInstance(os.Stdout, time.Now().UTC(), entry, config, flagShort, flagJSON) {
					os.Exit(1)
				}

			default:
				log.Fatal("show requires 1 argument")
			}
			return errSuccess
		}).
			Flag(&flagShort, "short", command.BoolFlag(false), "Omit filesystem information").
			Flag(&flagNoStore, "no-store", command.BoolFlag(false), "Do not attempt to match from active instances")
	}

	c.NewCommand("kill", "Terminate an active instance", func(args []string) error {
		if len(args) != 1 {
			log.Fatal("kill requires 1 argument")
		}

		var sc hst.Paths
		env.CopyPaths().Copy(&sc, new(outcome.Hsu).MustID(nil))
		entry := tryIdentifier(msg, args[0], outcome.NewStore(&sc))
		if entry == nil {
			log.Fatalf("%q does not match any active instance", args[0])
		}
		if err := syscall.Kill(entry.PID, syscall.SIGTERM); err != nil {
			log.Fatalf("cannot terminate %s: %v", entry.ID.String(), err)
		}
		return errSuccess
	})

	{
		var flagShort bool
		c.NewCommand("ps", "List active instances", func(args []string) error {
			var sc hst.Paths
			env.CopyPaths().Copy(&sc, new(outcome.Hsu).MustID(nil))
			printPs(msg, os.Stdout, time.Now().UTC(), outcome.NewStore(&sc), flagShort, flagJSON)
			return errSuccess
		}).Flag(&flagShort, "short", command.BoolFlag(false), "Print instance id")
	}

	c.Command("version", "Display version information", func(args []string) error { fmt.Println(info.Version()); return errSuccess })
	c.Command("license", "Show full license text", func(args []string) error { fmt.Println(license); return errSuccess })
	c.Command("template", "Produce a config template", func(args []string) error { encodeJSON(log.Fatal, os.Stdout, false, hst.Template()); return errSuccess })
	c.Command("help", "Show this help message", func([]string) error { c.PrintHelp(); return errSuccess })

	return c
}
