package docker

import (
	"fmt"
	"io"
	"math"
	"os"
	"strings"

	"golang.org/x/net/context"

	"github.com/Sirupsen/logrus"
	"github.com/docker/docker/pkg/promise"
	"github.com/docker/docker/pkg/stdcopy"
	"github.com/docker/docker/pkg/term"
	"github.com/docker/engine-api/client"
	"github.com/docker/engine-api/types"
	"github.com/docker/engine-api/types/container"
	"github.com/docker/go-connections/nat"
	"github.com/docker/libcompose/config"
	"github.com/docker/libcompose/labels"
	"github.com/docker/libcompose/logger"
	"github.com/docker/libcompose/project"
	"github.com/docker/libcompose/project/events"
	"github.com/docker/engine-api/types/network"
	util "github.com/docker/libcompose/utils"
)

// Container holds information about a docker container and the service it is tied on.
type Container struct {
	name            string
	serviceName     string
	projectName     string
	containerNumber int
	oneOff          bool
	eventNotifier   events.Notifier
	loggerFactory   logger.Factory
	client          client.APIClient

	// FIXME(vdemeester) Remove this dependency
	service *Service
}

// NewContainer creates a container struct with the specified docker client, name and service.
func NewContainer(client client.APIClient, name string, containerNumber int, service *Service) *Container {
	return &Container{
		client:          client,
		name:            name,
		containerNumber: containerNumber,

		// TODO(vdemeester) Move these to arguments
		serviceName:   service.name,
		projectName:   service.context.Project.Name,
		eventNotifier: service.context.Project,
		loggerFactory: service.context.LoggerFactory,

		// TODO(vdemeester) Remove this dependency
		service: service,
	}
}

// NewOneOffContainer creates a "oneoff" container struct with the specified docker client, name and service.
func NewOneOffContainer(client client.APIClient, name string, containerNumber int, service *Service) *Container {
	c := NewContainer(client, name, containerNumber, service)
	c.oneOff = true
	return c
}

func (c *Container) findExisting(ctx context.Context) (*types.ContainerJSON, error) {
	return GetContainer(ctx, c.client, c.name)
}

// Info returns info about the container, like name, command, state or ports.
func (c *Container) Info(ctx context.Context, qFlag bool) (project.Info, error) {
	container, err := c.findExisting(ctx)
	if err != nil || container == nil {
		return nil, err
	}

	infos, err := GetContainersByFilter(ctx, c.client, map[string][]string{
		"name": {container.Name},
	})
	if err != nil || len(infos) == 0 {
		return nil, err
	}
	info := infos[0]

	result := project.Info{}
	if qFlag {
		result = append(result, project.InfoPart{Key: "Id", Value: container.ID})
	} else {
		result = append(result, project.InfoPart{Key: "Name", Value: name(info.Names)})
		result = append(result, project.InfoPart{Key: "Command", Value: info.Command})
		result = append(result, project.InfoPart{Key: "State", Value: info.Status})
		result = append(result, project.InfoPart{Key: "Ports", Value: portString(info.Ports)})
	}

	return result, nil
}

func portString(ports []types.Port) string {
	result := []string{}

	for _, port := range ports {
		if port.PublicPort > 0 {
			result = append(result, fmt.Sprintf("%s:%d->%d/%s", port.IP, port.PublicPort, port.PrivatePort, port.Type))
		} else {
			result = append(result, fmt.Sprintf("%d/%s", port.PrivatePort, port.Type))
		}
	}

	return strings.Join(result, ", ")
}

func name(names []string) string {
	max := math.MaxInt32
	var current string

	for _, v := range names {
		if len(v) < max {
			max = len(v)
			current = v
		}
	}

	return current[1:]
}

// Recreate will not refresh the container by means of relaxation and enjoyment,
// just delete it and create a new one with the current configuration
func (c *Container) Recreate(ctx context.Context, imageName string) (*types.ContainerJSON, error) {
	container, err := c.findExisting(ctx)
	if err != nil || container == nil {
		return nil, err
	}

	hash := container.Config.Labels[labels.HASH.Str()]
	legacyHash := container.Config.Labels[labels.HASH_LEGACY.Str()]

	if hash == "" && legacyHash != "" {
		hash = legacyHash
	}
	if hash == "" {
		return nil, fmt.Errorf("Failed to find hash on old container: %s", container.Name)
	}

	name := container.Name[1:]
	newName := fmt.Sprintf("%s_%s", name, container.ID[:12])
	logrus.Debugf("Renaming %s => %s", name, newName)
	if err := c.client.ContainerRename(ctx, container.ID, newName); err != nil {
		logrus.Errorf("Failed to rename old container %s", c.name)
		return nil, err
	}

	newContainer, err := c.createContainer(ctx, imageName, container.ID, nil)
	if err != nil {
		return nil, err
	}
	logrus.Debugf("Created replacement container %s", newContainer.ID)

	if err := c.client.ContainerRemove(ctx, types.ContainerRemoveOptions{
		ContainerID:   container.ID,
		Force:         true,
		RemoveVolumes: false,
	}); err != nil {
		logrus.Errorf("Failed to remove old container %s", c.name)
		return nil, err
	}
	logrus.Debugf("Removed old container %s %s", c.name, container.ID)

	return newContainer, nil
}

