//go:build linux

package agent

import (
	"fmt"
	"os"
	"path/filepath"
	"strconv"
	"strings"

	"github.com/henrygd/beszel/agent/utils"
	"github.com/henrygd/beszel/internal/entities/smart"
)

// mdraidSysfsRoot is a test hook; production value is "/sys".
var mdraidSysfsRoot = "/sys"

type mdraidHealth struct {
	level         string
	arrayState    string
	degraded      uint64
	raidDisks     uint64
	syncAction    string
	syncCompleted string
	syncSpeed     string
	mismatchCnt   uint64
	capacity      uint64
}

// scanMdraidDevices discovers Linux md arrays exposed in sysfs.
func scanMdraidDevices() []*DeviceInfo {
	blockDir := filepath.Join(mdraidSysfsRoot, "block")
	entries, err := os.ReadDir(blockDir)
	if err != nil {
		return nil
	}

	devices := make([]*DeviceInfo, 0, 2)
	for _, ent := range entries {
		name := ent.Name()
		if !isMdraidBlockName(name) {
			continue
		}
		mdDir := filepath.Join(blockDir, name, "md")
		if !utils.FileExists(filepath.Join(mdDir, "array_state")) {
			continue
		}

		devPath := filepath.Join("/dev", name)
		devices = append(devices, &DeviceInfo{
			Name:     devPath,
			Type:     "mdraid",
			InfoName: devPath + " [mdraid]",
			Protocol: "MD",
		})
	}

	return devices
}

// collectMdraidHealth reads mdraid health and stores it in SmartDataMap.
func (sm *SmartManager) collectMdraidHealth(deviceInfo *DeviceInfo) (bool, error) {
	if deviceInfo == nil || deviceInfo.Name == "" {
		return false, nil
	}

	base := filepath.Base(deviceInfo.Name)
	if !isMdraidBlockName(base) && !strings.EqualFold(deviceInfo.Type, "mdraid") {
		return false, nil
	}

	health, ok := readMdraidHealth(base)
	if !ok {
		return false, nil
	}

	deviceInfo.Type = "mdraid"
	key := fmt.Sprintf("mdraid:%s", base)
	status := mdraidSmartStatus(health)

	attrs := make([]*smart.SmartAttribute, 0, 10)
	if health.arrayState != "" {
		attrs = append(attrs, &smart.SmartAttribute{Name: "ArrayState", RawString: health.arrayState})
	}
	if health.level != "" {
		attrs = append(attrs, &smart.SmartAttribute{Name: "RaidLevel", RawString: health.level})
	}
	if health.raidDisks > 0 {
		attrs = append(attrs, &smart.SmartAttribute{Name: "RaidDisks", RawValue: health.raidDisks})
	}
	if health.degraded > 0 {
		attrs = append(attrs, &smart.SmartAttribute{Name: "Degraded", RawValue: health.degraded})
	}
	if health.syncAction != "" {
		attrs = append(attrs, &smart.SmartAttribute{Name: "SyncAction", RawString: health.syncAction})
	}
	if health.syncCompleted != "" {
		attrs = append(attrs, &smart.SmartAttribute{Name: "SyncCompleted", RawString: health.syncCompleted})
	}
	if health.syncSpeed != "" {
		attrs = append(attrs, &smart.SmartAttribute{Name: "SyncSpeed", RawString: health.syncSpeed})
	}
	if health.mismatchCnt > 0 {
		attrs = append(attrs, &smart.SmartAttribute{Name: "MismatchCount", RawValue: health.mismatchCnt})
	}

	sm.Lock()
	defer sm.Unlock()

	if _, exists := sm.SmartDataMap[key]; !exists {
		sm.SmartDataMap[key] = &smart.SmartData{}
	}

	data := sm.SmartDataMap[key]
	data.ModelName = "Linux MD RAID"
	if health.level != "" {
		data.ModelName = "Linux MD RAID (" + health.level + ")"
	}
	data.Capacity = health.capacity
	data.SmartStatus = status
	data.DiskName = filepath.Join("/dev", base)
	data.DiskType = "mdraid"
	data.Attributes = attrs

	return true, nil
}

