// This file contains all code related to how a Group Manager operates over a group.
// Managed groups are canonically controlled by members setting
// the ManageGroup permission in the conversation ACL;  allowing the manager to
// take control of how this group is structured, see OnEvent below...
// TODO: This file represents stage 1 of the roll out which de-risks most of the
// integration into cwtch peer, new interfaces, and UI integration
// The following functionality is not yet implemented:
// - group-level encryption
// - key rotation / membership ACL
// Cwtch Hybrid Groups are still very experimental functionality and should
// only be used for testing purposes.
package hybrid

import (
	"cwtch.im/cwtch/event"
	"cwtch.im/cwtch/model"
	"cwtch.im/cwtch/model/attr"
	"cwtch.im/cwtch/model/constants"
	"cwtch.im/cwtch/peer"
	"cwtch.im/cwtch/protocol/connections"
	"cwtch.im/cwtch/settings"
	"encoding/json"
	"fmt"
	"git.openprivacy.ca/openprivacy/log"
)

// MANAGED_GROUP_HANDLE denotes the nominal name that the managed group is given, for easier handling
// Note: we could use id here, as the managed group should technically always be the first group
// But we don't want to assume that, and also allow conversations to be moved around without
// constantly referring to a magic id.
const MANAGED_GROUP_HANDLE = "managed:000"

type GroupManagerFunctionality struct {
}

func (f *GroupManagerFunctionality) NotifySettingsUpdate(settings settings.GlobalSettings) {
}

func (f *GroupManagerFunctionality) EventsToRegister() []event.Type {
	return []event.Type{event.PeerStateChange, event.NewMessageFromPeerEngine}
}

func (f *GroupManagerFunctionality) ExperimentsToRegister() []string {
	return []string{constants.GroupManagerExperiment, constants.GroupsExperiment}
}

// OnEvent handles File Sharing Hooks like Manifest Received and FileDownloaded
func (f *GroupManagerFunctionality) OnEvent(ev event.Event, profile peer.CwtchPeer) {

	// We only want to engage this functionality if the peer is managing a group.
	// In that case ALL peer connections and messages need to be routed through
	// the management logic
	// For now, we assume that a manager is a peer with a special management group.
	// In the future we may want to make this a profile-level switch/attribute.
	isManager := false
	if ci, err := profile.FetchConversationInfo(MANAGED_GROUP_HANDLE); ci != nil && err == nil {
		isManager = true
	}

	if isManager {
		switch ev.EventType {
		case event.PeerStateChange:
			handle := ev.Data["RemotePeer"]
			// check that we have authenticated with this peer
			if connections.ConnectionStateToType()[ev.Data[event.ConnectionState]] == connections.AUTHENTICATED {
				mg, err := f.GetManagedGroup(profile)
				if err != nil {
					log.Infof("group manager received peer connections but no suitable group has been found: %v %v", handle, err)
					profile.DisconnectFromPeer(handle)
					break
				}
				if _, err := CheckACL(handle, mg); err != nil {
					log.Infof("received managed group connection from unauthorized peer: %v %v", handle, err)
					profile.DisconnectFromPeer(handle)
					break
				}
			}
		// This is where most of the magic happens for managed groups. A few notes:
		// - CwtchPeer has already taken care of storing this for us, we don't need to worry about that
		// - Group Managers **only** speak overlays and **always** wrap their messages in a ManageGroupEvent anything else is fast-rejected.
		case event.NewMessageFromPeerEngine:
			log.Infof("received new message from peer: manager")
			ci, err := f.GetManagedGroup(profile)
			if err != nil {
				log.Errorf("unknown conversation %v", err)
				break // we don't care about unknown conversations...
			}
			var cm model.MessageWrapper
			err = json.Unmarshal([]byte(ev.Data[event.Data]), &cm)
			if err != nil {
				log.Errorf("could not deserialize json %s %v", ev.Data[event.Data], err)
				break
			}
			// The overlay type of this message **must** be ManageGroupEvent
			if cm.Overlay == model.OverlayManageGroupEvent {
				var mge ManageGroupEvent
				err = json.Unmarshal([]byte(cm.Data), &mge)
				if err == nil {
					f.handleEvent(profile, *ci, mge, ev.Data[event.Data])
				}
			}

		}
	}
}

// handleEvent takes in a high level ManageGroupEvent message, transforms it into the proper type, and passes it on for handling
// assumes we are called after an event provided by an authorized peer (i.e. ManageGroup == true)
func (f *GroupManagerFunctionality) handleEvent(profile peer.CwtchPeer, conversation model.Conversation, mge ManageGroupEvent, original string) {
	switch mge.EventType {
	case NewClearMessage:
		var nme NewClearMessageEvent
		err := json.Unmarshal([]byte(mge.Data), &nme)
		if err == nil {
			f.handleNewMessageEvent(profile, conversation, nme, original)
		}
	}
}

