package main

import (
	"errors"
	"os"

	log "github.com/drone/drone/Godeps/_workspace/src/github.com/Sirupsen/logrus"
	"github.com/drone/drone/Godeps/_workspace/src/github.com/samalba/dockerclient"
	"github.com/drone/drone/pkg/docker"
)

var (
	ErrTimeout = errors.New("Timeout")
	ErrLogging = errors.New("Logs not available")
)

var (
	// options to fetch the stdout and stderr logs
	logOpts = &dockerclient.LogOptions{
		Stdout: true,
		Stderr: true,
	}

	// options to fetch the stdout and stderr logs
	// by tailing the output.
	logOptsTail = &dockerclient.LogOptions{
		Follow: true,
		Stdout: true,
		Stderr: true,
	}
)

// client is a wrapper around the default Docker client
// that tracks all created containers ensures some default
// configurations are in place.
type client struct {
	dockerclient.Client
	info  *dockerclient.ContainerInfo
	names []string // names of created containers
}

func newClient(docker dockerclient.Client) (*client, error) {

	// creates an ambassador container
	conf := &dockerclient.ContainerConfig{}
	conf.HostConfig = dockerclient.HostConfig{}
	conf.Entrypoint = []string{"/bin/sleep"}
	conf.Cmd = []string{"86400"}
	conf.Image = "gliderlabs/alpine:3.1"
	conf.Volumes = map[string]struct{}{}
	conf.Volumes["/drone"] = struct{}{}
	info, err := daemon(docker, conf, false)
	if err != nil {
		return nil, err
	}

	return &client{Client: docker, info: info}, nil
}

// CreateContainer creates a container and internally
// caches its container id.
func (c *client) CreateContainer(conf *dockerclient.ContainerConfig, name string) (string, error) {
	conf.Env = append(conf.Env, "affinity:container=="+c.info.Id)
	id, err := c.Client.CreateContainer(conf, name)
	if err == nil {
		c.names = append(c.names, id)
	}
	return id, err
}

// StartContainer starts a container and links to an
// ambassador container sharing the build machiens volume.
func (c *client) StartContainer(id string, conf *dockerclient.HostConfig) error {
	conf.VolumesFrom = append(conf.VolumesFrom, c.info.Id)
	if len(conf.NetworkMode) == 0 {
		conf.NetworkMode = "container:" + c.info.Id
	}
	return c.Client.StartContainer(id, conf)
}

// Destroy will terminate and destroy all containers that
// were created by this client.
func (c *client) Destroy() error {
	for _, id := range c.names {
		c.Client.KillContainer(id, "9")
		c.Client.RemoveContainer(id, true, true)
	}
	c.Client.KillContainer(c.info.Id, "9")
	return c.Client.RemoveContainer(c.info.Id, true, true)
}

func run(client dockerclient.Client, conf *dockerclient.ContainerConfig, pull bool) (*dockerclient.ContainerInfo, error) {
	// force-pull the image if specified.
	// TEMPORARY while we are in beta mode we should always re-pull drone plugins
	if pull { //|| strings.HasPrefix(conf.Image, "plugins/") {
		client.PullImage(conf.Image, nil)
	}

	// attempts to create the contianer
	id, err := client.CreateContainer(conf, "")
	if err != nil {
		// and pull the image and re-create if that fails
		err = client.PullImage(conf.Image, nil)
		if err != nil {
			log.Errorf("Error pulling %s. %s\n", conf.Image, err)
			return nil, err
		}

		id, err = client.CreateContainer(conf, "")
		// make sure the container is removed in
		// the event of a creation error.
		if err != nil {
			log.Errorf("Error starting %s. %s\n", conf.Image, err)
			client.RemoveContainer(id, true, true)
			return nil, err
		}
	}

	// ensures the container is always stopped
	// and ready to be removed.
	defer func() {
		client.StopContainer(id, 5)
		client.KillContainer(id, "9")
	}()

	// fetches the container information.
	info, err := client.InspectContainer(id)
	if err != nil {
		log.Errorf("Error inspecting %s. %s\n", conf.Image, err)
		client.RemoveContainer(id, true, true)
		return nil, err
	}

	// channel listening for errors while the
	// container is running async.
	errc := make(chan error, 1)
	infoc := make(chan *dockerclient.ContainerInfo, 1)
	go func() {

		// starts the container
		err := client.StartContainer(id, &conf.HostConfig)
		if err != nil {
			log.Errorf("Error starting %s. %s\n", conf.Image, err)
			errc <- err
			return
		}

		// blocks and waits for the container to finish
		// by streaming the logs (to /dev/null). Ideally
		// we could use the `wait` function instead
		rc, err := client.ContainerLogs(id, logOptsTail)
		if err != nil {
			log.Errorf("Error tailing %s. %s\n", conf.Image, err)
			errc <- err
			return
		}
		defer rc.Close()
		docker.StdCopy(os.Stdout, os.Stdout, rc)

		// fetches the container information
		info, err := client.InspectContainer(id)
		if err != nil {
			log.Errorf("Error getting exit code for %s. %s\n", conf.Image, err)
			errc <- err
			return
		}
		infoc <- info
	}()

	select {
	case info := <-infoc:
		return info, nil
	case err := <-errc:
		return info, err
		// TODO checkout net.Context and cancel
		// case <-time.After(timeout):
		// 	return info, ErrTimeout
	}
}

func daemon(client dockerclient.Client, conf *dockerclient.ContainerConfig, pull bool) (*dockerclient.ContainerInfo, error) {
	// force-pull the image
	if pull {
		client.PullImage(conf.Image, nil)
	}

	// attempts to create the contianer
	id, err := client.CreateContainer(conf, "")
	if err != nil {
		// and pull the image and re-create if that fails
		err = client.PullImage(conf.Image, nil)
		if err != nil {
			log.Errorf("Error pulling %s. %s\n", conf.Image, err)
			return nil, err
		}
		id, err = client.CreateContainer(conf, "")
		if err != nil {
			log.Errorf("Error creating %s. %s\n", conf.Image, err)
			client.RemoveContainer(id, true, true)
			return nil, err
		}
	}

	// fetches the container information
	info, err := client.InspectContainer(id)
	if err != nil {
		log.Errorf("Error inspecting %s. %s\n", conf.Image, err)
		client.RemoveContainer(id, true, true)
		return nil, err
	}

	// starts the container
	err = client.StartContainer(id, &conf.HostConfig)
	if err != nil {
		log.Errorf("Error starting daemon %s. %s\n", conf.Image, err)
	}
	return info, err
}