// 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 scaleway

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

	"github.com/prometheus/common/model"
	"github.com/prometheus/common/version"
	"github.com/scaleway/scaleway-sdk-go/api/instance/v1"
	ipam "github.com/scaleway/scaleway-sdk-go/api/ipam/v1"
	"github.com/scaleway/scaleway-sdk-go/scw"

	"github.com/prometheus/prometheus/discovery/refresh"
	"github.com/prometheus/prometheus/discovery/targetgroup"
)

const (
	instanceLabelPrefix = metaLabelPrefix + "instance_"

	instanceBootTypeLabel            = instanceLabelPrefix + "boot_type"
	instanceHostnameLabel            = instanceLabelPrefix + "hostname"
	instanceIDLabel                  = instanceLabelPrefix + "id"
	instanceImageArchLabel           = instanceLabelPrefix + "image_arch"
	instanceImageIDLabel             = instanceLabelPrefix + "image_id"
	instanceImageNameLabel           = instanceLabelPrefix + "image_name"
	instanceLocationClusterID        = instanceLabelPrefix + "location_cluster_id"
	instanceLocationHypervisorID     = instanceLabelPrefix + "location_hypervisor_id"
	instanceLocationNodeID           = instanceLabelPrefix + "location_node_id"
	instanceNameLabel                = instanceLabelPrefix + "name"
	instanceOrganizationLabel        = instanceLabelPrefix + "organization_id"
	instancePrivateIPv4Label         = instanceLabelPrefix + "private_ipv4"
	instanceProjectLabel             = instanceLabelPrefix + "project_id"
	instancePublicIPv4Label          = instanceLabelPrefix + "public_ipv4"
	instancePublicIPv6Label          = instanceLabelPrefix + "public_ipv6"
	instancePublicIPv4AddressesLabel = instanceLabelPrefix + "public_ipv4_addresses"
	instancePublicIPv6AddressesLabel = instanceLabelPrefix + "public_ipv6_addresses"
	instanceSecurityGroupIDLabel     = instanceLabelPrefix + "security_group_id"
	instanceSecurityGroupNameLabel   = instanceLabelPrefix + "security_group_name"
	instanceStateLabel               = instanceLabelPrefix + "status"
	instanceTagsLabel                = instanceLabelPrefix + "tags"
	instanceTypeLabel                = instanceLabelPrefix + "type"
	instanceZoneLabel                = instanceLabelPrefix + "zone"
	instanceRegionLabel              = instanceLabelPrefix + "region"
)

type instanceDiscovery struct {
	*refresh.Discovery
	client     *scw.Client
	port       int
	zone       string
	project    string
	accessKey  string
	secretKey  string
	nameFilter string
	tagsFilter []string
}

func newInstanceDiscovery(conf *SDConfig) (*instanceDiscovery, error) {
	d := &instanceDiscovery{
		port:       conf.Port,
		zone:       conf.Zone,
		project:    conf.Project,
		accessKey:  conf.AccessKey,
		secretKey:  conf.secretKeyForConfig(),
		nameFilter: conf.NameFilter,
		tagsFilter: conf.TagsFilter,
	}

	client, err := newScalewayHTTPClient(conf)
	if err != nil {
		return nil, err
	}

	profile, err := loadProfile(conf)
	if err != nil {
		return nil, err
	}

	d.client, err = scw.NewClient(
		scw.WithHTTPClient(client),
		scw.WithUserAgent(version.PrometheusUserAgent()),
		scw.WithProfile(profile),
	)
	if err != nil {
		return nil, fmt.Errorf("error setting up scaleway client: %w", err)
	}

	return d, nil
}

