package service

import (
	"bufio"
	"fmt"
	"github.com/1Panel-dev/1Panel/agent/app/dto"
	"github.com/1Panel-dev/1Panel/agent/app/dto/response"
	"github.com/1Panel-dev/1Panel/agent/buserr"
	"github.com/1Panel-dev/1Panel/agent/utils/cmd"
	"os"
	"os/exec"
	"regexp"
	"strconv"
	"strings"
)

func organizeDiskInfo(diskInfos []response.DiskBasicInfo) response.CompleteDiskInfo {
	var result response.CompleteDiskInfo
	var systemDisk *response.DiskInfo
	diskMap := make(map[string]*response.DiskInfo)
	partitions := make(map[string][]response.DiskBasicInfo)

	for _, info := range diskInfos {
		isPartition := isPartitionDevice(info.Device)

		if isPartition {
			parentDevice := getParentDevice(info.Device)
			partitions[parentDevice] = append(partitions[parentDevice], info)
		} else {
			disk := &response.DiskInfo{
				DiskBasicInfo: info,
				Partitions:    []response.DiskBasicInfo{},
			}
			diskMap[info.Device] = disk
		}
	}

	for parentDevice, partList := range partitions {
		if disk, exists := diskMap[parentDevice]; exists {
			for index, part := range partList {
				part.Device = fmt.Sprintf("/dev/%s", part.Device)
				if part.IsSystem {
					disk.IsSystem = true
				}
				partList[index] = part
			}
			disk.Partitions = partList
		}
	}

	var totalCapacity int64
	for _, disk := range diskMap {
		capacity := parseSizeToBytes(disk.Size)
		totalCapacity += capacity

		if disk.IsSystem {
			systemDisk = disk
		} else if len(disk.Partitions) == 0 {
			result.UnpartitionedDisks = append(result.UnpartitionedDisks, disk.DiskBasicInfo)
		} else {
			result.Disks = append(result.Disks, *disk)
		}
	}

	result.SystemDisk = systemDisk
	result.TotalDisks = len(diskMap)
	result.TotalCapacity = totalCapacity

	return result
}

func parseLsblkOutput(output string) ([]response.DiskBasicInfo, error) {
	lines := strings.Split(strings.TrimSpace(output), "\n")
	if len(lines) == 0 {
		return nil, fmt.Errorf("invalid lsblk output")
	}

	var diskInfos []response.DiskBasicInfo
	lvmMap := make(map[string]response.DiskBasicInfo)
	var pendingDevices []map[string]string

	for _, line := range lines {
		line = strings.TrimSpace(line)
		if line == "" {
			continue
		}

		fields := parseKeyValuePairs(line)

		name, ok := fields["NAME"]
		if !ok {
			continue
		}

		if strings.HasPrefix(name, "loop") ||
			strings.HasPrefix(name, "dm-") {
			continue
		}

		diskType := fields["TYPE"]
		mountPoint := fields["MOUNTPOINT"]
		fsType := fields["FSTYPE"]
		size := fields["SIZE"]

		if diskType == "lvm" {
			total, used, avail, usePercent, _ := getDiskUsageInfo("/dev/mapper/" + name)
			if total != "" && fsType != "" {
				size = total
			}

			lvmInfo := response.DiskBasicInfo{
				Device:      name,
				Size:        size,
				Model:       fields["MODEL"],
				DiskType:    "LVM",
				IsRemovable: false,
				IsSystem:    isSystemDisk(mountPoint),
				Filesystem:  fsType,
				Used:        used,
				Avail:       avail,
				UsePercent:  usePercent,
				MountPoint:  mountPoint,
				IsMounted:   mountPoint != "" && mountPoint != "-",
				Serial:      fields["SERIAL"],
			}
			lvmMap[name] = lvmInfo
		} else if diskType == "disk" || diskType == "part" {
			pendingDevices = append(pendingDevices, fields)
		}
	}

	for _, fields := range pendingDevices {
		name := fields["NAME"]
		size := fields["SIZE"]
		mountPoint := fields["MOUNTPOINT"]
		fsType := fields["FSTYPE"]
		model := fields["MODEL"]
		serial := fields["SERIAL"]
		tran := fields["TRAN"]
		rota := fields["ROTA"]

		var (
			used, avail, totalSize string
			usePercent             int
		)
		if mountPoint != "" {
			totalSize, used, avail, usePercent, _ = getDiskUsageInfo("/dev/" + name)
			if totalSize != "" {
				size = totalSize
			}
		}

		actualMountPoint := mountPoint
		actualFsType := fsType
		actualUsed := used
		actualAvail := avail
		actualUsePercent := usePercent
		isMounted := mountPoint != "" && mountPoint != "-"
		isSystemPartition := isSystemDisk(mountPoint)

		if fsType == "LVM2_member" {
			for _, lvmInfo := range lvmMap {
				if lvmInfo.IsMounted {
					actualMountPoint = lvmInfo.MountPoint
					actualFsType = lvmInfo.Filesystem
					actualUsed = lvmInfo.Used
					size = lvmInfo.Size
					actualAvail = lvmInfo.Avail
					actualUsePercent = lvmInfo.UsePercent
					isMounted = true
					isSystemPartition = lvmInfo.IsSystem
					break
				}
			}
		}

		info := response.DiskBasicInfo{
			Device:      name,
			Size:        size,
			Model:       model,
			DiskType:    getDiskType(rota),
			IsRemovable: tran == "usb",
			IsSystem:    isSystemPartition,
			Filesystem:  actualFsType,
			Used:        actualUsed,
			Avail:       actualAvail,
			UsePercent:  actualUsePercent,
			MountPoint:  actualMountPoint,
			IsMounted:   isMounted,
			Serial:      serial,
		}

		diskInfos = append(diskInfos, info)
	}

	return diskInfos, nil
}