// readMdraidHealth reads md array health fields from sysfs.
func readMdraidHealth(blockName string) (mdraidHealth, bool) {
	var out mdraidHealth

	if !isMdraidBlockName(blockName) {
		return out, false
	}

	mdDir := filepath.Join(mdraidSysfsRoot, "block", blockName, "md")
	arrayState, okState := utils.ReadStringFileOK(filepath.Join(mdDir, "array_state"))
	if !okState {
		return out, false
	}

	out.arrayState = arrayState
	out.level = utils.ReadStringFile(filepath.Join(mdDir, "level"))
	out.syncAction = utils.ReadStringFile(filepath.Join(mdDir, "sync_action"))
	out.syncCompleted = utils.ReadStringFile(filepath.Join(mdDir, "sync_completed"))
	out.syncSpeed = utils.ReadStringFile(filepath.Join(mdDir, "sync_speed"))

	if val, ok := utils.ReadUintFile(filepath.Join(mdDir, "raid_disks")); ok {
		out.raidDisks = val
	}
	if val, ok := utils.ReadUintFile(filepath.Join(mdDir, "degraded")); ok {
		out.degraded = val
	}
	if val, ok := utils.ReadUintFile(filepath.Join(mdDir, "mismatch_cnt")); ok {
		out.mismatchCnt = val
	}

	if capBytes, ok := readMdraidBlockCapacityBytes(blockName, mdraidSysfsRoot); ok {
		out.capacity = capBytes
	}

	return out, true
}

// mdraidSmartStatus maps md state/sync signals to a SMART-like status.
func mdraidSmartStatus(health mdraidHealth) string {
	state := strings.ToLower(strings.TrimSpace(health.arrayState))
	switch state {
	case "inactive", "faulty", "broken", "stopped":
		return "FAILED"
	}
	// During rebuild/recovery, arrays are often temporarily degraded; report as
	// warning instead of hard failure while synchronization is in progress.
	syncAction := strings.ToLower(strings.TrimSpace(health.syncAction))
	switch syncAction {
	case "resync", "recover", "reshape":
		return "WARNING"
	}
	if health.degraded > 0 {
		return "FAILED"
	}
	switch syncAction {
	case "check", "repair":
		return "WARNING"
	}
	switch state {
	case "clean", "active", "active-idle", "write-pending", "read-auto", "readonly":
		return "PASSED"
	}
	return "UNKNOWN"
}

// isMdraidBlockName matches /dev/mdN-style block device names.
func isMdraidBlockName(name string) bool {
	if !strings.HasPrefix(name, "md") {
		return false
	}
	suffix := strings.TrimPrefix(name, "md")
	if suffix == "" {
		return false
	}
	for _, c := range suffix {
		if c < '0' || c > '9' {
			return false
		}
	}
	return true
}

// readMdraidBlockCapacityBytes converts block size metadata into bytes.
func readMdraidBlockCapacityBytes(blockName, root string) (uint64, bool) {
	sizePath := filepath.Join(root, "block", blockName, "size")
	lbsPath := filepath.Join(root, "block", blockName, "queue", "logical_block_size")

	sizeStr, ok := utils.ReadStringFileOK(sizePath)
	if !ok {
		return 0, false
	}
	sectors, err := strconv.ParseUint(sizeStr, 10, 64)
	if err != nil || sectors == 0 {
		return 0, false
	}

	logicalBlockSize := uint64(512)
	if lbsStr, ok := utils.ReadStringFileOK(lbsPath); ok {
		if parsed, err := strconv.ParseUint(lbsStr, 10, 64); err == nil && parsed > 0 {
			logicalBlockSize = parsed
		}
	}

	return sectors * logicalBlockSize, true
}
