//go:build windows

package rdp

import (
	"fmt"
	"sync"
	"syscall"
	"time"
	"unsafe"
)

var (
	libfreerdp       = syscall.NewLazyDLL("libfreerdp3.dll")
	libfreerdpClient = syscall.NewLazyDLL("libfreerdp-client3.dll")

	// Instance lifecycle
	procFreerdpNew        = libfreerdp.NewProc("freerdp_new")
	procFreerdpFree       = libfreerdp.NewProc("freerdp_free")
	procFreerdpConnect    = libfreerdp.NewProc("freerdp_connect")
	procFreerdpDisconnect = libfreerdp.NewProc("freerdp_disconnect")

	// Settings
	procSettingsSetString = libfreerdp.NewProc("freerdp_settings_set_string")
	procSettingsSetUint32 = libfreerdp.NewProc("freerdp_settings_set_uint32")
	procSettingsSetBool   = libfreerdp.NewProc("freerdp_settings_set_bool")

	// Input
	procInputSendMouse    = libfreerdp.NewProc("freerdp_input_send_mouse_event")
	procInputSendKeyboard = libfreerdp.NewProc("freerdp_input_send_keyboard_event")

	// Event loop
	procCheckEventHandles = libfreerdp.NewProc("freerdp_check_event_handles")

	// Client helpers
	procClientNew  = libfreerdpClient.NewProc("freerdp_client_context_new")
	procClientFree = libfreerdpClient.NewProc("freerdp_client_context_free")
)

// FreeRDP settings IDs (from FreeRDP3 freerdp/settings.h)
const (
	FreeRDP_ServerHostname     = 20
	FreeRDP_ServerPort         = 21
	FreeRDP_Username           = 22
	FreeRDP_Password           = 23
	FreeRDP_Domain             = 24
	FreeRDP_DesktopWidth       = 1025
	FreeRDP_DesktopHeight      = 1026
	FreeRDP_ColorDepth         = 1027
	FreeRDP_FullscreenMode     = 1028
	FreeRDP_IgnoreCertificate  = 4556
	FreeRDP_AuthenticationOnly = 4554
	FreeRDP_NlaSecurity        = 4560
	FreeRDP_TlsSecurity        = 4561
	FreeRDP_RdpSecurity        = 4562
)

// Keyboard event flags for FreeRDP input calls.
const (
	KBD_FLAGS_EXTENDED = 0x0100
	KBD_FLAGS_DOWN     = 0x4000
	KBD_FLAGS_RELEASE  = 0x8000
)

// FreeRDPBackend implements RDPBackend using the FreeRDP3 library loaded at
// runtime via syscall.NewLazyDLL. This avoids any CGO dependency and allows
// cross-compilation from Linux while the DLLs are resolved at runtime on
// Windows.
type FreeRDPBackend struct {
	instance  uintptr // freerdp*
	settings  uintptr // rdpSettings*
	input     uintptr // rdpInput*
	buffer    *PixelBuffer
	connected bool
	config    RDPConfig
	mu        sync.Mutex
	stopCh    chan struct{}
}

// NewFreeRDPBackend creates a new FreeRDP-backed RDP backend. The underlying
// DLLs are not loaded until Connect is called.
func NewFreeRDPBackend() *FreeRDPBackend {
	return &FreeRDPBackend{
		stopCh: make(chan struct{}),
	}
}

// Connect establishes an RDP session using FreeRDP3. It creates a new FreeRDP
// instance, configures connection settings, and starts the event loop.
func (f *FreeRDPBackend) Connect(config RDPConfig) error {
	f.mu.Lock()
	defer f.mu.Unlock()

	if f.connected {
		return fmt.Errorf("already connected")
	}

	f.config = config

	// Create a bare FreeRDP instance.
	ret, _, err := procFreerdpNew.Call()
	if ret == 0 {
		return fmt.Errorf("freerdp_new failed: %v", err)
	}
	f.instance = ret

	// Configure connection settings via the settings accessor functions.
	f.setString(FreeRDP_ServerHostname, config.Hostname)
	f.setUint32(FreeRDP_ServerPort, uint32(config.Port))
	f.setString(FreeRDP_Username, config.Username)
	f.setString(FreeRDP_Password, config.Password)
	if config.Domain != "" {
		f.setString(FreeRDP_Domain, config.Domain)
	}

	// Display settings with sensible defaults.
	width := config.Width
	if width == 0 {
		width = 1920
	}
	height := config.Height
	if height == 0 {
		height = 1080
	}
	f.setUint32(FreeRDP_DesktopWidth, uint32(width))
	f.setUint32(FreeRDP_DesktopHeight, uint32(height))

	colorDepth := config.ColorDepth
	if colorDepth == 0 {
		colorDepth = 32
	}
	f.setUint32(FreeRDP_ColorDepth, uint32(colorDepth))

	// Security mode selection.
	switch config.Security {
	case "nla":
		f.setBool(FreeRDP_NlaSecurity, true)
	case "tls":
		f.setBool(FreeRDP_TlsSecurity, true)
	case "rdp":
		f.setBool(FreeRDP_RdpSecurity, true)
	default:
		f.setBool(FreeRDP_NlaSecurity, true)
	}

	// Accept all server certificates. Per-host pinning is a future enhancement.
	f.setBool(FreeRDP_IgnoreCertificate, true)

	// Allocate the pixel buffer for frame capture.
	f.buffer = NewPixelBuffer(width, height)

	// TODO: Register PostConnect callback to set up bitmap update handler.
	// TODO: Register BitmapUpdate callback to write frames into f.buffer.

	// Initiate the RDP connection.
	ret, _, err = procFreerdpConnect.Call(f.instance)
	if ret == 0 {
		procFreerdpFree.Call(f.instance)
		f.instance = 0
		return fmt.Errorf("freerdp_connect failed: %v", err)
	}

	f.connected = true

	// Start the event processing loop in a background goroutine.
	go f.eventLoop()

	return nil
}

