/*
 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 service

import (
	"encoding/json"
	"os"
	"sync"

	"github.com/godbus/dbus/v5"
	"github.com/google/uuid"

	"gitlab.com/ubports/development/core/lomiri-push-service/bus"
	"gitlab.com/ubports/development/core/lomiri-push-service/bus/accounts"
	"gitlab.com/ubports/development/core/lomiri-push-service/bus/emblemcounter"
	"gitlab.com/ubports/development/core/lomiri-push-service/bus/haptic"
	"gitlab.com/ubports/development/core/lomiri-push-service/bus/notifications"
	"gitlab.com/ubports/development/core/lomiri-push-service/bus/greeter"
	"gitlab.com/ubports/development/core/lomiri-push-service/bus/focusinfo"
	"gitlab.com/ubports/development/core/lomiri-push-service/click"
	"gitlab.com/ubports/development/core/lomiri-push-service/click/cnotificationsettings"
	"gitlab.com/ubports/development/core/lomiri-push-service/launch_helper"
	"gitlab.com/ubports/development/core/lomiri-push-service/logger"
	"gitlab.com/ubports/development/core/lomiri-push-service/messaging"
	"gitlab.com/ubports/development/core/lomiri-push-service/messaging/reply"
	"gitlab.com/ubports/development/core/lomiri-push-service/nih"
	"gitlab.com/ubports/development/core/lomiri-push-service/sounds"
	"gitlab.com/ubports/development/core/lomiri-push-service/urldispatcher"
	"gitlab.com/ubports/development/core/lomiri-push-service/util"
)

type messageHandler func(*click.AppId, string, *launch_helper.HelperOutput) bool

// a Presenter is something that knows how to present a Notification
type Presenter interface {
	Present(*click.AppId, string, *launch_helper.Notification) bool
}

type notificationCentre interface {
	Presenter
	GetCh() chan *reply.MMActionReply
	RemoveNotification(string, bool)
	Tags(*click.AppId) []string
	Clear(*click.AppId, ...string) int
}

// PostalServiceSetup is a configuration object for the service
type PostalServiceSetup struct {
	InstalledChecker  click.InstalledChecker
	FallbackVibration *launch_helper.Vibration
	FallbackSound     string
}

// PostalService is the dbus api
type PostalService struct {
	DBusService
	mbox          map[string]*mBox
	msgHandler    messageHandler
	launchers     map[string]launch_helper.HelperLauncher
	HelperPool    launch_helper.HelperPool
	messagingMenu notificationCentre
	// the endpoints are only exposed for testing from client
	// XXX: uncouple some more so this isn't necessary
	EmblemCounterEndp bus.Endpoint
	AccountsEndp      bus.Endpoint
	HapticEndp        bus.Endpoint
	NotificationsEndp bus.Endpoint
	GreeterEndp  bus.Endpoint
	FocusInfoEndp   bus.Endpoint
	// presenters:
	Presenters    []Presenter
	emblemCounter *emblemcounter.EmblemCounter
	accounts      accounts.Accounts
	haptic        *haptic.Haptic
	notifications *notifications.RawNotifications
	sound         sounds.Sound
	// the url dispatcher, used for stuff.
	urlDispatcher urldispatcher.URLDispatcher
	greeter  *greeter.Greeter
	focusInfo   *focusinfo.FocusInfo
	// fallback values for simplified notification usage
	fallbackVibration *launch_helper.Vibration
	fallbackSound     string
}

var (
	PostalServiceBusAddress = bus.Address{
		Interface: "com.lomiri.Postal",
		Path:      "/com/lomiri/Postal",
		Name:      "com.lomiri.Postal",
	}
)

var (
	SystemUpdateUrl  = "settings:///system/system-update"
	useTrivialHelper = os.Getenv("LOMIRI_PUSH_USE_TRIVIAL_HELPER") != ""
)

// NewPostalService() builds a new service and returns it.
func NewPostalService(setup *PostalServiceSetup, log logger.Logger) *PostalService {
	var svc = &PostalService{}
	svc.Log = log
	svc.Bus = bus.SessionBus.Endpoint(PostalServiceBusAddress, log)
	svc.installedChecker = setup.InstalledChecker
	svc.fallbackVibration = setup.FallbackVibration
	svc.fallbackSound = setup.FallbackSound
	svc.NotificationsEndp = bus.SessionBus.Endpoint(notifications.BusAddress, log)
	svc.EmblemCounterEndp = bus.SessionBus.Endpoint(emblemcounter.BusAddress, log)
	svc.AccountsEndp = bus.SystemBus.Endpoint(accounts.BusAddress, log)
	svc.HapticEndp = bus.SystemBus.Endpoint(haptic.HfdBusAddress, log)
	svc.GreeterEndp = bus.SessionBus.Endpoint(greeter.BusAddress, log)
	svc.FocusInfoEndp = bus.SessionBus.Endpoint(focusinfo.BusAddress, log)
	svc.msgHandler = svc.messageHandler
	svc.launchers = launch_helper.DefaultLaunchers(log)
	return svc
}

// SetMessageHandler() sets the message-handling callback
func (svc *PostalService) SetMessageHandler(callback messageHandler) {
	svc.lock.RLock()
	defer svc.lock.RUnlock()
	svc.msgHandler = callback
}

// GetMessageHandler() returns the (possibly nil) messaging handler callback
func (svc *PostalService) GetMessageHandler() messageHandler {
	svc.lock.RLock()
	defer svc.lock.RUnlock()
	return svc.msgHandler
}

// Start() dials the bus, grab the name, and listens for method calls.
func (svc *PostalService) Start() error {
	return svc.DBusService.Start(bus.DispatchMap{
		"PopAll":              svc.popAll,
		"Post":                svc.post,
		"ListPersistent":      svc.listPersistent,
		"ClearPersistent":     svc.clearPersistent,
		"ClearPersistentList": svc.clearPersistentList,
		"SetCounter":          svc.setCounter,
	}, PostalServiceBusAddress, svc.init)
}

func (svc *PostalService) init() error {
	actionsCh, err := svc.takeTheBus()
	if err != nil {
		return err
	}
	svc.urlDispatcher = urldispatcher.New(svc.Log)

	svc.accounts = accounts.New(svc.AccountsEndp, svc.Log)
	err = svc.accounts.Start()
	if err != nil {
		return err
	}

	svc.sound = sounds.New(svc.Log, svc.accounts, svc.fallbackSound)
	svc.notifications = notifications.Raw(svc.NotificationsEndp, svc.Log, svc.sound)
	svc.emblemCounter = emblemcounter.New(svc.EmblemCounterEndp, svc.Log)
	svc.haptic = haptic.New(svc.HapticEndp, svc.Log, svc.accounts, svc.fallbackVibration)
	svc.messagingMenu = messaging.New(svc.Log)
	svc.Presenters = []Presenter{
		svc.notifications,
		svc.emblemCounter,
		svc.haptic,
		svc.messagingMenu,
	}
	if useTrivialHelper {
		svc.HelperPool = launch_helper.NewTrivialHelperPool(svc.Log)
	} else {
		svc.HelperPool = launch_helper.NewHelperPool(svc.launchers, svc.Log)
	}
	svc.greeter = greeter.New(svc.GreeterEndp, svc.Log)
	svc.focusInfo = focusinfo.New(svc.FocusInfoEndp, svc.Log)

	go svc.consumeHelperResults(svc.HelperPool.Start())
	go svc.handleActions(actionsCh, svc.messagingMenu.GetCh())
	return nil
}

// xxx Stop() closing channels and helper launcher

// handleactions loops on the actions channels waiting for actions and handling them
func (svc *PostalService) handleActions(actionsCh <-chan *notifications.RawAction, mmuActionsCh <-chan *reply.MMActionReply) {
Handle:
	for {
		select {
		case action, ok := <-actionsCh:
			if !ok {
				break Handle
			}
			if action == nil {
				svc.Log.Debugf("handleActions got nil action; ignoring")
			} else {
				url := action.Action
				// remove the notification from the messaging menu
				svc.messagingMenu.RemoveNotification(action.Nid, true)
				// this ignores the error (it's been logged already)
				svc.urlDispatcher.DispatchURL(url, action.App)
			}
		case mmuAction, ok := <-mmuActionsCh:
			if !ok {
				break Handle
			}
			if mmuAction == nil {
				svc.Log.Debugf("handleActions (MMU) got nil action; ignoring")
			} else {
				svc.Log.Debugf("handleActions (MMU) got: %v", mmuAction)
				url := mmuAction.Action
				// remove the notification from the messagingmenu map
				svc.messagingMenu.RemoveNotification(mmuAction.Notification, false)
				// this ignores the error (it's been logged already)
				svc.urlDispatcher.DispatchURL(url, mmuAction.App)
			}

		}
	}
}

func (svc *PostalService) takeTheBus() (<-chan *notifications.RawAction, error) {
	endps := []struct {
		name string
		endp bus.Endpoint
	}{
		{"notifications", svc.NotificationsEndp},
		{"emblemcounter", svc.EmblemCounterEndp},
		{"accounts", svc.AccountsEndp},
		{"haptic", svc.HapticEndp},
		{"greeter", svc.GreeterEndp},
		{"focusinfo", svc.FocusInfoEndp},
	}
	for _, endp := range endps {
		if endp.endp == nil {
			svc.Log.Errorf("endpoint for %s is nil", endp.name)
			return nil, ErrNotConfigured
		}
	}

	var wg sync.WaitGroup
	wg.Add(len(endps))
	for _, endp := range endps {
		go func(name string, endp bus.Endpoint) {
			util.NewAutoRedialer(endp).Redial()
			wg.Done()
		}(endp.name, endp.endp)
	}
	wg.Wait()

	return notifications.Raw(svc.NotificationsEndp, svc.Log, nil).WatchActions()
}

func (svc *PostalService) listPersistent(msg dbus.Message, id string) ([]string, *dbus.Error) {
	app, err := svc.grabDBusPackageAndAppId(&msg, id)
	if err != nil {
		return nil, MakeDBusNoObjectErrorWithReason(
			msg.Headers[dbus.FieldPath].Value().(string), err)
	}

	tagmap := svc.messagingMenu.Tags(app)
	return tagmap, nil
}

func (svc *PostalService) clearPersistent(msg dbus.Message, id string, tag string) (uint32, *dbus.Error) {
	app, err := svc.grabDBusPackageAndAppId(&msg, id)
	if err != nil {
		return 0, MakeDBusNoObjectErrorWithReason(
			msg.Headers[dbus.FieldPath].Value().(string), err)
	}

	// TODO: should we rate-limit this log?
	svc.Log.Infof("App '%s' calls a deprecated method 'clearPersistent'. "+
		"Uses 'clearPersistentList' instead.", app)

	return uint32(svc.messagingMenu.Clear(app, tag)), nil
}

func (svc *PostalService) clearPersistentList(msg dbus.Message, id string, tags []string) (uint32, *dbus.Error) {
	app, err := svc.grabDBusPackageAndAppId(&msg, id)
	if err != nil {
		return 0, MakeDBusNoObjectErrorWithReason(
			msg.Headers[dbus.FieldPath].Value().(string), err)
	}
	return uint32(svc.messagingMenu.Clear(app, tags...)), nil
}

func (svc *PostalService) setCounter(msg dbus.Message, id string, count int, visible bool) *dbus.Error {
	app, err := svc.grabDBusPackageAndAppId(&msg, id)
	if err != nil {
		return MakeDBusNoObjectErrorWithReason(
			msg.Headers[dbus.FieldPath].Value().(string), err)
	}

	svc.emblemCounter.SetCounter(app, int32(count), visible)
	return nil
}

func (svc *PostalService) popAll(msg dbus.Message, id string) ([]string, *dbus.Error) {
	app, err := svc.grabDBusPackageAndAppId(&msg, id)
	if err != nil {
		return nil, MakeDBusNoObjectErrorWithReason(
			msg.Headers[dbus.FieldPath].Value().(string), err)
	}

	svc.lock.Lock()
	defer svc.lock.Unlock()

	if svc.mbox == nil {
		return []string{}, nil
	}
	appId := app.Original()
	box, ok := svc.mbox[appId]
	if !ok {
		return []string{}, nil
	}
	msgs := box.AllMessages()
	delete(svc.mbox, appId)

	return msgs, nil
}

var newNid = func() string {
	return uuid.Must(uuid.NewRandom()).String()
}

func (svc *PostalService) post(msg dbus.Message, id, notif string) *dbus.Error {
	app, err := svc.grabDBusPackageAndAppId(&msg, id)
	if err != nil {
		return MakeDBusNoObjectErrorWithReason(
			msg.Headers[dbus.FieldPath].Value().(string), err)
	}
	var dummy interface{}
	rawJSON := json.RawMessage(notif)
	err = json.Unmarshal(rawJSON, &dummy)
	if err != nil {
		return dbus.MakeFailedError(ErrBadJSON)
	}

	svc.Post(app, "", rawJSON)
	return nil
}

// Post() signals to an application over dbus that a notification
// has arrived. If nid is "" generate one.
func (svc *PostalService) Post(app *click.AppId, nid string, payload json.RawMessage) {
	if nid == "" {
		nid = newNid()
	}
	arg := launch_helper.HelperInput{
		App:            app,
		NotificationId: nid,
		Payload:        payload,
	}
	var kind string
	if app.Click {
		kind = "click"
	} else {
		kind = "legacy"
	}
	svc.HelperPool.Run(kind, &arg)
}

func (svc *PostalService) consumeHelperResults(ch chan *launch_helper.HelperResult) {
	for res := range ch {
		svc.handleHelperResult(res)
	}
}

func (svc *PostalService) handleHelperResult(res *launch_helper.HelperResult) {
	svc.lock.Lock()
	defer svc.lock.Unlock()
	if svc.mbox == nil {
		svc.mbox = make(map[string]*mBox)
	}

	app := res.Input.App
	nid := res.Input.NotificationId
	output := res.HelperOutput

	appId := app.Original()
	box, ok := svc.mbox[appId]
	if !ok {
		box = new(mBox)
		svc.mbox[appId] = box
	}
	box.Append(output.Message, nid)

	if svc.msgHandler != nil {
		b := svc.msgHandler(app, nid, &output)
		if !b {
			svc.Log.Debugf("msgHandler did not present the notification")
		}
	}

	svc.Bus.Signal("Post", "/"+string(nih.Quote([]byte(app.Package))), []interface{}{appId})
}

func (svc *PostalService) validateActions(app *click.AppId, notif *launch_helper.Notification) bool {
	if notif.Card == nil || len(notif.Card.Actions) == 0 {
		return true
	}
	return svc.urlDispatcher.TestURL(app, notif.Card.Actions)
}

var areNotificationsEnabled = cnotificationsettings.AreNotificationsEnabled

func (svc *PostalService) messageHandler(app *click.AppId, nid string, output *launch_helper.HelperOutput) bool {
	if output == nil || output.Notification == nil {
		svc.Log.Debugf("skipping notification: nil.")
		return false
	}
	// validate actions
	if !svc.validateActions(app, output.Notification) {
		// no need to log, (it's been logged already)
		return false
	}

	locked := svc.greeter.IsActive()
	focused := svc.focusInfo.IsAppFocused(app)

	if !locked && focused {
		svc.Log.Debugf("notification skipped because app is focused.")
		return false
	}

	if !areNotificationsEnabled(app) {
		svc.Log.Debugf("notification skipped (except emblem counter) because app has notifications disabled")
		return svc.emblemCounter.Present(app, nid, output.Notification)
	}

	b := false
	for _, p := range svc.Presenters {
		// we don't want this to shortcut :)
		b = p.Present(app, nid, output.Notification) || b
	}
	return b
}
