package raid

import (
	"errors"
	"fmt"
	"log"
	"os"
	"os/exec"
	"path/filepath"
	"sort"
	"strconv"
	"strings"

	"imuslab.com/bokofs/bokofsd/mod/utils"
)

/*
	mdadm manager

	This script handles the interaction with mdadm
*/

// RAIDDevice represents information about a RAID device.
type RAIDMember struct {
	Name   string //sdX
	Seq    int    //Sequence in RAID arary
	Failed bool   //If details output with (F) tag this will set to true
}

type RAIDDevice struct {
	Name    string
	Status  string
	Level   string
	Members []*RAIDMember
}

// Return the uuid of the disk by its path name (e.g. /dev/sda)
func (m *Manager) GetDiskUUIDByPath(devicePath string) (string, error) {
	cmd := exec.Command("sudo", "blkid", devicePath)

	output, err := cmd.CombinedOutput()
	if err != nil {
		return "", fmt.Errorf("blkid error: %v", err)
	}

	// Parse the output to extract the UUID
	fields := strings.Fields(string(output))
	for _, field := range fields {
		if strings.HasPrefix(field, "UUID=") {
			uuid := strings.TrimPrefix(field, "UUID=\"")
			uuid = strings.TrimSuffix(uuid, "\"")
			return uuid, nil
		}
	}

	return "", fmt.Errorf("UUID not found for device %s", devicePath)
}

// CreateRAIDDevice creates a RAID device using the mdadm command.
func (m *Manager) CreateRAIDDevice(devName string, raidName string, raidLevel int, raidDeviceIds []string, spareDeviceIds []string) error {
	//Calculate the size of the raid devices
	raidDev := len(raidDeviceIds)
	spareDevice := len(spareDeviceIds)

	//Validate if raid level
	if !IsValidRAIDLevel("raid" + strconv.Itoa(raidLevel)) {
		return fmt.Errorf("invalid or unsupported raid level given: raid" + strconv.Itoa(raidLevel))
	}

	//Validate the number of disk is enough for the raid
	if raidLevel == 0 && raidDev < 2 {
		return fmt.Errorf("not enough disks for raid0")
	} else if raidLevel == 1 && raidDev < 2 {
		return fmt.Errorf("not enough disks for raid1")
	} else if raidLevel == 5 && raidDev < 3 {
		return fmt.Errorf("not enough disk for raid5")
	} else if raidLevel == 6 && raidDev < 4 {
		return fmt.Errorf("not enough disk for raid6")
	}

	//Append /dev to the name if missing
	if !strings.HasPrefix(devName, "/dev/") {
		devName = "/dev/" + devName
	}

	if utils.FileExists(devName) {
		//RAID device already exists
		return errors.New(devName + " already been used")
	}

	//Append /dev to the name of the raid device ids and spare device ids if missing
	for i, raidDev := range raidDeviceIds {
		if !strings.HasPrefix(raidDev, "/dev/") {
			raidDeviceIds[i] = filepath.Join("/dev/", raidDev)
		}
	}
	for i, spareDev := range spareDeviceIds {
		if !strings.HasPrefix(spareDev, "/dev/") {
			spareDeviceIds[i] = filepath.Join("/dev/", spareDev)
		}
	}

	// Concatenate RAID and spare device arrays
	allDeviceIds := append(raidDeviceIds, spareDeviceIds...)

	// Build the mdadm command
	mdadmCommand := fmt.Sprintf("yes | sudo mdadm --create %s --name %s --level=%d --raid-devices=%d --spare-devices=%d %s", devName, raidName, raidLevel, raidDev, spareDevice, strings.Join(allDeviceIds, " "))
	if raidLevel == 0 {
		//raid0 cannot use --spare-device command as there is no failover
		mdadmCommand = fmt.Sprintf("yes | sudo mdadm --create %s --name %s --level=%d --raid-devices=%d %s", devName, raidName, raidLevel, raidDev, strings.Join(allDeviceIds, " "))
	}
	cmd := exec.Command("bash", "-c", mdadmCommand)

	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr

	err := cmd.Run()
	if err != nil {
		return fmt.Errorf("error running mdadm command: %v", err)
	}

	return nil
}