// Create creates the container based on the specified image name and send an event
// to notify the container has been created. If the container already exists, does
// nothing.
func (c *Container) Create(ctx context.Context, imageName string) (*types.ContainerJSON, error) {
	return c.CreateWithOverride(ctx, imageName, nil)
}

// CreateWithOverride create container and override parts of the config to
// allow special situations to override the config generated from the compose
// file
func (c *Container) CreateWithOverride(ctx context.Context, imageName string, configOverride *config.ServiceConfig) (*types.ContainerJSON, error) {
	container, err := c.findExisting(ctx)
	if err != nil {
		return nil, err
	}

	if container == nil {
		container, err = c.createContainer(ctx, imageName, "", configOverride)
		if err != nil {
			return nil, err
		}
		c.eventNotifier.Notify(events.ContainerCreated, c.serviceName, map[string]string{
			"name": c.Name(),
		})
	}

	return container, err
}

// Stop stops the container.
func (c *Container) Stop(ctx context.Context, timeout int) error {
	return c.withContainer(ctx, func(container *types.ContainerJSON) error {
		return c.client.ContainerStop(ctx, container.ID, timeout)
	})
}

// Pause pauses the container. If the containers are already paused, don't fail.
func (c *Container) Pause(ctx context.Context) error {
	return c.withContainer(ctx, func(container *types.ContainerJSON) error {
		if !container.State.Paused {
			return c.client.ContainerPause(ctx, container.ID)
		}
		return nil
	})
}

// Unpause unpauses the container. If the containers are not paused, don't fail.
func (c *Container) Unpause(ctx context.Context) error {
	return c.withContainer(ctx, func(container *types.ContainerJSON) error {
		if container.State.Paused {
			return c.client.ContainerUnpause(ctx, container.ID)
		}
		return nil
	})
}

// Kill kill the container.
func (c *Container) Kill(ctx context.Context, signal string) error {
	return c.withContainer(ctx, func(container *types.ContainerJSON) error {
		return c.client.ContainerKill(ctx, container.ID, signal)
	})
}

// Delete removes the container if existing. If the container is running, it tries
// to stop it first.
func (c *Container) Delete(ctx context.Context, removeVolume bool) error {
	container, err := c.findExisting(ctx)
	if err != nil || container == nil {
		return err
	}

	info, err := c.client.ContainerInspect(ctx, container.ID)
	if err != nil {
		return err
	}

	if !info.State.Running {
		return c.client.ContainerRemove(ctx, types.ContainerRemoveOptions{
			ContainerID:   container.ID,
			Force:         true,
			RemoveVolumes: removeVolume,
		})
	}

	return nil
}

// IsRunning returns the running state of the container.
func (c *Container) IsRunning(ctx context.Context) (bool, error) {
	container, err := c.findExisting(ctx)
	if err != nil || container == nil {
		return false, err
	}

	info, err := c.client.ContainerInspect(ctx, container.ID)
	if err != nil {
		return false, err
	}

	return info.State.Running, nil
}