func getDiskType(rota string) string {
	if rota == "0" {
		return "SSD"
	} else if rota == "1" {
		return "HDD"
	}
	return "Unknown"
}

var kvRe = regexp.MustCompile(`([A-Za-z0-9_]+)=("([^"\\]|\\.)*"|[^ \t]+)`)

func parseKeyValuePairs(line string) map[string]string {
	fields := make(map[string]string)

	matches := kvRe.FindAllStringSubmatch(line, -1)
	for _, m := range matches {
		key := m[1]
		raw := m[2]

		val := raw
		if len(val) >= 2 && val[0] == '"' && val[len(val)-1] == '"' {
			if unq, err := strconv.Unquote(val); err == nil {
				val = unq
			} else {
				val = val[1 : len(val)-1]
			}
		}
		fields[key] = val
	}
	return fields
}

func isPartitionDevice(device string) bool {
	if strings.Contains(device, "nvme") {
		return strings.Contains(device, "p") &&
			strings.ContainsAny(device[strings.LastIndex(device, "p")+1:], "0123456789")
	} else if strings.HasPrefix(device, "sd") || strings.HasPrefix(device, "hd") {
		return len(device) > 3 &&
			strings.ContainsAny(device[len(device)-1:], "0123456789")
	} else if strings.HasPrefix(device, "vd") {
		return len(device) > 3 &&
			strings.ContainsAny(device[len(device)-1:], "0123456789")
	}

	return false
}

func getParentDevice(device string) string {
	if strings.Contains(device, "nvme") {
		if idx := strings.LastIndex(device, "p"); idx != -1 {
			return device[:idx]
		}
	} else {
		return strings.TrimRight(device, "0123456789")
	}

	return device
}

func getDiskUsageInfo(device string) (size, used, avail string, usePercent int, err error) {
	output, err := cmd.RunDefaultWithStdoutBashC(fmt.Sprintf("df -h %s | tail -1", device))
	if err != nil {
		return "", "", "", 0, nil
	}

	fields := strings.Fields(output)
	if len(fields) >= 5 {
		size = fields[1]
		used = fields[2]
		avail = fields[3]
		usePercentStr := strings.TrimSuffix(fields[4], "%")
		usePercent, _ = strconv.Atoi(usePercentStr)
	}

	return size, used, avail, usePercent, nil
}

