package handlers

import (
	"bytes"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"io"
	"io/ioutil"
	"net/http"
	"os"
	"path/filepath"
	"strconv"
	"strings"

	"github.com/containers/buildah"
	"github.com/containers/buildah/imagebuildah"
	"github.com/containers/libpod/pkg/api/handlers/utils"
	"github.com/containers/storage/pkg/archive"
	"github.com/gorilla/mux"
)

func BuildImage(w http.ResponseWriter, r *http.Request) {
	authConfigs := map[string]AuthConfig{}
	if hdr, found := r.Header["X-Registry-Config"]; found && len(hdr) > 0 {
		authConfigsJSON := base64.NewDecoder(base64.URLEncoding, strings.NewReader(hdr[0]))
		if json.NewDecoder(authConfigsJSON).Decode(&authConfigs) != nil {
			utils.BadRequest(w, "X-Registry-Config", hdr[0], json.NewDecoder(authConfigsJSON).Decode(&authConfigs))
			return
		}
	}

	if hdr, found := r.Header["Content-Type"]; found && len(hdr) > 0 {
		if hdr[0] != "application/x-tar" {
			utils.BadRequest(w, "Content-Type", hdr[0],
				fmt.Errorf("Content-Type: %s is not supported. Should be \"application/x-tar\"", hdr[0]))
		}
	}

	anchorDir, err := extractTarFile(r, w)
	if err != nil {
		utils.InternalServerError(w, err)
		return
	}
	defer os.RemoveAll(anchorDir)

	query := struct {
		Dockerfile  string `schema:"dockerfile"`
		Tag         string `schema:"t"`
		ExtraHosts  string `schema:"extrahosts"`
		Remote      string `schema:"remote"`
		Quiet       bool   `schema:"q"`
		NoCache     bool   `schema:"nocache"`
		CacheFrom   string `schema:"cachefrom"`
		Pull        bool   `schema:"pull"`
		Rm          bool   `schema:"rm"`
		ForceRm     bool   `schema:"forcerm"`
		Memory      int64  `schema:"memory"`
		MemSwap     int64  `schema:"memswap"`
		CpuShares   uint64 `schema:"cpushares"`
		CpuSetCpus  string `schema:"cpusetcpus"`
		CpuPeriod   uint64 `schema:"cpuperiod"`
		CpuQuota    int64  `schema:"cpuquota"`
		BuildArgs   string `schema:"buildargs"`
		ShmSize     int    `schema:"shmsize"`
		Squash      bool   `schema:"squash"`
		Labels      string `schema:"labels"`
		NetworkMode string `schema:"networkmode"`
		Platform    string `schema:"platform"`
		Target      string `schema:"target"`
		Outputs     string `schema:"outputs"`
		Registry    string `schema:"registry"`
	}{
		Dockerfile:  "Dockerfile",
		Tag:         "",
		ExtraHosts:  "",
		Remote:      "",
		Quiet:       false,
		NoCache:     false,
		CacheFrom:   "",
		Pull:        false,
		Rm:          true,
		ForceRm:     false,
		Memory:      0,
		MemSwap:     0,
		CpuShares:   0,
		CpuSetCpus:  "",
		CpuPeriod:   0,
		CpuQuota:    0,
		BuildArgs:   "",
		ShmSize:     64 * 1024 * 1024,
		Squash:      false,
		Labels:      "",
		NetworkMode: "",
		Platform:    "",
		Target:      "",
		Outputs:     "",
		Registry:    "docker.io",
	}

	if err := decodeQuery(r, &query); err != nil {
		utils.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest, err)
		return
	}

	var (
		// Tag is the name with optional tag...
		name = query.Tag
		tag  = "latest"
	)
	if strings.Contains(query.Tag, ":") {
		tokens := strings.SplitN(query.Tag, ":", 2)
		name = tokens[0]
		tag = tokens[1]
	}

	if t, found := mux.Vars(r)["target"]; found {
		name = t
	}

	var buildArgs = map[string]string{}
	if a, found := mux.Vars(r)["buildargs"]; found {
		if err := json.Unmarshal([]byte(a), &buildArgs); err != nil {
			utils.BadRequest(w, "buildargs", a, err)
			return
		}
	}

	// convert label formats
	var labels = []string{}
	if l, found := mux.Vars(r)["labels"]; found {
		var m = map[string]string{}
		if err := json.Unmarshal([]byte(l), &m); err != nil {
			utils.BadRequest(w, "labels", l, err)
			return
		}

		for k, v := range m {
			labels = append(labels, k+"="+v)
		}
	}

	pullPolicy := buildah.PullIfMissing
	if _, found := mux.Vars(r)["pull"]; found {
		if query.Pull {
			pullPolicy = buildah.PullAlways
		}
	}

	// build events will be recorded here
	var (
		buildEvents = []string{}
		progress    = bytes.Buffer{}
	)

	buildOptions := imagebuildah.BuildOptions{
		ContextDirectory:               filepath.Join(anchorDir, "build"),
		PullPolicy:                     pullPolicy,
		Registry:                       query.Registry,
		IgnoreUnrecognizedInstructions: true,
		Quiet:                          query.Quiet,
		Isolation:                      buildah.IsolationChroot,
		Runtime:                        "",
		RuntimeArgs:                    nil,
		TransientMounts:                nil,
		Compression:                    archive.Gzip,
		Args:                           buildArgs,
		Output:                         name,
		AdditionalTags:                 []string{tag},
		Log: func(format string, args ...interface{}) {
			buildEvents = append(buildEvents, fmt.Sprintf(format, args...))
		},
		In:                  nil,
		Out:                 &progress,
		Err:                 &progress,
		SignaturePolicyPath: "",
		ReportWriter:        &progress,
		OutputFormat:        buildah.Dockerv2ImageManifest,
		SystemContext:       nil,
		NamespaceOptions:    nil,
		ConfigureNetwork:    0,
		CNIPluginPath:       "",
		CNIConfigDir:        "",
		IDMappingOptions:    nil,
		AddCapabilities:     nil,
		DropCapabilities:    nil,
		CommonBuildOpts: &buildah.CommonBuildOptions{
			AddHost:            nil,
			CgroupParent:       "",
			CPUPeriod:          query.CpuPeriod,
			CPUQuota:           query.CpuQuota,
			CPUShares:          query.CpuShares,
			CPUSetCPUs:         query.CpuSetCpus,
			CPUSetMems:         "",
			HTTPProxy:          false,
			Memory:             query.Memory,
			DNSSearch:          nil,
			DNSServers:         nil,
			DNSOptions:         nil,
			MemorySwap:         query.MemSwap,
			LabelOpts:          nil,
			SeccompProfilePath: "",
			ApparmorProfile:    "",
			ShmSize:            strconv.Itoa(query.ShmSize),
			Ulimit:             nil,
			Volumes:            nil,
		},
		DefaultMountsFilePath:   "",
		IIDFile:                 "",
		Squash:                  query.Squash,
		Labels:                  labels,
		Annotations:             nil,
		OnBuild:                 nil,
		Layers:                  false,
		NoCache:                 query.NoCache,
		RemoveIntermediateCtrs:  query.Rm,
		ForceRmIntermediateCtrs: query.ForceRm,
		BlobDirectory:           "",
		Target:                  query.Target,
		Devices:                 nil,
	}

	id, _, err := getRuntime(r).Build(r.Context(), buildOptions, query.Dockerfile)
	if err != nil {
		utils.InternalServerError(w, err)
	}

	// Find image ID that was built...
	utils.WriteResponse(w, http.StatusOK,
		struct {
			Stream string `json:"stream"`
		}{
			Stream: progress.String() + "\n" +
				strings.Join(buildEvents, "\n") +
				fmt.Sprintf("\nSuccessfully built %s\n", id),
		})
}

func extractTarFile(r *http.Request, w http.ResponseWriter) (string, error) {
	// build a home for the request body
	anchorDir, err := ioutil.TempDir("", "libpod_builder")
	if err != nil {
		return "", err
	}
	buildDir := filepath.Join(anchorDir, "build")

	path := filepath.Join(anchorDir, "tarBall")
	tarBall, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666)
	if err != nil {
		return "", err
	}
	defer tarBall.Close()

	// Content-Length not used as too many existing API clients didn't honor it
	_, err = io.Copy(tarBall, r.Body)
	r.Body.Close()

	if err != nil {
		utils.InternalServerError(w,
			fmt.Errorf("failed Request: Unable to copy tar file from request body %s", r.RequestURI))
	}

	_, _ = tarBall.Seek(0, 0)
	if err := archive.Untar(tarBall, buildDir, &archive.TarOptions{}); err != nil {
		return "", err
	}
	return anchorDir, nil
}
