package main

import (
	"context"
	"errors"
	"sync"

	notify "github.com/dhaavi/go-notify"

	"github.com/safing/portbase/log"
)

type NotificationID uint32

var (
	capabilities notify.Capabilities
	notifsByID   sync.Map
)

func init() {
	var err error
	capabilities, err = notify.GetCapabilities()
	if err != nil {
		log.Errorf("failed to get notification system capabilities: %s", err)
	}
}

func handleActions(ctx context.Context, actions chan notify.Signal) {
	mainWg.Add(1)
	defer mainWg.Done()

listenForNotifications:
	for {
		select {
		case <-ctx.Done():
			return
		case sig := <-actions:
			if sig.Name != "org.freedesktop.Notifications.ActionInvoked" {
				// we don't care for anything else (dismissed, closed)
				continue listenForNotifications
			}

			// get notification by system ID
			n, ok := notifsByID.LoadAndDelete(NotificationID(sig.ID))

			if !ok {
				continue listenForNotifications
			}

			notification, ok := n.(*Notification)
			if !ok {
				log.Errorf("received invalid notification type %T", n)

				continue listenForNotifications
			}

			log.Tracef("notify: received signal: %+v", sig)
			if sig.ActionKey != "" {
				// send action
				if ok {
					notification.Lock()
					notification.SelectAction(sig.ActionKey)
					notification.Unlock()
				}
			} else {
				log.Tracef("notify: notification clicked: %+v", sig)
				// Global action invoked, start the app
				launchApp()
			}
		}
	}
}

func actionListener() {
	actions := make(chan notify.Signal, 100)

	go handleActions(mainCtx, actions)

	err := notify.SignalNotify(mainCtx, actions)
	if err != nil && errors.Is(err, context.Canceled) {
		log.Errorf("notify: signal listener failed: %s", err)
	}
}

// Show shows the notification.
func (n *Notification) Show() {
	sysN := notify.NewNotification("Portmaster", n.Message)
	// see https://developer.gnome.org/notification-spec/

	// The optional name of the application sending the notification.
	// Can be blank.
	sysN.AppName = "Portmaster"

	// The optional notification ID that this notification replaces.
	sysN.ReplacesID = uint32(n.systemID)

	// The optional program icon of the calling application.
	// sysN.AppIcon string

	// The summary text briefly describing the notification.
	// Summary string (arg 1)

	// The optional detailed body text.
	// Body string (arg 2)

	// The actions send a request message back to the notification client
	// when invoked.
	// sysN.Actions []string
	if capabilities.Actions {
		sysN.Actions = make([]string, 0, len(n.AvailableActions)*2)
		for _, action := range n.AvailableActions {
			if IsSupportedAction(*action) {
				sysN.Actions = append(sysN.Actions, action.ID)
				sysN.Actions = append(sysN.Actions, action.Text)
			}
		}
	}

	// Set Portmaster icon.
	iconLocation, err := ensureAppIcon()
	if err != nil {
		log.Warningf("notify: failed to write icon: %s", err)
	}
	sysN.AppIcon = iconLocation

	// TODO: Use hints to display icon of affected app.
	// Hints are a way to provide extra data to a notification server.
	// sysN.Hints = make(map[string]interface{})

	// The timeout time in milliseconds since the display of the
	// notification at which the notification should automatically close.
	// sysN.Timeout int32

	newID, err := sysN.Show()
	if err != nil {
		log.Warningf("notify: failed to show notification %s", n.EventID)
		return
	}

	notifsByID.Store(NotificationID(newID), n)

	n.Lock()
	defer n.Unlock()
	n.systemID = NotificationID(newID)
}

// Cancel cancels the notification.
func (n *Notification) Cancel() {
	n.Lock()
	defer n.Unlock()

	// TODO: could a ID of 0 be valid?
	if n.systemID != 0 {
		err := notify.CloseNotification(uint32(n.systemID))
		if err != nil {
			log.Warningf("notify: failed to close notification %s/%d", n.EventID, n.systemID)
		}
		notifsByID.Delete(n.systemID)
	}
}
