/*
 Copyright 2013-2014 Canonical Ltd.

 This program is free software: you can redistribute it and/or modify it
 under the terms of the GNU General Public License version 3, as published
 by the Free Software Foundation.

 This program is distributed in the hope that it will be useful, but
 WITHOUT ANY WARRANTY; without even the implied warranties of
 MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
 PURPOSE.  See the GNU General Public License for more details.

 You should have received a copy of the GNU General Public License along
 with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

package bus

// Here we define the Endpoint, which represents the DBus connection itself.

import (
	"errors"
	"fmt"
	"slices"
	"strings"

	"github.com/godbus/dbus/v5"

	"gitlab.com/ubports/development/core/lomiri-push-service/logger"
)

/*****************************************************************
 *    Endpoint (and its implementation)
 */

type DispatchMap map[string]interface{}

// Cancellable can be canceled.
type Cancellable interface {
	Cancel()
}

type signalSubscription struct {
	member      string
	signals     chan *dbus.Signal
	unsubscribe chan<- *signalSubscription
}

func (s *signalSubscription) Cancel() {
	s.unsubscribe <- s
}

// bus.Endpoint represents the DBus connection itself.
type Endpoint interface {
	GrabName(allowReplacement bool) <-chan error
	WatchSignal(member string, f func(...interface{}), d func()) (Cancellable, error)
	WatchMethod(DispatchMap, string) error
	Signal(string, string, []interface{}) error
	Call(member string, args []interface{}, rvs ...interface{}) error
	GetProperty(property string) (interface{}, error)
	SetProperty(property string, suffix string, value interface{}) error
	WatchProperties(f func(map[string]dbus.Variant, []string), d func()) (Cancellable, error)
	Dial() error
	Close()
	String() string
}

type endpoint struct {
	bus                Bus
	conn               *dbus.Conn
	obj                dbus.BusObject
	signals            chan *dbus.Signal
	subscribeSignals   chan *signalSubscription
	unsubscribeSignals chan *signalSubscription
	done               chan struct{}
	addr               Address
	log                logger.Logger
}

// constructor
func newEndpoint(bus Bus, addr Address, log logger.Logger) *endpoint {
	return &endpoint{bus: bus, addr: addr, log: log}
}

// ensure endpoint implements Endpoint
var _ Endpoint = (*endpoint)(nil)

/*
   public methods

XXX:   these are almost entirely untested, as that would need
XXX:   integration tests we are currently missing.
*/

// Dial() (re)establishes the connection with dbus
//
// XXX: mostly untested
func (endp *endpoint) Dial() error {
	var conn *dbus.Conn
	var err error
	if endp.bus == SystemBus {
		conn, err = dbus.ConnectSystemBus()
	} else {
		conn, err = dbus.ConnectSessionBus()
	}
	if err != nil {
		return err
	}

	name := endp.addr.Name
	var hasOwner bool
	if err := conn.BusObject().Call("org.freedesktop.DBus.NameHasOwner", 0, name).Store(&hasOwner); err != nil {
		endp.log.Debugf("unable to determine ownership of %#v: %v", name, err)
		conn.Close()
		return err
	}
	if !hasOwner {
		var names []string
		if err := conn.BusObject().Call("org.freedesktop.DBus.ListActivatableNames", 0).Store(&names); err != nil {
			endp.log.Debugf("%#v has no owner, and when listing activatable: %v", name, err)
			conn.Close()
			return err
		}
		if !slices.Contains(names, name) {
			err = fmt.Errorf("%#v has no owner, and not in activatables", name)
			endp.log.Debugf("%s", err)
			conn.Close()
			return err
		}
	}
	endp.log.Debugf("%#v dialed in.", name)
	endp.conn = conn
	endp.obj = conn.Object(name, dbus.ObjectPath(endp.addr.Path))
	endp.signals = make(chan *dbus.Signal)
	conn.Signal(endp.signals)
	endp.subscribeSignals = make(chan *signalSubscription)
	endp.unsubscribeSignals = make(chan *signalSubscription)
	endp.done = make(chan struct{})
	go endp.handleSignals()
	return nil
}