// eventLoop polls FreeRDP for incoming events at roughly 60 fps. It runs
// until the stop channel is closed or the connection is marked as inactive.
func (f *FreeRDPBackend) eventLoop() {
	for {
		select {
		case <-f.stopCh:
			return
		default:
			f.mu.Lock()
			if !f.connected {
				f.mu.Unlock()
				return
			}
			procCheckEventHandles.Call(f.instance)
			f.mu.Unlock()
			time.Sleep(16 * time.Millisecond) // ~60 fps
		}
	}
}

// Disconnect tears down the RDP session and frees the FreeRDP instance.
func (f *FreeRDPBackend) Disconnect() error {
	f.mu.Lock()
	defer f.mu.Unlock()

	if !f.connected {
		return nil
	}

	close(f.stopCh)
	f.connected = false

	procFreerdpDisconnect.Call(f.instance)
	procFreerdpFree.Call(f.instance)
	f.instance = 0

	return nil
}

// SendMouseEvent forwards a mouse event to the remote RDP session.
func (f *FreeRDPBackend) SendMouseEvent(x, y int, flags uint32) error {
	f.mu.Lock()
	defer f.mu.Unlock()

	if !f.connected || f.input == 0 {
		return fmt.Errorf("not connected")
	}
	procInputSendMouse.Call(f.input, uintptr(flags), uintptr(x), uintptr(y))
	return nil
}

// SendKeyEvent forwards a keyboard event to the remote RDP session. The
// scancode uses the same format as the ScancodeMap in input.go — extended
// keys have the 0xE0 prefix in the high byte.
func (f *FreeRDPBackend) SendKeyEvent(scancode uint32, pressed bool) error {
	f.mu.Lock()
	defer f.mu.Unlock()

	if !f.connected || f.input == 0 {
		return fmt.Errorf("not connected")
	}

	var flags uint32
	if pressed {
		flags = KBD_FLAGS_DOWN
	} else {
		flags = KBD_FLAGS_RELEASE
	}
	if scancode > 0xFF {
		flags |= KBD_FLAGS_EXTENDED
	}
	procInputSendKeyboard.Call(f.input, uintptr(flags), uintptr(scancode&0xFF))
	return nil
}

// SendClipboard sends clipboard text to the remote session.
// TODO: Implement via the FreeRDP cliprdr channel.
func (f *FreeRDPBackend) SendClipboard(data string) error {
	return nil
}

// GetFrame returns the current full-frame RGBA pixel buffer. The frame is
// populated by bitmap update callbacks registered during PostConnect.
func (f *FreeRDPBackend) GetFrame() ([]byte, error) {
	if f.buffer == nil {
		return nil, fmt.Errorf("no frame buffer")
	}
	return f.buffer.GetFrame(), nil
}

// IsConnected reports whether the backend has an active RDP connection.
func (f *FreeRDPBackend) IsConnected() bool {
	f.mu.Lock()
	defer f.mu.Unlock()
	return f.connected
}

// setString sets a string setting on the FreeRDP instance.
func (f *FreeRDPBackend) setString(id int, value string) {
	b, _ := syscall.BytePtrFromString(value)
	procSettingsSetString.Call(f.settings, uintptr(id), uintptr(unsafe.Pointer(b)))
}

// setUint32 sets a uint32 setting on the FreeRDP instance.
func (f *FreeRDPBackend) setUint32(id int, value uint32) {
	procSettingsSetUint32.Call(f.settings, uintptr(id), uintptr(value))
}

// setBool sets a boolean setting on the FreeRDP instance.
func (f *FreeRDPBackend) setBool(id int, value bool) {
	v := uintptr(0)
	if value {
		v = 1
	}
	procSettingsSetBool.Call(f.settings, uintptr(id), v)
}

// Ensure FreeRDPBackend satisfies the RDPBackend interface at compile time.
var _ RDPBackend = (*FreeRDPBackend)(nil)
