package client

import (
	"fmt"
	"log"
	"net"
	"strings"
	"sync"
	"time"

	"github.com/duffy/usb-server/internal/config"
	"github.com/duffy/usb-server/internal/crypto"
	"github.com/duffy/usb-server/internal/protocol"
	"github.com/duffy/usb-server/internal/usbip"
	"github.com/google/uuid"
)

// attachRequestTimeout bounds how long we wait for a share client to answer a
// device request before giving up.
const attachRequestTimeout = 30 * time.Second

// RemoteDevice represents a USB device available from a share client
type RemoteDevice struct {
	protocol.USBDevice
	ClientID   string `json:"client_id"`
	ClientName string `json:"client_name"`
}

// AttachedDevice represents a device currently attached via VHCI
type AttachedDevice struct {
	RemoteDevice
	TunnelID string `json:"tunnel_id"`
	VHCIPort int    `json:"vhci_port"`
}

// UseManager handles receiving/using remote USB devices
type UseManager struct {
	client          *Client
	cfg             *config.Config
	cfgPath         string
	mu              sync.RWMutex
	available       map[string][]RemoteDevice  // clientID -> devices
	attached        map[string]*AttachedDevice // busID@clientID -> attached info
	tunnels         map[string]*useTunnel      // tunnelID -> tunnel
	pending         map[string]*pendingRequest // requestID -> in-flight attach
	forceDetachable map[string]bool            // clientID -> allow_force_detach
}

// pendingRequest tracks an attach request waiting for the share client's reply.
// It carries the target so that a reply arriving after the caller gave up can
// still be undone — otherwise the share side would hold the device open for a
// user who is no longer waiting for it.
type pendingRequest struct {
	clientID string
	busID    string
	resp     chan *protocol.DeviceGranted
}

type useTunnel struct {
	id       string
	busID    string
	clientID string
	conn     net.Conn // our end of the socketpair

	// codec seals outgoing and opens incoming payloads, whichever transport
	// carries them.
	codec *tunnelCodec

	// send delivers an encoded frame; direct is non-nil when this tunnel
	// bypasses the relay.
	send   tunnelSender
	direct *directConn

	// done is closed exactly once. Several paths can tear down the same
	// tunnel — an explicit detach, a release from the share side, the peer
	// leaving, a relay disconnect — and closing it twice would panic.
	done      chan struct{}
	closeOnce sync.Once
}

// tryDirect attempts a direct connection to the granting peer, returning nil
// if none can be established.
//
// Every failure here is benign: the tunnel simply runs through the relay, the
// way it always did. Only the latency improves when this succeeds.
func (um *UseManager) tryDirect(granted *protocol.DeviceGranted, secret *crypto.TunnelSecret) *directConn {
	if um.cfg.DisableDirect || secret == nil || len(granted.Endpoints) == 0 {
		return nil
	}

	conn, addr, err := dialDirect(granted.Endpoints, granted.TunnelID, secret.PeerToken(granted.TunnelID))
	if err != nil {
		log.Printf("[use] no direct route to %s (%v), using the relay", granted.BusID, err)
		return nil
	}

	log.Printf("[use] direct connection to %s established for %s", addr, granted.BusID)
	return conn
}

// NewUseManager creates a use manager
func NewUseManager(client *Client, cfg *config.Config, cfgPath string) *UseManager {
	um := &UseManager{
		client:          client,
		cfg:             cfg,
		cfgPath:         cfgPath,
		available:       make(map[string][]RemoteDevice),
		attached:        make(map[string]*AttachedDevice),
		tunnels:         make(map[string]*useTunnel),
		pending:         make(map[string]*pendingRequest),
		forceDetachable: make(map[string]bool),
	}

	// Use-side messages only this manager handles.
	client.OnDeviceList = um.handleDeviceList
	client.OnDeviceGranted = um.handleDeviceGranted
	client.OnDeviceDenied = um.handleDeviceDenied
	client.OnDeviceReleased = um.handleDeviceReleased

	// Shared with the share manager in "both" mode, hence multicast.
	client.AddTunnelHandler(um.handleTunnelData)
	client.AddClientLeftHandler(um.handleClientLeft)
	client.AddDisconnectHandler(um.handleRelayDisconnect)

	return um
}

