package rancher

import (
	"context"
	"os"
	"time"

	"github.com/cenk/backoff"
	"github.com/containous/traefik/job"
	"github.com/containous/traefik/log"
	"github.com/containous/traefik/safe"
	"github.com/containous/traefik/types"
	"github.com/mitchellh/mapstructure"
	rancher "github.com/rancher/go-rancher/client"
)

const (
	labelRancherStackServiceName = "io.rancher.stack_service.name"
	hostNetwork                  = "host"
)

var withoutPagination *rancher.ListOpts

// APIConfiguration contains configuration properties specific to the Rancher
// API provider.
type APIConfiguration struct {
	Endpoint  string `description:"Rancher server API HTTP(S) endpoint"`
	AccessKey string `description:"Rancher server API access key"`
	SecretKey string `description:"Rancher server API secret key"`
}

func init() {
	withoutPagination = &rancher.ListOpts{
		Filters: map[string]interface{}{"limit": 0},
	}
}

func (p *Provider) createClient() (*rancher.RancherClient, error) {
	rancherURL := getenv("CATTLE_URL", p.API.Endpoint)
	accessKey := getenv("CATTLE_ACCESS_KEY", p.API.AccessKey)
	secretKey := getenv("CATTLE_SECRET_KEY", p.API.SecretKey)

	return rancher.NewRancherClient(&rancher.ClientOpts{
		Url:       rancherURL,
		AccessKey: accessKey,
		SecretKey: secretKey,
	})
}

func getenv(key, fallback string) string {
	value := os.Getenv(key)
	if len(value) == 0 {
		return fallback
	}
	return value
}

func (p *Provider) apiProvide(configurationChan chan<- types.ConfigMessage, pool *safe.Pool, constraints types.Constraints) error {
	p.Constraints = append(p.Constraints, constraints...)

	if p.API == nil {
		p.API = &APIConfiguration{}
	}

	safe.Go(func() {
		operation := func() error {
			rancherClient, err := p.createClient()

			if err != nil {
				log.Errorf("Failed to create a client for rancher, error: %s", err)
				return err
			}

			ctx := context.Background()
			var environments = listRancherEnvironments(rancherClient)
			var services = listRancherServices(rancherClient)
			var container = listRancherContainer(rancherClient)

			var rancherData = parseAPISourcedRancherData(environments, services, container)

			configuration := p.loadRancherConfig(rancherData)
			configurationChan <- types.ConfigMessage{
				ProviderName:  "rancher",
				Configuration: configuration,
			}

			if p.Watch {
				_, cancel := context.WithCancel(ctx)
				ticker := time.NewTicker(time.Second * time.Duration(p.RefreshSeconds))
				pool.Go(func(stop chan bool) {
					for {
						select {
						case <-ticker.C:

							log.Debugf("Refreshing new Data from Provider API")
							var environments = listRancherEnvironments(rancherClient)
							var services = listRancherServices(rancherClient)
							var container = listRancherContainer(rancherClient)

							rancherData := parseAPISourcedRancherData(environments, services, container)

							configuration := p.loadRancherConfig(rancherData)
							if configuration != nil {
								configurationChan <- types.ConfigMessage{
									ProviderName:  "rancher",
									Configuration: configuration,
								}
							}
						case <-stop:
							ticker.Stop()
							cancel()
							return
						}
					}
				})
			}

			return nil
		}
		notify := func(err error, time time.Duration) {
			log.Errorf("Provider connection error %+v, retrying in %s", err, time)
		}
		err := backoff.RetryNotify(operation, job.NewBackOff(backoff.NewExponentialBackOff()), notify)
		if err != nil {
			log.Errorf("Cannot connect to Provider Endpoint %+v", err)
		}
	})

	return nil
}

func listRancherEnvironments(client *rancher.RancherClient) []*rancher.Environment {

	// Rancher Environment in frontend UI is actually a stack
	// https://forums.rancher.com/t/api-key-for-all-environments/279/9

	var environmentList = []*rancher.Environment{}

	environments, err := client.Environment.List(nil)

	if err != nil {
		log.Errorf("Cannot get Rancher Environments %+v", err)
	}

	for k := range environments.Data {
		environmentList = append(environmentList, &environments.Data[k])
	}

	return environmentList
}

func listRancherServices(client *rancher.RancherClient) []*rancher.Service {

	var servicesList = []*rancher.Service{}

	services, err := client.Service.List(withoutPagination)

	if err != nil {
		log.Errorf("Cannot get Provider Services %+v", err)
	}

	for k := range services.Data {
		servicesList = append(servicesList, &services.Data[k])
	}

	return servicesList
}

func listRancherContainer(client *rancher.RancherClient) []*rancher.Container {

	containerList := []*rancher.Container{}

	container, err := client.Container.List(withoutPagination)

	if err != nil {
		log.Errorf("Cannot get Provider Services %+v", err)
	}

	valid := true

	for valid {
		for k := range container.Data {
			containerList = append(containerList, &container.Data[k])
		}

		container, err = container.Next()

		if err != nil {
			break
		}

		if container == nil || len(container.Data) == 0 {
			valid = false
		}
	}

	return containerList
}

func parseAPISourcedRancherData(environments []*rancher.Environment, services []*rancher.Service, containers []*rancher.Container) []rancherData {
	var rancherDataList []rancherData

	for _, environment := range environments {

		for _, service := range services {

			if service.EnvironmentId != environment.Id {
				continue
			}

			rancherData := rancherData{
				Name:       environment.Name + "/" + service.Name,
				Health:     service.HealthState,
				State:      service.State,
				Labels:     make(map[string]string),
				Containers: []string{},
			}

			if service.LaunchConfig == nil || service.LaunchConfig.Labels == nil {
				log.Warnf("Rancher Service Labels are missing. Environment: %s, service: %s", environment.Name, service.Name)
			} else {
				for key, value := range service.LaunchConfig.Labels {
					rancherData.Labels[key] = value.(string)
				}
			}

			for _, container := range containers {
				if container.Labels[labelRancherStackServiceName] == rancherData.Name &&
					containerFilter(container.Name, container.HealthState, container.State) {

					if container.NetworkMode == hostNetwork {
						var endpoints []*rancher.PublicEndpoint
						err := mapstructure.Decode(service.PublicEndpoints, &endpoints)

						if err != nil {
							log.Errorf("Failed to decode PublicEndpoint: %v", err)
							continue
						}

						if len(endpoints) > 0 {
							rancherData.Containers = append(rancherData.Containers, endpoints[0].IpAddress)
						}
					} else {
						rancherData.Containers = append(rancherData.Containers, container.PrimaryIpAddress)
					}
				}
			}
			rancherDataList = append(rancherDataList, rancherData)
		}
	}

	return rancherDataList
}
