// Copyright © 2016 Asteris, LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// +build !solaris

package docker

import (
	"strings"
	"time"

	log "github.com/Sirupsen/logrus"
	dc "github.com/fsouza/go-dockerclient"
	"github.com/pkg/errors"
)

// APIClient provides access to docker
type APIClient interface {
	FindImage(string) (*dc.Image, error)
	PullImage(string, string) error
	FindContainer(string) (*dc.Container, error)
	CreateContainer(dc.CreateContainerOptions) (*dc.Container, error)
	StartContainer(string, string) error
	ConnectNetwork(string, *dc.Container) error
}

// VolumeClient manages Docker volumes
type VolumeClient interface {
	FindVolume(string) (*dc.Volume, error)
	CreateVolume(dc.CreateVolumeOptions) (*dc.Volume, error)
	RemoveVolume(string) error
}

// Client provides api access to Docker
type Client struct {
	*dc.Client
	PullInactivityTimeout time.Duration
}

// NetworkClient manage Docker networks
type NetworkClient interface {
	ListNetworks() ([]dc.Network, error)
	FindNetwork(string) (*dc.Network, error)
	CreateNetwork(dc.CreateNetworkOptions) (*dc.Network, error)
	RemoveNetwork(string) error
}

// NewDockerClient returns a docker client with the default configuration
func NewDockerClient() (*Client, error) {
	c, err := dc.NewClientFromEnv()
	if err != nil {
		return nil, errors.Wrap(err, "failed to create docker client from environment")
	}
	return &Client{Client: c}, nil
}

// FindImage finds a local docker image with the specified repo tag
func (c *Client) FindImage(repoTag string) (*dc.Image, error) {
	// TODO: can I just call inspect with the repoTag?
	images, err := c.Client.ListImages(dc.ListImagesOptions{All: true})
	if err != nil {
		return nil, errors.Wrap(err, "failed to find image")
	}

	log.WithFields(log.Fields{
		"module":      "docker",
		"filter":      repoTag,
		"image_count": len(images),
	}).Debug("image filter found images")

	var imageID string
	for _, image := range images {
		if repoTag == image.ID {
			imageID = image.ID
			break
		}

		for _, tag := range image.RepoTags {
			log.WithField("module", "docker").WithField("tag", tag).Debug("found tag")
			if strings.EqualFold(repoTag, tag) {
				imageID = image.ID
				break
			}
		}
		if imageID != "" {
			break
		}
	}

	if imageID != "" {
		log.WithField("module", "docker").WithField("tag", repoTag).Debug("found image")
		image, err := c.Client.InspectImage(imageID)
		if err != nil {
			return nil, errors.Wrapf(err, "failed to inspect image %s (%s)", repoTag, imageID)
		}

		return image, nil
	}

	log.WithField("module", "docker").WithField("tag", repoTag).Debug("could not find image")
	return nil, nil
}

// PullImage pulls an image with the specified name and tag
func (c *Client) PullImage(name, tag string) error {
	log.WithFields(log.Fields{
		"module": "docker",
		"name":   name,
		"tag":    tag,
	}).Debug("pulling")
	opts := dc.PullImageOptions{
		Repository:        name,
		Tag:               tag,
		InactivityTimeout: c.PullInactivityTimeout,
	}

	err := c.Client.PullImage(opts, dc.AuthConfiguration{})
	if err != nil {
		return errors.Wrap(err, "failed to pull image")
	}

	log.WithFields(log.Fields{
		"module": "docker",
		"name":   name,
		"tag":    tag,
	}).Debug("done pulling")
	return nil
}

// FindContainer returns a container matching the specified name
func (c *Client) FindContainer(name string) (*dc.Container, error) {
	opts := dc.ListContainersOptions{All: true}
	containers, err := c.Client.ListContainers(opts)
	if err != nil {
		return nil, errors.Wrap(err, "failed to list containers")
	}

	var containerID string
	for _, container := range containers {
		// check if ID was specified first
		if name == container.ID {
			containerID = container.ID
			break
		}

		// check container names for a match
		for _, cname := range container.Names {
			if strings.EqualFold(name, strings.TrimPrefix(cname, "/")) {
				containerID = container.ID
				break
			}
		}

		if containerID != "" {
			break
		}
	}

	if containerID != "" {
		log.WithField("module", "docker").WithField("name", name).Debug("found container")
		container, err := c.Client.InspectContainer(containerID)
		if err != nil {
			return nil, errors.Wrapf(err, "failed to inspect container %s (%s)", name, containerID)
		}

		return container, nil
	}

	log.WithField("module", "docker").WithField("name", name).Debug("could not find container")
	return nil, nil
}

