// SPDX-License-Identifier: Apache-2.0
//
// 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.

//go:build windows

package dhcp

import (
	"errors"
	"fmt"
	"log/slog"
	"slices"
	"strconv"
	"strings"
	"time"

	"github.com/alecthomas/kingpin/v2"
	"github.com/prometheus-community/windows_exporter/internal/headers/dhcpsapi"
	"github.com/prometheus-community/windows_exporter/internal/mi"
	"github.com/prometheus-community/windows_exporter/internal/pdh"
	"github.com/prometheus-community/windows_exporter/internal/types"
	"github.com/prometheus/client_golang/prometheus"
)

const (
	Name = "dhcp"

	subCollectorServerMetrics = "server_metrics"
	subCollectorScopeMetrics  = "scope_metrics"
)

type Config struct {
	CollectorsEnabled []string `yaml:"enabled"`
}

//nolint:gochecknoglobals
var ConfigDefaults = Config{
	CollectorsEnabled: []string{
		subCollectorServerMetrics,
		subCollectorScopeMetrics,
	},
}

// A Collector is a Prometheus Collector perflib DHCP metrics.
type Collector struct {
	config Config

	logger *slog.Logger

	perfDataCollector *pdh.Collector
	perfDataObject    []perfDataCounterValues

	acksTotal                                        *prometheus.Desc
	activeQueueLength                                *prometheus.Desc
	conflictCheckQueueLength                         *prometheus.Desc
	declinesTotal                                    *prometheus.Desc
	deniedDueToMatch                                 *prometheus.Desc
	deniedDueToNonMatch                              *prometheus.Desc
	discoversTotal                                   *prometheus.Desc
	duplicatesDroppedTotal                           *prometheus.Desc
	failoverBndAckReceivedTotal                      *prometheus.Desc
	failoverBndAckSentTotal                          *prometheus.Desc
	failoverBndUpdDropped                            *prometheus.Desc
	failoverBndUpdPendingOutboundQueue               *prometheus.Desc
	failoverBndUpdReceivedTotal                      *prometheus.Desc
	failoverBndUpdSentTotal                          *prometheus.Desc
	failoverTransitionsCommunicationInterruptedState *prometheus.Desc
	failoverTransitionsPartnerDownState              *prometheus.Desc
	failoverTransitionsRecoverState                  *prometheus.Desc
	informsTotal                                     *prometheus.Desc
	nACKsTotal                                       *prometheus.Desc
	offerQueueLength                                 *prometheus.Desc
	offersTotal                                      *prometheus.Desc
	packetsExpiredTotal                              *prometheus.Desc
	packetsReceivedTotal                             *prometheus.Desc
	releasesTotal                                    *prometheus.Desc
	requestsTotal                                    *prometheus.Desc

	scopeInfo                               *prometheus.Desc
	scopeState                              *prometheus.Desc
	scopeAddressesFreeTotal                 *prometheus.Desc
	scopeAddressesFreeOnPartnerServerTotal  *prometheus.Desc
	scopeAddressesFreeOnThisServerTotal     *prometheus.Desc
	scopeAddressesInUseTotal                *prometheus.Desc
	scopeAddressesInUseOnPartnerServerTotal *prometheus.Desc
	scopeAddressesInUseOnThisServerTotal    *prometheus.Desc
	scopePendingOffersTotal                 *prometheus.Desc
	scopeReservedAddressTotal               *prometheus.Desc
}

func New(config *Config) *Collector {
	if config == nil {
		config = &ConfigDefaults
	}

	if config.CollectorsEnabled == nil {
		config.CollectorsEnabled = ConfigDefaults.CollectorsEnabled
	}

	c := &Collector{
		config: *config,
	}

	return c
}

