package builder

import (
	"fmt"
	"io/ioutil"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
	"time"

	dockercmd "github.com/docker/docker/builder/dockerfile/command"
	"github.com/docker/docker/builder/dockerfile/parser"
	docker "github.com/fsouza/go-dockerclient"

	kapi "k8s.io/kubernetes/pkg/api"
	utilruntime "k8s.io/kubernetes/pkg/util/runtime"

	s2iapi "github.com/openshift/source-to-image/pkg/api"
	"github.com/openshift/source-to-image/pkg/tar"
	"github.com/openshift/source-to-image/pkg/util"

	"github.com/openshift/origin/pkg/build/api"
	"github.com/openshift/origin/pkg/build/builder/cmd/dockercfg"
	"github.com/openshift/origin/pkg/build/controller/strategy"
	"github.com/openshift/origin/pkg/client"
	"github.com/openshift/origin/pkg/generate/git"
	imageapi "github.com/openshift/origin/pkg/image/api"
	"github.com/openshift/origin/pkg/util/docker/dockerfile"
)

// defaultDockerfilePath is the default path of the Dockerfile
const defaultDockerfilePath = "Dockerfile"

// DockerBuilder builds Docker images given a git repository URL
type DockerBuilder struct {
	dockerClient DockerClient
	gitClient    GitClient
	tar          tar.Tar
	build        *api.Build
	urlTimeout   time.Duration
	client       client.BuildInterface
	cgLimits     *s2iapi.CGroupLimits
}

// NewDockerBuilder creates a new instance of DockerBuilder
func NewDockerBuilder(dockerClient DockerClient, buildsClient client.BuildInterface, build *api.Build, gitClient GitClient, cgLimits *s2iapi.CGroupLimits) *DockerBuilder {
	return &DockerBuilder{
		dockerClient: dockerClient,
		build:        build,
		gitClient:    gitClient,
		tar:          tar.New(),
		urlTimeout:   initialURLCheckTimeout,
		client:       buildsClient,
		cgLimits:     cgLimits,
	}
}