// Run creates, start and attach to the container based on the image name,
// the specified configuration.
// It will always create a new container.
func (c *Container) Run(ctx context.Context, imageName string, configOverride *config.ServiceConfig) (int, error) {
	var (
		errCh       chan error
		out, stderr io.Writer
		in          io.ReadCloser
	)

	container, err := c.createContainer(ctx, imageName, "", configOverride)
	if err != nil {
		return -1, err
	}

	if configOverride.StdinOpen {
		in = os.Stdin
	}
	if configOverride.Tty {
		out = os.Stdout
	}
	if configOverride.Tty {
		stderr = os.Stderr
	}

	options := types.ContainerAttachOptions{
		ContainerID: container.ID,
		Stream:      true,
		Stdin:       configOverride.StdinOpen,
		Stdout:      configOverride.Tty,
		Stderr:      configOverride.Tty,
	}

	resp, err := c.client.ContainerAttach(ctx, options)
	if err != nil {
		return -1, err
	}

	// set raw terminal
	inFd, _ := term.GetFdInfo(in)
	state, err := term.SetRawTerminal(inFd)
	if err != nil {
		return -1, err
	}
	// restore raw terminal
	defer term.RestoreTerminal(inFd, state)
	// holdHijackedConnection (in goroutine)
	errCh = promise.Go(func() error {
		return holdHijackedConnection(configOverride.Tty, in, out, stderr, resp)
	})

	if err := c.client.ContainerStart(ctx, container.ID); err != nil {
		return -1, err
	}

	if err := <-errCh; err != nil {
		logrus.Debugf("Error hijack: %s", err)
		return -1, err
	}

	exitedContainer, err := c.client.ContainerInspect(ctx, container.ID)
	if err != nil {
		return -1, err
	}

	return exitedContainer.State.ExitCode, nil
}

func holdHijackedConnection(tty bool, inputStream io.ReadCloser, outputStream, errorStream io.Writer, resp types.HijackedResponse) error {
	var err error
	receiveStdout := make(chan error, 1)
	if outputStream != nil || errorStream != nil {
		go func() {
			// When TTY is ON, use regular copy
			if tty && outputStream != nil {
				_, err = io.Copy(outputStream, resp.Reader)
			} else {
				_, err = stdcopy.StdCopy(outputStream, errorStream, resp.Reader)
			}
			logrus.Debugf("[hijack] End of stdout")
			receiveStdout <- err
		}()
	}

	stdinDone := make(chan struct{})
	go func() {
		if inputStream != nil {
			io.Copy(resp.Conn, inputStream)
			logrus.Debugf("[hijack] End of stdin")
		}

		if err := resp.CloseWrite(); err != nil {
			logrus.Debugf("Couldn't send EOF: %s", err)
		}
		close(stdinDone)
	}()

	select {
	case err := <-receiveStdout:
		if err != nil {
			logrus.Debugf("Error receiveStdout: %s", err)
			return err
		}
	case <-stdinDone:
		if outputStream != nil || errorStream != nil {
			if err := <-receiveStdout; err != nil {
				logrus.Debugf("Error receiveStdout: %s", err)
				return err
			}
		}
	}

	return nil
}

// Up creates and start the container based on the image name and send an event
// to notify the container has been created. If the container exists but is stopped
// it tries to start it.
func (c *Container) Up(ctx context.Context, imageName string) error {
	var err error

	container, err := c.Create(ctx, imageName)
	if err != nil {
		return err
	}

	if !container.State.Running {
		c.Start(container)
	}

	return nil
}

// Start the specified container with the specified host config
func (c *Container) Start(container *types.ContainerJSON) error {
	logrus.WithFields(logrus.Fields{"container.ID": container.ID, "c.name": c.name}).Debug("Starting container")
	if err := c.client.ContainerStart(context.Background(), container.ID); err != nil {
		logrus.WithFields(logrus.Fields{"container.ID": container.ID, "c.name": c.name}).Debug("Failed to start container")
		return err
	}
	c.eventNotifier.Notify(events.ContainerStarted, c.serviceName, map[string]string{
		"name": c.Name(),
	})
	return nil
}

// OutOfSync checks if the container is out of sync with the service definition.
// It looks if the the service hash container label is the same as the computed one.
func (c *Container) OutOfSync(ctx context.Context, imageName string) (bool, error) {
	container, err := c.findExisting(ctx)
	if err != nil || container == nil {
		return false, err
	}

	if container.Config.Image != imageName {
		logrus.Debugf("Images for %s do not match %s!=%s", c.name, container.Config.Image, imageName)
		return true, nil
	}

	if container.Config.Labels[labels.HASH.Str()] != c.getHash() {
		logrus.Debugf("Hashes for %s do not match %s!=%s", c.name, container.Config.Labels[labels.HASH.Str()], c.getHash())
		return true, nil
	}

	image, _, err := c.client.ImageInspectWithRaw(ctx, container.Config.Image, false)
	if err != nil {
		if client.IsErrImageNotFound(err) {
			logrus.Debugf("Image %s do not exist, do not know if it's out of sync", container.Config.Image)
			return false, nil
		}
		return false, err
	}

	logrus.Debugf("Checking existing image name vs id: %s == %s", image.ID, container.Image)
	return image.ID != container.Image, err
}

