// Copyright 2020 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.

//go:build !nonetworkroute
// +build !nonetworkroute

package collector

import (
	"fmt"
	"log/slog"
	"net"
	"strconv"

	"github.com/jsimonetti/rtnetlink/v2"
	"github.com/prometheus/client_golang/prometheus"
	"golang.org/x/sys/unix"
)

type networkRouteCollector struct {
	routeInfoDesc *prometheus.Desc
	routesDesc    *prometheus.Desc
	logger        *slog.Logger
}

func init() {
	registerCollector("network_route", defaultDisabled, NewNetworkRouteCollector)
}

// NewNetworkRouteCollector returns a new Collector exposing systemd statistics.
func NewNetworkRouteCollector(logger *slog.Logger) (Collector, error) {
	const subsystem = "network"

	routeInfoDesc := prometheus.NewDesc(
		prometheus.BuildFQName(namespace, subsystem, "route_info"),
		"network routing table information", []string{"device", "src", "dest", "gw", "priority", "proto", "weight"}, nil,
	)
	routesDesc := prometheus.NewDesc(
		prometheus.BuildFQName(namespace, subsystem, "routes"),
		"network routes by interface", []string{"device"}, nil,
	)

	return &networkRouteCollector{
		routeInfoDesc: routeInfoDesc,
		routesDesc:    routesDesc,
		logger:        logger,
	}, nil
}

func (n networkRouteCollector) Update(ch chan<- prometheus.Metric) error {
	deviceRoutes := make(map[string]int)

	conn, err := rtnetlink.Dial(nil)
	if err != nil {
		return fmt.Errorf("couldn't connect rtnetlink: %w", err)
	}
	defer conn.Close()

	links, err := conn.Link.List()
	if err != nil {
		return fmt.Errorf("couldn't get links: %w", err)
	}

	routes, err := conn.Route.List()
	if err != nil {
		return fmt.Errorf("couldn't get routes: %w", err)
	}

	for _, route := range routes {
		if route.Type != unix.RTA_DST {
			continue
		}
		if len(route.Attributes.Multipath) != 0 {
			for _, nextHop := range route.Attributes.Multipath {
				ifName := ""
				for _, link := range links {
					if link.Index == nextHop.Hop.IfIndex {
						ifName = link.Attributes.Name
						break
					}
				}

				labels := []string{
					ifName, // if
					networkRouteIPToString(route.Attributes.Src),                            // src
					networkRouteIPWithPrefixToString(route.Attributes.Dst, route.DstLength), // dest
					networkRouteIPToString(nextHop.Gateway),                                 // gw
					strconv.FormatUint(uint64(route.Attributes.Priority), 10),               // priority(metrics)
					networkRouteProtocolToString(route.Protocol),                            // proto
					strconv.Itoa(int(nextHop.Hop.Hops) + 1),                                 // weight
				}
				ch <- prometheus.MustNewConstMetric(n.routeInfoDesc, prometheus.GaugeValue, 1, labels...)
				deviceRoutes[ifName]++
			}
		} else {
			ifName := ""
			for _, link := range links {
				if link.Index == route.Attributes.OutIface {
					ifName = link.Attributes.Name
					break
				}
			}

			labels := []string{
				ifName, // if
				networkRouteIPToString(route.Attributes.Src),                            // src
				networkRouteIPWithPrefixToString(route.Attributes.Dst, route.DstLength), // dest
				networkRouteIPToString(route.Attributes.Gateway),                        // gw
				strconv.FormatUint(uint64(route.Attributes.Priority), 10),               // priority(metrics)
				networkRouteProtocolToString(route.Protocol),                            // proto
				"", // weight
			}
			ch <- prometheus.MustNewConstMetric(n.routeInfoDesc, prometheus.GaugeValue, 1, labels...)
			deviceRoutes[ifName]++
		}
	}

	for dev, total := range deviceRoutes {
		ch <- prometheus.MustNewConstMetric(n.routesDesc, prometheus.GaugeValue, float64(total), dev)
	}

	return nil
}

func networkRouteIPWithPrefixToString(ip net.IP, len uint8) string {
	if len == 0 {
		return "default"
	}
	iplen := net.IPv4len
	if ip.To4() == nil {
		iplen = net.IPv6len
	}
	network := &net.IPNet{
		IP:   ip,
		Mask: net.CIDRMask(int(len), iplen*8),
	}
	return network.String()
}

func networkRouteIPToString(ip net.IP) string {
	if len(ip) == 0 {
		return ""
	}
	return ip.String()
}

func networkRouteProtocolToString(protocol uint8) string {
	// from linux kernel 'include/uapi/linux/rtnetlink.h'
	switch protocol {
	case 0:
		return "unspec"
	case 1:
		return "redirect"
	case 2:
		return "kernel"
	case 3:
		return "boot"
	case 4:
		return "static"
	case 8:
		return "gated"
	case 9:
		return "ra"
	case 10:
		return "mrt"
	case 11:
		return "zebra"
	case 12:
		return "bird"
	case 13:
		return "dnrouted"
	case 14:
		return "xorp"
	case 15:
		return "ntk"
	case 16:
		return "dhcp"
	case 17:
		return "mrouted"
	case 42:
		return "babel"
	case 186:
		return "bgp"
	case 187:
		return "isis"
	case 188:
		return "ospf"
	case 189:
		return "rip"
	case 192:
		return "eigrp"
	}
	return "unknown"
}