// Build executes a Docker build
func (d *DockerBuilder) Build() error {
	if d.build.Spec.Source.Git == nil && d.build.Spec.Source.Binary == nil &&
		d.build.Spec.Source.Dockerfile == nil && d.build.Spec.Source.Images == nil {
		return fmt.Errorf("must provide a value for at least one of source, binary, images, or dockerfile")
	}
	var push bool
	pushTag := d.build.Status.OutputDockerImageReference

	buildDir, err := ioutil.TempDir("", "docker-build")
	if err != nil {
		return err
	}
	sourceInfo, err := fetchSource(d.dockerClient, buildDir, d.build, d.urlTimeout, os.Stdin, d.gitClient)
	if err != nil {
		d.build.Status.Reason = api.StatusReasonFetchSourceFailed
		d.build.Status.Message = api.StatusMessageFetchSourceFailed
		if updateErr := retryBuildStatusUpdate(d.build, d.client, nil); updateErr != nil {
			utilruntime.HandleError(fmt.Errorf("error: An error occured while updating the build status: %v", updateErr))
		}
		return err
	}

	if sourceInfo != nil {
		glog.V(4).Infof("Setting build revision with details %#v", sourceInfo)
		revision := updateBuildRevision(d.build, sourceInfo)
		if updateErr := retryBuildStatusUpdate(d.build, d.client, revision); updateErr != nil {
			utilruntime.HandleError(fmt.Errorf("error: An error occured while updating the build status: %v", updateErr))
		}
	}
	if err = d.addBuildParameters(buildDir); err != nil {
		return err
	}

	glog.V(4).Infof("Starting Docker build from build config %s ...", d.build.Name)
	// if there is no output target, set one up so the docker build logic
	// (which requires a tag) will still work, but we won't push it at the end.
	if d.build.Spec.Output.To == nil || len(d.build.Spec.Output.To.Name) == 0 {
		d.build.Status.OutputDockerImageReference = d.build.Name
	} else {
		push = true
	}

	buildTag := randomBuildTag(d.build.Namespace, d.build.Name)
	dockerfilePath := d.getDockerfilePath(buildDir)
	imageNames := getDockerfileFrom(dockerfilePath)
	if len(imageNames) == 0 {
		return fmt.Errorf("no FROM image in Dockerfile")
	}
	for _, imageName := range imageNames {
		if imageName == "scratch" {
			glog.V(4).Infof("\nSkipping image \"scratch\"")
			continue
		}
		imageExists := true
		_, err = d.dockerClient.InspectImage(imageName)
		if err != nil {
			if err != docker.ErrNoSuchImage {
				return err
			}
			imageExists = false
		}
		// if forcePull or the image not exists on the node we should pull the image first
		if d.build.Spec.Strategy.DockerStrategy.ForcePull || !imageExists {
			pullAuthConfig, _ := dockercfg.NewHelper().GetDockerAuth(
				imageName,
				dockercfg.PullAuthType,
			)
			glog.V(0).Infof("\nPulling image %s ...", imageName)
			if err = pullImage(d.dockerClient, imageName, pullAuthConfig); err != nil {
				d.build.Status.Reason = api.StatusReasonPullBuilderImageFailed
				d.build.Status.Message = api.StatusMessagePullBuilderImageFailed
				if updateErr := retryBuildStatusUpdate(d.build, d.client, nil); updateErr != nil {
					utilruntime.HandleError(fmt.Errorf("error: An error occured while updating the build status: %v", updateErr))
				}
				return fmt.Errorf("failed to pull image: %v", err)
			}
		}
	}

	if err = d.dockerBuild(buildDir, buildTag, d.build.Spec.Source.Secrets); err != nil {
		d.build.Status.Reason = api.StatusReasonDockerBuildFailed
		d.build.Status.Message = api.StatusMessageDockerBuildFailed
		if updateErr := retryBuildStatusUpdate(d.build, d.client, nil); updateErr != nil {
			utilruntime.HandleError(fmt.Errorf("error: An error occured while updating the build status: %v", updateErr))
		}
		return err
	}

	cname := containerName("docker", d.build.Name, d.build.Namespace, "post-commit")
	if err := execPostCommitHook(d.dockerClient, d.build.Spec.PostCommit, buildTag, cname); err != nil {
		d.build.Status.Reason = api.StatusReasonPostCommitHookFailed
		d.build.Status.Message = api.StatusMessagePostCommitHookFailed
		if updateErr := retryBuildStatusUpdate(d.build, d.client, nil); updateErr != nil {
			utilruntime.HandleError(fmt.Errorf("error: An error occured while updating the build status: %v", updateErr))
		}
		return err
	}

	if push {
		if err := tagImage(d.dockerClient, buildTag, pushTag); err != nil {
			return err
		}
	}

	if err := removeImage(d.dockerClient, buildTag); err != nil {
		glog.V(0).Infof("warning: Failed to remove temporary build tag %v: %v", buildTag, err)
	}

	if push {
		// Get the Docker push authentication
		pushAuthConfig, authPresent := dockercfg.NewHelper().GetDockerAuth(
			pushTag,
			dockercfg.PushAuthType,
		)
		if authPresent {
			glog.V(4).Infof("Authenticating Docker push with user %q", pushAuthConfig.Username)
		}
		glog.V(0).Infof("\nPushing image %s ...", pushTag)
		if err := pushImage(d.dockerClient, pushTag, pushAuthConfig); err != nil {
			d.build.Status.Reason = api.StatusReasonPushImageToRegistryFailed
			d.build.Status.Message = api.StatusMessagePushImageToRegistryFailed
			if updateErr := retryBuildStatusUpdate(d.build, d.client, nil); updateErr != nil {
				utilruntime.HandleError(fmt.Errorf("error: An error occured while updating the build status: %v", updateErr))
			}
			return reportPushFailure(err, authPresent, pushAuthConfig)
		}
		glog.V(0).Infof("Push successful")
	}
	return nil
}