func (f *GroupManagerFunctionality) handleNewMessageEvent(profile peer.CwtchPeer, conversation model.Conversation, nme NewClearMessageEvent, original string) {
	log.Infof("handling new clear message event")
	hgm := nme.HybridGroupMessage
	if AuthenticateMessage(hgm) {
		log.Infof("authenticated message")
		group, err := f.GetManagedGroup(profile)
		if err != nil {
			log.Infof("received fraudulant hybrid message from group: %v", err)
			return
		}
		if acl, err := CheckACL(hgm.Author, group); err != nil {
			log.Infof("received fraudulant hybrid message from group: %v", err)
			return
		} else if !acl.Append {
			log.Infof("received fraudulant hybrid message from group: peer does not have append privileges")
			return
		} else {

			// TODO - Store this message locally in a format that makes it easier to
			// do assurance later on

			// forward the message to everyone who the server has added as a contact
			// and who are represented in the ACL...
			allConversations, _ := profile.FetchConversations()
			for _, ci := range allConversations {
				// NOTE: This check works for Open Groups too as CheckACL will return the default ACL
				// for the group....
				if ci.Handle != MANAGED_GROUP_HANDLE { // don't send to ourselves...
					if acl, err := CheckACL(hgm.Author, group); err == nil && acl.Read {
						log.Infof("forwarding group message to: %v", ci.Handle)
						profile.SendMessage(ci.ID, original)
					}
				}
			}
		}
	} else {
		log.Errorf("received fraudulant hybrid message fom group")
	}
}

// GetManagedGroup is a convieniance function that looks up the managed group
func (f *GroupManagerFunctionality) GetManagedGroup(profile peer.CwtchPeer) (*model.Conversation, error) {
	return profile.FetchConversationInfo(MANAGED_GROUP_HANDLE)
}

// Establish a new Managed Group and return its conversation id
func (f *GroupManagerFunctionality) ManageNewGroup(profile peer.CwtchPeer) (int, error) {
	// note: a manager can only manage one group. This will (probably) always be true and has a few benefits
	// and downsides.
	// The main downside is that it requires a new manager per group (and thus an onion service per group)
	// However, it means that we can lean on p2p functionality like profile images / metadata / name
	// etc. for group metadata and effectively get that for-free in the client.
	// HOWEVER: hedging our bets here by giving this group a numeric handle...
	if _, err := profile.FetchConversationInfo(MANAGED_GROUP_HANDLE); err == nil {
		return -1, fmt.Errorf("manager is already managing a group")
	}

	ac := model.DefaultP2PAccessControl()
	// by setting the ManageGroup permission in this ACL we are allowing the manager to
	// take control of how this group is structured, see OnEvent above...
	ac.ManageGroup = true
	acl := model.AccessControlList{}
	acl[profile.GetOnion()] = ac
	acl[MANAGED_GROUP_HANDLE] = model.NoAccessControl()
	ci, err := profile.NewConversation(MANAGED_GROUP_HANDLE, acl)
	if err != nil {
		return -1, err
	}

	profile.SetConversationAttribute(ci, attr.LocalScope.ConstructScopedZonedPath(attr.ConversationZone.ConstructZonedPath(ManagedGroupOpen)), event.False)

	return ci, nil
}

func (f *GroupManagerFunctionality) SetMembershipOpen(profile peer.CwtchPeer) error {
	if ci, err := profile.FetchConversationInfo(MANAGED_GROUP_HANDLE); err != nil {
		return fmt.Errorf("manager is already managing a group")
	} else {
		profile.SetConversationAttribute(ci.ID, attr.LocalScope.ConstructScopedZonedPath(attr.ConversationZone.ConstructZonedPath(ManagedGroupOpen)), event.True)
		return nil
	}
}

func (f *GroupManagerFunctionality) SetMembershipClosed(profile peer.CwtchPeer) error {
	if ci, err := profile.FetchConversationInfo(MANAGED_GROUP_HANDLE); err != nil {
		return fmt.Errorf("manager is already managing a group")
	} else {
		profile.SetConversationAttribute(ci.ID, attr.LocalScope.ConstructScopedZonedPath(attr.ConversationZone.ConstructZonedPath(ManagedGroupOpen)), event.True)
		return nil
	}
}

// AddHybridContact is a wrapper arround NewContactConversation which sets the contact
// up for Hybrid Group channel messages...
// TODO this function assumes that authorization has been done at a higher level..
func (f *GroupManagerFunctionality) AddHybridContact(profile peer.CwtchPeer, handle string) error {
	ac := model.DefaultP2PAccessControl()
	ac.ManageGroup = false
	ci, err := profile.NewContactConversation(handle, ac, true)
	if err != nil {
		return err
	}
	mg, err := f.GetManagedGroup(profile)
	if err != nil {
		return err
	}
	// Update the ACL list to add this contact...
	acl := mg.ACL
	acl[handle] = model.DefaultP2PAccessControl()
	profile.UpdateConversationAccessControlList(mg.ID, acl)
	// enable channel 2 on this conversation (hybrid groups management channel)
	profile.InitChannel(ci, constants.CHANNEL_MANAGER)
	key := fmt.Sprintf("channel.%d", constants.CHANNEL_MANAGER)
	profile.SetConversationAttribute(ci, attr.LocalScope.ConstructScopedZonedPath(attr.ConversationZone.ConstructZonedPath(key)), constants.True)
	// Group managers need to always save history (and manually deal with purging...)
	profile.SetConversationAttribute(ci, attr.LocalScope.ConstructScopedZonedPath(attr.ProfileZone.ConstructZonedPath(event.SaveHistoryKey)), event.SaveHistoryConfirmed)
	return nil
}

func (f *GroupManagerFunctionality) OnContactRequestValue(profile peer.CwtchPeer, conversation model.Conversation, eventID string, path attr.ScopedZonedPath) {
	// nop hybrid group conversations do not exchange contact requests
}

func (f *GroupManagerFunctionality) OnContactReceiveValue(profile peer.CwtchPeer, conversation model.Conversation, path attr.ScopedZonedPath, value string, exists bool) {
	// nop hybrid group conversations do not exchange contact requests
}
