package evdev

import (
	"fmt"
	"os"
	"path/filepath"
	"strings"
	"sync"
	"time"

	"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
	"github.com/AvengeMedia/dankgo/syncmap"
	"github.com/fsnotify/fsnotify"
	evdev "github.com/holoplot/go-evdev"
)

const (
	evKeyType      = 0x01
	evLedType      = 0x11
	keyCapslockKey = 58
	ledCapslockKey = 1
	keyStateOn     = 1
)

type EvdevDevice interface {
	Name() (string, error)
	Path() string
	Close() error
	ReadOne() (*evdev.InputEvent, error)
	State(t evdev.EvType) (evdev.StateMap, error)
}

type Manager struct {
	devices        []EvdevDevice
	devicesMutex   sync.RWMutex
	monitoredPaths map[string]bool
	state          State
	stateMutex     sync.RWMutex
	subscribers    syncmap.Map[string, chan State]
	closeChan      chan struct{}
	closeOnce      sync.Once
	watcher        *fsnotify.Watcher
}

func NewManager() (*Manager, error) {
	devices, err := findKeyboards()
	if err != nil {
		return nil, fmt.Errorf("failed to find keyboards: %w", err)
	}

	initialCapsLock, _ := capsLockFromDevices(devices)

	watcher, err := fsnotify.NewWatcher()
	if err != nil {
		log.Warnf("Failed to create fsnotify watcher, hotplug detection disabled: %v", err)
		watcher = nil
	} else if err := watcher.Add("/dev/input"); err != nil {
		log.Warnf("Failed to watch /dev/input, hotplug detection disabled: %v", err)
		watcher.Close()
		watcher = nil
	}

	monitoredPaths := make(map[string]bool)
	for _, device := range devices {
		monitoredPaths[device.Path()] = true
	}

	m := &Manager{
		devices:        devices,
		monitoredPaths: monitoredPaths,
		state:          State{Available: true, CapsLock: initialCapsLock},

		closeChan: make(chan struct{}),
		watcher:   watcher,
	}

	for i, device := range devices {
		go m.monitorDevice(device, i)
	}

	if watcher != nil {
		go m.watchForNewKeyboards()
	}

	return m, nil
}

func capsLockFromDevices(devices []EvdevDevice) (bool, bool) {
	for _, device := range devices {
		if device == nil {
			continue
		}

		ledStates, err := device.State(evLedType)
		if err != nil || len(ledStates) == 0 {
			continue
		}

		return ledStates[ledCapslockKey], true
	}

	return false, false
}

func findKeyboards() ([]EvdevDevice, error) {
	pattern := "/dev/input/event*"
	matches, err := filepath.Glob(pattern)
	if err != nil {
		return nil, fmt.Errorf("failed to glob input devices: %w", err)
	}

	if len(matches) == 0 {
		return nil, fmt.Errorf("no input devices found")
	}

	var keyboards []EvdevDevice
	for _, path := range matches {
		device, err := evdev.Open(path)
		if err != nil {
			continue
		}

		if !isKeyboard(device) {
			device.Close()
			continue
		}

		deviceName, _ := device.Name()
		log.Debugf("Found keyboard: %s at %s", deviceName, path)
		keyboards = append(keyboards, device)
	}

	if len(keyboards) == 0 {
		return nil, fmt.Errorf("no keyboard device found")
	}

	return keyboards, nil
}

func isKeyboard(device EvdevDevice) bool {
	deviceName, err := device.Name()
	if err != nil {
		return false
	}

	name := strings.ToLower(deviceName)

	switch {
	case strings.Contains(name, "keyboard"):
		return true
	case strings.Contains(name, "kbd"):
		return true
	case strings.Contains(name, "input") && strings.Contains(name, "key"):
		return true
	}

	keyStates, err := device.State(evKeyType)
	if err != nil {
		return false
	}

	hasKeyA := len(keyStates) > 30
	hasKeyZ := len(keyStates) > 44
	hasEnter := len(keyStates) > 28

	return hasKeyA && hasKeyZ && hasEnter && len(keyStates) > 100
}

func (m *Manager) watchForNewKeyboards() {
	defer func() {
		if r := recover(); r != nil {
			log.Errorf("Panic in keyboard hotplug monitor: %v", r)
		}
	}()

	for {
		select {
		case <-m.closeChan:
			return
		case event, ok := <-m.watcher.Events:
			if !ok {
				return
			}

			if !strings.HasPrefix(filepath.Base(event.Name), "event") {
				continue
			}

			if event.Op&fsnotify.Create == fsnotify.Create {
				time.Sleep(100 * time.Millisecond)

				m.devicesMutex.Lock()
				if m.monitoredPaths[event.Name] {
					m.devicesMutex.Unlock()
					continue
				}

				device, err := evdev.Open(event.Name)
				if err != nil {
					m.devicesMutex.Unlock()
					continue
				}

				if !isKeyboard(device) {
					device.Close()
					m.devicesMutex.Unlock()
					continue
				}

				deviceName, _ := device.Name()
				log.Debugf("Hotplugged keyboard: %s at %s", deviceName, event.Name)

				m.devices = append(m.devices, device)
				m.monitoredPaths[event.Name] = true
				deviceIndex := len(m.devices) - 1
				m.devicesMutex.Unlock()

				go m.monitorDevice(device, deviceIndex)
			} else if event.Op&fsnotify.Remove == fsnotify.Remove {
				m.devicesMutex.Lock()
				if !m.monitoredPaths[event.Name] {
					m.devicesMutex.Unlock()
					continue
				}

				delete(m.monitoredPaths, event.Name)
				for i, device := range m.devices {
					if device != nil && device.Path() == event.Name {
						log.Debugf("Keyboard removed: %s", event.Name)
						device.Close()
						m.devices[i] = nil
						break
					}
				}
				m.devicesMutex.Unlock()
			}

		case err, ok := <-m.watcher.Errors:
			if !ok {
				return
			}
			log.Warnf("Keyboard hotplug watcher error: %v", err)
		}
	}
}