// copySecrets copies all files from the directory where the secret is
// mounted in the builder pod to a directory where the is the Dockerfile, so
// users can ADD or COPY the files inside their Dockerfile.
func (d *DockerBuilder) copySecrets(secrets []api.SecretBuildSource, buildDir string) error {
	for _, s := range secrets {
		dstDir := filepath.Join(buildDir, s.DestinationDir)
		if err := os.MkdirAll(dstDir, 0777); err != nil {
			return err
		}
		srcDir := filepath.Join(strategy.SecretBuildSourceBaseMountPath, s.Secret.Name)
		glog.V(3).Infof("Copying files from the build secret %q to %q", s.Secret.Name, filepath.Clean(s.DestinationDir))
		out, err := exec.Command("cp", "-vrf", srcDir+"/.", dstDir+"/").Output()
		if err != nil {
			glog.V(4).Infof("Secret %q failed to copy: %q", s.Secret.Name, string(out))
			return err
		}
		// See what is copied where when debugging.
		glog.V(5).Infof(string(out))
	}
	return nil
}

// addBuildParameters checks if a Image is set to replace the default base image.
// If that's the case then change the Dockerfile to make the build with the given image.
// Also append the environment variables and labels in the Dockerfile.
func (d *DockerBuilder) addBuildParameters(dir string) error {
	dockerfilePath := d.getDockerfilePath(dir)
	node, err := parseDockerfile(dockerfilePath)
	if err != nil {
		return err
	}

	// Update base image if build strategy specifies the From field.
	if d.build.Spec.Strategy.DockerStrategy.From != nil && d.build.Spec.Strategy.DockerStrategy.From.Kind == "DockerImage" {
		// Reduce the name to a minimal canonical form for the daemon
		name := d.build.Spec.Strategy.DockerStrategy.From.Name
		if ref, err := imageapi.ParseDockerImageReference(name); err == nil {
			name = ref.DaemonMinimal().Exact()
		}
		err := replaceLastFrom(node, name)
		if err != nil {
			return err
		}
	}

	// Append build info as environment variables.
	err = appendEnv(node, d.buildInfo())
	if err != nil {
		return err
	}

	// Append build labels.
	err = appendLabel(node, d.buildLabels(dir))
	if err != nil {
		return err
	}

	// Insert environment variables defined in the build strategy.
	err = insertEnvAfterFrom(node, d.build.Spec.Strategy.DockerStrategy.Env)
	if err != nil {
		return err
	}

	instructions := dockerfile.ParseTreeToDockerfile(node)

	// Overwrite the Dockerfile.
	fi, err := os.Stat(dockerfilePath)
	if err != nil {
		return err
	}
	return ioutil.WriteFile(dockerfilePath, instructions, fi.Mode())
}

// buildInfo converts the buildInfo output to a format that appendEnv can
// consume.
func (d *DockerBuilder) buildInfo() []dockerfile.KeyValue {
	bi := buildInfo(d.build)
	kv := make([]dockerfile.KeyValue, len(bi))
	for i, item := range bi {
		kv[i] = dockerfile.KeyValue{Key: item.Key, Value: item.Value}
	}
	return kv
}

// buildLabels returns a slice of KeyValue pairs in a format that appendEnv can
// consume.
func (d *DockerBuilder) buildLabels(dir string) []dockerfile.KeyValue {
	labels := map[string]string{}
	// TODO: allow source info to be overridden by build
	sourceInfo := &git.SourceInfo{}
	if d.build.Spec.Source.Git != nil {
		var errors []error
		sourceInfo, errors = d.gitClient.GetInfo(dir)
		if len(errors) > 0 {
			for _, e := range errors {
				glog.V(0).Infof("warning: Unable to retrieve Git info: %v", e.Error())
			}
		}
	}
	if len(d.build.Spec.Source.ContextDir) > 0 {
		sourceInfo.ContextDir = d.build.Spec.Source.ContextDir
	}
	labels = util.GenerateLabelsFromSourceInfo(labels, &sourceInfo.SourceInfo, api.DefaultDockerLabelNamespace)
	kv := make([]dockerfile.KeyValue, 0, len(labels)+len(d.build.Spec.Output.ImageLabels))
	for k, v := range labels {
		kv = append(kv, dockerfile.KeyValue{Key: k, Value: v})
	}
	// override autogenerated labels
	for _, lbl := range d.build.Spec.Output.ImageLabels {
		kv = append(kv, dockerfile.KeyValue{Key: lbl.Name, Value: lbl.Value})
	}
	return kv
}