// handleRelayDisconnect detaches everything after the relay link drops.
// The relay discarded those tunnels, so the devices are dead: without this
// they would stay listed as attached while no traffic could reach them.
func (um *UseManager) handleRelayDisconnect() {
	um.mu.Lock()
	defer um.mu.Unlock()

	if n := len(um.attached); n > 0 {
		log.Printf("[use] detaching %d device(s) (relay connection lost)", n)
		for key, dev := range um.attached {
			um.closeAttachedLocked(key, dev)
		}
	}

	// The peers that advertised these are unreachable, and their device lists
	// are re-sent on reconnect. Keeping stale entries would show devices the
	// UI cannot actually attach.
	um.available = make(map[string][]RemoteDevice)
	um.forceDetachable = make(map[string]bool)
}

// closeAttachedLocked tears down one attached device: its tunnel, its VHCI
// port and its bookkeeping. Callers must hold um.mu.
func (um *UseManager) closeAttachedLocked(key string, dev *AttachedDevice) {
	if tunnel, ok := um.tunnels[dev.TunnelID]; ok {
		tunnel.closeOnce.Do(func() { close(tunnel.done) })
		if tunnel.conn != nil {
			tunnel.conn.Close()
		}
		if tunnel.direct != nil {
			tunnel.direct.Close()
		}
		delete(um.tunnels, dev.TunnelID)
	}

	if dev.VHCIPort >= 0 {
		if err := usbip.DetachDevice(dev.VHCIPort); err != nil {
			log.Printf("[use] warning: VHCI detach error for %s: %v", key, err)
		}
	}

	delete(um.attached, key)
}

// GetAvailableDevices returns all available remote devices
func (um *UseManager) GetAvailableDevices() []RemoteDevice {
	um.mu.RLock()
	defer um.mu.RUnlock()

	var all []RemoteDevice
	for _, devs := range um.available {
		all = append(all, devs...)
	}
	return all
}

// GetAttachedDevices returns currently attached devices
func (um *UseManager) GetAttachedDevices() []*AttachedDevice {
	um.mu.RLock()
	defer um.mu.RUnlock()

	var result []*AttachedDevice
	for _, dev := range um.attached {
		result = append(result, dev)
	}
	return result
}

// AttachDevice requests and attaches a remote USB device
func (um *UseManager) AttachDevice(clientID, busID string) error {
	// Check if VHCI is available
	if err := usbip.VHCIUnavailableError(); err != nil {
		return err
	}

	key := busID + "@" + clientID
	um.mu.RLock()
	if _, already := um.attached[key]; already {
		um.mu.RUnlock()
		return fmt.Errorf("device %s already attached", key)
	}
	um.mu.RUnlock()

	// Create request
	requestID := uuid.New().String()
	respChan := make(chan *protocol.DeviceGranted, 1)

	um.mu.Lock()
	um.pending[requestID] = &pendingRequest{clientID: clientID, busID: busID, resp: respChan}
	um.mu.Unlock()

	defer func() {
		um.mu.Lock()
		delete(um.pending, requestID)
		um.mu.Unlock()
	}()

	// Send request to relay
	err := um.client.SendJSON(&protocol.RequestDevice{
		Type:         protocol.MsgRequestDevice,
		TargetClient: clientID,
		BusID:        busID,
		RequestID:    requestID,
	})
	if err != nil {
		return fmt.Errorf("sending request: %w", err)
	}

	log.Printf("[use] requesting device %s from %s", busID, clientID)

	// Wait for a grant or denial. Without the timeout a share client that
	// never answers — because it crashed, or the relay dropped the message —
	// would leave this call blocked forever, and with it the HTTP request or
	// auto-connect goroutine that made it.
	timer := time.NewTimer(attachRequestTimeout)
	defer timer.Stop()

	select {
	case granted, ok := <-respChan:
		if !ok || granted == nil {
			return fmt.Errorf("device request denied")
		}
		return um.setupVHCI(clientID, busID, granted)
	case <-timer.C:
		return fmt.Errorf("no response from %s for device %s after %s",
			protocol.ShortID(clientID), busID, attachRequestTimeout)
	case <-um.client.ctx.Done():
		return fmt.Errorf("client shutting down")
	}
}

