package storage

import (
	"context"
	"database/sql"
	"encoding/json"
	"fmt"
	"os/exec"
	"strconv"
	"strings"
	"time"

	"github.com/atlasos/calypso/internal/common/database"
	"github.com/atlasos/calypso/internal/common/logger"
)

// DiskService handles disk discovery and management
type DiskService struct {
	db     *database.DB
	logger *logger.Logger
}

// NewDiskService creates a new disk service
func NewDiskService(db *database.DB, log *logger.Logger) *DiskService {
	return &DiskService{
		db:     db,
		logger: log,
	}
}

// PhysicalDisk represents a physical disk
type PhysicalDisk struct {
	ID             string                 `json:"id"`
	DevicePath     string                 `json:"device_path"`
	Vendor         string                 `json:"vendor"`
	Model          string                 `json:"model"`
	SerialNumber   string                 `json:"serial_number"`
	SizeBytes      int64                  `json:"size_bytes"`
	SectorSize     int                    `json:"sector_size"`
	IsSSD          bool                   `json:"is_ssd"`
	HealthStatus   string                 `json:"health_status"`
	HealthDetails  map[string]interface{} `json:"health_details"`
	IsUsed         bool                   `json:"is_used"`
	AttachedToPool string                 `json:"attached_to_pool"` // Pool name if disk is used in a ZFS pool
	CreatedAt      time.Time              `json:"created_at"`
	UpdatedAt      time.Time              `json:"updated_at"`
}

// DiscoverDisks discovers physical disks on the system
func (s *DiskService) DiscoverDisks(ctx context.Context) ([]PhysicalDisk, error) {
	// Use lsblk to discover block devices
	cmd := exec.CommandContext(ctx, "lsblk", "-b", "-o", "NAME,SIZE,TYPE", "-J")
	output, err := cmd.Output()
	if err != nil {
		return nil, fmt.Errorf("failed to run lsblk: %w", err)
	}

	var lsblkOutput struct {
		BlockDevices []struct {
			Name string      `json:"name"`
			Size interface{} `json:"size"` // Can be string or number
			Type string      `json:"type"`
		} `json:"blockdevices"`
	}

	if err := json.Unmarshal(output, &lsblkOutput); err != nil {
		return nil, fmt.Errorf("failed to parse lsblk output: %w", err)
	}

	var disks []PhysicalDisk
	for _, device := range lsblkOutput.BlockDevices {
		// Only process disk devices (not partitions)
		if device.Type != "disk" {
			continue
		}

		devicePath := "/dev/" + device.Name

		// Skip ZFS volume block devices (zd* devices are ZFS volumes exported as block devices)
		// These are not physical disks and should not appear in physical disk list
		if strings.HasPrefix(device.Name, "zd") {
			s.logger.Debug("Skipping ZFS volume block device", "device", devicePath)
			continue
		}

		// Skip devices under /dev/zvol (ZFS volume devices in zvol directory)
		// These are virtual block devices created from ZFS volumes, not physical hardware
		if strings.HasPrefix(devicePath, "/dev/zvol/") {
			s.logger.Debug("Skipping ZFS volume device", "device", devicePath)
			continue
		}

		// Skip OS disk (disk that has root or boot partition)
		if s.isOSDisk(ctx, devicePath) {
			s.logger.Debug("Skipping OS disk", "device", devicePath)
			continue
		}

		disk, err := s.getDiskInfo(ctx, devicePath)
		if err != nil {
			s.logger.Warn("Failed to get disk info", "device", devicePath, "error", err)
			continue
		}

		// Parse size (can be string or number)
		var sizeBytes int64
		switch v := device.Size.(type) {
		case string:
			if size, err := strconv.ParseInt(v, 10, 64); err == nil {
				sizeBytes = size
			}
		case float64:
			sizeBytes = int64(v)
		case int64:
			sizeBytes = v
		case int:
			sizeBytes = int64(v)
		}
		disk.SizeBytes = sizeBytes

		disks = append(disks, *disk)
	}

	return disks, nil
}

