// Copyright 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 moby

import (
	"context"
	"fmt"
	"net"
	"strconv"

	"github.com/moby/moby/api/types/network"
	"github.com/moby/moby/api/types/swarm"
	"github.com/moby/moby/client"
	"github.com/prometheus/common/model"

	"github.com/prometheus/prometheus/discovery/targetgroup"
	"github.com/prometheus/prometheus/util/strutil"
)

const (
	swarmLabelServicePrefix                  = swarmLabel + "service_"
	swarmLabelServiceEndpointPortName        = swarmLabelServicePrefix + "endpoint_port_name"
	swarmLabelServiceEndpointPortPublishMode = swarmLabelServicePrefix + "endpoint_port_publish_mode"
	swarmLabelServiceID                      = swarmLabelServicePrefix + "id"
	swarmLabelServiceLabelPrefix             = swarmLabelServicePrefix + "label_"
	swarmLabelServiceName                    = swarmLabelServicePrefix + "name"
	swarmLabelServiceMode                    = swarmLabelServicePrefix + "mode"
	swarmLabelServiceUpdatingStatus          = swarmLabelServicePrefix + "updating_status"
	swarmLabelServiceTaskPrefix              = swarmLabelServicePrefix + "task_"
	swarmLabelServiceTaskContainerImage      = swarmLabelServiceTaskPrefix + "container_image"
	swarmLabelServiceTaskContainerHostname   = swarmLabelServiceTaskPrefix + "container_hostname"
)

func (d *Discovery) refreshServices(ctx context.Context) ([]*targetgroup.Group, error) {
	tg := &targetgroup.Group{
		Source: "DockerSwarm",
	}

	services, err := d.client.ServiceList(ctx, client.ServiceListOptions{Filters: d.filters})
	if err != nil {
		return nil, fmt.Errorf("error while listing swarm services: %w", err)
	}

	networkLabels, err := getNetworksLabels(ctx, d.client, swarmLabel)
	if err != nil {
		return nil, fmt.Errorf("error while computing swarm network labels: %w", err)
	}

	for _, s := range services.Items {
		commonLabels := map[string]string{
			swarmLabelServiceID:   s.ID,
			swarmLabelServiceName: s.Spec.Name,
		}
		commonLabels[swarmLabelServiceMode] = getServiceValueMode(s)

		// ContainerSpec is only set for services whose task runtime is a
		// container. It is nil for plugin and network-attachment services.
		if s.Spec.TaskTemplate.ContainerSpec != nil {
			commonLabels[swarmLabelServiceTaskContainerHostname] = s.Spec.TaskTemplate.ContainerSpec.Hostname
			commonLabels[swarmLabelServiceTaskContainerImage] = s.Spec.TaskTemplate.ContainerSpec.Image
		}

		if s.UpdateStatus != nil {
			commonLabels[swarmLabelServiceUpdatingStatus] = string(s.UpdateStatus.State)
		}

		for k, v := range s.Spec.Labels {
			ln := strutil.SanitizeLabelName(k)
			commonLabels[swarmLabelServiceLabelPrefix+ln] = v
		}

		for _, p := range s.Endpoint.VirtualIPs {
			var added bool
			ip, _, err := net.ParseCIDR(p.Addr.String())
			if err != nil {
				return nil, fmt.Errorf("error while parsing address %s: %w", p.Addr, err)
			}

			for _, e := range s.Endpoint.Ports {
				if e.Protocol != network.TCP {
					continue
				}
				labels := model.LabelSet{
					swarmLabelServiceEndpointPortName:        model.LabelValue(e.Name),
					swarmLabelServiceEndpointPortPublishMode: model.LabelValue(e.PublishMode),
				}

				for k, v := range commonLabels {
					labels[model.LabelName(k)] = model.LabelValue(v)
				}

				for k, v := range networkLabels[p.NetworkID] {
					labels[model.LabelName(k)] = model.LabelValue(v)
				}

				addr := net.JoinHostPort(ip.String(), strconv.FormatUint(uint64(e.PublishedPort), 10))
				labels[model.AddressLabel] = model.LabelValue(addr)

				tg.Targets = append(tg.Targets, labels)
				added = true
			}

			if !added {
				labels := model.LabelSet{}

				for k, v := range commonLabels {
					labels[model.LabelName(k)] = model.LabelValue(v)
				}

				for k, v := range networkLabels[p.NetworkID] {
					labels[model.LabelName(k)] = model.LabelValue(v)
				}

				addr := net.JoinHostPort(ip.String(), strconv.Itoa(d.port))
				labels[model.AddressLabel] = model.LabelValue(addr)

				tg.Targets = append(tg.Targets, labels)
			}
		}
	}
	return []*targetgroup.Group{tg}, nil
}

func (d *Discovery) getServicesLabelsAndPorts(ctx context.Context) (map[string]map[string]string, map[string][]swarm.PortConfig, error) {
	services, err := d.client.ServiceList(ctx, client.ServiceListOptions{})
	if err != nil {
		return nil, nil, err
	}
	servicesLabels := make(map[string]map[string]string, len(services.Items))
	servicesPorts := make(map[string][]swarm.PortConfig, len(services.Items))
	for _, s := range services.Items {
		servicesLabels[s.ID] = map[string]string{
			swarmLabelServiceID:   s.ID,
			swarmLabelServiceName: s.Spec.Name,
		}

		servicesLabels[s.ID][swarmLabelServiceMode] = getServiceValueMode(s)

		for k, v := range s.Spec.Labels {
			ln := strutil.SanitizeLabelName(k)
			servicesLabels[s.ID][swarmLabelServiceLabelPrefix+ln] = v
		}
		servicesPorts[s.ID] = s.Endpoint.Ports
	}

	return servicesLabels, servicesPorts, nil
}

func getServiceValueMode(s swarm.Service) string {
	if s.Spec.Mode.Global != nil {
		return "global"
	}
	if s.Spec.Mode.Replicated != nil {
		return "replicated"
	}
	return ""
}
