// 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 (
	"encoding/json"
	"fmt"

	"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"
	"git.openprivacy.ca/openprivacy/log"
)

// Side-effect handling for events received by the group manager
// Note that these handlers are only called for messages that are authenticaed, and verified as valid
// this allows us to simplify the logic
type GroupManagerHandler struct {
	Profile peer.CwtchPeer
	f       *GroupManagerFunctionality
}

// HandleChangeGroupMode implements [ManagedGroupHandler].
func (g *GroupManagerHandler) HandleChangeGroupMode(ctx *ManagedGroupContext, cgm ChangeGroupModeEvent) {
	group, err := GetManagedGroup(g.Profile)
	if err != nil {
		log.Errorf("group manager did not recieve managed group...this should never happen")
		return
	}
	mode := group.GroupMode()
	log.Infof("hgmf: mode was %v, change is %v", mode, cgm.GroupMode)
	if cgm.Additive {
		mode |= cgm.GroupMode
	} else {
		mode &^= cgm.GroupMode
	}
	log.Infof("hgmf: now mode is %v", mode)
	szp := attr.ConversationScope.ConstructScopedZonedPath(attr.HybridGroupZone.ConstructZonedPath(constants.GroupMode))
	g.Profile.SetConversationAttributeInt(group.ID, szp, int(mode))

	// We could forward this mode to members, but since there may be access control related
	// updates, we are going to publish a memberlist update instead...
	// SendToAllMembers(g.Profile, ctx.GroupConversation, ctx.OriginalMessage.Author, ctx.OriginalPacket)
	g.HandleACLUpdate(ctx, ctx.GroupConversation.ACL)
}

// HandleRemoveMember implements [ManagedGroupHandler].
func (g *GroupManagerHandler) HandleRemoveMember(ctx *ManagedGroupContext, ame RemoveMemberEvent) {
	SendToAllMembers(g.Profile, ctx.GroupConversation, ctx.OriginalMessage.Author, ctx.OriginalPacket, true)
}

// HandleChangeRole implements [ManagedGroupHandler].
func (g *GroupManagerHandler) HandleChangeRole(ctx *ManagedGroupContext, cpe ChangeMemberRoleEvent) {
	SendToAllMembers(g.Profile, ctx.GroupConversation, ctx.OriginalMessage.Author, ctx.OriginalPacket, true)
}

// HandleNewMember implements [ManagedGroupHandler].
func (g *GroupManagerHandler) HandleNewMember(ctx *ManagedGroupContext, ame AddMemberEvent) {
	SendToAllMembers(g.Profile, ctx.GroupConversation, ctx.OriginalMessage.Author, ctx.OriginalPacket, true) // send the add member message to all members except the new one
	g.f.AddHybridContact(g.Profile, ame.Handle)
}

// HandleACLUpdate implements [ManagedGroupHandler].
func (g *GroupManagerHandler) HandleACLUpdate(ctx *ManagedGroupContext, acl model.AccessControlList) {
	err := g.Profile.UpdateConversationAccessControlList(ctx.GroupConversation.ID, acl)
	if err == nil {
		mlist, _ := memberListMessage(g.Profile)
		SendToAllMembers(g.Profile, ctx.GroupConversation, g.Profile.GetOnion(), mlist, true)
	}
}

// HandleMemberList implements [ManagedGroupHandler].
func (g *GroupManagerHandler) HandleMemberList(ctx *ManagedGroupContext, memberList MemberListEvent) {
	log.Errorf("group manager should never receive a memberlist")
}

// HandleNewMessage implements [ManagedGroupHandler].
func (g *GroupManagerHandler) HandleNewMessage(ctx *ManagedGroupContext, cme NewClearMessageEvent) {
	log.Infof("New clear message from group %v", cme)
	SendToAllMembers(g.Profile, ctx.GroupConversation, ctx.OriginalMessage.Author, ctx.OriginalPacket, false)
}