// DetachDevice releases an attached device
func (um *UseManager) DetachDevice(clientID, busID string) error {
	key := busID + "@" + clientID

	um.mu.Lock()
	dev, exists := um.attached[key]
	if !exists {
		um.mu.Unlock()
		return fmt.Errorf("device %s not attached", key)
	}
	um.closeAttachedLocked(key, dev)
	um.mu.Unlock()

	// Notify share client
	um.client.SendJSON(&protocol.ReleaseDevice{
		Type:         protocol.MsgReleaseDevice,
		TargetClient: clientID,
		BusID:        busID,
	})

	log.Printf("[use] device %s detached", key)
	return nil
}

func (um *UseManager) setupVHCI(clientID, busID string, granted *protocol.DeviceGranted) error {
	// Look up device info from available list (needed for Windows management phase)
	var devInfo *RemoteDevice
	um.mu.RLock()
	for _, d := range um.available[clientID] {
		if d.BusID == busID {
			cp := d
			devInfo = &cp
			break
		}
	}
	um.mu.RUnlock()

	// The granting side tells us whether it encrypts. Both ends must agree:
	// a mismatch would turn ciphertext into garbage USB traffic.
	secret := um.client.TunnelSecret()
	if granted.Encrypted && secret == nil {
		return fmt.Errorf("%s encrypts its tunnels but this client has no tokens configured, "+
			"only a group hash — copy the three tokens over to connect", protocol.ShortID(clientID))
	}
	if !granted.Encrypted {
		if secret != nil {
			log.Printf("[use] warning: %s does not encrypt tunnel traffic for %s",
				protocol.ShortID(clientID), busID)
		}
		secret = nil
	}

	codec, err := newTunnelCodec(secret, granted.TunnelID, crypto.DirUseToShare)
	if err != nil {
		return fmt.Errorf("setting up tunnel encryption: %w", err)
	}

	// Try to reach the peer directly before falling back to the relay. This
	// is where the latency win comes from: two machines on the same network
	// otherwise send every USB transfer out to the relay and back.
	direct := um.tryDirect(granted, secret)

	// Platform-specific VHCI attachment (Linux: socketpair+sysfs, Windows: TCP proxy+usbip.exe)
	tunnelConn, vhciPort, err := createVHCIAttachment(um.client.ctx, granted, devInfo)
	if err != nil {
		if direct != nil {
			direct.Close()
		}
		return fmt.Errorf("VHCI attachment: %w", err)
	}

	tunnel := &useTunnel{
		id:       granted.TunnelID,
		busID:    busID,
		clientID: clientID,
		conn:     tunnelConn,
		done:     make(chan struct{}),
		codec:    codec,
		direct:   direct,
	}
	if direct != nil {
		tunnel.send = directSender(direct)
	} else {
		tunnel.send = relaySender(um.client, granted.TunnelID)
	}

	key := busID + "@" + clientID
	remDev := RemoteDevice{
		USBDevice: protocol.USBDevice{BusID: busID},
		ClientID:  clientID,
	}
	if devInfo != nil {
		remDev = *devInfo
	}

	um.mu.Lock()
	um.tunnels[granted.TunnelID] = tunnel
	um.attached[key] = &AttachedDevice{
		RemoteDevice: remDev,
		TunnelID:     granted.TunnelID,
		VHCIPort:     vhciPort,
	}
	um.mu.Unlock()

	// Start reading from the tunnel socket (VHCI -> peer)
	go um.tunnelReadLoop(tunnel)

	// On a direct connection, incoming frames arrive here instead of through
	// the relay's tunnel-data callback.
	if direct != nil {
		go func() {
			receiveLoop(direct, tunnel.codec, func(payload []byte) error {
				_, err := tunnel.conn.Write(payload)
				return err
			}, tunnel.done, "use/"+busID)

			// Losing the direct connection ends the tunnel: the USB/IP stream
			// cannot be resumed on the relay mid-conversation.
			select {
			case <-tunnel.done:
			default:
				log.Printf("[use] direct connection for %s ended, detaching", key)
				um.DetachDevice(clientID, busID)
			}
		}()
	}

	transport := "relay"
	if direct != nil {
		transport = "direct " + direct.RemoteAddr()
	}
	log.Printf("[use] device %s attached on VHCI port %d (devID=0x%08x speed=%d, %s, encrypted=%v)",
		key, vhciPort, granted.DevID, granted.Speed, transport, codec.encrypted())

	// Check device status and fix permissions on newly created device nodes
	// VHCI-created devices don't get normal udev permissions
	go func() {
		logVHCIDeviceStatus(vhciPort)
		fixVHCIDevicePermissions(vhciPort)
	}()

	return nil
}