func (c *Container) getHash() string {
	return config.GetServiceHash(c.serviceName, c.service.Config())
}

func volumeBinds(volumes map[string]struct{}, container *types.ContainerJSON) []string {
	result := make([]string, 0, len(container.Mounts))
	for _, mount := range container.Mounts {
		if _, ok := volumes[mount.Destination]; ok {
			result = append(result, fmt.Sprint(mount.Source, ":", mount.Destination))
		}
	}
	return result
}

func (c *Container) createContainer(ctx context.Context, imageName, oldContainer string, configOverride *config.ServiceConfig) (*types.ContainerJSON, error) {
	serviceConfig := c.service.serviceConfig
	if configOverride != nil {
		serviceConfig.Command = configOverride.Command
		serviceConfig.Tty = configOverride.Tty
		serviceConfig.StdinOpen = configOverride.StdinOpen
	}
	configWrapper, err := ConvertToAPI(c.service)
	if err != nil {
		return nil, err
	}

	configWrapper.Config.Image = imageName

	if configWrapper.Config.Labels == nil {
		configWrapper.Config.Labels = map[string]string{}
	}

	oneOffString := "False"
	if c.oneOff {
		oneOffString = "True"
	}

	configWrapper.Config.Labels[labels.SERVICE.Str()] = c.serviceName
	configWrapper.Config.Labels[labels.PROJECT.Str()] = c.projectName
	configWrapper.Config.Labels[labels.HASH.Str()] = c.getHash()
	configWrapper.Config.Labels[labels.ONEOFF.Str()] = oneOffString
	configWrapper.Config.Labels[labels.NUMBER.Str()] = fmt.Sprint(c.containerNumber)
	configWrapper.Config.Labels[labels.VERSION.Str()] = ComposeVersion

	err = c.populateAdditionalHostConfig(configWrapper.HostConfig)
	if err != nil {
		return nil, err
	}

	if oldContainer != "" {
		info, err := c.client.ContainerInspect(ctx, oldContainer)
		if err != nil {
			return nil, err
		}
		configWrapper.HostConfig.Binds = util.Merge(configWrapper.HostConfig.Binds, volumeBinds(configWrapper.Config.Volumes, &info))
	}

	networkConfig := configWrapper.NetworkingConfig
	if configWrapper.HostConfig.NetworkMode != "" && configWrapper.HostConfig.NetworkMode.IsUserDefined() {
		if networkConfig == nil {
			// check user docker label, assign the user define ipv4 address
			if _, ok := configWrapper.Config.Labels["io.rancher.user_docker.fix_ip"]; ok {
				networkConfig = &network.NetworkingConfig{
					EndpointsConfig: map[string]*network.EndpointSettings{
						string(configWrapper.HostConfig.NetworkMode): {
							IPAddress: configWrapper.Config.Labels["io.rancher.user_docker.fix_ip"],
							IPAMConfig: &network.EndpointIPAMConfig{
								IPv4Address: configWrapper.Config.Labels["io.rancher.user_docker.fix_ip"],
							},
						},
					},
				}
			} else {
				networkConfig = &network.NetworkingConfig{
					EndpointsConfig: map[string]*network.EndpointSettings{
						string(configWrapper.HostConfig.NetworkMode): {},
					},
				}
			}
		}
		for key, value := range networkConfig.EndpointsConfig {
			conf := value
			if value.Aliases == nil {
				value.Aliases = []string{}
			}
			value.Aliases = append(value.Aliases, c.serviceName)
			networkConfig.EndpointsConfig[key] = conf
		}
	}

	logrus.Debugf("Creating container %s %#v", c.name, configWrapper)

	container, err := c.client.ContainerCreate(ctx, configWrapper.Config, configWrapper.HostConfig, networkConfig, c.name)
	if err != nil {
		logrus.Debugf("Failed to create container %s: %v", c.name, err)
		return nil, err
	}

	return GetContainer(ctx, c.client, container.ID)
}

