package common

import (
	"bufio"
	"bytes"
	"strconv"
	"strings"
)

// DiskCounters holds cumulative counter values for a single block device.
type DiskCounters struct {
	Device            string
	ReadsCompleted    float64
	WritesCompleted   float64
	ReadBytesTotal    float64
	WrittenBytesTotal float64
}

// TotalIOPS returns the total I/O operations (reads + writes).
func (d *DiskCounters) TotalIOPS() float64 {
	return d.ReadsCompleted + d.WritesCompleted
}

// TotalBytes returns the total bytes transferred (read + written).
func (d *DiskCounters) TotalBytes() float64 {
	return d.ReadBytesTotal + d.WrittenBytesTotal
}

// ParseNodeDiskMetrics parses raw Prometheus text from node-exporter and returns
// DiskCounters for the CSI data disk device. The CSI data disk is identified as
// the device whose mountpoint contains "csi" in node_filesystem_size_bytes.
//
// If no CSI device can be identified, it falls back to the device with the
// largest node_filesystem_size_bytes (which is typically the data volume).
func ParseNodeDiskMetrics(raw []byte) (*DiskCounters, error) {
	device := identifyCSIDataDevice(raw)
	if device == "" {
		return nil, nil //nolint:nilnil // No identifiable CSI data device in the metrics.
	}
	return extractDiskCounters(raw, device), nil
}

// identifyCSIDataDevice scans node_filesystem_size_bytes lines for the largest
// CSI-mounted filesystem, which corresponds to the TiKV data disk.
// Falls back to the device with the largest filesystem if no CSI mountpoint found.
//
// A node may have multiple CSI volumes (e.g., a small raft/WAL disk and a large
// data disk). We always pick the largest one because the data disk is the target
// for IOPS/throughput monitoring.
func identifyCSIDataDevice(raw []byte) string {
	scanner := bufio.NewScanner(bytes.NewReader(raw))

	var largestCSIDevice string
	var largestCSISize float64
	var largestDevice string
	var largestSize float64

	for scanner.Scan() {
		line := scanner.Text()
		if !strings.HasPrefix(line, "node_filesystem_size_bytes{") {
			continue
		}

		device := extractLabel(line, "device")
		mountpoint := extractLabel(line, "mountpoint")
		val := extractMetricValue(line)

		if device == "" || val == 0 {
			continue
		}

		// Strip partition suffix (e.g., /dev/nvme2n1 from /dev/nvme2n1p1)
		blockDev := stripPartition(device)

		if strings.Contains(mountpoint, "csi") {
			if val > largestCSISize {
				largestCSISize = val
				largestCSIDevice = blockDev
			}
			continue
		}

		if val > largestSize {
			largestSize = val
			largestDevice = blockDev
		}
	}

	if largestCSIDevice != "" {
		return largestCSIDevice
	}
	return largestDevice
}

// FindDeviceBySerial scans node_disk_info lines for a device whose serial label
// matches the given value (e.g., an EBS volume ID without the dash: "vol0abc123").
// Returns the device path (e.g., "/dev/nvme3n1") or "" if not found.
func FindDeviceBySerial(raw []byte, serial string) string {
	if serial == "" {
		return ""
	}
	scanner := bufio.NewScanner(bytes.NewReader(raw))
	for scanner.Scan() {
		line := scanner.Text()
		if !strings.HasPrefix(line, "node_disk_info{") {
			continue
		}
		if extractLabel(line, "serial") == serial {
			dev := extractLabel(line, "device")
			if dev != "" {
				return "/dev/" + dev
			}
		}
	}
	return ""
}

// ExtractDiskCountersForDevice extracts disk I/O counters for a specific device
// from raw Prometheus node-exporter text.
func ExtractDiskCountersForDevice(raw []byte, device string) *DiskCounters {
	return extractDiskCounters(raw, device)
}

// extractDiskCounters reads specific counter metrics for the given device.
func extractDiskCounters(raw []byte, device string) *DiskCounters {
	counters := &DiskCounters{Device: device}
	// Strip /dev/ prefix for matching against device label
	devName := strings.TrimPrefix(device, "/dev/")

	scanner := bufio.NewScanner(bytes.NewReader(raw))
	for scanner.Scan() {
		line := scanner.Text()
		if strings.HasPrefix(line, "#") {
			continue
		}

		dev := extractLabel(line, "device")
		if dev != devName {
			continue
		}

		switch {
		case strings.HasPrefix(line, "node_disk_reads_completed_total{"):
			counters.ReadsCompleted = extractMetricValue(line)
		case strings.HasPrefix(line, "node_disk_writes_completed_total{"):
			counters.WritesCompleted = extractMetricValue(line)
		case strings.HasPrefix(line, "node_disk_read_bytes_total{"):
			counters.ReadBytesTotal = extractMetricValue(line)
		case strings.HasPrefix(line, "node_disk_written_bytes_total{"):
			counters.WrittenBytesTotal = extractMetricValue(line)
		}
	}

	return counters
}

// extractLabel extracts a label value from a Prometheus metric line.
// e.g., extractLabel(`node_disk_info{device="nvme0n1",major="259"}`, "device") → "nvme0n1"
func extractLabel(line, labelName string) string {
	key := labelName + `="`
	idx := strings.Index(line, key)
	if idx < 0 {
		return ""
	}
	start := idx + len(key)
	end := strings.Index(line[start:], `"`)
	if end < 0 {
		return ""
	}
	return line[start : start+end]
}

// extractMetricValue extracts the numeric value from the end of a Prometheus metric line.
func extractMetricValue(line string) float64 {
	// Value is after the closing "} " or the last space
	idx := strings.LastIndex(line, "} ")
	if idx >= 0 {
		val, err := strconv.ParseFloat(strings.TrimSpace(line[idx+2:]), 64)
		if err != nil {
			return 0
		}
		return val
	}
	// No labels: "metric_name value"
	parts := strings.Fields(line)
	if len(parts) < 2 {
		return 0
	}
	val, err := strconv.ParseFloat(parts[len(parts)-1], 64)
	if err != nil {
		return 0
	}
	return val
}

// stripPartition strips the partition number suffix from a device path.
// e.g., "/dev/nvme2n1p1" → "/dev/nvme2n1", "nvme2n1" → "nvme2n1"
func stripPartition(device string) string {
	// Handle /dev/ prefix
	prefix := ""
	name := device
	if strings.HasPrefix(device, "/dev/") {
		prefix = "/dev/"
		name = device[5:]
	}

	// NVMe devices: nvme0n1p1 → nvme0n1, nvme0n1 → nvme0n1 (no partition)
	if strings.HasPrefix(name, "nvme") {
		if pIdx := strings.LastIndex(name, "p"); pIdx > 0 {
			suffix := name[pIdx+1:]
			if _, err := strconv.Atoi(suffix); err == nil {
				return prefix + name[:pIdx]
			}
		}
		// No partition suffix found on NVMe device
		return device
	}

	// Standard devices: sda1 → sda, xvda1 → xvda
	// Only strip trailing digits if there's at least one non-digit before them.
	for i := len(name) - 1; i >= 0; i-- {
		if name[i] < '0' || name[i] > '9' {
			if i < len(name)-1 {
				return prefix + name[:i+1]
			}
			break
		}
	}

	return device
}