// GetRAIDDevicesFromProcMDStat retrieves information about RAID devices from /proc/mdstat.
// if your RAID array is in auto-read-only mode, it is (usually) brand new
func (m *Manager) GetRAIDDevicesFromProcMDStat() ([]RAIDDevice, error) {
	// Execute the cat command to read /proc/mdstat
	cmd := exec.Command("cat", "/proc/mdstat")

	// Run the command and capture its output
	output, err := cmd.Output()
	if err != nil {
		return nil, fmt.Errorf("error running cat command: %v", err)
	}

	// Convert the output to a string and split it into lines
	lines := strings.Split(string(output), "\n")

	// Initialize an empty slice to store RAID devices
	raidDevices := make([]RAIDDevice, 0)

	// Iterate over the lines, skipping the first line (Personalities)
	// Lines usually looks like this
	// md0 : active raid1 sdc[1] sdb[0]
	for _, line := range lines[1:] {
		// Skip empty lines
		if line == "" {
			continue
		}

		// Split the line by colon (:)
		parts := strings.SplitN(line, " : ", 2)
		if len(parts) != 2 {
			continue
		}

		// Extract device name and status
		deviceName := parts[0]

		// Split the members string by space to get individual member devices
		info := strings.Fields(parts[1])
		if len(info) < 2 {
			//Malform output
			continue
		}

		deviceStatus := info[0]

		//Raid level usually appears at position 1 - 2, check both
		raidLevel := ""
		if strings.HasPrefix(info[1], "raid") {
			raidLevel = info[1]
		} else if strings.HasPrefix(info[2], "raid") {
			raidLevel = info[2]
		}

		//Get the members (disks) of the array
		members := []*RAIDMember{}
		for _, disk := range info[2:] {
			if !strings.HasPrefix(disk, "sd") {
				//Probably not a storage device
				continue
			}

			//In sda[0] format, we need to split out the number from the disk seq
			tmp := strings.Split(disk, "[")
			if len(tmp) != 2 {
				continue
			}

			//Convert the sequence to id
			diskFailed := false
			if strings.HasSuffix(strings.TrimSpace(tmp[1]), "(F)") {
				//Trim off the Fail label
				diskFailed = true
				tmp[1] = strings.TrimSuffix(strings.TrimSpace(tmp[1]), "(F)")
			}
			seqInt, err := strconv.Atoi(strings.TrimSuffix(strings.TrimSpace(tmp[1]), "]"))
			if err != nil {
				//Not an integer?
				log.Println("[RAID] Unable to parse " + disk + " sequence ID")
				continue
			}
			member := RAIDMember{
				Name:   strings.TrimSpace(tmp[0]),
				Seq:    seqInt,
				Failed: diskFailed,
			}

			members = append(members, &member)
		}

		//Sort the member disks
		sort.Slice(members[:], func(i, j int) bool {
			return members[i].Seq < members[j].Seq
		})

		// Create a RAIDDevice struct and append it to the slice
		raidDevice := RAIDDevice{
			Name:    deviceName,
			Status:  deviceStatus,
			Level:   raidLevel,
			Members: members,
		}
		raidDevices = append(raidDevices, raidDevice)
	}

	return raidDevices, nil
}

// Check if a disk is failed in given array
func (m *Manager) DiskIsFailed(mdDevice, diskPath string) (bool, error) {
	raidDevices, err := m.GetRAIDDeviceByDevicePath(mdDevice)
	if err != nil {
		return false, err
	}

	diskName := filepath.Base(diskPath)

	for _, disk := range raidDevices.Members {
		if disk.Name == diskName {
			return disk.Failed, nil
		}
	}

	return false, errors.New("target disk not found in this array")
}

// FailDisk label a disk as failed
func (m *Manager) FailDisk(mdDevice, diskPath string) error {
	//mdadm commands require full path
	if !strings.HasPrefix(diskPath, "/dev/") {
		diskPath = filepath.Join("/dev/", diskPath)
	}
	if !strings.HasPrefix(mdDevice, "/dev/") {
		mdDevice = filepath.Join("/dev/", mdDevice)
	}

	cmd := exec.Command("sudo", "mdadm", mdDevice, "--fail", diskPath)
	if err := cmd.Run(); err != nil {
		return fmt.Errorf("failed to fail disk: %v", err)
	}
	return nil
}

// RemoveDisk removes a failed disk from the specified RAID array using mdadm.
// must be failed before remove
func (m *Manager) RemoveDisk(mdDevice, diskPath string) error {
	//mdadm commands require full path
	if !strings.HasPrefix(diskPath, "/dev/") {
		diskPath = filepath.Join("/dev/", diskPath)
	}
	if !strings.HasPrefix(mdDevice, "/dev/") {
		mdDevice = filepath.Join("/dev/", mdDevice)
	}

	cmd := exec.Command("sudo", "mdadm", mdDevice, "--remove", diskPath)
	if err := cmd.Run(); err != nil {
		return fmt.Errorf("failed to remove disk: %v", err)
	}
	return nil
}

// Add disk to a given RAID array, must be unmounted and not in-use
func (m *Manager) AddDisk(mdDevice, diskPath string) error {
	//mdadm commands require full path
	if !strings.HasPrefix(diskPath, "/dev/") {
		diskPath = filepath.Join("/dev/", diskPath)
	}
	if !strings.HasPrefix(mdDevice, "/dev/") {
		mdDevice = filepath.Join("/dev/", mdDevice)
	}

	cmd := exec.Command("sudo", "mdadm", mdDevice, "--add", diskPath)
	if err := cmd.Run(); err != nil {
		return fmt.Errorf("failed to add disk: %v", err)
	}
	return nil
}

// GrowRAIDDevice grows the specified RAID device to its maximum size
func (m *Manager) GrowRAIDDevice(deviceName string) error {
	//Prevent anyone passing /dev/md0 into the deviceName field
	deviceName = strings.TrimPrefix(deviceName, "/dev/")

	// Construct the mdadm command
	cmd := exec.Command("sudo", "mdadm", "--grow", fmt.Sprintf("/dev/%s", deviceName), "--size=max")

	// Execute the command
	output, err := cmd.CombinedOutput()
	if err != nil {
		return fmt.Errorf("failed to grow RAID device: %v, output: %s", err, string(output))
	}

	fmt.Printf("[RAID] Successfully grew RAID device %s. Output: %s\n", deviceName, string(output))
	return nil
}