func (c *Container) populateAdditionalHostConfig(hostConfig *container.HostConfig) error {
	links := map[string]string{}

	for _, link := range c.service.DependentServices() {
		if !c.service.context.Project.ServiceConfigs.Has(link.Target) {
			continue
		}

		service, err := c.service.context.Project.CreateService(link.Target)
		if err != nil {
			return err
		}

		// FIXME(vdemeester) container should not know service
		containers, err := service.Containers(context.Background())
		if err != nil {
			return err
		}

		if link.Type == project.RelTypeLink {
			c.addLinks(links, service, link, containers)
		} else if link.Type == project.RelTypeIpcNamespace {
			hostConfig, err = c.addIpc(hostConfig, service, containers)
		} else if link.Type == project.RelTypeNetNamespace {
			hostConfig, err = c.addNetNs(hostConfig, service, containers)
		}

		if err != nil {
			return err
		}
	}

	hostConfig.Links = []string{}
	for k, v := range links {
		hostConfig.Links = append(hostConfig.Links, strings.Join([]string{v, k}, ":"))
	}
	for _, v := range c.service.Config().ExternalLinks {
		hostConfig.Links = append(hostConfig.Links, v)
	}

	return nil
}

func (c *Container) addLinks(links map[string]string, service project.Service, rel project.ServiceRelationship, containers []project.Container) {
	for _, container := range containers {
		if _, ok := links[rel.Alias]; !ok {
			links[rel.Alias] = container.Name()
		}

		links[container.Name()] = container.Name()
	}
}

func (c *Container) addIpc(config *container.HostConfig, service project.Service, containers []project.Container) (*container.HostConfig, error) {
	if len(containers) == 0 {
		return nil, fmt.Errorf("Failed to find container for IPC %v", c.service.Config().Ipc)
	}

	id, err := containers[0].ID()
	if err != nil {
		return nil, err
	}

	config.IpcMode = container.IpcMode("container:" + id)
	return config, nil
}

func (c *Container) addNetNs(config *container.HostConfig, service project.Service, containers []project.Container) (*container.HostConfig, error) {
	if len(containers) == 0 {
		return nil, fmt.Errorf("Failed to find container for networks ns %v", c.service.Config().NetworkMode)
	}

	id, err := containers[0].ID()
	if err != nil {
		return nil, err
	}

	config.NetworkMode = container.NetworkMode("container:" + id)
	return config, nil
}

// ID returns the container Id.
func (c *Container) ID() (string, error) {
	// FIXME(vdemeester) container should not ask for his ID..
	container, err := c.findExisting(context.Background())
	if container == nil {
		return "", err
	}
	return container.ID, err
}

// Name returns the container name.
func (c *Container) Name() string {
	return c.name
}

// Restart restarts the container if existing, does nothing otherwise.
func (c *Container) Restart(ctx context.Context, timeout int) error {
	container, err := c.findExisting(ctx)
	if err != nil || container == nil {
		return err
	}

	return c.client.ContainerRestart(ctx, container.ID, timeout)
}

// Log forwards container logs to the project configured logger.
func (c *Container) Log(ctx context.Context, follow bool) error {
	container, err := c.findExisting(ctx)
	if container == nil || err != nil {
		return err
	}

	info, err := c.client.ContainerInspect(ctx, container.ID)
	if err != nil {
		return err
	}

	// FIXME(vdemeester) update container struct to do less API calls
	name := fmt.Sprintf("%s_%d", c.service.name, c.containerNumber)
	l := c.loggerFactory.Create(name)

	options := types.ContainerLogsOptions{
		ContainerID: c.name,
		ShowStdout:  true,
		ShowStderr:  true,
		Follow:      follow,
		Tail:        "all",
	}
	responseBody, err := c.client.ContainerLogs(ctx, options)
	if err != nil {
		return err
	}
	defer responseBody.Close()

	if info.Config.Tty {
		_, err = io.Copy(&logger.Wrapper{Logger: l}, responseBody)
	} else {
		_, err = stdcopy.StdCopy(&logger.Wrapper{Logger: l}, &logger.Wrapper{Logger: l, Err: true}, responseBody)
	}
	logrus.WithFields(logrus.Fields{"Logger": l, "err": err}).Debug("c.client.Logs() returned error")

	return err
}

func (c *Container) withContainer(ctx context.Context, action func(*types.ContainerJSON) error) error {
	container, err := c.findExisting(ctx)
	if err != nil {
		return err
	}

	if container != nil {
		return action(container)
	}

	return nil
}

// Port returns the host port the specified port is mapped on.
func (c *Container) Port(ctx context.Context, port string) (string, error) {
	container, err := c.findExisting(ctx)
	if err != nil {
		return "", err
	}

	if bindings, ok := container.NetworkSettings.Ports[nat.Port(port)]; ok {
		result := []string{}
		for _, binding := range bindings {
			result = append(result, binding.HostIP+":"+binding.HostPort)
		}

		return strings.Join(result, "\n"), nil
	}
	return "", nil
}
