//go:build windows

package main

import (
	"fmt"
	"io"
	"os"
	"strings"

	"github.com/Microsoft/go-winio"
	"github.com/Microsoft/go-winio/pkg/etwlogrus"
	"github.com/opencontainers/runtime-spec/specs-go"
	"github.com/sirupsen/logrus"
	"github.com/urfave/cli"

	"github.com/Microsoft/hcsshim/internal/regstate"
	"github.com/Microsoft/hcsshim/internal/runhcs"
	hcsversion "github.com/Microsoft/hcsshim/internal/version"
)

// Add a manifest to get proper Windows version detection.
//go:generate pwsh -Command "../../scripts/New-ResourceObjectFile.ps1 -ErrorAction 'Stop' -Destination '.' -Name 'runhcs' -UseVersionFile -Architecture 'all'"

// `-ldflags '-X ...'` only works if the variable is uninitialized or set to a constant value.
// keep empty and override with data from [internal/version] only if empty to allow
// workflows currently setting these values to work.
var (
	// version will be the repo version that the binary was built from
	version = ""
	// gitCommit will be the hash that the binary was built from
	gitCommit = ""
)

var stateKey *regstate.Key

var logFormat string

const (
	specConfig = "config.json"
	usage      = `Open Container Initiative runtime for Windows

runhcs is a fork of runc, modified to run containers on Windows with or without Hyper-V isolation.  Like runc, it is a command line client for running applications packaged according to the Open Container Initiative (OCI) format.

runhcs integrates with existing process supervisors to provide a production container runtime environment for applications. It can be used with your existing process monitoring tools and the container will be spawned as a direct child of the process supervisor.

Containers are configured using bundles. A bundle for a container is a directory that includes a specification file named "` + specConfig + `".  Bundle contents will depend on the container type.

To start a new instance of a container:

    # runhcs run [ -b bundle ] <container-id>

Where "<container-id>" is your name for the instance of the container that you are starting. The name you provide for the container instance must be unique on your host. Providing the bundle directory using "-b" is optional. The default value for "bundle" is the current directory.`
)

func main() {
	// Provider ID: 0b52781f-b24d-5685-ddf6-69830ed40ec3
	// Hook isn't closed explicitly, as it will exist until process exit.
	if hook, err := etwlogrus.NewHook("Microsoft.Virtualization.RunHCS"); err == nil {
		logrus.AddHook(hook)
	} else {
		logrus.Error(err)
	}

	app := cli.NewApp()
	app.Name = "runhcs"
	app.Usage = usage

	// fall back on embedded version info (if any), if variables above were not set
	if version == "" {
		version = hcsversion.Version
	}
	if gitCommit == "" {
		gitCommit = hcsversion.Commit
	}

	var v []string
	if version != "" {
		v = append(v, version)
	}
	if gitCommit != "" {
		v = append(v, fmt.Sprintf("commit: %s", gitCommit))
	}
	v = append(v, fmt.Sprintf("spec: %s", specs.Version))
	app.Version = strings.Join(v, "\n")

	app.Flags = []cli.Flag{
		cli.BoolFlag{
			Name:  "debug",
			Usage: "enable debug output for logging",
		},
		cli.StringFlag{
			Name:  "log",
			Value: "nul",
			Usage: `set the log file path or named pipe (e.g. \\.\pipe\ProtectedPrefix\Administrators\runhcs-log) where internal debug information is written`,
		},
		cli.StringFlag{
			Name:  "log-format",
			Value: "text",
			Usage: "set the format used by logs ('text' (default), or 'json')",
		},
		cli.StringFlag{
			Name:  "owner",
			Value: "runhcs",
			Usage: "compute system owner",
		},
		cli.StringFlag{
			Name:  "root",
			Value: "default",
			Usage: "registry key for storage of container state",
		},
	}
	app.Commands = []cli.Command{
		createCommand,
		createScratchCommand,
		deleteCommand,
		// eventsCommand,
		execCommand,
		killCommand,
		listCommand,
		pauseCommand,
		prepareDiskCommand,
		psCommand,
		resizeTtyCommand,
		resumeCommand,
		runCommand,
		shimCommand,
		startCommand,
		stateCommand,
		// updateCommand,
		vmshimCommand,
	}
	app.Before = func(context *cli.Context) error {
		if context.GlobalBool("debug") {
			logrus.SetLevel(logrus.DebugLevel)
		}
		if path := context.GlobalString("log"); path != "" {
			var f io.Writer
			var err error
			if strings.HasPrefix(path, runhcs.SafePipePrefix) {
				f, err = winio.DialPipe(path, nil)
			} else {
				f, err = os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_APPEND|os.O_SYNC, 0666)
			}
			if err != nil {
				return err
			}
			logrus.SetOutput(f)
		}
		switch logFormat = context.GlobalString("log-format"); logFormat {
		case "text":
			// retain logrus's default.
		case "json":
			logrus.SetFormatter(new(logrus.JSONFormatter))
		default:
			return fmt.Errorf("unknown log-format %q", logFormat)
		}

		var err error
		stateKey, err = regstate.Open(context.GlobalString("root"), false)
		if err != nil {
			return err
		}
		return nil
	}
	// If the command returns an error, cli takes upon itself to print
	// the error on cli.ErrWriter and exit.
	// Use our own writer here to ensure the log gets sent to the right location.
	fatalWriter.Writer = cli.ErrWriter
	cli.ErrWriter = &fatalWriter
	if err := app.Run(os.Args); err != nil {
		fmt.Fprintln(cli.ErrWriter, err)
		os.Exit(1)
	}
}

type logErrorWriter struct {
	Writer io.Writer
}

var fatalWriter logErrorWriter

func (f *logErrorWriter) Write(p []byte) (n int, err error) {
	logrus.Error(string(p))
	return f.Writer.Write(p)
}