// setupPullSecret provides a Docker authentication configuration when the
// PullSecret is specified.
func (d *DockerBuilder) setupPullSecret() (*docker.AuthConfigurations, error) {
	if len(os.Getenv(dockercfg.PullAuthType)) == 0 {
		return nil, nil
	}
	glog.V(0).Infof("Checking for Docker config file for %s in path %s", dockercfg.PullAuthType, os.Getenv(dockercfg.PullAuthType))
	dockercfgPath := dockercfg.GetDockercfgFile(os.Getenv(dockercfg.PullAuthType))
	if len(dockercfgPath) == 0 {
		return nil, fmt.Errorf("no docker config file found in '%s'", os.Getenv(dockercfg.PullAuthType))
	}
	glog.V(0).Infof("Using Docker config file %s", dockercfgPath)
	r, err := os.Open(dockercfgPath)
	if err != nil {
		return nil, fmt.Errorf("'%s': %s", dockercfgPath, err)
	}
	return docker.NewAuthConfigurations(r)

}

// dockerBuild performs a docker build on the source that has been retrieved
func (d *DockerBuilder) dockerBuild(dir string, tag string, secrets []api.SecretBuildSource) error {
	var noCache bool
	var forcePull bool
	dockerfilePath := defaultDockerfilePath
	if d.build.Spec.Strategy.DockerStrategy != nil {
		if d.build.Spec.Source.ContextDir != "" {
			dir = filepath.Join(dir, d.build.Spec.Source.ContextDir)
		}
		if d.build.Spec.Strategy.DockerStrategy.DockerfilePath != "" {
			dockerfilePath = d.build.Spec.Strategy.DockerStrategy.DockerfilePath
		}
		noCache = d.build.Spec.Strategy.DockerStrategy.NoCache
		forcePull = d.build.Spec.Strategy.DockerStrategy.ForcePull
	}
	auth, err := d.setupPullSecret()
	if err != nil {
		return err
	}
	if err := d.copySecrets(secrets, dir); err != nil {
		return err
	}

	opts := docker.BuildImageOptions{
		Name:           tag,
		RmTmpContainer: true,
		OutputStream:   os.Stdout,
		Dockerfile:     dockerfilePath,
		NoCache:        noCache,
		Pull:           forcePull,
	}
	if d.cgLimits != nil {
		opts.Memory = d.cgLimits.MemoryLimitBytes
		opts.Memswap = d.cgLimits.MemorySwap
		opts.CPUShares = d.cgLimits.CPUShares
		opts.CPUPeriod = d.cgLimits.CPUPeriod
		opts.CPUQuota = d.cgLimits.CPUQuota
	}
	if auth != nil {
		opts.AuthConfigs = *auth
	}

	return buildImage(d.dockerClient, dir, d.tar, &opts)
}