// tunnelReadLoop reads from the VHCI socket and sends to relay
func (um *UseManager) tunnelReadLoop(tunnel *useTunnel) {
	buf := make([]byte, 65536)
	for {
		select {
		case <-tunnel.done:
			return
		default:
		}

		n, err := tunnel.conn.Read(buf)
		if err != nil {
			select {
			case <-tunnel.done:
				return
			default:
				log.Printf("[use] tunnel read error: %v", err)
				return
			}
		}

		if protocol.Debug {
			usbip.TraceRequest("use-tunnel", buf[:n])
		}

		if err := send(tunnel.codec, tunnel.send, buf[:n]); err != nil {
			log.Printf("[use] tunnel send error: %v", err)
			return
		}
	}
}

func (um *UseManager) handleDeviceList(msg *protocol.DeviceList) {
	um.mu.Lock()
	var remoteDevs []RemoteDevice
	for _, dev := range msg.Devices {
		remoteDevs = append(remoteDevs, RemoteDevice{
			USBDevice:  dev,
			ClientID:   msg.ClientID,
			ClientName: msg.ClientName,
		})
	}
	um.available[msg.ClientID] = remoteDevs
	um.forceDetachable[msg.ClientID] = msg.AllowForceDetach

	// Collect devices to auto-connect (while holding the lock to check attached map)
	var toAutoConnect []RemoteDevice
	for _, dev := range remoteDevs {
		if dev.Status != protocol.StatusAvailable {
			continue
		}
		key := dev.BusID + "@" + msg.ClientID
		if _, attached := um.attached[key]; attached {
			continue
		}
		if um.matchesAutoConnect(dev) {
			toAutoConnect = append(toAutoConnect, dev)
		}
	}
	um.mu.Unlock()

	log.Printf("[use] received device list from %s (%s): %d devices",
		msg.ClientName, protocol.ShortID(msg.ClientID), len(msg.Devices))

	// Auto-connect matching devices (outside lock, each in its own goroutine)
	for _, dev := range toAutoConnect {
		log.Printf("[use] auto-connecting %s (%s:%s) from %s",
			dev.Name, dev.VendorID, dev.ProductID, msg.ClientName)
		go um.AttachDevice(msg.ClientID, dev.BusID)
	}
}