// WatchSignal() takes a member name, sets up a watch for it (on the name,
// path and interface provided when creating the endpoint), and then calls f()
// with the unpacked value. If it's unable to set up the watch it returns an
// error. If the watch fails once established, d() is called. Typically f()
// sends the values over a channel, and d() would close the channel.
//
// XXX: untested
func (endp *endpoint) WatchSignal(member string, f func(...interface{}), d func()) (Cancellable, error) {
	ch := make(chan *dbus.Signal)
	sub := &signalSubscription{
		member: endp.addr.Interface + "." + member,
		signals: ch,
		unsubscribe: endp.unsubscribeSignals,
	}
	endp.subscribeSignals <- sub

	go func() {
		for {
			sig, ok := <-ch
			if !ok {
				break
			}
			f(sig.Body...)
		}
		d()
	}()
	return sub, nil
}

// Call() invokes the provided member method (on the name, path and
// interface provided when creating the endpoint). args can be built
// using bus.Args(...). The return value is unpacked into rvs before being
// returned.
//
// XXX: untested
func (endp *endpoint) Call(member string, args []interface{}, rvs ...interface{}) (err error) {
	err = endp.obj.Call(endp.addr.Interface + "." + member, 0, args...).Store(rvs...)
	return
}

// GetProperty uses the org.freedesktop.DBus.Properties interface's Get method
// to read a given property on the name, path and interface provided when
// creating the endpoint. The return value is unpacked into a dbus.Variant,
// and its value returned.
//
// XXX: untested
func (endp *endpoint) GetProperty(property string) (interface{}, error) {
	variant, err := endp.obj.GetProperty(endp.addr.Interface + "." + property)
	if err != nil {
		return nil, err
	}
	return variant.Value(), nil
}

// SetProperty calls org.freedesktop.DBus.Properties's Set method
//
// XXX: untested
func (endp *endpoint) SetProperty(property string, suffix string, value interface{}) error {
	// can't use the pre-existing ObjectProxy for this one
	obj := endp.conn.Object(endp.addr.Name, dbus.ObjectPath(endp.addr.Path+suffix))
	return obj.SetProperty(endp.addr.Interface + "." + property, value)
}

// WatchProperties() sets up a watch for
// org.freedesktop.DBus.Properties PropertiesChanged signal for the
// path and interface provided when creating the endpoint, and then
// calls f() with the unpacked value. If it's unable to set up the
// watch it returns an error. If the watch fails once established, d()
// is called. Typically f() sends the values over a channel, and d()
// would close the channel.
//
// XXX: untested
func (endp *endpoint) WatchProperties(f func(map[string]dbus.Variant, []string), d func()) (Cancellable, error) {
	ch := make(chan *dbus.Signal)
	sub := &signalSubscription{
		member: "org.freedesktop.DBus.Properties.PropertiesChanged",
		signals: ch,
		unsubscribe: endp.unsubscribeSignals,
	}
	endp.subscribeSignals <- sub

	go func() {
		for {
			sig, ok := <-ch
			if !ok {
				break
			}
			var intfName string
			var changed map[string]dbus.Variant
			var invalidated []string
			if err := dbus.Store(sig.Body, &intfName, &changed, &invalidated); err != nil {
				endp.log.Errorf("unexpected values from Properties watch: %v", err)
				break
			}
			if intfName != endp.addr.Interface {
				// ignore
				continue
			}
			f(changed, invalidated)
		}
		d()
	}()
	return sub, nil
}

// Close the connection to dbus.
//
// XXX: untested
func (endp *endpoint) Close() {
	if endp.conn != nil {
		close(endp.done)
		close(endp.subscribeSignals)
		close(endp.unsubscribeSignals)
		endp.conn.Close()
		endp.conn = nil
		endp.obj = nil
	}
}

// String() performs advanced endpoint stringification
//
// XXX: untested
func (endp *endpoint) String() string {
	return fmt.Sprintf("<Connection to %s %#v>", endp.conn.Names()[0], endp.addr)
}

