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

package collector

import (
	"fmt"
	"log/slog"
	"reflect"

	"github.com/prometheus/client_golang/prometheus"
	"github.com/prometheus/procfs"
)

const zoneinfoSubsystem = "zoneinfo"

type zoneinfoCollector struct {
	gaugeMetricDescs   map[string]*prometheus.Desc
	counterMetricDescs map[string]*prometheus.Desc
	logger             *slog.Logger
	fs                 procfs.FS
}

func init() {
	registerCollector("zoneinfo", defaultDisabled, NewZoneinfoCollector)
}

// NewZoneinfoCollector returns a new Collector exposing zone stats.
func NewZoneinfoCollector(logger *slog.Logger) (Collector, error) {
	fs, err := procfs.NewFS(*procPath)
	if err != nil {
		return nil, fmt.Errorf("failed to open procfs: %w", err)
	}
	return &zoneinfoCollector{
		gaugeMetricDescs:   createGaugeMetricDescriptions(),
		counterMetricDescs: createCounterMetricDescriptions(),
		logger:             logger,
		fs:                 fs,
	}, nil
}

func (c *zoneinfoCollector) Update(ch chan<- prometheus.Metric) error {
	metrics, err := c.fs.Zoneinfo()
	if err != nil {
		return fmt.Errorf("couldn't get zoneinfo: %w", err)
	}
	for _, metric := range metrics {
		node := metric.Node
		zone := metric.Zone
		metricStruct := reflect.ValueOf(metric)
		typeOfMetricStruct := metricStruct.Type()
		for i := 0; i < metricStruct.NumField(); i++ {
			value := reflect.Indirect(metricStruct.Field(i))
			if value.Kind() != reflect.Int64 {
				continue
			}
			metricName := typeOfMetricStruct.Field(i).Name
			desc, ok := c.gaugeMetricDescs[metricName]
			metricType := prometheus.GaugeValue
			if !ok {
				desc = c.counterMetricDescs[metricName]
				metricType = prometheus.CounterValue
			}
			ch <- prometheus.MustNewConstMetric(desc, metricType,
				float64(reflect.Indirect(metricStruct.Field(i)).Int()),
				node, zone)
		}
		for i, value := range metric.Protection {
			metricName := fmt.Sprintf("protection_%d", i)
			desc, ok := c.gaugeMetricDescs[metricName]
			if !ok {
				desc = prometheus.NewDesc(
					prometheus.BuildFQName(namespace, zoneinfoSubsystem, metricName),
					fmt.Sprintf("Protection array %d. field", i),
					[]string{"node", "zone"}, nil)
				c.gaugeMetricDescs[metricName] = desc
			}
			ch <- prometheus.MustNewConstMetric(desc, prometheus.GaugeValue,
				float64(*value), node, zone)
		}

	}
	return nil
}
func createGaugeMetricDescriptions() map[string]*prometheus.Desc {
	return map[string]*prometheus.Desc{
		"NrFreePages": prometheus.NewDesc(
			prometheus.BuildFQName(namespace, zoneinfoSubsystem, "nr_free_pages"),
			"Total number of free pages in the zone",
			[]string{"node", "zone"}, nil),
		"Min": prometheus.NewDesc(
			prometheus.BuildFQName(namespace, zoneinfoSubsystem, "min_pages"),
			"Zone watermark pages_min",
			[]string{"node", "zone"}, nil),
		"Low": prometheus.NewDesc(
			prometheus.BuildFQName(namespace, zoneinfoSubsystem, "low_pages"),
			"Zone watermark pages_low",
			[]string{"node", "zone"}, nil),
		"High": prometheus.NewDesc(
			prometheus.BuildFQName(namespace, zoneinfoSubsystem, "high_pages"),
			"Zone watermark pages_high",
			[]string{"node", "zone"}, nil),
		"Scanned": prometheus.NewDesc(
			prometheus.BuildFQName(namespace, zoneinfoSubsystem, "scanned_pages"),
			"Pages scanned since last reclaim",
			[]string{"node", "zone"}, nil),
		"Spanned": prometheus.NewDesc(
			prometheus.BuildFQName(namespace, zoneinfoSubsystem, "spanned_pages"),
			"Total pages spanned by the zone, including holes",
			[]string{"node", "zone"}, nil),
		"Present": prometheus.NewDesc(
			prometheus.BuildFQName(namespace, zoneinfoSubsystem, "present_pages"),
			"Physical pages existing within the zone",
			[]string{"node", "zone"}, nil),
		"Managed": prometheus.NewDesc(
			prometheus.BuildFQName(namespace, zoneinfoSubsystem, "managed_pages"),
			"Present pages managed by the buddy system",
			[]string{"node", "zone"}, nil),
		"NrActiveAnon": prometheus.NewDesc(
			prometheus.BuildFQName(namespace, zoneinfoSubsystem, "nr_active_anon_pages"),
			"Number of anonymous pages recently more used",
			[]string{"node", "zone"}, nil),
		"NrInactiveAnon": prometheus.NewDesc(
			prometheus.BuildFQName(namespace, zoneinfoSubsystem, "nr_inactive_anon_pages"),
			"Number of anonymous pages recently less used",
			[]string{"node", "zone"}, nil),
		"NrIsolatedAnon": prometheus.NewDesc(
			prometheus.BuildFQName(namespace, zoneinfoSubsystem, "nr_isolated_anon_pages"),
			"Temporary isolated pages from anon lru",
			[]string{"node", "zone"}, nil),
		"NrAnonPages": prometheus.NewDesc(
			prometheus.BuildFQName(namespace, zoneinfoSubsystem, "nr_anon_pages"),
			"Number of anonymous pages currently used by the system",
			[]string{"node", "zone"}, nil),
		"NrAnonTransparentHugepages": prometheus.NewDesc(
			prometheus.BuildFQName(namespace, zoneinfoSubsystem, "nr_anon_transparent_hugepages"),
			"Number of anonymous transparent huge pages currently used by the system",
			[]string{"node", "zone"}, nil),
		"NrActiveFile": prometheus.NewDesc(
			prometheus.BuildFQName(namespace, zoneinfoSubsystem, "nr_active_file_pages"),
			"Number of active pages with file-backing",
			[]string{"node", "zone"}, nil),
		"NrInactiveFile": prometheus.NewDesc(
			prometheus.BuildFQName(namespace, zoneinfoSubsystem, "nr_inactive_file_pages"),
			"Number of inactive pages with file-backing",
			[]string{"node", "zone"}, nil),
		"NrIsolatedFile": prometheus.NewDesc(
			prometheus.BuildFQName(namespace, zoneinfoSubsystem, "nr_isolated_file_pages"),
			"Temporary isolated pages from file lru",
			[]string{"node", "zone"}, nil),
		"NrFilePages": prometheus.NewDesc(
			prometheus.BuildFQName(namespace, zoneinfoSubsystem, "nr_file_pages"),
			"Number of file pages",
			[]string{"node", "zone"}, nil),
		"NrSlabReclaimable": prometheus.NewDesc(
			prometheus.BuildFQName(namespace, zoneinfoSubsystem, "nr_slab_reclaimable_pages"),
			"Number of reclaimable slab pages",
			[]string{"node", "zone"}, nil),
		"NrSlabUnreclaimable": prometheus.NewDesc(
			prometheus.BuildFQName(namespace, zoneinfoSubsystem, "nr_slab_unreclaimable_pages"),
			"Number of unreclaimable slab pages",
			[]string{"node", "zone"}, nil),
		"NrMlockStack": prometheus.NewDesc(
			prometheus.BuildFQName(namespace, zoneinfoSubsystem, "nr_mlock_stack_pages"),
			"mlock()ed pages found and moved off LRU",
			[]string{"node", "zone"}, nil),
		"NrKernelStack": prometheus.NewDesc(
			prometheus.BuildFQName(namespace, zoneinfoSubsystem, "nr_kernel_stacks"),
			"Number of kernel stacks",
			[]string{"node", "zone"}, nil),
		"NrMapped": prometheus.NewDesc(
			prometheus.BuildFQName(namespace, zoneinfoSubsystem, "nr_mapped_pages"),
			"Number of mapped pages",
			[]string{"node", "zone"}, nil),
		"NrDirty": prometheus.NewDesc(
			prometheus.BuildFQName(namespace, zoneinfoSubsystem, "nr_dirty_pages"),
			"Number of dirty pages",
			[]string{"node", "zone"}, nil),
		"NrWriteback": prometheus.NewDesc(
			prometheus.BuildFQName(namespace, zoneinfoSubsystem, "nr_writeback_pages"),
			"Number of writeback pages",
			[]string{"node", "zone"}, nil),
		"NrUnevictable": prometheus.NewDesc(
			prometheus.BuildFQName(namespace, zoneinfoSubsystem, "nr_unevictable_pages"),
			"Number of unevictable pages",
			[]string{"node", "zone"}, nil),
		"NrShmem": prometheus.NewDesc(
			prometheus.BuildFQName(namespace, zoneinfoSubsystem, "nr_shmem_pages"),
			"Number of shmem pages (included tmpfs/GEM pages)",
			[]string{"node", "zone"}, nil),
	}

}
func createCounterMetricDescriptions() map[string]*prometheus.Desc {
	return map[string]*prometheus.Desc{
		"NrDirtied": prometheus.NewDesc(
			prometheus.BuildFQName(namespace, zoneinfoSubsystem, "nr_dirtied_total"),
			"Page dirtyings since bootup",
			[]string{"node", "zone"}, nil),
		"NrWritten": prometheus.NewDesc(
			prometheus.BuildFQName(namespace, zoneinfoSubsystem, "nr_written_total"),
			"Page writings since bootup",
			[]string{"node", "zone"}, nil),
		"NumaHit": prometheus.NewDesc(
			prometheus.BuildFQName(namespace, zoneinfoSubsystem, "numa_hit_total"),
			"Allocated in intended node",
			[]string{"node", "zone"}, nil),
		"NumaMiss": prometheus.NewDesc(
			prometheus.BuildFQName(namespace, zoneinfoSubsystem, "numa_miss_total"),
			"Allocated in non intended node",
			[]string{"node", "zone"}, nil),
		"NumaForeign": prometheus.NewDesc(
			prometheus.BuildFQName(namespace, zoneinfoSubsystem, "numa_foreign_total"),
			"Was intended here, hit elsewhere",
			[]string{"node", "zone"}, nil),
		"NumaInterleave": prometheus.NewDesc(
			prometheus.BuildFQName(namespace, zoneinfoSubsystem, "numa_interleave_total"),
			"Interleaver preferred this zone",
			[]string{"node", "zone"}, nil),
		"NumaLocal": prometheus.NewDesc(
			prometheus.BuildFQName(namespace, zoneinfoSubsystem, "numa_local_total"),
			"Allocation from local node",
			[]string{"node", "zone"}, nil),
		"NumaOther": prometheus.NewDesc(
			prometheus.BuildFQName(namespace, zoneinfoSubsystem, "numa_other_total"),
			"Allocation from other node",
			[]string{"node", "zone"}, nil),
	}
}