// getDiskInfo retrieves detailed information about a disk
func (s *DiskService) getDiskInfo(ctx context.Context, devicePath string) (*PhysicalDisk, error) {
	disk := &PhysicalDisk{
		DevicePath:    devicePath,
		HealthStatus:  "unknown",
		HealthDetails: make(map[string]interface{}),
	}

	// Get disk information using udevadm
	cmd := exec.CommandContext(ctx, "udevadm", "info", "--query=property", "--name="+devicePath)
	output, err := cmd.Output()
	if err != nil {
		return nil, fmt.Errorf("failed to get udev info: %w", err)
	}

	props := parseUdevProperties(string(output))
	disk.Vendor = props["ID_VENDOR"]
	disk.Model = props["ID_MODEL"]
	disk.SerialNumber = props["ID_SERIAL_SHORT"]

	if props["ID_ATA_ROTATION_RATE"] == "0" {
		disk.IsSSD = true
	}

	// Get sector size
	if sectorSize, err := strconv.Atoi(props["ID_SECTOR_SIZE"]); err == nil {
		disk.SectorSize = sectorSize
	}

	// Check if disk is in use (part of a volume group or ZFS pool)
	disk.IsUsed = s.isDiskInUse(ctx, devicePath)

	// Check if disk is used in a ZFS pool
	poolName := s.getZFSPoolForDisk(ctx, devicePath)
	if poolName != "" {
		disk.IsUsed = true
		disk.AttachedToPool = poolName
	}

	// Get health status (simplified - would use smartctl in production)
	disk.HealthStatus = "healthy" // Placeholder

	return disk, nil
}

// parseUdevProperties parses udevadm output
func parseUdevProperties(output string) map[string]string {
	props := make(map[string]string)
	lines := strings.Split(output, "\n")
	for _, line := range lines {
		parts := strings.SplitN(line, "=", 2)
		if len(parts) == 2 {
			props[parts[0]] = parts[1]
		}
	}
	return props
}

// isDiskInUse checks if a disk is part of a volume group
func (s *DiskService) isDiskInUse(ctx context.Context, devicePath string) bool {
	cmd := exec.CommandContext(ctx, "pvdisplay", devicePath)
	err := cmd.Run()
	return err == nil
}

// getZFSPoolForDisk checks if a disk is used in a ZFS pool and returns the pool name
func (s *DiskService) getZFSPoolForDisk(ctx context.Context, devicePath string) string {
	// Extract device name (e.g., /dev/sde -> sde)
	deviceName := strings.TrimPrefix(devicePath, "/dev/")

	// Get all ZFS pools
	cmd := exec.CommandContext(ctx, "sudo", "zpool", "list", "-H", "-o", "name")
	output, err := cmd.Output()
	if err != nil {
		return ""
	}

	pools := strings.Split(strings.TrimSpace(string(output)), "\n")
	for _, poolName := range pools {
		if poolName == "" {
			continue
		}

		// Check pool status for this device
		statusCmd := exec.CommandContext(ctx, "sudo", "zpool", "status", poolName)
		statusOutput, err := statusCmd.Output()
		if err != nil {
			continue
		}

		statusStr := string(statusOutput)
		// Check if device is in the pool (as data disk or spare)
		if strings.Contains(statusStr, deviceName) {
			return poolName
		}
	}

	return ""
}

// isOSDisk checks if a disk is used as OS disk (has root or boot partition)
func (s *DiskService) isOSDisk(ctx context.Context, devicePath string) bool {
	// Extract device name (e.g., /dev/sda -> sda)
	deviceName := strings.TrimPrefix(devicePath, "/dev/")

	// Check if any partition of this disk is mounted as root or boot
	// Use lsblk to get mount points for this device and its children
	cmd := exec.CommandContext(ctx, "lsblk", "-n", "-o", "NAME,MOUNTPOINT", devicePath)
	output, err := cmd.Output()
	if err != nil {
		return false
	}

	lines := strings.Split(string(output), "\n")
	for _, line := range lines {
		fields := strings.Fields(line)
		if len(fields) >= 2 {
			mountPoint := fields[1]
			// Check if mounted as root or boot
			if mountPoint == "/" || mountPoint == "/boot" || mountPoint == "/boot/efi" {
				return true
			}
		}
	}

	// Also check all partitions of this disk using lsblk with recursive listing
	partCmd := exec.CommandContext(ctx, "lsblk", "-n", "-o", "NAME,MOUNTPOINT", "-l")
	partOutput, err := partCmd.Output()
	if err == nil {
		partLines := strings.Split(string(partOutput), "\n")
		for _, line := range partLines {
			if strings.HasPrefix(line, deviceName) {
				fields := strings.Fields(line)
				if len(fields) >= 2 {
					mountPoint := fields[1]
					if mountPoint == "/" || mountPoint == "/boot" || mountPoint == "/boot/efi" {
						return true
					}
				}
			}
		}
	}

	return false
}