// GrabName() takes over the name on the bus, reporting errors over the
// returned channel.
//
// While the first result will be nil on success, successive results would
// typically indicate another process trying to take over the name.
//
// XXX: untested
func (endp *endpoint) GrabName(allowReplacement bool) <-chan error {
	errs := make(chan error, 2)
	flags := dbus.NameFlagAllowReplacement | dbus.NameFlagReplaceExisting
	if !allowReplacement {
		flags = 0
	}
	reply, err := endp.conn.RequestName(endp.addr.Name, flags)
	if err == nil && reply != dbus.RequestNameReplyPrimaryOwner {
		switch reply {
		case dbus.RequestNameReplyInQueue:
			err = errors.New("failed to grab DBus name: in waiting queue")
		case dbus.RequestNameReplyExists:
			err = errors.New("failed to grab DBus name: already exists")
		case dbus.RequestNameReplyAlreadyOwner:
			err = errors.New("failed to grab DBus name: already owned")
		default:
			err = errors.New("failed to grab DBus name: unknown error")
		}
	}
	if err != nil {
		errs <-err
		return errs
	}

	nameLost := make(chan *dbus.Signal)
	sub := &signalSubscription{
		member: "org.freedesktop.DBus.NameLost",
		signals: nameLost,
		unsubscribe: endp.unsubscribeSignals,
	}
	endp.subscribeSignals <- sub

	go func() {
		for {
			sig := <-nameLost
			var name string
			if err := dbus.Store(sig.Body, &name); err != nil {
				errs <- fmt.Errorf("failed to parse org.freedesktop.DBus.NameLost: %w", err)
				break
			}
			if name == endp.addr.Name {
				errs <-fmt.Errorf("DBus ownership of name %s was lost", name)
				break
			}
		}
		sub.Cancel()
		close(errs)
	}()

	return errs
}

// Signal() sends out a signal called <member> containing <args>.
//
// XXX: untested
func (endp *endpoint) Signal(member string, suffix string, args []interface{}) error {
	path := dbus.ObjectPath(endp.addr.Path + suffix)
	iface := endp.addr.Interface + "." + member
	err := endp.conn.Emit(path, iface, args...)
	if err != nil {
		endp.log.Errorf("unable to send dbus signal: %v", err)
	} else {
		endp.log.Debugf("sent dbus signal %s(%#v)", member, args)
	}
	return err
}

// WatchMethod() uses the given DispatchMap to answer incoming method
// calls.
//
// XXX: untested
func (endp *endpoint) WatchMethod(dispatch DispatchMap, suffix string) error {
	fullPath := endp.addr.Path + suffix
	subTreePath, isSubTree := strings.CutSuffix(fullPath, "/*")

	var err error
	if isSubTree {
		err = endp.conn.ExportSubtreeMethodTable(
			dispatch,
			dbus.ObjectPath(subTreePath),
			endp.addr.Interface)
	} else {
		err = endp.conn.ExportMethodTable(
			dispatch,
			dbus.ObjectPath(fullPath),
			endp.addr.Interface)
	}

	if err != nil {
		endp.log.Errorf("Failed to export methods: %v", err)
	}
	return err
}

/*
   private methods
*/

// signal handling multiplexer
func (endp *endpoint) handleSignals() {
	subs := make(map[string]map[chan *dbus.Signal]bool)

loop:
	for {
		select {
		case sub := <-endp.subscribeSignals:
			if subs[sub.member] == nil {
				subs[sub.member] = make(map[chan *dbus.Signal]bool)
			}
			subs[sub.member][sub.signals] = true
		case sub := <-endp.unsubscribeSignals:
			if subs[sub.member][sub.signals] {
				delete(subs[sub.member], sub.signals)
				close(sub.signals)
			}
		case sig := <-endp.signals:
			for ch, _ := range subs[sig.Name] {
				ch <- sig
			}
		case <-endp.done:
			break loop
		}
	}

	for _, chans := range subs {
		for c, _ := range chans {
			close(c)
		}
	}
	clear(subs)
}
