package app

import (
	"crypto/rand"
	"encoding/base64"
	"fmt"
	"net"
	"net/url"
	"reflect"
	"strconv"
	"strings"

	"github.com/pborman/uuid"
	kapi "k8s.io/kubernetes/pkg/api"
	"k8s.io/kubernetes/pkg/api/unversioned"
	"k8s.io/kubernetes/pkg/conversion"
	"k8s.io/kubernetes/pkg/runtime"

	buildapi "github.com/openshift/origin/pkg/build/api"
	deployapi "github.com/openshift/origin/pkg/deploy/api"
	"github.com/openshift/origin/pkg/generate/git"
	imageapi "github.com/openshift/origin/pkg/image/api"
	"github.com/openshift/origin/pkg/util"
)

const (
	volumeNameInfix = "volume"

	GenerationWarningAnnotation = "app.generate.openshift.io/warnings"
)

// NameSuggester is an object that can suggest a name for itself
type NameSuggester interface {
	SuggestName() (string, bool)
}

// NameSuggestions suggests names from a collection of NameSuggesters
type NameSuggestions []NameSuggester

// SuggestName suggests a name given a collection of NameSuggesters
func (s NameSuggestions) SuggestName() (string, bool) {
	for i := range s {
		if s[i] == nil {
			continue
		}
		if name, ok := s[i].SuggestName(); ok {
			return name, true
		}
	}
	return "", false
}

// IsParameterizableValue returns true if the value contains standard replacement
// syntax, to preserve the value for use inside of the generated output. Passing
// parameters into output is only valid if the output is used inside of a template.
func IsParameterizableValue(s string) bool {
	return strings.Contains(s, "${") || strings.Contains(s, "$(")
}

// Generated is a list of runtime objects
type Generated struct {
	Items []runtime.Object
}

// WithType extracts a list of runtime objects with the specified type
func (g *Generated) WithType(slicePtr interface{}) bool {
	found := false
	v, err := conversion.EnforcePtr(slicePtr)
	if err != nil || v.Kind() != reflect.Slice {
		// This should not happen at runtime.
		panic("need ptr to slice")
	}
	t := v.Type().Elem()
	for i := range g.Items {
		obj := reflect.ValueOf(g.Items[i]).Elem()
		if !obj.Type().ConvertibleTo(t) {
			continue
		}
		found = true
		v.Set(reflect.Append(v, obj.Convert(t)))
	}
	return found
}

func nameFromGitURL(url *url.URL) (string, bool) {
	if url == nil {
		return "", false
	}
	// from path
	if name, ok := git.NameFromRepositoryURL(url); ok {
		return name, true
	}
	// TODO: path is questionable
	if len(url.Host) > 0 {
		// from host with port
		if host, _, err := net.SplitHostPort(url.Host); err == nil {
			return host, true
		}
		// from host without port
		return url.Host, true
	}
	return "", false
}

// SourceRef is a reference to a build source
type SourceRef struct {
	URL        *url.URL
	Ref        string
	Dir        string
	Name       string
	ContextDir string
	Secrets    []buildapi.SecretBuildSource

	SourceImage     *ImageRef
	ImageSourcePath string
	ImageDestPath   string

	DockerfileContents string

	Binary bool
}

func urlWithoutRef(url url.URL) string {
	url.Fragment = ""
	return url.String()
}

// SuggestName returns a name derived from the source URL
func (r *SourceRef) SuggestName() (string, bool) {
	if r == nil {
		return "", false
	}
	if len(r.Name) > 0 {
		return r.Name, true
	}
	return nameFromGitURL(r.URL)
}

