//go:build linux

package client

import (
	"context"
	"fmt"
	"log"
	"net"
	"os"
	"path/filepath"
	"strings"
	"time"

	"github.com/duffy/usb-server/internal/protocol"
	"github.com/duffy/usb-server/internal/usbip"
	"golang.org/x/sys/unix"
)

// createVHCIAttachment creates a VHCI attachment on Linux using socketpair + sysfs.
// Returns the tunnel connection (our end of the socketpair), the VHCI port number, and any error.
func createVHCIAttachment(_ context.Context, granted *protocol.DeviceGranted, _ *RemoteDevice) (net.Conn, int, error) {
	// Create a socketpair - one end for VHCI, one for our tunnel
	fds, err := createSocketPair()
	if err != nil {
		return nil, -1, fmt.Errorf("creating socketpair: %w", err)
	}

	vhciFD := fds[0]
	tunnelFD := fds[1]

	// Find a free VHCI port
	port, err := usbip.FindFreePort(granted.Speed)
	if err != nil {
		closeFDs(fds)
		return nil, -1, fmt.Errorf("finding free VHCI port: %w", err)
	}

	// Attach to VHCI
	if err := usbip.AttachDevice(port, vhciFD, granted.DevID, granted.Speed); err != nil {
		closeFDs(fds)
		return nil, -1, fmt.Errorf("VHCI attach: %w", err)
	}

	// The VHCI driver holds a kernel reference to the socket via sockfd_lookup,
	// so we can close our copy of the fd to avoid leaking it.
	unix.Close(vhciFD)

	// Create a net.Conn from the tunnel FD
	tunnelFile := fdToFile(tunnelFD, "usb-tunnel")
	tunnelConn, err := net.FileConn(tunnelFile)
	tunnelFile.Close() // FileConn dups the fd
	if err != nil {
		usbip.DetachDevice(port)
		return nil, -1, fmt.Errorf("creating tunnel conn: %w", err)
	}

	return tunnelConn, port, nil
}

// createSocketPair creates a Unix domain socket pair
func createSocketPair() ([2]int, error) {
	fds, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM, 0)
	if err != nil {
		return [2]int{}, fmt.Errorf("socketpair: %w", err)
	}
	return fds, nil
}

func closeFDs(fds [2]int) {
	unix.Close(fds[0])
	unix.Close(fds[1])
}

func fdToFile(fd int, name string) *os.File {
	return os.NewFile(uintptr(fd), name)
}

// logVHCIDeviceStatus reads the VHCI sysfs tree to check what happened
// with a newly attached device. Logs driver binding, device class, etc.
//
// This is diagnostics only, so it stays behind USBSRV_DEBUG: it waits three
// seconds and then walks the whole sysfs tree on every attach.
func logVHCIDeviceStatus(port int) {
	if !protocol.Debug {
		return
	}

	time.Sleep(3 * time.Second) // wait for enumeration

	basePath := "/sys/devices/platform/vhci_hcd.0"
	entries, err := os.ReadDir(basePath)
	if err != nil {
		log.Printf("[use-diag] cannot read VHCI sysfs: %v", err)
		return
	}

	// Find the USB device for this port (usbN/N-M pattern)
	for _, entry := range entries {
		if !strings.HasPrefix(entry.Name(), "usb") {
			continue
		}
		usbPath := filepath.Join(basePath, entry.Name())
		devEntries, err := os.ReadDir(usbPath)
		if err != nil {
			continue
		}
		for _, devEntry := range devEntries {
			devName := devEntry.Name()
			// Device dirs look like "3-1", not "3-1:1.0"
			if !strings.Contains(devName, "-") || strings.Contains(devName, ":") {
				continue
			}
			devPath := filepath.Join(usbPath, devName)

			// Read device info
			readAttr := func(name string) string {
				data, err := os.ReadFile(filepath.Join(devPath, name))
				if err != nil {
					return ""
				}
				return strings.TrimSpace(string(data))
			}

			vid := readAttr("idVendor")
			pid := readAttr("idProduct")
			product := readAttr("product")
			manufacturer := readAttr("manufacturer")
			speed := readAttr("speed")
			devClass := readAttr("bDeviceClass")

			if vid == "" {
				continue // not a real device
			}

			log.Printf("[use-diag] VHCI device: %s %s:%s speed=%s class=%s %s %s",
				devName, vid, pid, speed, devClass, manufacturer, product)

			// Check interfaces and their drivers
			ifEntries, _ := os.ReadDir(devPath)
			for _, ifEntry := range ifEntries {
				ifName := ifEntry.Name()
				if !strings.Contains(ifName, ":") {
					continue
				}
				ifPath := filepath.Join(devPath, ifName)
				ifClass, _ := os.ReadFile(filepath.Join(ifPath, "bInterfaceClass"))
				ifProto, _ := os.ReadFile(filepath.Join(ifPath, "bInterfaceProtocol"))

				driverLink, err := os.Readlink(filepath.Join(ifPath, "driver"))
				driver := "(no driver)"
				if err == nil {
					driver = filepath.Base(driverLink)
				}

				log.Printf("[use-diag]   interface %s: class=%s proto=%s driver=%s",
					ifName, strings.TrimSpace(string(ifClass)), strings.TrimSpace(string(ifProto)), driver)

				// Check for input devices under this interface
				filepath.WalkDir(ifPath, func(path string, d os.DirEntry, err error) error {
					if err != nil {
						return nil
					}
					if strings.HasPrefix(d.Name(), "event") && strings.Contains(path, "/input/input") {
						log.Printf("[use-diag]     → /dev/input/%s", d.Name())
					}
					if strings.HasPrefix(d.Name(), "hidraw") && filepath.Base(filepath.Dir(path)) == "hidraw" {
						log.Printf("[use-diag]     → /dev/%s", d.Name())
					}
					return nil
				})
			}
		}
	}
}

