package run

import (
	"bytes"
	"fmt"
	"io"
	"io/ioutil"
	"sync"

	docker "github.com/fsouza/go-dockerclient"
	"github.com/golang/glog"

	"github.com/openshift/origin/pkg/bootstrap/docker/errors"
)

type RunHelper struct {
	client *docker.Client
}

func NewRunHelper(client *docker.Client) *RunHelper {
	return &RunHelper{
		client: client,
	}
}

func (h *RunHelper) New() *Runner {
	return &Runner{
		client:     h.client,
		config:     &docker.Config{},
		hostConfig: &docker.HostConfig{},
	}
}

// Runner is a helper to run new containers on Docker
type Runner struct {
	name            string
	client          *docker.Client
	config          *docker.Config
	hostConfig      *docker.HostConfig
	input           io.Reader
	removeContainer bool
}

// Name sets the name of the container to create
func (h *Runner) Name(name string) *Runner {
	h.name = name
	return h
}

// Image sets the image to run
func (h *Runner) Image(image string) *Runner {
	h.config.Image = image
	return h
}

func (h *Runner) PortForward(local, remote int) *Runner {
	if h.hostConfig.PortBindings == nil {
		h.hostConfig.PortBindings = map[docker.Port][]docker.PortBinding{}
	}
	containerPort := docker.Port(fmt.Sprintf("%d/tcp", remote))
	binding := docker.PortBinding{
		HostPort: fmt.Sprintf("%d", local),
	}
	h.hostConfig.PortBindings[containerPort] = []docker.PortBinding{binding}

	if h.config.ExposedPorts == nil {
		h.config.ExposedPorts = map[docker.Port]struct{}{}
	}
	h.config.ExposedPorts[containerPort] = struct{}{}
	return h
}

// Entrypoint sets the entrypoint to use when running
func (h *Runner) Entrypoint(cmd ...string) *Runner {
	h.config.Entrypoint = cmd
	return h
}

// Command sets the command to run
func (h *Runner) Command(cmd ...string) *Runner {
	h.config.Cmd = cmd
	return h
}

// HostPid tells Docker to run using the host's pid namespace
func (h *Runner) HostPid() *Runner {
	h.hostConfig.PidMode = "host"
	return h
}

// HostNetwork tells Docker to run using the host's Network namespace
func (h *Runner) HostNetwork() *Runner {
	h.hostConfig.NetworkMode = "host"
	return h
}

// Bind tells Docker to bind host dirs to container dirs
func (h *Runner) Bind(binds ...string) *Runner {
	h.hostConfig.Binds = append(h.hostConfig.Binds, binds...)
	return h
}

// Env tells Docker to add environment variables to the container getting started
func (h *Runner) Env(env ...string) *Runner {
	h.config.Env = append(h.config.Env, env...)
	return h
}

// Privileged tells Docker to run the container as privileged
func (h *Runner) Privileged() *Runner {
	h.hostConfig.Privileged = true
	return h
}

// DiscardContainer if true will cause the container to be removed when done executing.
// Will be ignored in the case of Start
func (h *Runner) DiscardContainer() *Runner {
	h.removeContainer = true
	return h
}

// Input sets an input stream for the Docker run command
func (h *Runner) Input(reader io.Reader) *Runner {
	h.config.OpenStdin = true
	h.config.StdinOnce = true
	h.input = reader
	return h
}

// Start starts the container as a daemon and returns
func (h *Runner) Start() (string, error) {
	id, err := h.Create()
	if err != nil {
		return "", err
	}
	return id, h.startContainer(id)
}

// Output starts the container, waits for it to finish and returns its output
func (h *Runner) Output() (string, string, int, error) {
	stdOut, errOut := &bytes.Buffer{}, &bytes.Buffer{}
	rc, err := h.runWithOutput(h.input, stdOut, errOut)
	return stdOut.String(), errOut.String(), rc, err
}

// CombinedOutput  is the same as Output, except both output and error streams
// are combined into one.
func (h *Runner) CombinedOutput() (string, int, error) {
	out := &bytes.Buffer{}
	rc, err := h.runWithOutput(h.input, out, out)
	return out.String(), rc, err
}

// Run executes the container and waits until it completes
func (h *Runner) Run() (int, error) {
	return h.runWithOutput(h.input, ioutil.Discard, ioutil.Discard)
}