// BuildSource returns an OpenShift BuildSource from the SourceRef
func (r *SourceRef) BuildSource() (*buildapi.BuildSource, []buildapi.BuildTriggerPolicy) {
	triggers := []buildapi.BuildTriggerPolicy{
		{
			Type: buildapi.GitHubWebHookBuildTriggerType,
			GitHubWebHook: &buildapi.WebHookTrigger{
				Secret: GenerateSecret(20),
			},
		},
		{
			Type: buildapi.GenericWebHookBuildTriggerType,
			GenericWebHook: &buildapi.WebHookTrigger{
				Secret: GenerateSecret(20),
			},
		},
	}
	source := &buildapi.BuildSource{}
	source.Secrets = r.Secrets

	if len(r.DockerfileContents) != 0 {
		source.Dockerfile = &r.DockerfileContents
	}
	if r.URL != nil {
		source.Git = &buildapi.GitBuildSource{
			URI: urlWithoutRef(*r.URL),
			Ref: r.Ref,
		}
		source.ContextDir = r.ContextDir
	}
	if r.Binary {
		source.Binary = &buildapi.BinaryBuildSource{}
	}
	if r.SourceImage != nil {
		objRef := r.SourceImage.ObjectReference()
		imgSrc := buildapi.ImageSource{}
		imgSrc.From = objRef
		imgSrc.Paths = []buildapi.ImageSourcePath{
			{
				SourcePath:     r.ImageSourcePath,
				DestinationDir: r.ImageDestPath,
			},
		}
		triggers = append(triggers, buildapi.BuildTriggerPolicy{
			Type: buildapi.ImageChangeBuildTriggerType,
			ImageChange: &buildapi.ImageChangeTrigger{
				From: &objRef,
			},
		})
		source.Images = []buildapi.ImageSource{imgSrc}
	}
	return source, triggers
}

// BuildStrategyRef is a reference to a build strategy
type BuildStrategyRef struct {
	IsDockerBuild bool
	Base          *ImageRef
}

// BuildStrategy builds an OpenShift BuildStrategy from a BuildStrategyRef
func (s *BuildStrategyRef) BuildStrategy(env Environment) (*buildapi.BuildStrategy, []buildapi.BuildTriggerPolicy) {
	if s.IsDockerBuild {
		var triggers []buildapi.BuildTriggerPolicy
		strategy := &buildapi.DockerBuildStrategy{
			Env: env.List(),
		}
		if s.Base != nil {
			ref := s.Base.ObjectReference()
			strategy.From = &ref
			triggers = s.Base.BuildTriggers()
		}
		return &buildapi.BuildStrategy{
			DockerStrategy: strategy,
		}, triggers
	}

	return &buildapi.BuildStrategy{
		SourceStrategy: &buildapi.SourceBuildStrategy{
			From: s.Base.ObjectReference(),
			Env:  env.List(),
		},
	}, s.Base.BuildTriggers()
}

// BuildRef is a reference to a build configuration
type BuildRef struct {
	Source   *SourceRef
	Input    *ImageRef
	Strategy *BuildStrategyRef
	Output   *ImageRef
	Env      Environment
}

// BuildConfig creates a buildConfig resource from the build configuration reference
func (r *BuildRef) BuildConfig() (*buildapi.BuildConfig, error) {
	name, ok := NameSuggestions{r.Source, r.Output}.SuggestName()
	if !ok {
		return nil, fmt.Errorf("unable to suggest a name for this BuildConfig from %q", r.Source.URL)
	}
	var source *buildapi.BuildSource
	triggers := []buildapi.BuildTriggerPolicy{}
	if r.Source != nil {
		source, triggers = r.Source.BuildSource()
	}
	if source == nil {
		source = &buildapi.BuildSource{}
	}
	strategy := &buildapi.BuildStrategy{}
	strategyTriggers := []buildapi.BuildTriggerPolicy{}
	if r.Strategy != nil {
		strategy, strategyTriggers = r.Strategy.BuildStrategy(r.Env)
	}
	output, err := r.Output.BuildOutput()
	if err != nil {
		return nil, err
	}

	if source.Binary == nil {
		configChangeTrigger := buildapi.BuildTriggerPolicy{
			Type: buildapi.ConfigChangeBuildTriggerType,
		}
		triggers = append(triggers, configChangeTrigger)
		triggers = append(triggers, strategyTriggers...)
	}

	return &buildapi.BuildConfig{
		ObjectMeta: kapi.ObjectMeta{
			Name: name,
		},
		Spec: buildapi.BuildConfigSpec{
			Triggers: triggers,
			CommonSpec: buildapi.CommonSpec{
				Source:   *source,
				Strategy: *strategy,
				Output:   *output,
			},
		},
	}, nil
}