// CreateContainer creates a container with the specified options
func (c *Client) CreateContainer(opts dc.CreateContainerOptions) (*dc.Container, error) {
	name := opts.Name

	container, err := c.FindContainer(name)
	if err != nil {
		return nil, err
	}

	// the container already exists
	if container != nil {
		log.WithField("module", "docker").WithField("name", name).Debug("container exists")

		// stop the container if running
		if container.State.Running {
			log.WithField("module", "docker").WithFields(log.Fields{"name": name, "id": container.ID}).Debug("stopping container")
			err = c.Client.StopContainer(container.ID, 60)
			if err != nil {
				return nil, errors.Wrapf(err, "failed to stop container %s (%s)", name, container.ID)
			}
		}

		// remove the container
		log.WithField("module", "docker").WithFields(log.Fields{"name": name, "id": container.ID}).Debug("removing container")
		err = c.Client.RemoveContainer(dc.RemoveContainerOptions{ID: container.ID})
		if err != nil {
			return nil, errors.Wrapf(err, "failed to remove container %s (%s)", name, container.ID)
		}
	}

	// create the container
	log.WithField("module", "docker").WithField("name", name).Debug("creating container")
	container, err = c.Client.CreateContainer(opts)

	if err != nil {
		return nil, errors.Wrapf(err, "failed to create container %s", name)
	}

	return container, err
}

// StartContainer starts the container with the specified ID
func (c *Client) StartContainer(name, containerID string) error {
	log.WithField("module", "docker").WithFields(log.Fields{"name": name, "id": containerID}).Debug("starting container")
	err := c.Client.StartContainer(containerID, nil)
	if err != nil {
		err = errors.Wrapf(err, "failed to start container %s (%s)", name, containerID)
	}
	return err
}

// CreateVolume creates a docker volume
func (c *Client) CreateVolume(opts dc.CreateVolumeOptions) (*dc.Volume, error) {
	log.WithFields(log.Fields{
		"module":   "docker",
		"function": "CreateVolume",
	}).Debugf("creating volume %s: %+v", opts.Name, opts)

	vol, err := c.Client.CreateVolume(opts)
	if err != nil {
		return nil, errors.Wrap(err, "failed to create volume")
	}
	return vol, nil
}

// RemoveVolume removes a docker volume
func (c *Client) RemoveVolume(name string) error {
	log.WithFields(log.Fields{
		"module":   "docker",
		"function": "RemoveVolume",
	}).Debugf("removing volume %s", name)

	err := c.Client.RemoveVolume(name)
	if err != nil {
		return errors.Wrap(err, "failed to remove volume")
	}
	return nil
}

// FindVolume finds the volume with the specified name
func (c *Client) FindVolume(name string) (*dc.Volume, error) {
	volume, err := c.Client.InspectVolume(name)
	if err != nil && err != dc.ErrNoSuchVolume {
		return nil, errors.Wrap(err, "failed to inspect volume")
	}
	return volume, nil
}

// ConnectNetwork connects the container to a network
func (c *Client) ConnectNetwork(name string, container *dc.Container) error {
	log.WithField("module", "docker").WithFields(
		log.Fields{"name": container.Name, "network": name, "id": container.ID},
	).Debug("connecting to network")

	err := c.Client.ConnectNetwork(name, dc.NetworkConnectionOptions{Container: container.ID})
	if err != nil {
		err = errors.Wrapf(err, "failed to connect container %s (%s) to network %s", container.Name, container.ID, name)
	}

	return err
}

// FindNetwork finds the network with the specified name
func (c *Client) FindNetwork(name string) (*dc.Network, error) {
	networks, err := c.Client.FilteredListNetworks(dc.NetworkFilterOpts{
		"type": map[string]bool{"custom": true},
		"name": map[string]bool{name: true},
	})

	if err != nil {
		return nil, errors.Wrap(err, "failed to list networks")
	}

	for _, nw := range networks {
		if strings.EqualFold(nw.Name, name) {
			return &nw, nil
		}
	}

	return nil, nil
}

// ListNetworks returns the docker networks
func (c *Client) ListNetworks() ([]dc.Network, error) {
	return c.Client.ListNetworks()
}

// CreateNetwork creates a docker network
func (c *Client) CreateNetwork(opts dc.CreateNetworkOptions) (*dc.Network, error) {
	log.WithFields(log.Fields{
		"module":   "docker",
		"function": "CreateNetwork",
	}).Debugf("creating network %s: %+v", opts.Name, opts)

	nw, err := c.Client.CreateNetwork(opts)
	if err != nil {
		return nil, errors.Wrap(err, "failed to create network")
	}
	return nw, err
}

// RemoveNetwork removes a docker volume
func (c *Client) RemoveNetwork(name string) error {
	log.WithFields(log.Fields{
		"module":   "docker",
		"function": "RemoveNetwork",
	}).Debugf("removing network %s", name)

	nw, err := c.FindNetwork(name)
	if err != nil {
		return err
	}

	err = c.Client.RemoveNetwork(nw.ID)
	if err != nil {
		return errors.Wrap(err, "failed to remove network")
	}
	return nil
}
