package hybrid

import (
	"crypto/rand"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"math"
	"math/big"
	"strconv"
	"time"

	"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/settings"
	"git.openprivacy.ca/openprivacy/connectivity/tor"
	"git.openprivacy.ca/openprivacy/log"
	"golang.org/x/crypto/nacl/secretbox"
)

type ManagedGroupFunctionality struct {
}

func (f ManagedGroupFunctionality) NotifySettingsUpdate(settings settings.GlobalSettings) {
}

func (f ManagedGroupFunctionality) EventsToRegister() []event.Type {
	return []event.Type{event.NewMessageFromPeerEngine}
}

func (f ManagedGroupFunctionality) ExperimentsToRegister() []string {
	return []string{constants.GroupsExperiment}
}

// OnEvent handles File Sharing Hooks like Manifest Received and FileDownloaded
func (f *ManagedGroupFunctionality) OnEvent(ev event.Event, profile peer.CwtchPeer) {
	switch ev.EventType {
	// 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:
		handle := ev.Data[event.RemotePeer]
		ci, err := profile.FetchConversationInfo(handle)
		if err != nil {
			break // we don't care about unknown conversations...
		}

		// We reject managed group requests for groups not setup as managed groups...
		if ci.ACL[handle].ManageGroup {
			var cm model.MessageWrapper
			err = json.Unmarshal([]byte(ev.Data[event.Data]), &cm)
			if err != nil {
				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 {
					cid, err := profile.FetchConversationInfo(handle)
					if err == nil {
						f.handleEvent(profile, *cid, mge)
					}
				}
			}
		}
	}
}

// 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 *ManagedGroupFunctionality) handleEvent(profile peer.CwtchPeer, conversation model.Conversation, mge ManageGroupEvent) {
	switch mge.EventType {
	case AddMember:
		var ame AddMemberEvent
		err := json.Unmarshal([]byte(mge.Data), &ame)
		if err == nil {
			f.handleAddMemberEvent(profile, conversation, ame)
		}
	case RemoveMember:
		var rme RemoveMemberEvent
		err := json.Unmarshal([]byte(mge.Data), &rme)
		if err == nil {
			f.handleRemoveMemberEvent(profile, conversation, rme)
		}
	case NewMessage:
		var nme NewMessageEvent
		err := json.Unmarshal([]byte(mge.Data), &nme)
		if err == nil {
			f.handleNewMessageEvent(profile, conversation, nme)
		}
	case NewClearMessage:
		var nme NewClearMessageEvent
		err := json.Unmarshal([]byte(mge.Data), &nme)
		if err == nil {
			f.handleNewClearMessageEvent(profile, conversation, nme)
		}
	case RotateKey:
		var rke RotateKeyEvent
		err := json.Unmarshal([]byte(mge.Data), &rke)
		if err == nil {
			f.handleRotateKeyEvent(profile, conversation, rke)
		}
	}
}

// handleAddMemberEvent adds a group member to the conversation ACL
// assumes we are called after an event provided by an authorized peer (i.e. ManageGroup == true)
func (f *ManagedGroupFunctionality) handleAddMemberEvent(profile peer.CwtchPeer, conversation model.Conversation, ame AddMemberEvent) {
	acl := conversation.ACL
	acl[ame.Handle] = model.DefaultP2PAccessControl()
	profile.UpdateConversationAccessControlList(conversation.ID, acl)
}

// handleRemoveMemberEvent removes a group member from the conversation ACL
// assumes we are called after an event provided by an authorized peer (i.e. ManageGroup == true)
func (f *ManagedGroupFunctionality) handleRemoveMemberEvent(profile peer.CwtchPeer, conversation model.Conversation, rme RemoveMemberEvent) {
	acl := conversation.ACL
	delete(acl, rme.Handle)
	profile.UpdateConversationAccessControlList(conversation.ID, acl)
}

// handleRotateKeyEvent rotates the encryption key for a given group
// assumes we are called after an event provided by an authorized peer (i.e. ManageGroup == true)
// TODO this currently is a noop as group levle encryption is unimplemented
func (f *ManagedGroupFunctionality) handleRotateKeyEvent(profile peer.CwtchPeer, conversation model.Conversation, rke RotateKeyEvent) {
	keyScope := attr.LocalScope.ConstructScopedZonedPath(attr.ConversationZone.ConstructZonedPath("key"))
	keyB64 := base64.StdEncoding.EncodeToString(rke.Key)
	profile.SetConversationAttribute(conversation.ID, keyScope, keyB64)
}

