package commands

import (
	"errors"
	"fmt"
	"os"
	"regexp"
	"sort"
	"strings"
	"text/tabwriter"
	"text/template"
	"time"

	"io"

	"github.com/docker/machine/libmachine"
	"github.com/docker/machine/libmachine/drivers"
	"github.com/docker/machine/libmachine/engine"
	"github.com/docker/machine/libmachine/host"
	"github.com/docker/machine/libmachine/log"
	"github.com/docker/machine/libmachine/mcndockerclient"
	"github.com/docker/machine/libmachine/persist"
	"github.com/docker/machine/libmachine/state"
	"github.com/docker/machine/libmachine/swarm"
	"github.com/skarademir/naturalsort"
)

const (
	lsDefaultTimeout = 10
	tableFormatKey   = "table"
	lsDefaultFormat  = "table {{ .Name }}\t{{ .Active }}\t{{ .DriverName}}\t{{ .State }}\t{{ .URL }}\t{{ .Swarm }}\t{{ .DockerVersion }}\t{{ .Error}}"
)

var (
	headers = map[string]string{
		"Name":          "NAME",
		"Active":        "ACTIVE",
		"ActiveHost":    "ACTIVE_HOST",
		"ActiveSwarm":   "ACTIVE_SWARM",
		"DriverName":    "DRIVER",
		"State":         "STATE",
		"URL":           "URL",
		"SwarmOptions":  "SWARM_OPTIONS",
		"Swarm":         "SWARM",
		"EngineOptions": "ENGINE_OPTIONS",
		"Error":         "ERRORS",
		"DockerVersion": "DOCKER",
		"ResponseTime":  "RESPONSE",
	}
)

type HostListItem struct {
	Name          string
	Active        string
	ActiveHost    bool
	ActiveSwarm   bool
	DriverName    string
	State         state.State
	URL           string
	SwarmOptions  *swarm.Options
	Swarm         string
	EngineOptions *engine.Options
	Error         string
	DockerVersion string
	ResponseTime  time.Duration
}

// FilterOptions -
type FilterOptions struct {
	SwarmName  []string
	DriverName []string
	State      []string
	Name       []string
	Labels     []string
}

func cmdLs(c CommandLine, api libmachine.API) error {
	filters, err := parseFilters(c.StringSlice("filter"))
	if err != nil {
		return err
	}

	hostList, hostInError, err := persist.LoadAllHosts(api)
	if err != nil {
		return err
	}

	hostList = filterHosts(hostList, filters)

	// Just print out the names if we're being quiet
	if c.Bool("quiet") {
		for _, host := range hostList {
			fmt.Println(host.Name)
		}
		return nil
	}

	template, table, err := parseFormat(c.String("format"))
	if err != nil {
		return err
	}

	var w io.Writer
	if table {
		tabWriter := tabwriter.NewWriter(os.Stdout, 5, 1, 3, ' ', 0)
		defer tabWriter.Flush()

		w = tabWriter

		if err := template.Execute(w, headers); err != nil {
			return err
		}
	} else {
		w = os.Stdout
	}

	timeout := time.Duration(c.Int("timeout")) * time.Second
	items := getHostListItems(hostList, hostInError, timeout)

	swarmMasters := make(map[string]string)
	swarmInfo := make(map[string]string)

	for _, host := range hostList {
		if host.HostOptions != nil {
			swarmOptions := host.HostOptions.SwarmOptions
			if swarmOptions.Master {
				swarmMasters[swarmOptions.Discovery] = host.Name
			}

			if swarmOptions.Discovery != "" {
				swarmInfo[host.Name] = swarmOptions.Discovery
			}
		}
	}

	for _, item := range items {
		swarmColumn := ""
		if item.SwarmOptions != nil && item.SwarmOptions.Discovery != "" {
			swarmColumn = swarmMasters[item.SwarmOptions.Discovery]
			if item.SwarmOptions.Master {
				swarmColumn = fmt.Sprintf("%s (master)", swarmColumn)
			}
		}
		item.Swarm = swarmColumn

		if err := template.Execute(w, item); err != nil {
			return err
		}
	}

	return nil
}

func parseFormat(format string) (*template.Template, bool, error) {
	table := false
	finalFormat := format

	if finalFormat == "" {
		finalFormat = lsDefaultFormat
	}

	if strings.HasPrefix(finalFormat, tableFormatKey) {
		table = true
		finalFormat = finalFormat[len(tableFormatKey):]
	}

	finalFormat = strings.Trim(finalFormat, " ")
	r := strings.NewReplacer(`\t`, "\t", `\n`, "\n")
	finalFormat = r.Replace(finalFormat)

	template, err := template.New("").Parse(finalFormat + "\n")
	if err != nil {
		return nil, false, err
	}

	return template, table, nil
}