func (d *DockerBuilder) getDockerfilePath(dir string) string {
	var contextDirPath string
	if d.build.Spec.Strategy.DockerStrategy != nil && len(d.build.Spec.Source.ContextDir) > 0 {
		contextDirPath = filepath.Join(dir, d.build.Spec.Source.ContextDir)
	} else {
		contextDirPath = dir
	}

	var dockerfilePath string
	if d.build.Spec.Strategy.DockerStrategy != nil && len(d.build.Spec.Strategy.DockerStrategy.DockerfilePath) > 0 {
		dockerfilePath = filepath.Join(contextDirPath, d.build.Spec.Strategy.DockerStrategy.DockerfilePath)
	} else {
		dockerfilePath = filepath.Join(contextDirPath, defaultDockerfilePath)
	}
	return dockerfilePath
}
func parseDockerfile(dockerfilePath string) (*parser.Node, error) {
	f, err := os.Open(dockerfilePath)
	defer f.Close()
	if err != nil {
		return nil, err
	}

	// Parse the Dockerfile.
	node, err := parser.Parse(f)
	if err != nil {
		return nil, err
	}
	return node, nil
}

// replaceLastFrom changes the last FROM instruction of node to point to the
// base image.
func replaceLastFrom(node *parser.Node, image string) error {
	if node == nil {
		return nil
	}
	for i := len(node.Children) - 1; i >= 0; i-- {
		child := node.Children[i]
		if child != nil && child.Value == dockercmd.From {
			from, err := dockerfile.From(image)
			if err != nil {
				return err
			}
			fromTree, err := parser.Parse(strings.NewReader(from))
			if err != nil {
				return err
			}
			node.Children[i] = fromTree.Children[0]
			return nil
		}
	}
	return nil
}

// appendEnv appends an ENV Dockerfile instruction as the last child of node
// with keys and values from m.
func appendEnv(node *parser.Node, m []dockerfile.KeyValue) error {
	return appendKeyValueInstruction(dockerfile.Env, node, m)
}

// appendLabel appends a LABEL Dockerfile instruction as the last child of node
// with keys and values from m.
func appendLabel(node *parser.Node, m []dockerfile.KeyValue) error {
	if len(m) == 0 {
		return nil
	}
	return appendKeyValueInstruction(dockerfile.Label, node, m)
}

// appendKeyValueInstruction is a primitive used to avoid code duplication.
// Callers should use a derivative of this such as appendEnv or appendLabel.
// appendKeyValueInstruction appends a Dockerfile instruction with key-value
// syntax created by f as the last child of node with keys and values from m.
func appendKeyValueInstruction(f func([]dockerfile.KeyValue) (string, error), node *parser.Node, m []dockerfile.KeyValue) error {
	if node == nil {
		return nil
	}
	instruction, err := f(m)
	if err != nil {
		return err
	}
	return dockerfile.InsertInstructions(node, len(node.Children), instruction)
}

// insertEnvAfterFrom inserts an ENV instruction with the environment variables
// from env after every FROM instruction in node.
func insertEnvAfterFrom(node *parser.Node, env []kapi.EnvVar) error {
	if node == nil || len(env) == 0 {
		return nil
	}

	// Build ENV instruction.
	var m []dockerfile.KeyValue
	for _, e := range env {
		m = append(m, dockerfile.KeyValue{Key: e.Name, Value: e.Value})
	}
	buildEnv, err := dockerfile.Env(m)
	if err != nil {
		return err
	}

	// Insert the buildEnv after every FROM instruction.
	// We iterate in reverse order, otherwise indices would have to be
	// recomputed after each step, because we're changing node in-place.
	indices := dockerfile.FindAll(node, dockercmd.From)
	for i := len(indices) - 1; i >= 0; i-- {
		err := dockerfile.InsertInstructions(node, indices[i]+1, buildEnv)
		if err != nil {
			return err
		}
	}

	return nil
}

// getDockerfilefrom returns all the images behind "FROM" instruction in the dockerfile
func getDockerfileFrom(dockerfilePath string) []string {
	var froms []string
	if "" == dockerfilePath {
		return froms
	}
	node, err := parseDockerfile(dockerfilePath)
	if err != nil {
		return froms
	}
	for i := 0; i < len(node.Children); i++ {
		child := node.Children[i]
		if child == nil || child.Value != dockercmd.From {
			continue
		}
		from := child.Next.Value
		if len(from) > 0 {
			froms = append(froms, from)
		}
	}
	return froms
}