// TODO this is a sketch implementation that is not yet complete.
func (f *ManagedGroupFunctionality) handleNewMessageEvent(profile peer.CwtchPeer, conversation model.Conversation, nme NewMessageEvent) {
	keyScope := attr.LocalScope.ConstructScopedZonedPath(attr.ConversationZone.ConstructZonedPath("key"))
	if keyB64, err := profile.GetConversationAttribute(conversation.ID, keyScope); err == nil {
		key, err := base64.StdEncoding.DecodeString(keyB64)
		if err != nil || len(key) != 32 {
			log.Errorf("hybrid group key is corrupted")
			return
		}
		// decrypt the message with key...
		hgm, err := f.decryptMessage(key, nme.EncryptedHybridGroupMessage)
		if hgm == nil || err != nil {
			log.Errorf("unable to decrypt hybrid group message: %v", err)
			return
		}
		f.handleNewClearMessageEvent(profile, conversation, NewClearMessageEvent{HybridGroupMessage: *hgm})
	}
}

func (f *ManagedGroupFunctionality) handleNewClearMessageEvent(profile peer.CwtchPeer, conversation model.Conversation, nme NewClearMessageEvent) {
	hgm := nme.HybridGroupMessage
	if AuthenticateMessage(hgm) {
		// TODO Closed Group Membership Check - right now we only support open groups...
		if profile.GetOnion() == hgm.Author {
			// ack
			signatureB64 := base64.StdEncoding.EncodeToString(hgm.Signature)
			id, err := profile.GetChannelMessageBySignature(conversation.ID, constants.CHANNEL_CHAT, signatureB64)
			if err == nil {
				profile.UpdateMessageAttribute(conversation.ID, constants.CHANNEL_CHAT, id, constants.AttrAck, constants.True)
				profile.PublishEvent(event.NewEvent(event.IndexedAcknowledgement, map[event.Field]string{event.ConversationID: strconv.Itoa(conversation.ID), event.Index: strconv.Itoa(id)}))
			}
		} else {
			mgidstr := strconv.Itoa(int(nme.HybridGroupMessage.MemberGroupID)) // we need both MemberGroupId and MemberMessageId for attestation later on...
			newmmidstr := strconv.Itoa(int(nme.HybridGroupMessage.MemberMessageID))
			// Set the attributes of this message...
			attr := model.Attributes{MemberGroupIDKey: mgidstr, MemberMessageIDKey: newmmidstr,
				constants.AttrAuthor:        hgm.Author,
				constants.AttrAck:           event.True,
				constants.AttrSentTimestamp: time.UnixMilli(int64(hgm.Sent)).Format(time.RFC3339Nano)}

			// Note: The Channel here is 0...this is the main channel that UIs understand as the default, so this message is
			// becomes part of the conversation...
			mid, err := profile.InternalInsertMessage(conversation.ID, constants.CHANNEL_CHAT, hgm.Author, hgm.MessageBody, attr, hgm.Signature)
			contenthash := model.CalculateContentHash(hgm.Author, hgm.MessageBody)
			if err == nil {
				profile.PublishEvent(event.NewEvent(event.NewMessageFromGroup, map[event.Field]string{event.ConversationID: strconv.Itoa(conversation.ID), event.TimestampSent: time.UnixMilli(int64(hgm.Sent)).Format(time.RFC3339Nano), event.RemotePeer: hgm.Author, event.Index: strconv.Itoa(mid), event.Data: hgm.MessageBody, event.ContentHash: contenthash}))
			}
		}

		// TODO need to send an event here...
	} else {
		log.Errorf("received fraudulant hybrid message fom group")
	}
}

// todo sketch function
func (f *ManagedGroupFunctionality) decryptMessage(key []byte, ciphertext []byte) (*HybridGroupMessage, error) {
	if len(ciphertext) > 24 {
		var decryptNonce [24]byte
		copy(decryptNonce[:], ciphertext[:24])
		var fixedSizeKey [32]byte
		copy(fixedSizeKey[:], key[:32])
		decrypted, ok := secretbox.Open(nil, ciphertext[24:], &decryptNonce, &fixedSizeKey)
		if ok {
			var hgm HybridGroupMessage
			err := json.Unmarshal(decrypted, &hgm)
			return &hgm, err
		}
	}
	return nil, fmt.Errorf("invalid ciphertext/key error")
}