// fixVHCIDevicePermissions waits for the VHCI-attached device to create
// device nodes (e.g. /dev/video*, /dev/input/event*, /dev/hidraw*) and sets
// them to world-accessible. VHCI-created devices don't get normal udev
// rules applied, so they default to root-only access.
func fixVHCIDevicePermissions(port int) {
	// Wait for the device to finish enumerating and create device nodes.
	// The kernel needs time to enumerate descriptors and bind drivers.
	for attempt := 0; attempt < 15; attempt++ {
		time.Sleep(500 * time.Millisecond)

		found := false

		// Walk the VHCI sysfs tree to find device nodes at any depth.
		// Paths look like: vhci_hcd.0/usb3/3-1/3-1:1.0/video4linux/video0
		filepath.WalkDir("/sys/devices/platform/vhci_hcd.0", func(path string, d os.DirEntry, err error) error {
			if err != nil {
				return nil
			}

			dir := filepath.Dir(path)
			parent := filepath.Base(dir)

			// video4linux devices → /dev/videoN
			if parent == "video4linux" && strings.HasPrefix(d.Name(), "video") {
				devPath := "/dev/" + d.Name()
				if err := os.Chmod(devPath, 0666); err == nil {
					log.Printf("[use] set permissions 0666 on %s", devPath)
					found = true
				} else {
					log.Printf("[use] chmod %s failed: %v", devPath, err)
				}
			}

			// sound devices → /dev/snd/*
			if parent == "sound" && strings.HasPrefix(d.Name(), "card") {
				sndDir := filepath.Join(path, "device")
				if _, err := os.Stat(sndDir); err == nil {
					filepath.WalkDir("/dev/snd", func(sndPath string, sd os.DirEntry, err error) error {
						if err == nil && !sd.IsDir() {
							os.Chmod(sndPath, 0666)
						}
						return nil
					})
				}
			}

			// input devices → /dev/input/eventN
			if strings.HasPrefix(d.Name(), "event") && strings.Contains(path, "/input/input") {
				devPath := "/dev/input/" + d.Name()
				if err := os.Chmod(devPath, 0666); err == nil {
					log.Printf("[use] set permissions 0666 on %s", devPath)
					found = true
				} else {
					log.Printf("[use] chmod %s failed: %v", devPath, err)
				}
			}

			// hidraw devices → /dev/hidrawN
			if parent == "hidraw" && strings.HasPrefix(d.Name(), "hidraw") {
				devPath := "/dev/" + d.Name()
				if err := os.Chmod(devPath, 0666); err == nil {
					log.Printf("[use] set permissions 0666 on %s", devPath)
					found = true
				} else {
					log.Printf("[use] chmod %s failed: %v", devPath, err)
				}
			}

			return nil
		})

		if attempt >= 2 && found {
			return
		}
	}

	log.Printf("[use] fixVHCIDevicePermissions: no device nodes found after 7.5s (port %d)", port)
}
