package main

import (
	"compress/gzip"
	"crypto/sha256"
	"encoding/json"
	"io"
	"io/ioutil"
	"os"
	"path/filepath"
	"time"

	"github.com/containers/image/copy"
	"github.com/containers/image/docker"
	"github.com/containers/image/signature"
	"github.com/containers/image/transports"
	"github.com/containers/image/types"
)

type BuildDir struct {
	dir      string
	manifest manifestSchema2
	image    image
}

func NewBuildDir() (*BuildDir, error) {
	t, err := ioutil.TempDir("", "tarpoon")
	if err != nil {
		return nil, err
	}
	d := &BuildDir{
		dir: t,
	}
	return d, nil
}

func (t *BuildDir) LoadBase(base string) error {
	if base == "scratch" {
		return t.createEmptyBase()
	}
	pc, err := getPolicyContext()
	if err != nil {
		return err
	}
	defer pc.Destroy()
	src, err := docker.ParseReference(base)
	if err != nil {
		return err
	}
	dest, err := transports.ParseImageName("dir:" + t.dir)
	if err != nil {
		return err
	}
	// TODO: sigs
	err = copy.Image(systemContext(), pc, dest, src, &copy.Options{
		RemoveSignatures: false,
		SignBy:           "",
		ReportWriter:     os.Stdout,
	})
	if err != nil {
		return err
	}
	manifest, err := os.Open(filepath.Join(t.dir, "manifest.json"))
	if err != nil {
		return err
	}
	defer manifest.Close()
	dec := json.NewDecoder(manifest)
	err = dec.Decode(&t.manifest)
	if err != nil {
		return err
	}
	configfile := t.manifest.ConfigDescriptor.Digest[7:] + ".tar"
	image, err := os.Open(filepath.Join(t.dir, configfile))
	if err != nil {
		return err
	}
	defer image.Close()
	dec = json.NewDecoder(image)
	err = dec.Decode(&t.image)
	if err != nil {
		return err
	}
	return nil
}

func systemContext() *types.SystemContext {
	tlsVerify := true
	return &types.SystemContext{
		RegistriesDirPath:           "",
		DockerCertPath:              "",
		DockerInsecureSkipTLSVerify: !tlsVerify
	}
}

func (t *BuildDir) createEmptyBase() error {
	t.manifest = manifestSchema2{
		SchemaVersion: 2,
		MediaType:     "application/vnd.docker.distribution.manifest.v2+json",
	}
	t.image = image{
		v1Image: v1Image{
			Created: time.Now(),
			ContainerConfig: &config{
				Cmd: []string{"/bin/sh", "-c"},
				Env: []string{"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"},
			},
			Architecture: "amd64",
			OS:           "linux",
			Author:       "tarpoon",
		},
		RootFS: &rootFS{
			Type: "layers",
		},
	}
	return nil
}

func (t *BuildDir) DumpImageConfig() error {
	path := filepath.Join(t.dir, "_config.json")
	configf, err := os.Create(path)
	if err != nil {
		return err
	}
	enc := json.NewEncoder(configf)
	err = enc.Encode(t.image)
	if err != nil {
		configf.Close()
		return err
	}
	configf.Sync()
	configf.Close()
	sha := sha256File(path)
	t.manifest.ConfigDescriptor = descriptor{
		Size:      sha.Size(),
		Digest:    sha.String(),
		MediaType: "application/vnd.docker.container.image.v1+json",
	}
	return os.Rename(path, filepath.Join(t.dir, sha.Name()+".tar"))
}

func (t *BuildDir) WriteManifest() error {
	path := filepath.Join(t.dir, "manifest.json")
	f, err := os.Create(path)
	if err != nil {
		return err
	}
	defer f.Close()
	enc := json.NewEncoder(f)
	enc.SetIndent("", "\t")
	return enc.Encode(t.manifest)
}

func (t *BuildDir) AddTarLayer(tarfile string) error {
	at := time.Now()
	sha, err := t.gzipLayer(tarfile)
	if err != nil {
		return err
	}
	gzpath := filepath.Join(t.dir, "_layer.gz")
	gzsha := sha256File(gzpath)
	t.image.RootFS.DiffIDs = append(t.image.RootFS.DiffIDs, sha.String())
	t.image.History = append(t.image.History, imageHistory{
		Created:   at,
		CreatedBy: "#(nop) TARPOON",
		Comment:   "Created by tarpoon",
	})
	t.image.Created = at
	t.manifest.LayersDescriptors = append(t.manifest.LayersDescriptors, descriptor{
		MediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip",
		Size:      gzsha.Size(),
		Digest:    gzsha.String(),
	})
	return os.Rename(gzpath, filepath.Join(t.dir, gzsha.Name()+".tar"))
}

func (t *BuildDir) gzipLayer(tarfile string) (fileSha, error) {
	targetpath := filepath.Join(t.dir, "_layer.gz")
	tar, err := os.Open(tarfile)
	if err != nil {
		return fileSha{}, err
	}
	defer tar.Close()
	tarsha := sha256.New()
	tr := io.TeeReader(tar, tarsha)
	gz, err := os.Create(targetpath)
	if err != nil {
		return fileSha{}, err
	}
	defer gz.Close()
	gw := gzip.NewWriter(gz)
	io.Copy(gw, tr)
	gw.Flush()
	gw.Close()
	return fileSha{tarsha.Sum(nil), 0}, nil
}

func (t *BuildDir) Push(dockerTarget string) error {
	pc, err := getPolicyContext()
	if err != nil {
		return err
	}
	defer pc.Destroy()
	src, err := transports.ParseImageName("dir:" + t.dir)
	if err != nil {
		return err
	}
	dest, err := docker.ParseReference(dockerTarget)
	if err != nil {
		return err
	}
	// TODO: sigs
	err = copy.Image(systemContext(), pc, dest, src, &copy.Options{
		RemoveSignatures: false,
		SignBy:           "",
		ReportWriter:     os.Stdout,
	})
	return err
}

func (t *BuildDir) Close() error {
	return os.RemoveAll(t.dir)
}

func getPolicyContext() (*signature.PolicyContext, error) {
	policy, err := signature.DefaultPolicy(nil)
	if err == nil {
		return signature.NewPolicyContext(policy)
	}
	// Okay, so. I know DefaultPolicy explicitly says not to do this.
	// But until /etc/containers/policy.json (a RedHat-ism) becomes either
	// the standard or there's a better way, let's go ahead and use the default policy
	// that skopeo installs anyway
	policy, err = signature.NewPolicyFromBytes([]byte(rhDefaultPolicy))
	if err != nil {
		return nil, err
	}
	return signature.NewPolicyContext(policy)
}

const rhDefaultPolicy = `
{
    "default": [
        {
            "type": "insecureAcceptAnything"
        }
    ],
    "transports":
        {
            "docker-daemon":
                {
                    "": [{"type":"insecureAcceptAnything"}]
                }
        }
}
`