// Define a new managed group, managed by the manager...
func (f *ManagedGroupFunctionality) NewManagedGroup(profile peer.CwtchPeer, manager string) error {

	if !tor.IsValidHostname(manager) {
		return fmt.Errorf("manager handle must be a tor v3 onion")
	}

	// generate a truely random member id for this group in [0..2^32)
	nBig, err := rand.Int(rand.Reader, big.NewInt(math.MaxUint32))
	if err != nil {
		return err // if there is a problem with random we want to exit now rather than have to clean up group setup...
	}

	ac := model.DefaultP2PAccessControl()
	ac.ManageGroup = true // by setting the ManageGroup permission in this ACL we are allowing the manager to control of how this group is structured
	ci, err := profile.NewContactConversation(manager, ac, true)
	if err != nil {
		return err
	}
	// enable channel 2 on this conversation (hybrid groups management channel)
	key := fmt.Sprintf("channel.%d", 2)
	err = profile.SetConversationAttribute(ci, attr.LocalScope.ConstructScopedZonedPath(attr.ConversationZone.ConstructZonedPath(key)), constants.True)
	if err != nil {
		return fmt.Errorf("could not enable channel 2 on hybrid group: %v", err) // likely a catestrophic error...fail
	}
	err = profile.InitChannel(ci, 2)
	if err != nil {
		return fmt.Errorf("could not enable channel 2 on hybrid group: %v", err) // likely a catestrophic error...fail
	}

	// finally, set the member group id on this group...
	mgidkey := attr.LocalScope.ConstructScopedZonedPath(attr.ConversationZone.ConstructZonedPath(MemberGroupIDKey))
	err = profile.SetConversationAttributeInt(ci, mgidkey, int(nBig.Uint64()))
	if err != nil {
		return fmt.Errorf("could not set group id on hybrid group: %v", err) // likely a catestrophic error...fail
	}
	return nil
}

// SendMessageToManagedGroup acts like SendMessage(ToPeer), but with a few additional bookkeeping steps for Hybrid Groups
func (f *ManagedGroupFunctionality) SendMessageToManagedGroup(profile peer.CwtchPeer, conversation int, message string) (int, error) {
	mgidkey := attr.LocalScope.ConstructScopedZonedPath(attr.ConversationZone.ConstructZonedPath(MemberGroupIDKey))
	mgid, err := profile.GetConversationAttributeInt(conversation, mgidkey)
	if err != nil {
		return -1, err
	}

	mmidkey := attr.LocalScope.ConstructScopedZonedPath(attr.ConversationZone.ConstructZonedPath(MemberMessageIDKey))
	mmid, err := profile.GetConversationAttributeInt(conversation, mmidkey)
	if err != nil {
		mmid = 0 // first message
	}

	mmid += 1

	// Now time to package this whole thing in layers of JSON...
	hgm := HybridGroupMessage{
		MemberGroupID:   uint32(mgid),
		MemberMessageID: uint32(mmid),
		Sent:            uint64(time.Now().UnixMilli()),
		Author:          profile.GetOnion(),
		MessageBody:     message,
		Signature:       []byte{}, // Leave blank so we can sign this message...
	}

	data, err := json.Marshal(hgm)
	if err != nil {
		return -1, err
	}

	// Don't forget to sign the message...
	sig, err := profile.SignMessage(data)
	if err != nil {
		return -1, err
	}

	hgm.Signature = sig

	ncm := NewClearMessageEvent{
		HybridGroupMessage: hgm,
	}

	signedData, err := json.Marshal(ncm)
	if err != nil {
		return -1, err
	}

	mgm := ManageGroupEvent{
		EventType: NewClearMessage,
		Data:      string(signedData),
	}

	odata, err := json.Marshal(mgm)
	if err != nil {
		return -1, err
	}

	overlay := model.MessageWrapper{
		Overlay: model.OverlayManageGroupEvent,
		Data:    string(odata),
	}

	ojson, err := json.Marshal(overlay)
	if err != nil {
		return -1, err
	}

	// send the message to the manager and update our message is string for tracking...
	_, err = profile.SendMessage(conversation, string(ojson))
	if err != nil {
		return -1, err
	}
	profile.SetConversationAttributeInt(conversation, mmidkey, mmid)

	// ok there is still one more thing we need to do...
	// insert this message as part of our group log, for members of the group
	// this exists in channel 0 of the conversation with the group manager...
	mgidstr := strconv.Itoa(mgid) // we need both MemberGroupId and MemberMessageId for attestation later on...
	newmmidstr := strconv.Itoa(mmid)
	attr := model.Attributes{MemberGroupIDKey: mgidstr, MemberMessageIDKey: newmmidstr, constants.AttrAuthor: profile.GetOnion(), constants.AttrAck: event.False, constants.AttrSentTimestamp: time.Now().Format(time.RFC3339Nano)}
	return profile.InternalInsertMessage(conversation, 0, hgm.Author, message, attr, hgm.Signature)
}

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

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