package docker

import (
	"context"
	"fmt"
	"github.com/docker/docker/api/types"
	"github.com/docker/docker/api/types/container"
	"github.com/docker/docker/pkg/stdcopy"
	"github.com/docker/go-connections/nat"
	"github.com/pkg/errors"
	"os"
	"strings"
	"time"

	"github.com/docker/docker/client"

	bosonFunc "github.com/boson-project/func"
)

// Runner of functions using the docker command.
type Runner struct {
	// Verbose logging flag.
	Verbose bool
}

// NewRunner creates an instance of a docker-backed runner.
func NewRunner() *Runner {
	return &Runner{}
}

// Run the function at path
func (n *Runner) Run(ctx context.Context, f bosonFunc.Function) error {
	ctx, cancel := context.WithCancel(ctx)
	defer cancel()

	cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
	if err != nil {
		return errors.Wrap(err, "failed to create docker api client")
	}

	if f.Image == "" {
		return errors.New("Function has no associated Image. Has it been built?")
	}

	envs := make([]string, 0, len(f.EnvVars)+1)
	for name, value := range f.EnvVars {
		if !strings.HasSuffix(name, "-") {
			envs = append(envs, fmt.Sprintf("%s=%s", name, value))
		}
	}
	if n.Verbose {
		envs = append(envs, "VERBOSE=true")
	}

	httpPort := nat.Port("8080/tcp")
	ports := map[nat.Port][]nat.PortBinding{
		httpPort: {
			nat.PortBinding{
				HostPort: "8080",
				HostIP:   "127.0.0.1",
			},
		},
	}

	conf := &container.Config{
		Env:          envs,
		Tty:          false,
		AttachStderr: true,
		AttachStdout: true,
		AttachStdin:  false,
		Image:        f.Image,
		ExposedPorts: map[nat.Port]struct{}{httpPort: {}},
	}

	hostConf := &container.HostConfig{
		PortBindings: ports,
	}

	cont, err := cli.ContainerCreate(ctx, conf, hostConf, nil, nil, "")
	if err != nil {
		return errors.Wrap(err, "failed to create container")
	}
	defer func() {
		err := cli.ContainerRemove(context.Background(), cont.ID, types.ContainerRemoveOptions{})
		if err != nil {
			fmt.Fprintf(os.Stderr, "failed to remove container: %v", err)
		}
	}()

	attachOptions := types.ContainerAttachOptions{
		Stdout: true,
		Stderr: true,
		Stdin:  false,
		Stream: true,
	}

	resp, err := cli.ContainerAttach(ctx, cont.ID, attachOptions)
	if err != nil {
		return errors.Wrap(err, "failed to attach container")
	}
	defer resp.Close()

	copyErrChan := make(chan error, 1)
	go func() {
		_, err := stdcopy.StdCopy(os.Stdout, os.Stderr, resp.Reader)
		copyErrChan <- err
	}()

	waitBodyChan, waitErrChan := cli.ContainerWait(ctx, cont.ID, container.WaitConditionNextExit)

	err = cli.ContainerStart(ctx, cont.ID, types.ContainerStartOptions{})
	if err != nil {
		return errors.Wrap(err, "failed to start container")
	}
	defer func() {
		t := time.Second * 10
		err := cli.ContainerStop(context.Background(), cont.ID, &t)
		if err != nil {
			fmt.Fprintf(os.Stderr, "failed to stop container: %v", err)
		}
	}()

	select {
	case body := <-waitBodyChan:
		if body.StatusCode != 0 {
			return fmt.Errorf("failed with status code: %d", body.StatusCode)
		}
	case err := <-waitErrChan:
		return err
	case err := <-copyErrChan:
		return err
	case <-ctx.Done():
		return ctx.Err()
	}

	return nil
}
