package udisks

import (
	"bytes"
	"fmt"

	"github.com/godbus/dbus/v5"
)

// Client communicates with udisks2 over D-Bus.
type Client struct {
	conn *dbus.Conn
}

// NewClient connects to the system D-Bus and returns a udisks2 client.
func NewClient() (*Client, error) {
	conn, err := dbus.SystemBus()
	if err != nil {
		return nil, fmt.Errorf("cannot connect to udisks2 — is the udisks2 service running? (%w)", err)
	}
	return &Client{conn: conn}, nil
}

// Close closes the D-Bus connection.
func (c *Client) Close() {
	c.conn.Close()
}

// FindDevice finds a block device by UUID (if non-empty) or device path.
func (c *Client) FindDevice(uuid, pathOrName string) (*BlockDevice, error) {
	devices, err := c.listBlockDevices()
	if err != nil {
		return nil, err
	}

	if uuid != "" {
		for i := range devices {
			if devices[i].UUID == uuid {
				return &devices[i], nil
			}
		}
		return nil, fmt.Errorf("no device with UUID %s — run 'arca status' to list devices", uuid)
	}

	for i := range devices {
		if devices[i].DevicePath == pathOrName {
			return &devices[i], nil
		}
	}
	return nil, fmt.Errorf("device %s not found — run 'arca status' to list devices or 'arca init' to generate config", pathOrName)
}

// ListEncryptedDevices returns all block devices with the Encrypted interface.
func (c *Client) ListEncryptedDevices() ([]BlockDevice, error) {
	devices, err := c.listBlockDevices()
	if err != nil {
		return nil, err
	}

	var encrypted []BlockDevice
	for _, d := range devices {
		if d.HasEncrypted {
			encrypted = append(encrypted, d)
		}
	}
	return encrypted, nil
}

// CleartextDevice finds the unlocked cleartext device for a LUKS device.
func (c *Client) CleartextDevice(dev *BlockDevice) (*BlockDevice, error) {
	devices, err := c.listBlockDevices()
	if err != nil {
		return nil, err
	}

	for i := range devices {
		if devices[i].CryptoBackingDevice == dev.ObjectPath {
			return &devices[i], nil
		}
	}
	return nil, fmt.Errorf("no cleartext device for %s (is it unlocked?)", dev.DevicePath)
}

// MountPoint returns the current mount point of a device, or "" if not mounted.
func (c *Client) MountPoint(dev *BlockDevice) (string, error) {
	obj := c.conn.Object(busName, dev.ObjectPath)
	variant, err := obj.GetProperty(ifaceFilesystem + ".MountPoints")
	if err != nil {
		return "", err
	}

	mountPoints, ok := variant.Value().([][]byte)
	if !ok || len(mountPoints) == 0 {
		return "", nil
	}

	return string(bytes.TrimRight(mountPoints[0], "\x00")), nil
}

// IsMounted returns the mount point and true if the device is mounted.
func (c *Client) IsMounted(dev *BlockDevice) (string, bool) {
	if !dev.HasFilesystem {
		return "", false
	}
	mp, err := c.MountPoint(dev)
	if err != nil || mp == "" {
		return "", false
	}
	return mp, true
}

// DeviceAtPath returns the block device at the given D-Bus object path.
func (c *Client) DeviceAtPath(path dbus.ObjectPath) (*BlockDevice, error) {
	devices, err := c.listBlockDevices()
	if err != nil {
		return nil, err
	}
	for i := range devices {
		if devices[i].ObjectPath == path {
			return &devices[i], nil
		}
	}
	return nil, fmt.Errorf("device at %s not found", path)
}

// RootBackingDevices returns the object paths of LUKS devices that back
// the root filesystem. Used to exclude them from init config generation.
func (c *Client) RootBackingDevices() ([]dbus.ObjectPath, error) {
	devices, err := c.listBlockDevices()
	if err != nil {
		return nil, err
	}

	var backing []dbus.ObjectPath
	for i := range devices {
		if !devices[i].HasFilesystem {
			continue
		}
		mp, err := c.MountPoint(&devices[i])
		if err != nil || mp != "/" {
			continue
		}
		if devices[i].CryptoBackingDevice != "" && devices[i].CryptoBackingDevice != "/" {
			backing = append(backing, devices[i].CryptoBackingDevice)
		}
	}
	return backing, nil
}

func (c *Client) listBlockDevices() ([]BlockDevice, error) {
	obj := c.conn.Object(busName, dbus.ObjectPath(objectPrefix))
	var managed map[dbus.ObjectPath]map[string]map[string]dbus.Variant
	err := obj.Call(ifaceObjectManager+".GetManagedObjects", 0).Store(&managed)
	if err != nil {
		return nil, fmt.Errorf("listing devices — is the udisks2 service running? (%w)", err)
	}

	var devices []BlockDevice
	for path, ifaces := range managed {
		blockProps, ok := ifaces[ifaceBlock]
		if !ok {
			continue
		}

		dev := BlockDevice{ObjectPath: path}

		if v, ok := blockProps["Device"]; ok {
			if bs, ok := v.Value().([]byte); ok {
				dev.DevicePath = string(bytes.TrimRight(bs, "\x00"))
			}
		}
		if v, ok := blockProps["IdUUID"]; ok {
			if s, ok := v.Value().(string); ok {
				dev.UUID = s
			}
		}
		if v, ok := blockProps["CryptoBackingDevice"]; ok {
			if p, ok := v.Value().(dbus.ObjectPath); ok {
				dev.CryptoBackingDevice = p
			}
		}

		_, dev.HasEncrypted = ifaces[ifaceEncrypted]
		_, dev.HasFilesystem = ifaces[ifaceFilesystem]

		devices = append(devices, dev)
	}
	return devices, nil
}