func (h *Runner) Create() (string, error) {
	glog.V(4).Infof("Creating container named %q\nconfig:\n%s\nhost config:\n%s\n", h.name, printConfig(h.config), printHostConfig(h.hostConfig))
	container, err := h.client.CreateContainer(docker.CreateContainerOptions{
		Name:       h.name,
		Config:     h.config,
		HostConfig: h.hostConfig,
	})
	if err != nil {
		return "", errors.NewError("cannot create container using image %s", h.config.Image).WithCause(err)
	}
	glog.V(5).Infof("Container created with id %q", container.ID)
	glog.V(5).Infof("Container: %#v", container)
	return container.ID, nil
}

func (h *Runner) startContainer(id string) error {
	err := h.client.StartContainer(id, nil)
	if err != nil {
		return errors.NewError("cannot start container %s", id).WithCause(err)
	}
	return nil
}

func (h *Runner) runWithOutput(stdIn io.Reader, stdOut, stdErr io.Writer) (int, error) {
	id, err := h.Create()
	if err != nil {
		return 0, err
	}
	if h.removeContainer {
		defer func() {
			glog.V(5).Infof("Deleting container %q", id)
			if err = h.client.RemoveContainer(docker.RemoveContainerOptions{ID: id}); err != nil {
				glog.V(1).Infof("Error deleting container %q: %v", id, err)
			}
		}()
	}
	logOut, logErr := &bytes.Buffer{}, &bytes.Buffer{}
	outStream := io.MultiWriter(stdOut, logOut)
	errStream := io.MultiWriter(stdErr, logErr)
	attached := make(chan struct{})
	attachErr := make(chan error)
	streamingWG := &sync.WaitGroup{}
	streamingWG.Add(1)
	go func() {
		glog.V(5).Infof("Attaching to container %q", id)
		err = h.client.AttachToContainer(docker.AttachToContainerOptions{
			Container:    id,
			Logs:         true,
			Stream:       true,
			Stdout:       true,
			Stderr:       true,
			Stdin:        stdIn != nil,
			OutputStream: outStream,
			ErrorStream:  errStream,
			InputStream:  stdIn,
			Success:      attached,
		})
		if err != nil {
			glog.V(2).Infof("Error occurred while attaching: %v", err)
			attachErr <- err
		}
		streamingWG.Done()
		glog.V(5).Infof("Done attaching to container %q", id)
	}()

	select {
	case <-attached:
		glog.V(5).Infof("Attach is successful.")
	case err = <-attachErr:
		return 0, err
	}
	glog.V(5).Infof("Starting container %q", id)
	err = h.startContainer(id)
	if err != nil {
		glog.V(2).Infof("Error occurred starting container %q: %v", id, err)
		return 0, err
	}
	glog.V(5).Infof("signaling attached channel")
	attached <- struct{}{}
	glog.V(5).Infof("Waiting for container %q", id)
	rc, err := h.client.WaitContainer(id)
	if err != nil {
		glog.V(2).Infof("Error occurred waiting for container %q: %v, rc = %d", id, err, rc)
	}
	glog.V(5).Infof("Done waiting for container %q, rc=%d. Waiting for attach routine", id, rc)
	streamingWG.Wait()
	glog.V(5).Infof("Done waiting for attach routine")
	glog.V(5).Infof("Stdout:\n%s", logOut.String())
	glog.V(5).Infof("Stderr:\n%s", logErr.String())
	if rc != 0 || err != nil {
		return rc, newRunError(rc, err, logOut.Bytes(), logErr.Bytes(), h.config)
	}
	glog.V(4).Infof("Container run successful\n")
	return 0, nil
}

// printConfig prints out the relevant parts of a container's Docker config
func printConfig(c *docker.Config) string {
	out := &bytes.Buffer{}
	fmt.Fprintf(out, "  image: %s\n", c.Image)
	if len(c.Entrypoint) > 0 {
		fmt.Fprintf(out, "  entry point:\n")
		for _, e := range c.Entrypoint {
			fmt.Fprintf(out, "    %s\n", e)
		}
	}
	if len(c.Cmd) > 0 {
		fmt.Fprintf(out, "  command:\n")
		for _, c := range c.Cmd {
			fmt.Fprintf(out, "    %s\n", c)
		}
	}
	if len(c.Env) > 0 {
		fmt.Fprintf(out, "  environment:\n")
		for _, e := range c.Env {
			fmt.Fprintf(out, "    %s\n", e)
		}
	}
	return out.String()
}

func printHostConfig(c *docker.HostConfig) string {
	out := &bytes.Buffer{}
	fmt.Fprintf(out, "  pid mode: %s\n", c.PidMode)
	fmt.Fprintf(out, "  network mode: %s\n", c.NetworkMode)
	if len(c.Binds) > 0 {
		fmt.Fprintf(out, "  volume binds:\n")
		for _, b := range c.Binds {
			fmt.Fprintf(out, "    %s\n", b)
		}
	}
	return out.String()
}