//go:build linux

package settings

import (
	"context"
	"strings"
	"time"

	"github.com/sagernet/sing-box/adapter"

	"github.com/godbus/dbus/v5"
)

type networkManagerMonitor struct {
	conn       *dbus.Conn
	callback   func(adapter.WIFIState)
	cancel     context.CancelFunc
	signalChan chan *dbus.Signal
}

func newNetworkManagerMonitor(callback func(adapter.WIFIState)) (WIFIMonitor, error) {
	conn, err := dbus.ConnectSystemBus()
	if err != nil {
		return nil, err
	}
	nmObj := conn.Object("org.freedesktop.NetworkManager", "/org/freedesktop/NetworkManager")
	ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
	defer cancel()
	var state uint32
	err = nmObj.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, "org.freedesktop.NetworkManager", "State").Store(&state)
	if err != nil {
		conn.Close()
		return nil, err
	}
	return &networkManagerMonitor{conn: conn, callback: callback}, nil
}

func (m *networkManagerMonitor) ReadWIFIState(ctx context.Context) adapter.WIFIState {
	ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
	defer cancel()

	nmObj := m.conn.Object("org.freedesktop.NetworkManager", "/org/freedesktop/NetworkManager")

	var activeConnectionPaths []dbus.ObjectPath
	err := nmObj.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, "org.freedesktop.NetworkManager", "ActiveConnections").Store(&activeConnectionPaths)
	if err != nil || len(activeConnectionPaths) == 0 {
		return adapter.WIFIState{}
	}

	for _, connectionPath := range activeConnectionPaths {
		connObj := m.conn.Object("org.freedesktop.NetworkManager", connectionPath)

		var devicePaths []dbus.ObjectPath
		err = connObj.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, "org.freedesktop.NetworkManager.Connection.Active", "Devices").Store(&devicePaths)
		if err != nil || len(devicePaths) == 0 {
			continue
		}

		for _, devicePath := range devicePaths {
			deviceObj := m.conn.Object("org.freedesktop.NetworkManager", devicePath)

			var deviceType uint32
			err = deviceObj.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, "org.freedesktop.NetworkManager.Device", "DeviceType").Store(&deviceType)
			if err != nil || deviceType != 2 {
				continue
			}

			var accessPointPath dbus.ObjectPath
			err = deviceObj.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, "org.freedesktop.NetworkManager.Device.Wireless", "ActiveAccessPoint").Store(&accessPointPath)
			if err != nil || accessPointPath == "/" {
				continue
			}

			apObj := m.conn.Object("org.freedesktop.NetworkManager", accessPointPath)

			var ssidBytes []byte
			err = apObj.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, "org.freedesktop.NetworkManager.AccessPoint", "Ssid").Store(&ssidBytes)
			if err != nil {
				continue
			}

			var hwAddress string
			err = apObj.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, "org.freedesktop.NetworkManager.AccessPoint", "HwAddress").Store(&hwAddress)
			if err != nil {
				continue
			}

			ssid := strings.TrimSpace(string(ssidBytes))
			if ssid == "" {
				continue
			}

			return adapter.WIFIState{
				SSID:  ssid,
				BSSID: strings.ToUpper(strings.ReplaceAll(hwAddress, ":", "")),
			}
		}
	}

	return adapter.WIFIState{}
}

func (m *networkManagerMonitor) Start() error {
	if m.callback == nil {
		return nil
	}
	ctx, cancel := context.WithCancel(context.Background())
	m.cancel = cancel

	m.signalChan = make(chan *dbus.Signal, 10)
	m.conn.Signal(m.signalChan)

	err := m.conn.AddMatchSignal(
		dbus.WithMatchSender("org.freedesktop.NetworkManager"),
		dbus.WithMatchInterface("org.freedesktop.DBus.Properties"),
	)
	if err != nil {
		return err
	}

	state := m.ReadWIFIState(ctx)
	go m.monitorSignals(ctx, m.signalChan, state)
	m.callback(state)

	return nil
}

func (m *networkManagerMonitor) monitorSignals(ctx context.Context, signalChan chan *dbus.Signal, lastState adapter.WIFIState) {
	for {
		select {
		case <-ctx.Done():
			return
		case signal, ok := <-signalChan:
			if !ok {
				return
			}
			if signal.Name == "org.freedesktop.DBus.Properties.PropertiesChanged" {
				state := m.ReadWIFIState(ctx)
				if state != lastState {
					lastState = state
					m.callback(state)
				}
			}
		}
	}
}

func (m *networkManagerMonitor) Close() error {
	if m.cancel != nil {
		m.cancel()
	}
	if m.signalChan != nil {
		m.conn.RemoveSignal(m.signalChan)
		close(m.signalChan)
	}
	if m.conn != nil {
		m.conn.RemoveMatchSignal(
			dbus.WithMatchSender("org.freedesktop.NetworkManager"),
			dbus.WithMatchInterface("org.freedesktop.DBus.Properties"),
		)
		return m.conn.Close()
	}
	return nil
}
