package usbgadget

import (
	"fmt"
	"os"
	"os/exec"
	"path"
	"path/filepath"
	"sort"
)

type gadgetConfigItem struct {
	order       uint
	device      string
	path        []string
	attrs       gadgetAttributes
	configAttrs gadgetAttributes
	configPath  []string
	reportDesc  []byte
}

type gadgetAttributes map[string]string

type gadgetConfigItemWithKey struct {
	key  string
	item gadgetConfigItem
}

type orderedGadgetConfigItems []gadgetConfigItemWithKey

var defaultGadgetConfig = map[string]gadgetConfigItem{
	"base": {
		order: 0,
		attrs: gadgetAttributes{
			"bcdUSB":    "0x0200", // USB 2.0
			"idVendor":  "0x1d6b", // The Linux Foundation
			"idProduct": "0104",   // Multifunction Composite Gadget
			"bcdDevice": "0100",
		},
		configAttrs: gadgetAttributes{
			"MaxPower": "250", // in unit of 2mA
		},
	},
	"base_info": {
		order:      1,
		path:       []string{"strings", "0x409"},
		configPath: []string{"strings", "0x409"},
		attrs: gadgetAttributes{
			"serialnumber": "",
			"manufacturer": "JetKVM",
			"product":      "JetKVM USB Emulation Device",
		},
		configAttrs: gadgetAttributes{
			"configuration": "Config 1: HID",
		},
	},
	// keyboard HID
	"keyboard": keyboardConfig,
	// mouse HID
	"absolute_mouse": absoluteMouseConfig,
	// relative mouse HID
	"relative_mouse": relativeMouseConfig,
	// mass storage
	"mass_storage_base": massStorageBaseConfig,
	"mass_storage_lun0": massStorageLun0Config,
}

func (u *UsbGadget) isGadgetConfigItemEnabled(itemKey string) bool {
	switch itemKey {
	case "absolute_mouse":
		return u.enabledDevices.AbsoluteMouse
	case "relative_mouse":
		return u.enabledDevices.RelativeMouse
	case "keyboard":
		return u.enabledDevices.Keyboard
	case "mass_storage_base":
		return u.enabledDevices.MassStorage
	case "mass_storage_lun0":
		return u.enabledDevices.MassStorage
	default:
		return true
	}
}

func (u *UsbGadget) loadGadgetConfig() {
	if u.customConfig.isEmpty {
		u.log.Trace().Msg("using default gadget config")
		return
	}

	u.configMap["base"].attrs["idVendor"] = u.customConfig.VendorId
	u.configMap["base"].attrs["idProduct"] = u.customConfig.ProductId

	u.configMap["base_info"].attrs["serialnumber"] = u.customConfig.SerialNumber
	u.configMap["base_info"].attrs["manufacturer"] = u.customConfig.Manufacturer
	u.configMap["base_info"].attrs["product"] = u.customConfig.Product
}

func (u *UsbGadget) SetGadgetConfig(config *Config) {
	u.configLock.Lock()
	defer u.configLock.Unlock()

	if config == nil {
		return // nothing to do
	}

	u.customConfig = *config
	u.loadGadgetConfig()
}

func (u *UsbGadget) SetGadgetDevices(devices *Devices) {
	u.configLock.Lock()
	defer u.configLock.Unlock()

	if devices == nil {
		return // nothing to do
	}

	u.enabledDevices = *devices
}

// GetConfigPath returns the path to the config item.
func (u *UsbGadget) GetConfigPath(itemKey string) (string, error) {
	item, ok := u.configMap[itemKey]
	if !ok {
		return "", fmt.Errorf("config item %s not found", itemKey)
	}
	return joinPath(u.kvmGadgetPath, item.configPath), nil
}

// GetPath returns the path to the item.
func (u *UsbGadget) GetPath(itemKey string) (string, error) {
	item, ok := u.configMap[itemKey]
	if !ok {
		return "", fmt.Errorf("config item %s not found", itemKey)
	}
	return joinPath(u.kvmGadgetPath, item.path), nil
}

func mountConfigFS() error {
	_, err := os.Stat(gadgetPath)
	// TODO: check if it's mounted properly
	if err == nil {
		return nil
	}

	if os.IsNotExist(err) {
		err = exec.Command("mount", "-t", "configfs", "none", configFSPath).Run()
		if err != nil {
			return fmt.Errorf("failed to mount configfs: %w", err)
		}
	} else {
		return fmt.Errorf("unable to access usb gadget path: %w", err)
	}
	return nil
}