func (m *Manager) monitorDevice(device EvdevDevice, deviceIndex int) {
	defer func() {
		if r := recover(); r != nil {
			log.Errorf("Panic in evdev monitor: %v", r)
		}
	}()

	for {
		select {
		case <-m.closeChan:
			return
		default:
		}

		event, err := device.ReadOne()
		if err != nil {
			if isClosedError(err) {
				return
			}
			log.Warnf("Failed to read evdev event: %v", err)
			time.Sleep(100 * time.Millisecond)
			continue
		}

		if event == nil {
			continue
		}

		if event.Type == evKeyType && event.Code == keyCapslockKey && event.Value == keyStateOn {
			time.Sleep(50 * time.Millisecond)
			m.readAndUpdateCapsLockState(deviceIndex)
		} else if event.Type == evLedType && event.Code == ledCapslockKey {
			capsLockState := event.Value == keyStateOn
			m.updateCapsLockStateDirect(capsLockState)
		}
	}
}

func isClosedError(err error) bool {
	if err == nil {
		return false
	}

	errStr := err.Error()
	switch {
	case strings.Contains(errStr, "closed"):
		return true
	case strings.Contains(errStr, "bad file descriptor"):
		return true
	default:
		return false
	}
}

func (m *Manager) readAndUpdateCapsLockState(deviceIndex int) {
	m.devicesMutex.RLock()
	if deviceIndex >= len(m.devices) {
		m.devicesMutex.RUnlock()
		return
	}
	ordered := make([]EvdevDevice, 0, len(m.devices))
	ordered = append(ordered, m.devices[deviceIndex])
	for i, device := range m.devices {
		if i == deviceIndex {
			continue
		}
		ordered = append(ordered, device)
	}
	m.devicesMutex.RUnlock()

	capsLockState, ok := capsLockFromDevices(ordered)
	if !ok {
		log.Debug("No LED-capable device available for caps lock state")
		return
	}

	m.updateCapsLockStateDirect(capsLockState)
}

func (m *Manager) updateCapsLockStateDirect(capsLockState bool) {
	m.stateMutex.Lock()
	if m.state.CapsLock == capsLockState {
		m.stateMutex.Unlock()
		return
	}

	m.state.CapsLock = capsLockState
	newState := m.state
	m.stateMutex.Unlock()

	log.Debugf("Caps lock state: %v", newState.CapsLock)
	m.notifySubscribers(newState)
}

func (m *Manager) GetState() State {
	m.stateMutex.RLock()
	defer m.stateMutex.RUnlock()
	return m.state
}

func (m *Manager) Subscribe(id string) chan State {
	ch := make(chan State, 16)
	m.subscribers.Store(id, ch)
	return ch
}

func (m *Manager) Unsubscribe(id string) {
	if val, ok := m.subscribers.LoadAndDelete(id); ok {
		close(val)
	}
}

func (m *Manager) notifySubscribers(state State) {
	m.subscribers.Range(func(key string, ch chan State) bool {
		select {
		case ch <- state:
		default:
		}
		return true
	})
}

func (m *Manager) Close() {
	m.closeOnce.Do(func() {
		close(m.closeChan)

		if m.watcher != nil {
			m.watcher.Close()
		}

		m.devicesMutex.Lock()
		for _, device := range m.devices {
			if device == nil {
				continue
			}
			if err := device.Close(); err != nil && !isClosedError(err) {
				log.Warnf("Error closing evdev device: %v", err)
			}
		}
		m.devicesMutex.Unlock()

		m.subscribers.Range(func(key string, ch chan State) bool {
			close(ch)
			m.subscribers.Delete(key)
			return true
		})
	})
}

func InitializeManager() (*Manager, error) {
	if os.Getuid() != 0 && !hasInputGroupAccess() {
		return nil, fmt.Errorf("insufficient permissions to access input devices. Add your user to the 'input' group: `sudo usermod -a -G input $USER` or run `dms setup`")
	}

	return NewManager()
}

func hasInputGroupAccess() bool {
	pattern := "/dev/input/event*"
	matches, err := filepath.Glob(pattern)
	if err != nil || len(matches) == 0 {
		return false
	}

	testFile, err := os.Open(matches[0])
	if err != nil {
		return false
	}
	testFile.Close()
	return true
}