// SyncDisksToDatabase syncs discovered disks to the database
func (s *DiskService) SyncDisksToDatabase(ctx context.Context) error {
	s.logger.Info("Starting disk discovery and sync")
	disks, err := s.DiscoverDisks(ctx)
	if err != nil {
		s.logger.Error("Failed to discover disks", "error", err)
		return fmt.Errorf("failed to discover disks: %w", err)
	}

	s.logger.Info("Discovered disks", "count", len(disks))

	for _, disk := range disks {
		// Check if disk exists
		var existingID string
		err := s.db.QueryRowContext(ctx,
			"SELECT id FROM physical_disks WHERE device_path = $1",
			disk.DevicePath,
		).Scan(&existingID)

		healthDetailsJSON, _ := json.Marshal(disk.HealthDetails)

		if err == sql.ErrNoRows {
			// Insert new disk
			_, err = s.db.ExecContext(ctx, `
				INSERT INTO physical_disks (
					device_path, vendor, model, serial_number, size_bytes,
					sector_size, is_ssd, health_status, health_details, is_used
				) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
			`, disk.DevicePath, disk.Vendor, disk.Model, disk.SerialNumber,
				disk.SizeBytes, disk.SectorSize, disk.IsSSD,
				disk.HealthStatus, healthDetailsJSON, disk.IsUsed)
			if err != nil {
				s.logger.Error("Failed to insert disk", "device", disk.DevicePath, "error", err)
			}
		} else if err == nil {
			// Update existing disk
			_, err = s.db.ExecContext(ctx, `
				UPDATE physical_disks SET
					vendor = $1, model = $2, serial_number = $3,
					size_bytes = $4, sector_size = $5, is_ssd = $6,
					health_status = $7, health_details = $8, is_used = $9,
					updated_at = NOW()
				WHERE id = $10
			`, disk.Vendor, disk.Model, disk.SerialNumber,
				disk.SizeBytes, disk.SectorSize, disk.IsSSD,
				disk.HealthStatus, healthDetailsJSON, disk.IsUsed, existingID)
			if err != nil {
				s.logger.Error("Failed to update disk", "device", disk.DevicePath, "error", err)
			} else {
				s.logger.Debug("Updated disk", "device", disk.DevicePath)
			}
		}
	}

	s.logger.Info("Disk sync completed", "total_disks", len(disks))
	return nil
}

// ListDisksFromDatabase retrieves all physical disks from the database
func (s *DiskService) ListDisksFromDatabase(ctx context.Context) ([]PhysicalDisk, error) {
	query := `
		SELECT 
			id, device_path, vendor, model, serial_number, size_bytes,
			sector_size, is_ssd, health_status, health_details, is_used,
			created_at, updated_at
		FROM physical_disks
		ORDER BY device_path
	`

	rows, err := s.db.QueryContext(ctx, query)
	if err != nil {
		return nil, fmt.Errorf("failed to query disks: %w", err)
	}
	defer rows.Close()

	var disks []PhysicalDisk
	for rows.Next() {
		var disk PhysicalDisk
		var healthDetailsJSON []byte
		var attachedToPool sql.NullString

		err := rows.Scan(
			&disk.ID, &disk.DevicePath, &disk.Vendor, &disk.Model,
			&disk.SerialNumber, &disk.SizeBytes, &disk.SectorSize,
			&disk.IsSSD, &disk.HealthStatus, &healthDetailsJSON,
			&disk.IsUsed, &disk.CreatedAt, &disk.UpdatedAt,
		)
		if err != nil {
			s.logger.Warn("Failed to scan disk row", "error", err)
			continue
		}

		// Parse health details JSON
		if len(healthDetailsJSON) > 0 {
			if err := json.Unmarshal(healthDetailsJSON, &disk.HealthDetails); err != nil {
				s.logger.Warn("Failed to parse health details", "error", err)
				disk.HealthDetails = make(map[string]interface{})
			}
		} else {
			disk.HealthDetails = make(map[string]interface{})
		}

		// Get ZFS pool attachment if disk is used
		if disk.IsUsed {
			err := s.db.QueryRowContext(ctx,
				`SELECT zp.name FROM zfs_pools zp
				INNER JOIN zfs_pool_disks zpd ON zp.id = zpd.pool_id
				WHERE zpd.disk_id = $1
				LIMIT 1`,
				disk.ID,
			).Scan(&attachedToPool)
			if err == nil && attachedToPool.Valid {
				disk.AttachedToPool = attachedToPool.String
			}
		}

		disks = append(disks, disk)
	}

	if err := rows.Err(); err != nil {
		return nil, fmt.Errorf("error iterating disk rows: %w", err)
	}

	return disks, nil
}