func parseFilters(filters []string) (FilterOptions, error) {
	options := FilterOptions{}
	for _, f := range filters {
		kv := strings.SplitN(f, "=", 2)
		if len(kv) != 2 {
			return options, errors.New("Unsupported filter syntax")
		}
		key, value := strings.ToLower(kv[0]), kv[1]

		switch key {
		case "swarm":
			options.SwarmName = append(options.SwarmName, value)
		case "driver":
			options.DriverName = append(options.DriverName, value)
		case "state":
			options.State = append(options.State, value)
		case "name":
			options.Name = append(options.Name, value)
		case "label":
			options.Labels = append(options.Labels, value)
		default:
			return options, fmt.Errorf("Unsupported filter key '%s'", key)
		}
	}
	return options, nil
}

func filterHosts(hosts []*host.Host, filters FilterOptions) []*host.Host {
	if len(filters.SwarmName) == 0 &&
		len(filters.DriverName) == 0 &&
		len(filters.State) == 0 &&
		len(filters.Name) == 0 &&
		len(filters.Labels) == 0 {
		return hosts
	}

	filteredHosts := []*host.Host{}
	swarmMasters := getSwarmMasters(hosts)

	for _, h := range hosts {
		if filterHost(h, filters, swarmMasters) {
			filteredHosts = append(filteredHosts, h)
		}
	}
	return filteredHosts
}

func getSwarmMasters(hosts []*host.Host) map[string]string {
	swarmMasters := make(map[string]string)
	for _, h := range hosts {
		if h.HostOptions != nil {
			swarmOptions := h.HostOptions.SwarmOptions
			if swarmOptions != nil && swarmOptions.Master {
				swarmMasters[swarmOptions.Discovery] = h.Name
			}
		}
	}
	return swarmMasters
}

func filterHost(host *host.Host, filters FilterOptions, swarmMasters map[string]string) bool {
	swarmMatches := matchesSwarmName(host, filters.SwarmName, swarmMasters)
	driverMatches := matchesDriverName(host, filters.DriverName)
	stateMatches := matchesState(host, filters.State)
	nameMatches := matchesName(host, filters.Name)
	labelMatches := matchesLabel(host, filters.Labels)

	return swarmMatches && driverMatches && stateMatches && nameMatches && labelMatches
}

func matchesSwarmName(host *host.Host, swarmNames []string, swarmMasters map[string]string) bool {
	if len(swarmNames) == 0 {
		return true
	}
	for _, n := range swarmNames {
		if host.HostOptions != nil && host.HostOptions.SwarmOptions != nil {
			if strings.EqualFold(n, swarmMasters[host.HostOptions.SwarmOptions.Discovery]) {
				return true
			}
		}
	}
	return false
}

func matchesDriverName(host *host.Host, driverNames []string) bool {
	if len(driverNames) == 0 {
		return true
	}
	for _, n := range driverNames {
		if strings.EqualFold(host.DriverName, n) {
			return true
		}
	}
	return false
}

func matchesState(host *host.Host, states []string) bool {
	if len(states) == 0 {
		return true
	}
	for _, n := range states {
		s, err := host.Driver.GetState()
		if err != nil {
			log.Warn(err)
		}
		if strings.EqualFold(n, s.String()) {
			return true
		}
	}
	return false
}

func matchesName(host *host.Host, names []string) bool {
	if len(names) == 0 {
		return true
	}
	for _, n := range names {
		r, err := regexp.Compile(n)
		if err != nil {
			log.Error(err)
			os.Exit(1) // TODO: Can we get rid of this call, and exit 'properly' ?
		}
		if r.MatchString(host.Driver.GetMachineName()) {
			return true
		}
	}
	return false
}

func matchesLabel(host *host.Host, labels []string) bool {
	if len(labels) == 0 {
		return true
	}

	var englabels = make(map[string]string, len(host.HostOptions.EngineOptions.Labels))

	if host.HostOptions != nil && host.HostOptions.EngineOptions.Labels != nil {
		for _, s := range host.HostOptions.EngineOptions.Labels {
			kv := strings.SplitN(s, "=", 2)
			englabels[kv[0]] = kv[1]
		}
	}

	for _, l := range labels {
		kv := strings.SplitN(l, "=", 2)
		if val, exists := englabels[kv[0]]; exists && strings.EqualFold(val, kv[1]) {
			return true
		}
	}
	return false
}

