// Copyright 2016 The Prometheus Authors
// 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.

package kubernetes

import (
	"fmt"

	"github.com/prometheus/common/log"
	"github.com/prometheus/common/model"
	"github.com/prometheus/prometheus/config"
	"github.com/prometheus/prometheus/util/strutil"
	"golang.org/x/net/context"
	"k8s.io/client-go/pkg/apis/extensions/v1beta1"
	"k8s.io/client-go/tools/cache"
)

// Ingress implements discovery of Kubernetes ingresss.
type Ingress struct {
	logger   log.Logger
	informer cache.SharedInformer
	store    cache.Store
}

// NewIngress returns a new ingress discovery.
func NewIngress(l log.Logger, inf cache.SharedInformer) *Ingress {
	return &Ingress{logger: l, informer: inf, store: inf.GetStore()}
}

// Run implements the TargetProvider interface.
func (s *Ingress) Run(ctx context.Context, ch chan<- []*config.TargetGroup) {
	// Send full initial set of pod targets.
	var initial []*config.TargetGroup
	for _, o := range s.store.List() {
		tg := s.buildIngress(o.(*v1beta1.Ingress))
		initial = append(initial, tg)
	}
	select {
	case <-ctx.Done():
		return
	case ch <- initial:
	}

	// Send target groups for ingress updates.
	send := func(tg *config.TargetGroup) {
		select {
		case <-ctx.Done():
		case ch <- []*config.TargetGroup{tg}:
		}
	}
	s.informer.AddEventHandler(cache.ResourceEventHandlerFuncs{
		AddFunc: func(o interface{}) {
			eventCount.WithLabelValues("ingress", "add").Inc()

			ingress, err := convertToIngress(o)
			if err != nil {
				s.logger.With("err", err).Errorln("converting to Ingress object failed")
				return
			}
			send(s.buildIngress(ingress))
		},
		DeleteFunc: func(o interface{}) {
			eventCount.WithLabelValues("ingress", "delete").Inc()

			ingress, err := convertToIngress(o)
			if err != nil {
				s.logger.With("err", err).Errorln("converting to Ingress object failed")
				return
			}
			send(&config.TargetGroup{Source: ingressSource(ingress)})
		},
		UpdateFunc: func(_, o interface{}) {
			eventCount.WithLabelValues("ingress", "update").Inc()

			ingress, err := convertToIngress(o)
			if err != nil {
				s.logger.With("err", err).Errorln("converting to Ingress object failed")
				return
			}
			send(s.buildIngress(ingress))
		},
	})

	// Block until the target provider is explicitly canceled.
	<-ctx.Done()
}

func convertToIngress(o interface{}) (*v1beta1.Ingress, error) {
	ingress, ok := o.(*v1beta1.Ingress)
	if ok {
		return ingress, nil
	}

	deletedState, ok := o.(cache.DeletedFinalStateUnknown)
	if !ok {
		return nil, fmt.Errorf("Received unexpected object: %v", o)
	}
	ingress, ok = deletedState.Obj.(*v1beta1.Ingress)
	if !ok {
		return nil, fmt.Errorf("DeletedFinalStateUnknown contained non-Ingress object: %v", deletedState.Obj)
	}
	return ingress, nil
}

func ingressSource(s *v1beta1.Ingress) string {
	return "ingress/" + s.Namespace + "/" + s.Name
}

const (
	ingressNameLabel        = metaLabelPrefix + "ingress_name"
	ingressLabelPrefix      = metaLabelPrefix + "ingress_label_"
	ingressAnnotationPrefix = metaLabelPrefix + "ingress_annotation_"
	ingressSchemeLabel      = metaLabelPrefix + "ingress_scheme"
	ingressHostLabel        = metaLabelPrefix + "ingress_host"
	ingressPathLabel        = metaLabelPrefix + "ingress_path"
)

func ingressLabels(ingress *v1beta1.Ingress) model.LabelSet {
	ls := make(model.LabelSet, len(ingress.Labels)+len(ingress.Annotations)+2)
	ls[ingressNameLabel] = lv(ingress.Name)
	ls[namespaceLabel] = lv(ingress.Namespace)

	for k, v := range ingress.Labels {
		ln := strutil.SanitizeLabelName(ingressLabelPrefix + k)
		ls[model.LabelName(ln)] = lv(v)
	}

	for k, v := range ingress.Annotations {
		ln := strutil.SanitizeLabelName(ingressAnnotationPrefix + k)
		ls[model.LabelName(ln)] = lv(v)
	}
	return ls
}

func pathsFromIngressRule(rv *v1beta1.IngressRuleValue) []string {
	if rv.HTTP == nil {
		return []string{"/"}
	}
	paths := make([]string, len(rv.HTTP.Paths))
	for n, p := range rv.HTTP.Paths {
		path := p.Path
		if path == "" {
			path = "/"
		}
		paths[n] = path
	}
	return paths
}

func (s *Ingress) buildIngress(ingress *v1beta1.Ingress) *config.TargetGroup {
	tg := &config.TargetGroup{
		Source: ingressSource(ingress),
	}
	tg.Labels = ingressLabels(ingress)

	schema := "http"
	if ingress.Spec.TLS != nil {
		schema = "https"
	}
	for _, rule := range ingress.Spec.Rules {
		paths := pathsFromIngressRule(&rule.IngressRuleValue)

		for _, path := range paths {
			tg.Targets = append(tg.Targets, model.LabelSet{
				model.AddressLabel: lv(rule.Host),
				ingressSchemeLabel: lv(schema),
				ingressHostLabel:   lv(rule.Host),
				ingressPathLabel:   lv(path),
			})
		}
	}

	return tg
}