func formatDisk(req dto.DiskFormatRequest) error {
	var mkfsCmd *exec.Cmd

	switch req.Filesystem {
	case "ext4":
		mkfsCmd = exec.Command("mkfs.ext4", "-F", req.Device)
	case "xfs":
		if !cmd.Which("mkfs.xfs") {
			return buserr.New("XfsNotFound")
		}
		mkfsCmd = exec.Command("mkfs.xfs", "-f", req.Device)
	default:
		return fmt.Errorf("unsupport type: %s", req.Filesystem)
	}
	if err := mkfsCmd.Run(); err != nil {
		return err
	}
	return nil
}

func deviceExists(device string) bool {
	_, err := os.Stat(device)
	return err == nil
}

func isDeviceMounted(device string) bool {
	file, err := os.Open("/proc/mounts")
	if err != nil {
		return false
	}
	defer file.Close()

	scanner := bufio.NewScanner(file)
	for scanner.Scan() {
		fields := strings.Fields(scanner.Text())
		if len(fields) >= 2 && fields[0] == device {
			return true
		}
	}
	return false
}

func isPointMounted(mountPoint string) bool {
	file, err := os.Open("/proc/mounts")
	if err != nil {
		return false
	}
	defer file.Close()

	scanner := bufio.NewScanner(file)
	for scanner.Scan() {
		fields := strings.Fields(scanner.Text())
		if len(fields) >= 2 && fields[1] == mountPoint {
			return true
		}
	}
	return false
}

func addToFstabWithOptions(device, mountPoint, filesystem, options string) error {
	if filesystem == "" {
		fsType, err := getFilesystemType(device)
		if err != nil {
			filesystem = "auto"
		} else {
			filesystem = fsType
		}
	}

	if options == "" {
		options = "defaults"
	}

	entry := fmt.Sprintf("%s %s %s %s 0 2\n", device, mountPoint, filesystem, options)

	file, err := os.OpenFile("/etc/fstab", os.O_APPEND|os.O_WRONLY, 0644)
	if err != nil {
		return err
	}
	defer file.Close()

	_, err = file.WriteString(entry)
	return err
}

func removeFromFstab(mountPoint string) error {
	file, err := os.Open("/etc/fstab")
	if err != nil {
		return err
	}
	defer file.Close()

	var lines []string
	scanner := bufio.NewScanner(file)
	for scanner.Scan() {
		line := scanner.Text()
		fields := strings.Fields(line)
		if len(fields) >= 2 && fields[1] == mountPoint {
			continue
		}
		lines = append(lines, line)
	}

	return os.WriteFile("/etc/fstab", []byte(strings.Join(lines, "\n")+"\n"), 0644)
}

func getFilesystemType(device string) (string, error) {
	output, err := cmd.RunDefaultWithStdoutBashC(fmt.Sprintf("blkid -o value -s TYPE %s", device))
	if err != nil {
		return "", err
	}
	return strings.TrimSpace(output), nil
}

func isSystemDisk(mountPoint string) bool {
	systemMountPoints := []string{
		"/",
		"/boot",
		"/boot/efi",
		"/usr",
		"/var",
		"/home",
	}

	for _, sysMount := range systemMountPoints {
		if mountPoint == sysMount {
			return true
		}
	}

	return false
}

func parseSizeToBytes(sizeStr string) int64 {
	if sizeStr == "" || sizeStr == "-" {
		return 0
	}

	sizeStr = strings.TrimSpace(sizeStr)
	if len(sizeStr) < 2 {
		return 0
	}

	unit := strings.ToUpper(sizeStr[len(sizeStr)-1:])
	valueStr := sizeStr[:len(sizeStr)-1]

	value, err := strconv.ParseFloat(valueStr, 64)
	if err != nil {
		return 0
	}

	switch unit {
	case "K":
		return int64(value * 1024)
	case "M":
		return int64(value * 1024 * 1024)
	case "G":
		return int64(value * 1024 * 1024 * 1024)
	case "T":
		return int64(value * 1024 * 1024 * 1024 * 1024)
	default:
		val, _ := strconv.ParseInt(sizeStr, 10, 64)
		return val
	}
}