func NewWithFlags(app *kingpin.Application) *Collector {
	c := &Collector{
		config: ConfigDefaults,
	}
	c.config.CollectorsEnabled = make([]string, 0)

	var collectorsEnabled string

	app.Flag(
		"collector.dhcp.enabled",
		"Comma-separated list of collectors to use. Defaults to all, if not specified.",
	).Default(strings.Join(ConfigDefaults.CollectorsEnabled, ",")).StringVar(&collectorsEnabled)

	app.Action(func(*kingpin.ParseContext) error {
		c.config.CollectorsEnabled = strings.Split(collectorsEnabled, ",")

		return nil
	})

	return c
}

func (c *Collector) GetName() string {
	return Name
}

func (c *Collector) Close() error {
	if slices.Contains(c.config.CollectorsEnabled, subCollectorServerMetrics) {
		c.perfDataCollector.Close()
	}

	return nil
}

func (c *Collector) Build(logger *slog.Logger, _ *mi.Session) error {
	c.logger = logger.With(slog.String("collector", Name))

	var err error

	if slices.Contains(c.config.CollectorsEnabled, subCollectorScopeMetrics) {
		c.scopeInfo = prometheus.NewDesc(
			prometheus.BuildFQName(types.Namespace, Name, "scope_info"),
			"DHCP Scope information",
			[]string{"name", "superscope_name", "superscope_id", "scope"},
			nil,
		)

		c.scopeState = prometheus.NewDesc(
			prometheus.BuildFQName(types.Namespace, Name, "scope_state"),
			"DHCP Scope state",
			[]string{"scope", "state"},
			nil,
		)

		c.scopeAddressesFreeTotal = prometheus.NewDesc(
			prometheus.BuildFQName(types.Namespace, Name, "scope_addresses_free"),
			"DHCP Scope free addresses",
			[]string{"scope"},
			nil,
		)

		c.scopeAddressesFreeOnPartnerServerTotal = prometheus.NewDesc(
			prometheus.BuildFQName(types.Namespace, Name, "scope_addresses_free_on_partner_server"),
			"DHCP Scope free addresses on partner server",
			[]string{"scope"},
			nil,
		)

		c.scopeAddressesFreeOnThisServerTotal = prometheus.NewDesc(
			prometheus.BuildFQName(types.Namespace, Name, "scope_addresses_free_on_this_server"),
			"DHCP Scope free addresses on this server",
			[]string{"scope"},
			nil,
		)

		c.scopeAddressesInUseTotal = prometheus.NewDesc(
			prometheus.BuildFQName(types.Namespace, Name, "scope_addresses_in_use"),
			"DHCP Scope addresses in use",
			[]string{"scope"},
			nil,
		)

		c.scopeAddressesInUseOnPartnerServerTotal = prometheus.NewDesc(
			prometheus.BuildFQName(types.Namespace, Name, "scope_addresses_in_use_on_partner_server"),
			"DHCP Scope addresses in use on partner server",
			[]string{"scope"},
			nil,
		)

		c.scopeAddressesInUseOnThisServerTotal = prometheus.NewDesc(
			prometheus.BuildFQName(types.Namespace, Name, "scope_addresses_in_use_on_this_server"),
			"DHCP Scope addresses in use on this server",
			[]string{"scope"},
			nil,
		)

		c.scopePendingOffersTotal = prometheus.NewDesc(
			prometheus.BuildFQName(types.Namespace, Name, "scope_pending_offers"),
			"DHCP Scope pending offers",
			[]string{"scope"},
			nil,
		)

		c.scopeReservedAddressTotal = prometheus.NewDesc(
			prometheus.BuildFQName(types.Namespace, Name, "scope_reserved_address"),
			"DHCP Scope reserved addresses",
			[]string{"scope"},
			nil,
		)
	}

	if slices.Contains(c.config.CollectorsEnabled, subCollectorServerMetrics) {
		c.packetsReceivedTotal = prometheus.NewDesc(
			prometheus.BuildFQName(types.Namespace, Name, "packets_received_total"),
			"Total number of packets received by the DHCP server (PacketsReceivedTotal)",
			nil,
			nil,
		)
		c.duplicatesDroppedTotal = prometheus.NewDesc(
			prometheus.BuildFQName(types.Namespace, Name, "duplicates_dropped_total"),
			"Total number of duplicate packets received by the DHCP server (DuplicatesDroppedTotal)",
			nil,
			nil,
		)
		c.packetsExpiredTotal = prometheus.NewDesc(
			prometheus.BuildFQName(types.Namespace, Name, "packets_expired_total"),
			"Total number of packets expired in the DHCP server message queue (PacketsExpiredTotal)",
			nil,
			nil,
		)
		c.activeQueueLength = prometheus.NewDesc(
			prometheus.BuildFQName(types.Namespace, Name, "active_queue_length"),
			"Number of packets in the processing queue of the DHCP server (ActiveQueueLength)",
			nil,
			nil,
		)
		c.conflictCheckQueueLength = prometheus.NewDesc(
			prometheus.BuildFQName(types.Namespace, Name, "conflict_check_queue_length"),
			"Number of packets in the DHCP server queue waiting on conflict detection (ping). (ConflictCheckQueueLength)",
			nil,
			nil,
		)
		c.discoversTotal = prometheus.NewDesc(
			prometheus.BuildFQName(types.Namespace, Name, "discovers_total"),
			"Total DHCP Discovers received by the DHCP server (DiscoversTotal)",
			nil,
			nil,
		)
		c.offersTotal = prometheus.NewDesc(
			prometheus.BuildFQName(types.Namespace, Name, "offers_total"),
			"Total DHCP Offers sent by the DHCP server (OffersTotal)",
			nil,
			nil,
		)
		c.requestsTotal = prometheus.NewDesc(
			prometheus.BuildFQName(types.Namespace, Name, "requests_total"),
			"Total DHCP Requests received by the DHCP server (RequestsTotal)",
			nil,
			nil,
		)
		c.informsTotal = prometheus.NewDesc(
			prometheus.BuildFQName(types.Namespace, Name, "informs_total"),
			"Total DHCP Informs received by the DHCP server (InformsTotal)",
			nil,
			nil,
		)
		c.acksTotal = prometheus.NewDesc(
			prometheus.BuildFQName(types.Namespace, Name, "acks_total"),
			"Total DHCP Acks sent by the DHCP server (AcksTotal)",
			nil,
			nil,
		)
		c.nACKsTotal = prometheus.NewDesc(
			prometheus.BuildFQName(types.Namespace, Name, "nacks_total"),
			"Total DHCP Nacks sent by the DHCP server (NacksTotal)",
			nil,
			nil,
		)
		c.declinesTotal = prometheus.NewDesc(
			prometheus.BuildFQName(types.Namespace, Name, "declines_total"),
			"Total DHCP Declines received by the DHCP server (DeclinesTotal)",
			nil,
			nil,
		)
		c.releasesTotal = prometheus.NewDesc(
			prometheus.BuildFQName(types.Namespace, Name, "releases_total"),
			"Total DHCP Releases received by the DHCP server (ReleasesTotal)",
			nil,
			nil,
		)
		c.offerQueueLength = prometheus.NewDesc(
			prometheus.BuildFQName(types.Namespace, Name, "offer_queue_length"),
			"Number of packets in the offer queue of the DHCP server (OfferQueueLength)",
			nil,
			nil,
		)
		c.deniedDueToMatch = prometheus.NewDesc(
			prometheus.BuildFQName(types.Namespace, Name, "denied_due_to_match_total"),
			"Total number of DHCP requests denied, based on matches from the Deny list (DeniedDueToMatch)",
			nil,
			nil,
		)
		c.deniedDueToNonMatch = prometheus.NewDesc(
			prometheus.BuildFQName(types.Namespace, Name, "denied_due_to_nonmatch_total"),
			"Total number of DHCP requests denied, based on non-matches from the Allow list (DeniedDueToNonMatch)",
			nil,
			nil,
		)
		c.failoverBndUpdSentTotal = prometheus.NewDesc(
			prometheus.BuildFQName(types.Namespace, Name, "failover_bndupd_sent_total"),
			"Number of DHCP fail over Binding Update messages sent (FailoverBndupdSentTotal)",
			nil,
			nil,
		)
		c.failoverBndUpdReceivedTotal = prometheus.NewDesc(
			prometheus.BuildFQName(types.Namespace, Name, "failover_bndupd_received_total"),
			"Number of DHCP fail over Binding Update messages received (FailoverBndupdReceivedTotal)",
			nil,
			nil,
		)
		c.failoverBndAckSentTotal = prometheus.NewDesc(
			prometheus.BuildFQName(types.Namespace, Name, "failover_bndack_sent_total"),
			"Number of DHCP fail over Binding Ack messages sent (FailoverBndackSentTotal)",
			nil,
			nil,
		)
		c.failoverBndAckReceivedTotal = prometheus.NewDesc(
			prometheus.BuildFQName(types.Namespace, Name, "failover_bndack_received_total"),
			"Number of DHCP fail over Binding Ack messages received (FailoverBndackReceivedTotal)",
			nil,
			nil,
		)
		c.failoverBndUpdPendingOutboundQueue = prometheus.NewDesc(
			prometheus.BuildFQName(types.Namespace, Name, "failover_bndupd_pending_in_outbound_queue"),
			"Number of pending outbound DHCP fail over Binding Update messages (FailoverBndupdPendingOutboundQueue)",
			nil,
			nil,
		)
		c.failoverTransitionsCommunicationInterruptedState = prometheus.NewDesc(
			prometheus.BuildFQName(types.Namespace, Name, "failover_transitions_communicationinterrupted_state_total"),
			"Total number of transitions into COMMUNICATION INTERRUPTED state (FailoverTransitionsCommunicationinterruptedState)",
			nil,
			nil,
		)
		c.failoverTransitionsPartnerDownState = prometheus.NewDesc(
			prometheus.BuildFQName(types.Namespace, Name, "failover_transitions_partnerdown_state_total"),
			"Total number of transitions into PARTNER DOWN state (FailoverTransitionsPartnerdownState)",
			nil,
			nil,
		)
		c.failoverTransitionsRecoverState = prometheus.NewDesc(
			prometheus.BuildFQName(types.Namespace, Name, "failover_transitions_recover_total"),
			"Total number of transitions into RECOVER state (FailoverTransitionsRecoverState)",
			nil,
			nil,
		)
		c.failoverBndUpdDropped = prometheus.NewDesc(
			prometheus.BuildFQName(types.Namespace, Name, "failover_bndupd_dropped_total"),
			"Total number of DHCP fail over Binding Updates dropped (FailoverBndupdDropped)",
			nil,
			nil,
		)

		c.perfDataCollector, err = pdh.NewCollector[perfDataCounterValues](c.logger, pdh.CounterTypeRaw, "DHCP Server", nil)
		if err != nil {
			return fmt.Errorf("failed to create DHCP Server collector: %w", err)
		}
	}

	return nil
}

