package kubernetes

import (
	"io"
	"io/ioutil"
	"regexp"
	"strconv"
	"strings"

	"github.com/docker/cli/cli/compose/loader"
	"github.com/docker/cli/cli/compose/schema"
	composeTypes "github.com/docker/cli/cli/compose/types"
	composetypes "github.com/docker/cli/cli/compose/types"
	"github.com/docker/cli/kubernetes/compose/v1beta1"
	"github.com/docker/cli/kubernetes/compose/v1beta2"
	"github.com/pkg/errors"
	yaml "gopkg.in/yaml.v2"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

// NewStackConverter returns a converter from types.Config (compose) to the specified
// stack version or error out if the version is not supported or existent.
func NewStackConverter(version string) (StackConverter, error) {
	switch version {
	case "v1beta1":
		return stackV1Beta1Converter{}, nil
	case "v1beta2":
		return stackV1Beta2Converter{}, nil
	default:
		return nil, errors.Errorf("stack version %s unsupported", version)
	}
}

// StackConverter converts a compose types.Config to a Stack
type StackConverter interface {
	FromCompose(stderr io.Writer, name string, cfg *composetypes.Config) (Stack, error)
}

type stackV1Beta1Converter struct{}

func (s stackV1Beta1Converter) FromCompose(stderr io.Writer, name string, cfg *composetypes.Config) (Stack, error) {
	cfg.Version = v1beta1.MaxComposeVersion
	st, err := fromCompose(stderr, name, cfg)
	if err != nil {
		return Stack{}, err
	}
	res, err := yaml.Marshal(cfg)
	if err != nil {
		return Stack{}, err
	}
	// reload the result to check that it produced a valid 3.5 compose file
	resparsedConfig, err := loader.ParseYAML(res)
	if err != nil {
		return Stack{}, err
	}
	if err = schema.Validate(resparsedConfig, v1beta1.MaxComposeVersion); err != nil {
		return Stack{}, errors.Wrapf(err, "the compose yaml file is invalid with v%s", v1beta1.MaxComposeVersion)
	}

	st.ComposeFile = string(res)
	return st, nil
}

type stackV1Beta2Converter struct{}

func (s stackV1Beta2Converter) FromCompose(stderr io.Writer, name string, cfg *composetypes.Config) (Stack, error) {
	return fromCompose(stderr, name, cfg)
}

func fromCompose(stderr io.Writer, name string, cfg *composetypes.Config) (Stack, error) {
	return Stack{
		Name: name,
		Spec: fromComposeConfig(stderr, cfg),
	}, nil
}

func loadStackData(composefile string) (*composetypes.Config, error) {
	parsed, err := loader.ParseYAML([]byte(composefile))
	if err != nil {
		return nil, err
	}
	return loader.Load(composetypes.ConfigDetails{
		ConfigFiles: []composetypes.ConfigFile{
			{
				Config: parsed,
			},
		},
	})
}

// Conversions from internal stack to different stack compose component versions.
func stackFromV1beta1(in *v1beta1.Stack) (Stack, error) {
	cfg, err := loadStackData(in.Spec.ComposeFile)
	if err != nil {
		return Stack{}, err
	}
	return Stack{
		Name:        in.ObjectMeta.Name,
		Namespace:   in.ObjectMeta.Namespace,
		ComposeFile: in.Spec.ComposeFile,
		Spec:        fromComposeConfig(ioutil.Discard, cfg),
	}, nil
}

func stackToV1beta1(s Stack) *v1beta1.Stack {
	return &v1beta1.Stack{
		ObjectMeta: metav1.ObjectMeta{
			Name: s.Name,
		},
		Spec: v1beta1.StackSpec{
			ComposeFile: s.ComposeFile,
		},
	}
}

func stackFromV1beta2(in *v1beta2.Stack) Stack {
	return Stack{
		Name:      in.ObjectMeta.Name,
		Namespace: in.ObjectMeta.Namespace,
		Spec:      in.Spec,
	}
}

func stackToV1beta2(s Stack) *v1beta2.Stack {
	return &v1beta2.Stack{
		ObjectMeta: metav1.ObjectMeta{
			Name: s.Name,
		},
		Spec: s.Spec,
	}
}

func fromComposeConfig(stderr io.Writer, c *composeTypes.Config) *v1beta2.StackSpec {
	if c == nil {
		return nil
	}
	warnUnsupportedFeatures(stderr, c)
	serviceConfigs := make([]v1beta2.ServiceConfig, len(c.Services))
	for i, s := range c.Services {
		serviceConfigs[i] = fromComposeServiceConfig(s)
	}
	return &v1beta2.StackSpec{
		Services: serviceConfigs,
		Secrets:  fromComposeSecrets(c.Secrets),
		Configs:  fromComposeConfigs(c.Configs),
	}
}

func fromComposeSecrets(s map[string]composeTypes.SecretConfig) map[string]v1beta2.SecretConfig {
	if s == nil {
		return nil
	}
	m := map[string]v1beta2.SecretConfig{}
	for key, value := range s {
		m[key] = v1beta2.SecretConfig{
			Name: value.Name,
			File: value.File,
			External: v1beta2.External{
				Name:     value.External.Name,
				External: value.External.External,
			},
			Labels: value.Labels,
		}
	}
	return m
}

func fromComposeConfigs(s map[string]composeTypes.ConfigObjConfig) map[string]v1beta2.ConfigObjConfig {
	if s == nil {
		return nil
	}
	m := map[string]v1beta2.ConfigObjConfig{}
	for key, value := range s {
		m[key] = v1beta2.ConfigObjConfig{
			Name: value.Name,
			File: value.File,
			External: v1beta2.External{
				Name:     value.External.Name,
				External: value.External.External,
			},
			Labels: value.Labels,
		}
	}
	return m
}

func fromComposeServiceConfig(s composeTypes.ServiceConfig) v1beta2.ServiceConfig {
	var userID *int64
	if s.User != "" {
		numerical, err := strconv.Atoi(s.User)
		if err == nil {
			unixUserID := int64(numerical)
			userID = &unixUserID
		}
	}
	return v1beta2.ServiceConfig{
		Name:    s.Name,
		CapAdd:  s.CapAdd,
		CapDrop: s.CapDrop,
		Command: s.Command,
		Configs: fromComposeServiceConfigs(s.Configs),
		Deploy: v1beta2.DeployConfig{
			Mode:          s.Deploy.Mode,
			Replicas:      s.Deploy.Replicas,
			Labels:        s.Deploy.Labels,
			UpdateConfig:  fromComposeUpdateConfig(s.Deploy.UpdateConfig),
			Resources:     fromComposeResources(s.Deploy.Resources),
			RestartPolicy: fromComposeRestartPolicy(s.Deploy.RestartPolicy),
			Placement:     fromComposePlacement(s.Deploy.Placement),
		},
		Entrypoint:      s.Entrypoint,
		Environment:     s.Environment,
		ExtraHosts:      s.ExtraHosts,
		Hostname:        s.Hostname,
		HealthCheck:     fromComposeHealthcheck(s.HealthCheck),
		Image:           s.Image,
		Ipc:             s.Ipc,
		Labels:          s.Labels,
		Pid:             s.Pid,
		Ports:           fromComposePorts(s.Ports),
		Privileged:      s.Privileged,
		ReadOnly:        s.ReadOnly,
		Secrets:         fromComposeServiceSecrets(s.Secrets),
		StdinOpen:       s.StdinOpen,
		StopGracePeriod: s.StopGracePeriod,
		Tmpfs:           s.Tmpfs,
		Tty:             s.Tty,
		User:            userID,
		Volumes:         fromComposeServiceVolumeConfig(s.Volumes),
		WorkingDir:      s.WorkingDir,
	}
}

func fromComposePorts(ports []composeTypes.ServicePortConfig) []v1beta2.ServicePortConfig {
	if ports == nil {
		return nil
	}
	p := make([]v1beta2.ServicePortConfig, len(ports))
	for i, port := range ports {
		p[i] = v1beta2.ServicePortConfig{
			Mode:      port.Mode,
			Target:    port.Target,
			Published: port.Published,
			Protocol:  port.Protocol,
		}
	}
	return p
}

func fromComposeServiceSecrets(secrets []composeTypes.ServiceSecretConfig) []v1beta2.ServiceSecretConfig {
	if secrets == nil {
		return nil
	}
	c := make([]v1beta2.ServiceSecretConfig, len(secrets))
	for i, secret := range secrets {
		c[i] = v1beta2.ServiceSecretConfig{
			Source: secret.Source,
			Target: secret.Target,
			UID:    secret.UID,
			Mode:   secret.Mode,
		}
	}
	return c
}

func fromComposeServiceConfigs(configs []composeTypes.ServiceConfigObjConfig) []v1beta2.ServiceConfigObjConfig {
	if configs == nil {
		return nil
	}
	c := make([]v1beta2.ServiceConfigObjConfig, len(configs))
	for i, config := range configs {
		c[i] = v1beta2.ServiceConfigObjConfig{
			Source: config.Source,
			Target: config.Target,
			UID:    config.UID,
			Mode:   config.Mode,
		}
	}
	return c
}

func fromComposeHealthcheck(h *composeTypes.HealthCheckConfig) *v1beta2.HealthCheckConfig {
	if h == nil {
		return nil
	}
	return &v1beta2.HealthCheckConfig{
		Test:     h.Test,
		Timeout:  h.Timeout,
		Interval: h.Interval,
		Retries:  h.Retries,
	}
}

func fromComposePlacement(p composeTypes.Placement) v1beta2.Placement {
	return v1beta2.Placement{
		Constraints: fromComposeConstraints(p.Constraints),
	}
}

var constraintEquals = regexp.MustCompile(`([\w\.]*)\W*(==|!=)\W*([\w\.]*)`)

const (
	swarmOs          = "node.platform.os"
	swarmArch        = "node.platform.arch"
	swarmHostname    = "node.hostname"
	swarmLabelPrefix = "node.labels."
)

func fromComposeConstraints(s []string) *v1beta2.Constraints {
	if len(s) == 0 {
		return nil
	}
	constraints := &v1beta2.Constraints{}
	for _, constraint := range s {
		matches := constraintEquals.FindStringSubmatch(constraint)
		if len(matches) == 4 {
			key := matches[1]
			operator := matches[2]
			value := matches[3]
			constraint := &v1beta2.Constraint{
				Operator: operator,
				Value:    value,
			}
			switch {
			case key == swarmOs:
				constraints.OperatingSystem = constraint
			case key == swarmArch:
				constraints.Architecture = constraint
			case key == swarmHostname:
				constraints.Hostname = constraint
			case strings.HasPrefix(key, swarmLabelPrefix):
				if constraints.MatchLabels == nil {
					constraints.MatchLabels = map[string]v1beta2.Constraint{}
				}
				constraints.MatchLabels[strings.TrimPrefix(key, swarmLabelPrefix)] = *constraint
			}
		}
	}
	return constraints
}

func fromComposeResources(r composeTypes.Resources) v1beta2.Resources {
	return v1beta2.Resources{
		Limits:       fromComposeResourcesResource(r.Limits),
		Reservations: fromComposeResourcesResource(r.Reservations),
	}
}

func fromComposeResourcesResource(r *composeTypes.Resource) *v1beta2.Resource {
	if r == nil {
		return nil
	}
	return &v1beta2.Resource{
		MemoryBytes: int64(r.MemoryBytes),
		NanoCPUs:    r.NanoCPUs,
	}
}

func fromComposeUpdateConfig(u *composeTypes.UpdateConfig) *v1beta2.UpdateConfig {
	if u == nil {
		return nil
	}
	return &v1beta2.UpdateConfig{
		Parallelism: u.Parallelism,
	}
}

func fromComposeRestartPolicy(r *composeTypes.RestartPolicy) *v1beta2.RestartPolicy {
	if r == nil {
		return nil
	}
	return &v1beta2.RestartPolicy{
		Condition: r.Condition,
	}
}

func fromComposeServiceVolumeConfig(vs []composeTypes.ServiceVolumeConfig) []v1beta2.ServiceVolumeConfig {
	if vs == nil {
		return nil
	}
	volumes := []v1beta2.ServiceVolumeConfig{}
	for _, v := range vs {
		volumes = append(volumes, v1beta2.ServiceVolumeConfig{
			Type:     v.Type,
			Source:   v.Source,
			Target:   v.Target,
			ReadOnly: v.ReadOnly,
		})
	}
	return volumes
}