// 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.
	ci, err := profile.FetchConversationInfo(MANAGED_GROUP_HANDLE)
	if ci != nil && err == nil {
		log.Debugf("isManager = true")

		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 := 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 {
					// they're not in the group - see if they can join
					if MayJoin(handle, mg) {
						err = f.AddHybridContact(profile, handle)
						if err == nil {
							log.Infof("adding %v to open group", handle)
							mlist, _ := memberListMessage(profile)
							SendToAllMembers(profile, ci, profile.GetOnion(), mlist, true)
						} else {
							log.Errorf("error adding %v to open group: %v", handle, err)
						}
					} else {
						log.Infof("received managed group connection from unauthorized peer: %v %v", handle, err)
						profile.DisconnectFromPeer(handle)
					}
					break
				}

				// log.Debugf("hgmf prep start")
				pci, err := profile.FetchConversationInfo(handle)
				if err == nil {
					list, err := memberListMessage(profile)
					if err == nil {
						profile.SendMessage(pci.ID, list)
					}
				}
			}
		// 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 := 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
			}

			ctx := NewManagedGroupContext(profile.GetOnion(), ci, &GroupManagerHandler{Profile: profile})
			log.Debugf("hgmf member ishybridgroup")
			// The overlay type of this message **must** be ManageGroupEvent
			if cm.Overlay == model.OverlayManageGroupEvent {
				var mge HybridGroupMessage
				err = json.Unmarshal([]byte(cm.Data), &mge)
				if err == nil {
					if err = ctx.Handle(ev.Data[event.Data], &mge); err != nil {
						log.Errorf("issue processing group management event: %v", err)
					}
				} else {
				}
			}
		}
	} else {
		log.Debugf("isManager = false")
	}
}

func memberListMessage(profile peer.CwtchPeer) (string, error) {
	if ci, err := GetManagedGroup(profile); err == nil {
		ml := &MemberListEvent{
			Members:   ConstructMemberList(ci.ACL, profile),
			GroupMode: ci.GroupMode(),
		}
		_, ojson, err := PrepMessageToManagedGroup(profile, profile.GetOnion(), ci.ID, ml)
		return ojson, err
	} else {
		return "", err
	}
}

func (f *GroupManagerFunctionality) PromoteAdmin(profile peer.CwtchPeer, handle string) error {
	if ci, err := GetManagedGroup(profile); err == nil {
		updatedHandleACL := ci.ACL[handle]
		updatedHandleACL.GroupAdmin = true
		updatedHandleACL.AssignedRole = model.ROLE_ADMIN
		ci.ACL[handle] = updatedHandleACL
		err := profile.UpdateConversationAccessControlList(ci.ID, ci.ACL)
		if err == nil {
			mlist, _ := memberListMessage(profile)
			return SendToAllMembers(profile, ci, profile.GetOnion(), mlist, true)
		}
		return err
	} else {
		return err
	}
}

func SendToAllMembers(profile peer.CwtchPeer, group *model.Conversation, authorHandle string, wrapped string, isControlMessage bool) error {
	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(authorHandle, group); err == nil && (acl.Read || isControlMessage) {
				log.Infof("forwarding group message to: %v", ci.Handle)
				profile.SendMessage(ci.ID, wrapped)
			}
		}
	}
	return nil
}

// GetManagedGroup is a convieniance function that looks up the managed group
func 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()
	acl := model.AccessControlList{}
	acl[profile.GetOnion()] = ac
	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.False)
		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()
	ci, err := profile.NewContactConversation(handle, ac, true)
	if err != nil {
		return err
	}
	mg, err := GetManagedGroup(profile)
	if err != nil {
		return err
	}
	// Update the ACL list to add this contact...
	acl := mg.ACL
	gac := acl[handle]
	gac.AssignedRole = model.ROLE_USER
	acl[handle] = gac
	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
}

func ConstructMemberList(acl model.AccessControlList, profile peer.CwtchPeer) model.MemberList {
	ml := model.MemberList{}
	mg, err := GetManagedGroup(profile)
	if err != nil {
		return ml
	}

	for onion, ac := range acl {
		ci, err := profile.FetchConversationInfo(onion)
		name := onion
		if err == nil {
			att, exists := ci.GetAttribute(attr.LocalScope, attr.ProfileZone, constants.Name)
			if exists {
				name = att
			}
		}
		ml[onion] = model.MemberInfo{
			Name: name,
			AC:   ac.AssignedRole.ComputeEffectiveACL(mg.GroupMode()),
		}
	}
	return ml
}
