package dockerbuild

import (
	"fmt"
	"io"
	"os"
	"path/filepath"
	"strings"

	dockertypes "github.com/docker/engine-api/types"
	docker "github.com/fsouza/go-dockerclient"
	"github.com/golang/glog"
	"github.com/spf13/cobra"

	"k8s.io/kubernetes/pkg/credentialprovider"
	kcmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util"
	"k8s.io/kubernetes/pkg/util/interrupt"

	dockerbuilder "github.com/openshift/imagebuilder/dockerclient"
	"github.com/openshift/origin/pkg/cmd/templates"
	cmdutil "github.com/openshift/origin/pkg/cmd/util"
	"github.com/openshift/origin/pkg/cmd/util/clientcmd"
	"github.com/openshift/origin/pkg/generate/app"
)

var (
	dockerbuildLong = templates.LongDesc(`
		Build a Dockerfile into a single layer

		Builds the provided directory with a Dockerfile into a single layered image.
		Requires that you have a working connection to a Docker engine. You may mount
		secrets or config into the build with the --mount flag - these files will not
		be included in the final image.

		Experimental: This command is under active development and may change without notice.`)

	dockerbuildExample = templates.Examples(`
		# Build the current directory into a single layer and tag
	  %[1]s ex dockerbuild . myimage:latest

	  # Mount a client secret into the build at a certain path
	  %[1]s ex dockerbuild . myimage:latest --mount ~/mysecret.pem:/etc/pki/secret/mysecret.pem`)
)

type DockerbuildOptions struct {
	Out io.Writer
	Err io.Writer

	Client *docker.Client

	MountSpecs []string

	Mounts         []dockerbuilder.Mount
	Directory      string
	Tag            string
	DockerfilePath string
	AllowPull      bool
	Keyring        credentialprovider.DockerKeyring
	Arguments      app.Environment
}

func NewCmdDockerbuild(fullName string, f *clientcmd.Factory, out, errOut io.Writer) *cobra.Command {
	options := &DockerbuildOptions{
		Out: out,
		Err: errOut,
	}
	cmd := &cobra.Command{
		Use:     "dockerbuild DIRECTORY TAG [--dockerfile=PATH]",
		Short:   "Perform a direct Docker build",
		Long:    dockerbuildLong,
		Example: fmt.Sprintf(dockerbuildExample, fullName),
		Run: func(cmd *cobra.Command, args []string) {
			kcmdutil.CheckErr(options.Complete(f, cmd, args))
			kcmdutil.CheckErr(options.Validate())
			if err := options.Run(); err != nil {
				// TODO: move me to kcmdutil
				if err == cmdutil.ErrExit {
					os.Exit(1)
				}
				kcmdutil.CheckErr(err)
			}
		},
	}

	cmd.Flags().StringSliceVar(&options.MountSpecs, "mount", options.MountSpecs, "An optional list of files and directories to mount during the build. Use SRC:DST syntax for each path.")
	cmd.Flags().StringVar(&options.DockerfilePath, "dockerfile", options.DockerfilePath, "An optional path to a Dockerfile to use.")
	cmd.Flags().BoolVar(&options.AllowPull, "allow-pull", true, "Pull the images that are not present.")
	cmd.MarkFlagFilename("dockerfile")

	return cmd
}

func (o *DockerbuildOptions) Complete(f *clientcmd.Factory, cmd *cobra.Command, args []string) error {
	paths, envArgs, ok := cmdutil.SplitEnvironmentFromResources(args)
	if !ok {
		return kcmdutil.UsageError(cmd, "context directory must be specified before environment changes: %s", strings.Join(args, " "))
	}
	if len(paths) != 2 {
		return kcmdutil.UsageError(cmd, "the directory to build and tag must be specified")
	}
	o.Arguments, _, _ = app.ParseEnvironment(envArgs...)
	o.Directory = paths[0]
	o.Tag = paths[1]
	if len(o.DockerfilePath) == 0 {
		o.DockerfilePath = filepath.Join(o.Directory, "Dockerfile")
	}

	var mounts []dockerbuilder.Mount
	for _, s := range o.MountSpecs {
		segments := strings.Split(s, ":")
		if len(segments) != 2 {
			return kcmdutil.UsageError(cmd, "--mount must be of the form SOURCE:DEST")
		}
		mounts = append(mounts, dockerbuilder.Mount{SourcePath: segments[0], DestinationPath: segments[1]})
	}
	o.Mounts = mounts

	client, err := docker.NewClientFromEnv()
	if err != nil {
		return err
	}
	o.Client = client

	o.Keyring = credentialprovider.NewDockerKeyring()

	return nil
}

func (o *DockerbuildOptions) Validate() error {
	return nil
}

func (o *DockerbuildOptions) Run() error {
	f, err := os.Open(o.DockerfilePath)
	if err != nil {
		return err
	}
	defer f.Close()
	e := dockerbuilder.NewClientExecutor(o.Client)
	e.Out, e.ErrOut = o.Out, o.Err
	e.AllowPull = o.AllowPull
	e.Directory = o.Directory
	e.TransientMounts = o.Mounts
	e.Tag = o.Tag
	e.AuthFn = func(image string) ([]dockertypes.AuthConfig, bool) {
		auth, ok := o.Keyring.Lookup(image)
		if !ok {
			return nil, false
		}
		var engineAuth []dockertypes.AuthConfig
		for _, c := range auth {
			engineAuth = append(engineAuth, c.AuthConfig)
		}
		return engineAuth, true
	}
	e.LogFn = func(format string, args ...interface{}) {
		if glog.V(2) {
			glog.Infof("Builder: "+format, args...)
		} else {
			fmt.Fprintf(e.ErrOut, "--> %s\n", fmt.Sprintf(format, args...))
		}
	}
	safe := interrupt.New(func(os.Signal) { os.Exit(1) }, func() {
		glog.V(5).Infof("invoking cleanup")
		if err := e.Cleanup(); err != nil {
			fmt.Fprintf(o.Err, "error: Unable to clean up build: %v\n", err)
		}
	})
	return safe.Run(func() error { return stripLeadingError(e.Build(f, o.Arguments)) })
}

func stripLeadingError(err error) error {
	if err == nil {
		return nil
	}
	if strings.HasPrefix(err.Error(), "Error: ") {
		return fmt.Errorf(strings.TrimPrefix(err.Error(), "Error: "))
	}
	return err
}