// matchesAutoConnect checks if a device matches any auto-connect rule.
// Must be called with um.mu held (at least RLock).
func (um *UseManager) matchesAutoConnect(dev RemoteDevice) bool {
	for _, rule := range um.cfg.AutoConnect {
		if rule.VendorID != "" && !strings.EqualFold(rule.VendorID, dev.VendorID) {
			continue
		}
		if rule.ProductID != "" && !strings.EqualFold(rule.ProductID, dev.ProductID) {
			continue
		}
		if rule.BusID != "" && rule.BusID != dev.BusID {
			continue
		}
		if rule.ClientName != "" && rule.ClientName != dev.ClientName {
			continue
		}
		return true // all specified fields match
	}
	return false
}

// SetAutoConnect adds or removes an auto-connect rule for a VendorID:ProductID pair.
func (um *UseManager) SetAutoConnect(vendorID, productID string, enabled bool) error {
	um.mu.Lock()
	defer um.mu.Unlock()

	if enabled {
		// Check if rule already exists
		for _, rule := range um.cfg.AutoConnect {
			if strings.EqualFold(rule.VendorID, vendorID) && strings.EqualFold(rule.ProductID, productID) {
				return nil // already exists
			}
		}
		um.cfg.AutoConnect = append(um.cfg.AutoConnect, config.AutoConnectRule{
			VendorID:  vendorID,
			ProductID: productID,
		})
	} else {
		// Remove matching rule
		filtered := um.cfg.AutoConnect[:0]
		for _, rule := range um.cfg.AutoConnect {
			if strings.EqualFold(rule.VendorID, vendorID) && strings.EqualFold(rule.ProductID, productID) {
				continue
			}
			filtered = append(filtered, rule)
		}
		um.cfg.AutoConnect = filtered
	}

	if err := um.cfg.Save(um.cfgPath); err != nil {
		return fmt.Errorf("saving config: %w", err)
	}

	log.Printf("[use] auto-connect %s:%s = %v", vendorID, productID, enabled)
	return nil
}

// IsAutoConnect checks if there is an auto-connect rule for this VendorID:ProductID.
func (um *UseManager) IsAutoConnect(vendorID, productID string) bool {
	um.mu.RLock()
	defer um.mu.RUnlock()

	for _, rule := range um.cfg.AutoConnect {
		if strings.EqualFold(rule.VendorID, vendorID) && strings.EqualFold(rule.ProductID, productID) {
			return true
		}
	}
	return false
}

// IsForceDetachable checks if a share client allows force-detach
func (um *UseManager) IsForceDetachable(clientID string) bool {
	um.mu.RLock()
	defer um.mu.RUnlock()
	return um.forceDetachable[clientID]
}

// ForceDetachDevice sends a force-release request to the share client
func (um *UseManager) ForceDetachDevice(clientID, busID string) error {
	return um.client.SendJSON(&protocol.ForceRelease{
		Type:         protocol.MsgForceRelease,
		TargetClient: clientID,
		BusID:        busID,
	})
}

// resolvePending hands a response to the waiting AttachDevice call and removes
// the request, so that a duplicate or late reply cannot reach the channel
// twice — a grant arriving after a denial closed it would panic.
func (um *UseManager) resolvePending(requestID string) (*pendingRequest, bool) {
	um.mu.Lock()
	defer um.mu.Unlock()

	req, exists := um.pending[requestID]
	if exists {
		delete(um.pending, requestID)
	}
	return req, exists
}

func (um *UseManager) handleDeviceGranted(msg *protocol.DeviceGranted) {
	req, exists := um.resolvePending(msg.RequestID)
	if !exists {
		// Nobody is waiting any more — the request timed out, or the caller
		// gave up. The share client has already claimed the device for us, so
		// hand it back instead of leaving it stuck in "in use".
		log.Printf("[use] late grant for %s, releasing it again", msg.BusID)
		um.releaseOrphanedGrant(msg)
		return
	}

	// The channel is buffered with capacity 1 and we are the only sender for
	// this request ID, so this never blocks.
	req.resp <- msg
}