func (c *Collector) Collect(ch chan<- prometheus.Metric, _ time.Duration) error {
	var errs []error

	if slices.Contains(c.config.CollectorsEnabled, subCollectorServerMetrics) {
		if err := c.collectServerMetrics(ch); err != nil {
			errs = append(errs, err)
		}
	}

	if slices.Contains(c.config.CollectorsEnabled, subCollectorScopeMetrics) {
		if err := c.collectScopeMetrics(ch); err != nil {
			errs = append(errs, err)
		}
	}

	return errors.Join(errs...)
}

func (c *Collector) collectServerMetrics(ch chan<- prometheus.Metric) error {
	err := c.perfDataCollector.Collect(&c.perfDataObject)
	if err != nil {
		return fmt.Errorf("failed to collect DHCP Server metrics: %w", err)
	} else if len(c.perfDataObject) == 0 {
		return fmt.Errorf("failed to collect DHCP Server metrics: %w", types.ErrNoDataUnexpected)
	}

	ch <- prometheus.MustNewConstMetric(
		c.packetsReceivedTotal,
		prometheus.CounterValue,
		c.perfDataObject[0].PacketsReceivedTotal,
	)

	ch <- prometheus.MustNewConstMetric(
		c.duplicatesDroppedTotal,
		prometheus.CounterValue,
		c.perfDataObject[0].DuplicatesDroppedTotal,
	)

	ch <- prometheus.MustNewConstMetric(
		c.packetsExpiredTotal,
		prometheus.CounterValue,
		c.perfDataObject[0].PacketsExpiredTotal,
	)

	ch <- prometheus.MustNewConstMetric(
		c.activeQueueLength,
		prometheus.GaugeValue,
		c.perfDataObject[0].ActiveQueueLength,
	)

	ch <- prometheus.MustNewConstMetric(
		c.conflictCheckQueueLength,
		prometheus.GaugeValue,
		c.perfDataObject[0].ConflictCheckQueueLength,
	)

	ch <- prometheus.MustNewConstMetric(
		c.discoversTotal,
		prometheus.CounterValue,
		c.perfDataObject[0].DiscoversTotal,
	)

	ch <- prometheus.MustNewConstMetric(
		c.offersTotal,
		prometheus.CounterValue,
		c.perfDataObject[0].OffersTotal,
	)

	ch <- prometheus.MustNewConstMetric(
		c.requestsTotal,
		prometheus.CounterValue,
		c.perfDataObject[0].RequestsTotal,
	)

	ch <- prometheus.MustNewConstMetric(
		c.informsTotal,
		prometheus.CounterValue,
		c.perfDataObject[0].InformsTotal,
	)

	ch <- prometheus.MustNewConstMetric(
		c.acksTotal,
		prometheus.CounterValue,
		c.perfDataObject[0].AcksTotal,
	)

	ch <- prometheus.MustNewConstMetric(
		c.nACKsTotal,
		prometheus.CounterValue,
		c.perfDataObject[0].NacksTotal,
	)

	ch <- prometheus.MustNewConstMetric(
		c.declinesTotal,
		prometheus.CounterValue,
		c.perfDataObject[0].DeclinesTotal,
	)

	ch <- prometheus.MustNewConstMetric(
		c.releasesTotal,
		prometheus.CounterValue,
		c.perfDataObject[0].ReleasesTotal,
	)

	ch <- prometheus.MustNewConstMetric(
		c.offerQueueLength,
		prometheus.GaugeValue,
		c.perfDataObject[0].OfferQueueLength,
	)

	ch <- prometheus.MustNewConstMetric(
		c.deniedDueToMatch,
		prometheus.CounterValue,
		c.perfDataObject[0].DeniedDueToMatch,
	)

	ch <- prometheus.MustNewConstMetric(
		c.deniedDueToNonMatch,
		prometheus.CounterValue,
		c.perfDataObject[0].DeniedDueToNonMatch,
	)

	ch <- prometheus.MustNewConstMetric(
		c.failoverBndUpdSentTotal,
		prometheus.CounterValue,
		c.perfDataObject[0].FailoverBndUpdSentTotal,
	)

	ch <- prometheus.MustNewConstMetric(
		c.failoverBndUpdReceivedTotal,
		prometheus.CounterValue,
		c.perfDataObject[0].FailoverBndUpdReceivedTotal,
	)

	ch <- prometheus.MustNewConstMetric(
		c.failoverBndAckSentTotal,
		prometheus.CounterValue,
		c.perfDataObject[0].FailoverBndAckSentTotal,
	)

	ch <- prometheus.MustNewConstMetric(
		c.failoverBndAckReceivedTotal,
		prometheus.CounterValue,
		c.perfDataObject[0].FailoverBndAckReceivedTotal,
	)

	ch <- prometheus.MustNewConstMetric(
		c.failoverBndUpdPendingOutboundQueue,
		prometheus.GaugeValue,
		c.perfDataObject[0].FailoverBndUpdPendingOutboundQueue,
	)

	ch <- prometheus.MustNewConstMetric(
		c.failoverTransitionsCommunicationInterruptedState,
		prometheus.CounterValue,
		c.perfDataObject[0].FailoverTransitionsCommunicationInterruptedState,
	)

	ch <- prometheus.MustNewConstMetric(
		c.failoverTransitionsPartnerDownState,
		prometheus.CounterValue,
		c.perfDataObject[0].FailoverTransitionsPartnerDownState,
	)

	ch <- prometheus.MustNewConstMetric(
		c.failoverTransitionsRecoverState,
		prometheus.CounterValue,
		c.perfDataObject[0].FailoverTransitionsRecoverState,
	)

	ch <- prometheus.MustNewConstMetric(
		c.failoverBndUpdDropped,
		prometheus.CounterValue,
		c.perfDataObject[0].FailoverBndUpdDropped,
	)

	return nil
}