func (u *UsbGadget) Init() error {
	u.configLock.Lock()
	defer u.configLock.Unlock()

	u.loadGadgetConfig()

	udcs := getUdcs()
	if len(udcs) < 1 {
		u.log.Error().Msg("no udc found, skipping USB stack init")
		return nil
	}

	u.udc = udcs[0]
	_, err := os.Stat(u.kvmGadgetPath)
	if err == nil {
		u.log.Info().Msg("usb gadget already exists")
	}

	if err := mountConfigFS(); err != nil {
		u.log.Error().Err(err).Msg("failed to mount configfs, usb stack might not function properly")
	}

	if err := os.MkdirAll(u.configC1Path, 0755); err != nil {
		u.log.Error().Err(err).Msg("failed to create config path")
	}

	if err := u.writeGadgetConfig(); err != nil {
		u.log.Error().Err(err).Msg("failed to start gadget")
	}

	return nil
}

func (u *UsbGadget) UpdateGadgetConfig() error {
	u.configLock.Lock()
	defer u.configLock.Unlock()

	u.loadGadgetConfig()

	if err := u.writeGadgetConfig(); err != nil {
		u.log.Error().Err(err).Msg("failed to update gadget")
	}

	return nil
}

func (u *UsbGadget) getOrderedConfigItems() orderedGadgetConfigItems {
	items := make([]gadgetConfigItemWithKey, 0)
	for key, item := range u.configMap {
		items = append(items, gadgetConfigItemWithKey{key, item})
	}

	sort.Slice(items, func(i, j int) bool {
		return items[i].item.order < items[j].item.order
	})

	return items
}

func (u *UsbGadget) writeGadgetConfig() error {
	// create kvm gadget path
	err := os.MkdirAll(u.kvmGadgetPath, 0755)
	if err != nil {
		return err
	}

	u.log.Trace().Msg("writing gadget config")
	for _, val := range u.getOrderedConfigItems() {
		key := val.key
		item := val.item

		// check if the item is enabled in the config
		if !u.isGadgetConfigItemEnabled(key) {
			u.log.Trace().Str("key", key).Msg("disabling gadget config")
			err = u.disableGadgetItemConfig(item)
			if err != nil {
				return err
			}
			continue
		}
		u.log.Trace().Str("key", key).Msg("writing gadget config")
		err = u.writeGadgetItemConfig(item)
		if err != nil {
			return err
		}
	}

	if err = u.writeUDC(); err != nil {
		u.log.Error().Err(err).Msg("failed to write UDC")
		return err
	}

	if err = u.rebindUsb(true); err != nil {
		u.log.Info().Err(err).Msg("failed to rebind usb")
	}

	return nil
}

func (u *UsbGadget) disableGadgetItemConfig(item gadgetConfigItem) error {
	// remove symlink if exists
	if item.configPath == nil {
		return nil
	}

	configPath := joinPath(u.configC1Path, item.configPath)

	if _, err := os.Lstat(configPath); os.IsNotExist(err) {
		u.log.Trace().Str("path", configPath).Msg("symlink does not exist")
		return nil
	}

	if err := os.Remove(configPath); err != nil {
		return fmt.Errorf("failed to remove symlink %s: %w", item.configPath, err)
	}

	return nil
}

func (u *UsbGadget) writeGadgetItemConfig(item gadgetConfigItem) error {
	// create directory for the item
	gadgetItemPath := joinPath(u.kvmGadgetPath, item.path)
	err := os.MkdirAll(gadgetItemPath, 0755)
	if err != nil {
		return fmt.Errorf("failed to create path %s: %w", gadgetItemPath, err)
	}

	if len(item.attrs) > 0 {
		// write attributes for the item
		err = u.writeGadgetAttrs(gadgetItemPath, item.attrs)
		if err != nil {
			return fmt.Errorf("failed to write attributes for %s: %w", gadgetItemPath, err)
		}
	}

	// write report descriptor if available
	if item.reportDesc != nil {
		err = u.writeIfDifferent(path.Join(gadgetItemPath, "report_desc"), item.reportDesc, 0644)
		if err != nil {
			return err
		}
	}

	// create config directory if configAttrs are set
	if len(item.configAttrs) > 0 {
		configItemPath := joinPath(u.configC1Path, item.configPath)
		err = os.MkdirAll(configItemPath, 0755)
		if err != nil {
			return fmt.Errorf("failed to create path %s: %w", configItemPath, err)
		}

		err = u.writeGadgetAttrs(configItemPath, item.configAttrs)
		if err != nil {
			return fmt.Errorf("failed to write config attributes for %s: %w", configItemPath, err)
		}
	}

	// create symlink if configPath is set
	if item.configPath != nil && item.configAttrs == nil {
		configPath := joinPath(u.configC1Path, item.configPath)
		u.log.Trace().Str("source", configPath).Str("target", gadgetItemPath).Msg("creating symlink")
		if err := ensureSymlink(configPath, gadgetItemPath); err != nil {
			return err
		}
	}

	return nil
}

func (u *UsbGadget) writeGadgetAttrs(basePath string, attrs gadgetAttributes) error {
	for key, val := range attrs {
		filePath := filepath.Join(basePath, key)
		err := u.writeIfDifferent(filePath, []byte(val), 0644)
		if err != nil {
			return fmt.Errorf("failed to write to %s: %w", filePath, err)
		}
	}
	return nil
}
