package main

import (
	"archive/tar"
	"bytes"
	"context"
	"encoding/json"
	"io"
	"os"
	"runtime"

	"github.com/containers/image/v5/pkg/blobinfocache/memory"
	"github.com/containers/image/v5/storage"
	"github.com/containers/image/v5/types"
	"github.com/containers/libpod/pkg/rootless"
	sstorage "github.com/containers/storage"
	"github.com/containers/storage/pkg/reexec"
	digest "github.com/opencontainers/go-digest"
	specs "github.com/opencontainers/image-spec/specs-go"
	v1 "github.com/opencontainers/image-spec/specs-go/v1"
	"github.com/sirupsen/logrus"
	"github.com/urfave/cli"
)

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

	app := cli.NewApp()
	app.Name = "bin2img"
	app.Usage = "barebones image builder"
	app.Version = "0.0.1"

	app.Flags = []cli.Flag{
		cli.BoolFlag{
			Name:  "debug",
			Usage: "turn on debug logging",
		},
		cli.StringFlag{
			Name:  "root, r",
			Usage: "graph root directory",
		},
		cli.StringFlag{
			Name:  "runroot",
			Usage: "run root directory",
		},
		cli.StringFlag{
			Name:  "storage-driver, s",
			Usage: "storage driver",
		},
		cli.StringSliceFlag{
			Name:  "storage-opt",
			Usage: "storage option",
		},
		cli.StringFlag{
			Name:  "image-name",
			Usage: "set image name",
			Value: "k8s.io/gcr/pause:3.1",
		},
		cli.StringFlag{
			Name:  "source-binary",
			Usage: "source binary",
			Value: "../../pause/pause",
		},
		cli.StringFlag{
			Name:  "image-binary",
			Usage: "image binary",
			Value: "/pause",
		},
	}

	app.Action = func(c *cli.Context) error {
		debug := c.GlobalBool("debug")
		rootDir := c.GlobalString("root")
		runrootDir := c.GlobalString("runroot")
		storageDriver := c.GlobalString("storage-driver")
		storageOptions := c.GlobalStringSlice("storage-opt")
		imageName := c.GlobalString("image-name")
		sourceBinary := c.GlobalString("source-binary")
		imageBinary := c.GlobalString("image-binary")

		if debug {
			logrus.SetLevel(logrus.DebugLevel)
		} else {
			logrus.SetLevel(logrus.ErrorLevel)
		}
		if rootDir == "" && runrootDir != "" {
			logrus.Errorf("must set --root and --runroot, or neither")
			os.Exit(1)
		}
		if rootDir != "" && runrootDir == "" {
			logrus.Errorf("must set --root and --runroot, or neither")
			os.Exit(1)
		}
		storeOptions, err := sstorage.DefaultStoreOptions(rootless.IsRootless(), rootless.GetRootlessUID())
		if err != nil {
			return err
		}
		if rootDir != "" && runrootDir != "" {
			storeOptions.GraphDriverName = storageDriver
			storeOptions.GraphDriverOptions = storageOptions
			storeOptions.GraphRoot = rootDir
			storeOptions.RunRoot = runrootDir
		}
		store, err := sstorage.GetStore(storeOptions)
		if err != nil {
			logrus.Errorf("error opening storage: %v", err)
			os.Exit(1)
		}
		defer func() {
			_, err = store.Shutdown(false)
			if err != nil {
				logrus.Warnf("unable to shutdown store: %v", err)
			}
		}()

		layerBuffer := &bytes.Buffer{}
		binary, err := os.Open(sourceBinary)
		if err != nil {
			logrus.Errorf("error opening image binary: %v", err)
			os.Exit(1)
		}
		binInfo, err := binary.Stat()
		if err != nil {
			logrus.Errorf("error statting image binary: %v", err)
			os.Exit(1)
		}
		archive := tar.NewWriter(layerBuffer)
		err = archive.WriteHeader(&tar.Header{
			Name:     imageBinary,
			Size:     binInfo.Size(),
			Mode:     0555,
			ModTime:  binInfo.ModTime(),
			Typeflag: tar.TypeReg,
			Uname:    "root",
			Gname:    "root",
		})
		if err != nil {
			logrus.Errorf("error writing archive header: %v", err)
			os.Exit(1)
		}
		_, err = io.Copy(archive, binary)
		if err != nil {
			logrus.Errorf("error archiving image binary: %v", err)
			os.Exit(1)
		}
		archive.Close()
		binary.Close()
		layerInfo := types.BlobInfo{
			Digest: digest.Canonical.FromBytes(layerBuffer.Bytes()),
			Size:   int64(layerBuffer.Len()),
		}

		ref, err := storage.Transport.ParseStoreReference(store, imageName)
		if err != nil {
			logrus.Errorf("error parsing image name: %v", err)
			os.Exit(1)
		}

		ctx := context.Background()

		img, err := ref.NewImageDestination(ctx, nil)
		if err != nil {
			logrus.Errorf("error preparing to write image: %v", err)
			os.Exit(1)
		}
		defer img.Close()
		cache := memory.New()
		layer, err := img.PutBlob(ctx, layerBuffer, layerInfo, cache, true)
		if err != nil {
			logrus.Errorf("error preparing to write image: %v", err)
			os.Exit(1)
		}
		config := &v1.Image{
			Architecture: runtime.GOARCH,
			OS:           runtime.GOOS,
			Config: v1.ImageConfig{
				User:       "root",
				Entrypoint: []string{imageBinary},
			},
			RootFS: v1.RootFS{
				Type: "layers",
				DiffIDs: []digest.Digest{
					layer.Digest,
				},
			},
		}
		cbytes, err := json.Marshal(config)
		if err != nil {
			logrus.Errorf("error encoding configuration: %v", err)
			os.Exit(1)
		}
		configInfo := types.BlobInfo{
			Digest: digest.Canonical.FromBytes(cbytes),
			Size:   int64(len(cbytes)),
		}
		configInfo, err = img.PutBlob(ctx, bytes.NewBuffer(cbytes), configInfo, cache, false)
		if err != nil {
			logrus.Errorf("error saving configuration: %v", err)
			os.Exit(1)
		}
		manifest := &v1.Manifest{
			Versioned: specs.Versioned{
				SchemaVersion: 2,
			},
			Config: v1.Descriptor{
				MediaType: v1.MediaTypeImageConfig,
				Digest:    configInfo.Digest,
				Size:      int64(len(cbytes)),
			},
			Layers: []v1.Descriptor{{
				MediaType: v1.MediaTypeImageLayer,
				Digest:    layer.Digest,
				Size:      layer.Size,
			}},
		}
		mbytes, err := json.Marshal(manifest)
		if err != nil {
			logrus.Errorf("error encoding manifest: %v", err)
			os.Exit(1)
		}
		err = img.PutManifest(ctx, mbytes, nil)
		if err != nil {
			logrus.Errorf("error saving manifest: %v", err)
			os.Exit(1)
		}
		err = img.Commit(ctx, &unparsedImage{
			imageReference: ref,
			manifestBytes:  mbytes,
			manifestType:   v1.MediaTypeImageManifest,
		})
		if err != nil {
			logrus.Errorf("error committing image: %v", err)
			os.Exit(1)
		}
		return nil
	}

	if err := app.Run(os.Args); err != nil {
		logrus.Fatal(err)
	}
}

type unparsedImage struct {
	imageReference types.ImageReference
	manifestBytes  []byte
	manifestType   string
	signatures     [][]byte
}

func (u *unparsedImage) Reference() types.ImageReference {
	return u.imageReference
}
func (u *unparsedImage) Manifest(context.Context) (manifestBytes []byte, manifestType string, err error) {
	return u.manifestBytes, u.manifestType, nil
}
func (u *unparsedImage) Signatures(context.Context) ([][]byte, error) {
	return u.signatures, nil
}