// PERFORMANCE: The code of this function is complicated because we try
// to call the underlying drivers as less as possible to get the information
// we need.
func attemptGetHostState(h *host.Host, stateQueryChan chan<- HostListItem) {
	requestBeginning := time.Now()
	url := ""
	currentState := state.None
	dockerVersion := "Unknown"
	hostError := ""

	url, err := h.URL()

	// PERFORMANCE: if we have the url, it's ok to assume the host is running
	// This reduces the number of calls to the drivers
	if err == nil {
		if url != "" {
			currentState = state.Running
		} else {
			currentState, err = h.Driver.GetState()
		}
	} else {
		currentState, _ = h.Driver.GetState()
	}

	if err == nil && url != "" {
		// PERFORMANCE: Reuse the url instead of asking the host again.
		// This reduces the number of calls to the drivers
		dockerHost := &mcndockerclient.RemoteDocker{
			HostURL:    url,
			AuthOption: h.AuthOptions(),
		}
		dockerVersion, err = mcndockerclient.DockerVersion(dockerHost)

		if err != nil {
			dockerVersion = "Unknown"
		} else {
			dockerVersion = fmt.Sprintf("v%s", dockerVersion)
		}
	}

	if err != nil {
		hostError = err.Error()
	}
	if hostError == drivers.ErrHostIsNotRunning.Error() {
		hostError = ""
	}

	var swarmOptions *swarm.Options
	var engineOptions *engine.Options
	if h.HostOptions != nil {
		swarmOptions = h.HostOptions.SwarmOptions
		engineOptions = h.HostOptions.EngineOptions
	}

	isMaster := false
	swarmHost := ""
	if swarmOptions != nil {
		isMaster = swarmOptions.Master
		swarmHost = swarmOptions.Host
	}

	activeHost := isActive(currentState, url)
	activeSwarm := isSwarmActive(currentState, url, isMaster, swarmHost)
	active := "-"
	if activeHost {
		active = "*"
	}
	if activeSwarm {
		active = "* (swarm)"
	}

	stateQueryChan <- HostListItem{
		Name:          h.Name,
		Active:        active,
		ActiveHost:    activeHost,
		ActiveSwarm:   activeSwarm,
		DriverName:    h.Driver.DriverName(),
		State:         currentState,
		URL:           url,
		SwarmOptions:  swarmOptions,
		EngineOptions: engineOptions,
		DockerVersion: dockerVersion,
		Error:         hostError,
		ResponseTime:  time.Now().Round(time.Millisecond).Sub(requestBeginning.Round(time.Millisecond)),
	}
}

func getHostState(h *host.Host, hostListItemsChan chan<- HostListItem, timeout time.Duration) {
	// This channel is used to communicate the properties we are querying
	// about the host in the case of a successful read.
	stateQueryChan := make(chan HostListItem)

	go attemptGetHostState(h, stateQueryChan)

	select {
	// If we get back useful information, great.  Forward it straight to
	// the original parent channel.
	case hli := <-stateQueryChan:
		hostListItemsChan <- hli

	// Otherwise, give up after a predetermined duration.
	case <-time.After(timeout):
		hostListItemsChan <- HostListItem{
			Name:         h.Name,
			DriverName:   h.Driver.DriverName(),
			State:        state.Timeout,
			ResponseTime: timeout,
		}
	}
}

func getHostListItems(hostList []*host.Host, hostsInError map[string]error, timeout time.Duration) []HostListItem {
	log.Debugf("timeout set to %s", timeout)

	hostListItems := []HostListItem{}
	hostListItemsChan := make(chan HostListItem)

	for _, h := range hostList {
		go getHostState(h, hostListItemsChan, timeout)
	}

	for range hostList {
		hostListItems = append(hostListItems, <-hostListItemsChan)
	}

	close(hostListItemsChan)

	for name, err := range hostsInError {
		hostListItems = append(hostListItems, newHostListItemInError(name, err))
	}

	sortHostListItemsByName(hostListItems)
	return hostListItems
}

func newHostListItemInError(name string, err error) HostListItem {
	return HostListItem{
		Name:       name,
		DriverName: "not found",
		State:      state.Error,
		Error:      strings.Replace(err.Error(), "\n", "", -1),
	}
}

func sortHostListItemsByName(items []HostListItem) {
	m := make(map[string]HostListItem, len(items))
	s := make([]string, len(items))
	for i, v := range items {
		name := strings.ToLower(v.Name)
		m[name] = v
		s[i] = name
	}
	sort.Sort(naturalsort.NaturalSort(s))
	for i, v := range s {
		items[i] = m[v]
	}
}

func isActive(currentState state.State, hostURL string) bool {
	return currentState == state.Running && hostURL == os.Getenv("DOCKER_HOST")
}

func isSwarmActive(currentState state.State, hostURL string, isMaster bool, swarmHost string) bool {
	return isMaster && currentState == state.Running && toSwarmURL(hostURL, swarmHost) == os.Getenv("DOCKER_HOST")
}

func urlPort(urlWithPort string) string {
	parts := strings.Split(urlWithPort, ":")
	return parts[len(parts)-1]
}

func toSwarmURL(hostURL string, swarmHost string) string {
	hostPort := urlPort(hostURL)
	swarmPort := urlPort(swarmHost)
	return strings.Replace(hostURL, ":"+hostPort, ":"+swarmPort, 1)
}
