//go:build linux

package settings

import (
	"context"
	"strings"
	"time"

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

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

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

func newConnManMonitor(callback func(adapter.WIFIState)) (WIFIMonitor, error) {
	conn, err := dbus.ConnectSystemBus()
	if err != nil {
		return nil, err
	}
	cmObj := conn.Object("net.connman", "/")
	ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
	defer cancel()
	call := cmObj.CallWithContext(ctx, "net.connman.Manager.GetServices", 0)
	if call.Err != nil {
		conn.Close()
		return nil, call.Err
	}
	return &connmanMonitor{conn: conn, callback: callback}, nil
}

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

	cmObj := m.conn.Object("net.connman", "/")
	var services []any
	err := cmObj.CallWithContext(ctx, "net.connman.Manager.GetServices", 0).Store(&services)
	if err != nil {
		return adapter.WIFIState{}
	}

	for _, service := range services {
		servicePair, ok := service.([]any)
		if !ok || len(servicePair) != 2 {
			continue
		}

		serviceProps, ok := servicePair[1].(map[string]dbus.Variant)
		if !ok {
			continue
		}

		typeVariant, hasType := serviceProps["Type"]
		if !hasType {
			continue
		}
		serviceType, ok := typeVariant.Value().(string)
		if !ok || serviceType != "wifi" {
			continue
		}

		stateVariant, hasState := serviceProps["State"]
		if !hasState {
			continue
		}
		state, ok := stateVariant.Value().(string)
		if !ok || (state != "online" && state != "ready") {
			continue
		}

		nameVariant, hasName := serviceProps["Name"]
		if !hasName {
			continue
		}
		ssid, ok := nameVariant.Value().(string)
		if !ok || ssid == "" {
			continue
		}

		bssidVariant, hasBSSID := serviceProps["BSSID"]
		if !hasBSSID {
			return adapter.WIFIState{SSID: ssid}
		}
		bssid, ok := bssidVariant.Value().(string)
		if !ok {
			return adapter.WIFIState{SSID: ssid}
		}

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

	return adapter.WIFIState{}
}

func (m *connmanMonitor) 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.WithMatchInterface("net.connman.Service"),
		dbus.WithMatchSender("net.connman"),
	)
	if err != nil {
		return err
	}

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

	return nil
}

func (m *connmanMonitor) monitorSignals(ctx context.Context, signalChan chan *dbus.Signal, lastState adapter.WIFIState) {
	for {
		select {
		case <-ctx.Done():
			return
		case signal, ok := <-signalChan:
			if !ok {
				return
			}
			// godbus Signal.Name uses "interface.member" format (e.g. "net.connman.Service.PropertyChanged"),
			// not just the member name. This differs from the D-Bus signal member in the match rule.
			if signal.Name == "net.connman.Service.PropertyChanged" {
				state := m.ReadWIFIState(ctx)
				if state != lastState {
					lastState = state
					m.callback(state)
				}
			}
		}
	}
}

func (m *connmanMonitor) 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.WithMatchInterface("net.connman.Service"),
			dbus.WithMatchSender("net.connman"),
		)
		return m.conn.Close()
	}
	return nil
}