func (d *instanceDiscovery) refresh(ctx context.Context) ([]*targetgroup.Group, error) {
	instanceAPI := instance.NewAPI(d.client)
	ipamAPI := ipam.NewAPI(d.client)

	req := &instance.ListServersRequest{}

	if d.nameFilter != "" {
		req.Name = scw.StringPtr(d.nameFilter)
	}

	if d.tagsFilter != nil {
		req.Tags = d.tagsFilter
	}

	servers, err := instanceAPI.ListServers(req, scw.WithAllPages(), scw.WithContext(ctx))
	if err != nil {
		return nil, err
	}

	privateNICIPByID, err := privateNICIPs(ctx, ipamAPI, servers.Servers)
	if err != nil {
		return nil, err
	}

	var targets []model.LabelSet
	for _, server := range servers.Servers {
		labels := model.LabelSet{
			instanceBootTypeLabel:     model.LabelValue(server.BootType),
			instanceHostnameLabel:     model.LabelValue(server.Hostname),
			instanceIDLabel:           model.LabelValue(server.ID),
			instanceNameLabel:         model.LabelValue(server.Name),
			instanceOrganizationLabel: model.LabelValue(server.Organization),
			instanceProjectLabel:      model.LabelValue(server.Project),
			instanceStateLabel:        model.LabelValue(server.State),
			instanceTypeLabel:         model.LabelValue(server.CommercialType),
			instanceZoneLabel:         model.LabelValue(server.Zone.String()),
		}

		if server.Image != nil {
			labels[instanceImageArchLabel] = model.LabelValue(server.Image.Arch)
			labels[instanceImageIDLabel] = model.LabelValue(server.Image.ID)
			labels[instanceImageNameLabel] = model.LabelValue(server.Image.Name)
		}

		if server.Location != nil {
			labels[instanceLocationClusterID] = model.LabelValue(server.Location.ClusterID)
			labels[instanceLocationHypervisorID] = model.LabelValue(server.Location.HypervisorID)
			labels[instanceLocationNodeID] = model.LabelValue(server.Location.NodeID)
		}

		if server.SecurityGroup != nil {
			labels[instanceSecurityGroupIDLabel] = model.LabelValue(server.SecurityGroup.ID)
			labels[instanceSecurityGroupNameLabel] = model.LabelValue(server.SecurityGroup.Name)
		}

		if region, err := server.Zone.Region(); err == nil {
			labels[instanceRegionLabel] = model.LabelValue(region.String())
		}

		if len(server.Tags) > 0 {
			// We surround the separated list with the separator as well. This way regular expressions
			// in relabeling rules don't have to consider tag positions.
			tags := separator + strings.Join(server.Tags, separator) + separator
			labels[instanceTagsLabel] = model.LabelValue(tags)
		}

		addr := ""
		if len(server.PublicIPs) > 0 {
			var ipv4Addresses []string
			var ipv6Addresses []string

			for _, ip := range server.PublicIPs {
				switch ip.Family {
				case instance.ServerIPIPFamilyInet:
					ipv4Addresses = append(ipv4Addresses, ip.Address.String())
				case instance.ServerIPIPFamilyInet6:
					ipv6Addresses = append(ipv6Addresses, ip.Address.String())
				}
			}

			if len(ipv6Addresses) > 0 {
				labels[instancePublicIPv6AddressesLabel] = model.LabelValue(
					separator +
						strings.Join(ipv6Addresses, separator) +
						separator)
			}
			if len(ipv4Addresses) > 0 {
				labels[instancePublicIPv4AddressesLabel] = model.LabelValue(
					separator +
						strings.Join(ipv4Addresses, separator) +
						separator)
			}
		}

		if server.IPv6 != nil { //nolint:staticcheck
			labels[instancePublicIPv6Label] = model.LabelValue(server.IPv6.Address.String()) //nolint:staticcheck
			addr = server.IPv6.Address.String()                                              //nolint:staticcheck
		}

		if server.PublicIP != nil { //nolint:staticcheck
			if server.PublicIP.Family != instance.ServerIPIPFamilyInet6 { //nolint:staticcheck
				labels[instancePublicIPv4Label] = model.LabelValue(server.PublicIP.Address.String()) //nolint:staticcheck
			}
			addr = server.PublicIP.Address.String() //nolint:staticcheck
		}

		if server.PrivateIP != nil {
			labels[instancePrivateIPv4Label] = model.LabelValue(*server.PrivateIP)
			addr = *server.PrivateIP
		}

		if addr == "" {
			for _, privateNIC := range server.PrivateNics {
				if privateNIC == nil {
					continue
				}
				if privateIP := privateNICIPByID[privateNIC.ID]; privateIP != "" {
					labels[instancePrivateIPv4Label] = model.LabelValue(privateIP)
					addr = privateIP
					break
				}
			}
		}

		if addr != "" {
			addr := net.JoinHostPort(addr, strconv.FormatUint(uint64(d.port), 10))
			labels[model.AddressLabel] = model.LabelValue(addr)
			targets = append(targets, labels)
		}
	}

	return []*targetgroup.Group{{Source: "scaleway", Targets: targets}}, nil
}

func privateNICIPs(ctx context.Context, api *ipam.API, servers []*instance.Server) (map[string]string, error) {
	privateNICIDsByRegion := map[scw.Region][]string{}
	for _, server := range servers {
		if server.PrivateIP != nil || server.PublicIP != nil || server.IPv6 != nil { //nolint:staticcheck
			continue
		}

		region, err := server.Zone.Region()
		if err != nil {
			return nil, err
		}

		for _, privateNIC := range server.PrivateNics {
			if privateNIC != nil && privateNIC.ID != "" {
				privateNICIDsByRegion[region] = append(privateNICIDsByRegion[region], privateNIC.ID)
			}
		}
	}

	privateNICIPs := map[string]string{}
	for region, privateNICIDs := range privateNICIDsByRegion {
		ips, err := api.ListIPs(&ipam.ListIPsRequest{
			Region:        region,
			ResourceIDs:   privateNICIDs,
			ResourceTypes: []ipam.ResourceType{ipam.ResourceTypeInstancePrivateNic},
		}, scw.WithAllPages(), scw.WithContext(ctx))
		if err != nil {
			return nil, err
		}

		for _, ip := range ips.IPs {
			if ip != nil && ip.Resource != nil && !ip.IsIPv6 && ip.Address.IP != nil {
				privateNICIPs[ip.Resource.ID] = ip.Address.IP.String()
			}
		}
	}

	return privateNICIPs, nil
}