type DeploymentHook struct {
	Shell string
}

// DeploymentConfigRef is a reference to a deployment configuration
type DeploymentConfigRef struct {
	Name     string
	Images   []*ImageRef
	Env      Environment
	Labels   map[string]string
	AsTest   bool
	PostHook *DeploymentHook
}

// DeploymentConfig creates a deploymentConfig resource from the deployment configuration reference
//
// TODO: take a pod template spec as argument
func (r *DeploymentConfigRef) DeploymentConfig() (*deployapi.DeploymentConfig, error) {
	if len(r.Name) == 0 {
		suggestions := NameSuggestions{}
		for i := range r.Images {
			suggestions = append(suggestions, r.Images[i])
		}
		name, ok := suggestions.SuggestName()
		if !ok {
			return nil, fmt.Errorf("unable to suggest a name for this DeploymentConfig")
		}
		r.Name = name
	}

	selector := map[string]string{
		"deploymentconfig": r.Name,
	}
	if len(r.Labels) > 0 {
		if err := util.MergeInto(selector, r.Labels, 0); err != nil {
			return nil, err
		}
	}

	triggers := []deployapi.DeploymentTriggerPolicy{
		// By default, always deploy on change
		{
			Type: deployapi.DeploymentTriggerOnConfigChange,
		},
	}

	annotations := make(map[string]string)

	template := kapi.PodSpec{}
	for i := range r.Images {
		c, containerTriggers, err := r.Images[i].DeployableContainer()
		if err != nil {
			return nil, err
		}
		triggers = append(triggers, containerTriggers...)
		template.Containers = append(template.Containers, *c)
		if cmd, ok := r.Images[i].Command(); ok {
			imageapi.SetContainerImageEntrypointAnnotation(annotations, c.Name, cmd)
		}
	}

	// Create EmptyDir volumes for all container volume mounts
	for _, c := range template.Containers {
		for _, v := range c.VolumeMounts {
			template.Volumes = append(template.Volumes, kapi.Volume{
				Name: v.Name,
				VolumeSource: kapi.VolumeSource{
					EmptyDir: &kapi.EmptyDirVolumeSource{Medium: kapi.StorageMediumDefault},
				},
			})
		}
	}

	for i := range template.Containers {
		template.Containers[i].Env = append(template.Containers[i].Env, r.Env.List()...)
	}

	dc := &deployapi.DeploymentConfig{
		ObjectMeta: kapi.ObjectMeta{
			Name: r.Name,
		},
		Spec: deployapi.DeploymentConfigSpec{
			Replicas: 1,
			Test:     r.AsTest,
			Selector: selector,
			Template: &kapi.PodTemplateSpec{
				ObjectMeta: kapi.ObjectMeta{
					Labels:      selector,
					Annotations: annotations,
				},
				Spec: template,
			},
			Triggers: triggers,
		},
	}
	if r.PostHook != nil {
		//dc.Spec.Strategy.Type = "Rolling"
		if len(r.PostHook.Shell) > 0 {
			dc.Spec.Strategy.RecreateParams = &deployapi.RecreateDeploymentStrategyParams{
				Post: &deployapi.LifecycleHook{
					ExecNewPod: &deployapi.ExecNewPodHook{
						Command: []string{"/bin/sh", "-c", r.PostHook.Shell},
					},
				},
			}
		}
	}

	return dc, nil
}

// GenerateSecret generates a random secret string
func GenerateSecret(n int) string {
	n = n * 3 / 4
	b := make([]byte, n)
	read, _ := rand.Read(b)
	if read != n {
		return uuid.NewRandom().String()
	}
	return base64.URLEncoding.EncodeToString(b)
}