func (c *Collector) collectScopeMetrics(ch chan<- prometheus.Metric) error {
	dhcpScopes, err := dhcpsapi.GetDHCPV4ScopeStatistics()
	if err != nil {
		return fmt.Errorf("failed to get DHCP scopes: %w", err)
	}

	for _, scope := range dhcpScopes {
		scopeID := scope.ScopeIPAddress.String()

		ch <- prometheus.MustNewConstMetric(
			c.scopeInfo,
			prometheus.GaugeValue,
			1,
			scope.Name,
			scope.SuperScopeName,
			strconv.Itoa(int(scope.SuperScopeNumber)),
			scopeID,
		)

		for state, name := range dhcpsapi.DHCP_SUBNET_STATE_NAMES {
			metric := 0.0
			if state == scope.State {
				metric = 1.0
			}

			ch <- prometheus.MustNewConstMetric(
				c.scopeState,
				prometheus.GaugeValue,
				metric,
				scopeID,
				name,
			)
		}

		if scope.AddressesFree != -1 {
			ch <- prometheus.MustNewConstMetric(
				c.scopeAddressesFreeTotal,
				prometheus.GaugeValue,
				scope.AddressesFree,
				scopeID,
			)
		}

		if scope.AddressesFreeOnPartnerServer != -1 {
			ch <- prometheus.MustNewConstMetric(
				c.scopeAddressesFreeOnPartnerServerTotal,
				prometheus.GaugeValue,
				scope.AddressesFreeOnPartnerServer,
				scopeID,
			)
		}

		if scope.AddressesFreeOnThisServer != -1 {
			ch <- prometheus.MustNewConstMetric(
				c.scopeAddressesFreeOnThisServerTotal,
				prometheus.GaugeValue,
				scope.AddressesFreeOnThisServer,
				scopeID,
			)
		}

		if scope.AddressesInUse != -1 {
			ch <- prometheus.MustNewConstMetric(
				c.scopeAddressesInUseTotal,
				prometheus.GaugeValue,
				scope.AddressesInUse,
				scopeID,
			)
		}

		if scope.AddressesInUseOnPartnerServer != -1 {
			ch <- prometheus.MustNewConstMetric(
				c.scopeAddressesInUseOnPartnerServerTotal,
				prometheus.GaugeValue,
				scope.AddressesInUseOnPartnerServer,
				scopeID,
			)
		}

		if scope.AddressesInUseOnThisServer != -1 {
			ch <- prometheus.MustNewConstMetric(
				c.scopeAddressesInUseOnThisServerTotal,
				prometheus.GaugeValue,
				scope.AddressesInUseOnThisServer,
				scopeID,
			)
		}

		if scope.PendingOffers != -1 {
			ch <- prometheus.MustNewConstMetric(
				c.scopePendingOffersTotal,
				prometheus.GaugeValue,
				scope.PendingOffers,
				scopeID,
			)
		}

		if scope.ReservedAddress != -1 {
			ch <- prometheus.MustNewConstMetric(
				c.scopeReservedAddressTotal,
				prometheus.GaugeValue,
				scope.ReservedAddress,
				scopeID,
			)
		}
	}

	return nil
}