// releaseOrphanedGrant tells the share client to take back a device that was
// granted to a request nobody is waiting for.
func (um *UseManager) releaseOrphanedGrant(msg *protocol.DeviceGranted) {
	// Find who owns this bus ID; the grant message does not name the sender.
	um.mu.RLock()
	var owner string
	for clientID, devs := range um.available {
		for _, d := range devs {
			if d.BusID == msg.BusID {
				owner = clientID
				break
			}
		}
		if owner != "" {
			break
		}
	}
	um.mu.RUnlock()

	if owner == "" {
		return
	}

	um.client.SendJSON(&protocol.ReleaseDevice{
		Type:         protocol.MsgReleaseDevice,
		TargetClient: owner,
		BusID:        msg.BusID,
	})
}

func (um *UseManager) handleDeviceDenied(msg *protocol.DeviceDenied) {
	log.Printf("[use] device request denied: %s - %s", msg.BusID, msg.Reason)

	if req, exists := um.resolvePending(msg.RequestID); exists {
		close(req.resp) // a closed channel reads as a denial
	}
}

func (um *UseManager) handleDeviceReleased(msg *protocol.DeviceReleased) {
	log.Printf("[use] device released by share client: %s", msg.BusID)

	um.mu.Lock()
	// Find and clean up any attached device matching this BusID (and ClientID if provided)
	for key, dev := range um.attached {
		if dev.BusID != msg.BusID {
			continue
		}
		if msg.ClientID != "" && dev.ClientID != msg.ClientID {
			continue
		}

		um.closeAttachedLocked(key, dev)
		log.Printf("[use] device %s cleaned up (released by share client)", key)
		break
	}
	um.mu.Unlock()
}

func (um *UseManager) handleTunnelData(tunnelID string, data []byte) {
	um.mu.RLock()
	tunnel, exists := um.tunnels[tunnelID]
	um.mu.RUnlock()

	if !exists {
		// In "both" mode the share manager sees the same frames and owns the
		// other tunnels, so an unknown ID here is normal, not an error.
		return
	}

	// A tunnel running directly gets its frames from that connection; anything
	// arriving via the relay for it is stale or spoofed.
	if tunnel.direct != nil {
		return
	}

	payload, err := tunnel.codec.decode(data)
	if err != nil {
		log.Printf("[use] tunnel %s: rejecting relayed frame: %v", protocol.ShortID(tunnelID), err)
		tunnel.closeOnce.Do(func() { close(tunnel.done) })
		tunnel.conn.Close()
		return
	}

	if protocol.Debug {
		usbip.TraceResponse("use-tunnel", payload)
	}

	// Write to the tunnel socket (peer -> VHCI). A failed write would desync
	// the USB/IP stream permanently, so treat it as fatal for this tunnel.
	if _, err := tunnel.conn.Write(payload); err != nil {
		log.Printf("[use] tunnel %s write error: %v", protocol.ShortID(tunnelID), err)
		tunnel.closeOnce.Do(func() { close(tunnel.done) })
		tunnel.conn.Close()
	}
}

func (um *UseManager) handleClientLeft(msg *protocol.ClientLeft) {
	um.mu.Lock()
	delete(um.available, msg.ClientID)
	delete(um.forceDetachable, msg.ClientID)

	// Detach any devices from this client
	for key, dev := range um.attached {
		if dev.ClientID == msg.ClientID {
			um.closeAttachedLocked(key, dev)
			log.Printf("[use] device %s auto-detached (client left)", key)
		}
	}
	um.mu.Unlock()
}

// Cleanup releases all attached devices
func (um *UseManager) Cleanup() {
	um.mu.Lock()
	defer um.mu.Unlock()

	for key, dev := range um.attached {
		um.closeAttachedLocked(key, dev)
		log.Printf("[use] cleaned up device %s", key)
	}

	um.attached = make(map[string]*AttachedDevice)
	um.tunnels = make(map[string]*useTunnel)
}