// ContainerPortsFromString extracts sets of port specifications from a comma-delimited string. Each segment
// must be a single port number (container port) or a colon delimited pair of ports (container port and host port).
func ContainerPortsFromString(portString string) ([]kapi.ContainerPort, error) {
	ports := []kapi.ContainerPort{}
	for _, s := range strings.Split(portString, ",") {
		port, ok := checkPortSpecSegment(s)
		if !ok {
			return nil, fmt.Errorf("%q is not valid: you must specify one (container) or two (container:host) port numbers", s)
		}
		ports = append(ports, port)
	}
	return ports, nil
}

func checkPortSpecSegment(s string) (port kapi.ContainerPort, ok bool) {
	if strings.Contains(s, ":") {
		pair := strings.Split(s, ":")
		if len(pair) != 2 {
			return
		}
		container, err := strconv.Atoi(pair[0])
		if err != nil {
			return
		}
		host, err := strconv.Atoi(pair[1])
		if err != nil {
			return
		}
		return kapi.ContainerPort{ContainerPort: int32(container), HostPort: int32(host)}, true
	}

	container, err := strconv.Atoi(s)
	if err != nil {
		return
	}
	return kapi.ContainerPort{ContainerPort: int32(container)}, true
}

// LabelsFromSpec turns a set of specs NAME=VALUE or NAME- into a map of labels,
// a remove label list, or an error.
func LabelsFromSpec(spec []string) (map[string]string, []string, error) {
	labels := map[string]string{}
	var remove []string
	for _, labelSpec := range spec {
		if strings.Index(labelSpec, "=") != -1 {
			parts := strings.Split(labelSpec, "=")
			if len(parts) != 2 {
				return nil, nil, fmt.Errorf("invalid label spec: %v", labelSpec)
			}
			labels[parts[0]] = parts[1]
		} else if strings.HasSuffix(labelSpec, "-") {
			remove = append(remove, labelSpec[:len(labelSpec)-1])
		} else {
			return nil, nil, fmt.Errorf("unknown label spec: %s", labelSpec)
		}
	}
	for _, removeLabel := range remove {
		if _, found := labels[removeLabel]; found {
			return nil, nil, fmt.Errorf("can not both modify and remove a label in the same command")
		}
	}
	return labels, remove, nil
}

// TODO: move to pkg/runtime or pkg/api
func AsVersionedObjects(objects []runtime.Object, typer runtime.ObjectTyper, convertor runtime.ObjectConvertor, versions ...unversioned.GroupVersion) []error {
	var errs []error
	for i, object := range objects {
		kinds, _, err := typer.ObjectKinds(object)
		if err != nil {
			errs = append(errs, err)
			continue
		}
		if kindsInVersions(kinds, versions) {
			continue
		}
		if !isInternalOnly(kinds) {
			continue
		}
		converted, err := tryConvert(convertor, object, versions)
		if err != nil {
			errs = append(errs, err)
			continue
		}
		objects[i] = converted
	}
	return errs
}

func isInternalOnly(kinds []unversioned.GroupVersionKind) bool {
	for _, kind := range kinds {
		if kind.Version != runtime.APIVersionInternal {
			return false
		}
	}
	return true
}

func kindsInVersions(kinds []unversioned.GroupVersionKind, versions []unversioned.GroupVersion) bool {
	for _, kind := range kinds {
		for _, version := range versions {
			if kind.GroupVersion() == version {
				return true
			}
		}
	}
	return false
}

// tryConvert attempts to convert the given object to the provided versions in order.
func tryConvert(convertor runtime.ObjectConvertor, object runtime.Object, versions []unversioned.GroupVersion) (runtime.Object, error) {
	var last error
	for _, version := range versions {
		obj, err := convertor.ConvertToVersion(object, version)
		if err != nil {
			last = err
			continue
		}
		return obj, nil
	}
	return nil, last
}