package main

import (
	"context"
	"sync"

	"github.com/godbus/dbus/v5"
	"go.arsenm.dev/infinitime"
	"go.arsenm.dev/itd/internal/utils"
	"go.arsenm.dev/logger/log"
)

func initCallNotifs(ctx context.Context, dev *infinitime.Device) error {
	// Connect to system bus. This connection is for method calls.
	conn, err := utils.NewSystemBusConn(ctx)
	if err != nil {
		return err
	}

	// Check if modem manager interface exists
	exists, err := modemManagerExists(ctx, conn)
	if err != nil {
		return err
	}

	// If it does not exist, stop function
	if !exists {
		conn.Close()
		return nil
	}

	// Connect to system bus. This connection is for monitoring.
	monitorConn, err := utils.NewSystemBusConn(ctx)
	if err != nil {
		return err
	}

	// Add match for new calls to monitor connection
	err = monitorConn.AddMatchSignal(
		dbus.WithMatchSender("org.freedesktop.ModemManager1"),
		dbus.WithMatchInterface("org.freedesktop.ModemManager1.Modem.Voice"),
		dbus.WithMatchMember("CallAdded"),
	)
	if err != nil {
		return err
	}

	// Create channel to receive calls
	callCh := make(chan *dbus.Message, 5)
	// Notify channel upon received message
	monitorConn.Eavesdrop(callCh)

	var respHandlerOnce sync.Once
	var callObj dbus.BusObject

	go func() {
		// For every message received
		for event := range callCh {
			// Get path to call object
			callPath := event.Body[0].(dbus.ObjectPath)
			// Get call object
			callObj = conn.Object("org.freedesktop.ModemManager1", callPath)

			// Get phone number from call object using method call connection
			phoneNum, err := getPhoneNum(conn, callObj)
			if err != nil {
				log.Error("Error getting phone number").Err(err).Send()
				continue
			}

			// Send call notification to InfiniTime
			resCh, err := dev.NotifyCall(phoneNum)
			if err != nil {
				continue
			}

			go respHandlerOnce.Do(func() {
				// Wait for PineTime response
				for res := range resCh {
					switch res {
					case infinitime.CallStatusAccepted:
						// Attempt to accept call
						err = acceptCall(ctx, conn, callObj)
						if err != nil {
							log.Warn("Error accepting call").Err(err).Send()
						}
					case infinitime.CallStatusDeclined:
						// Attempt to decline call
						err = declineCall(ctx, conn, callObj)
						if err != nil {
							log.Warn("Error declining call").Err(err).Send()
						}
					case infinitime.CallStatusMuted:
						// Warn about unimplemented muting
						log.Warn("Muting calls is not implemented").Send()
					}
				}
			})
		}
	}()

	log.Info("Relaying calls to InfiniTime").Send()
	return nil
}

func modemManagerExists(ctx context.Context, conn *dbus.Conn) (bool, error) {
	var names []string
	err := conn.BusObject().CallWithContext(
		ctx, "org.freedesktop.DBus.ListNames", 0,
	).Store(&names)
	if err != nil {
		return false, err
	}
	return strSlcContains(names, "org.freedesktop.ModemManager1"), nil
}

// getPhoneNum gets a phone number from a call object using a DBus connection
func getPhoneNum(conn *dbus.Conn, callObj dbus.BusObject) (string, error) {
	var out string
	// Get number property on DBus object and store return value in out
	err := callObj.StoreProperty("org.freedesktop.ModemManager1.Call.Number", &out)
	if err != nil {
		return "", err
	}
	return out, nil
}

// getPhoneNum accepts a call using a DBus connection
func acceptCall(ctx context.Context, conn *dbus.Conn, callObj dbus.BusObject) error {
	// Call Accept() method on DBus object
	call := callObj.CallWithContext(
		ctx, "org.freedesktop.ModemManager1.Call.Accept", 0,
	)
	if call.Err != nil {
		return call.Err
	}
	return nil
}

// getPhoneNum declines a call using a DBus connection
func declineCall(ctx context.Context, conn *dbus.Conn, callObj dbus.BusObject) error {
	// Call Hangup() method on DBus object
	call := callObj.CallWithContext(
		ctx, "org.freedesktop.ModemManager1.Call.Hangup", 0,
	)
	if call.Err != nil {
		return call.Err
	}
	return nil
}