package hybrid

import (
	"crypto/ed25519"
	"encoding/base32"
	"encoding/json"
	"fmt"
	"strconv"
	"strings"
	"time"

	"cwtch.im/cwtch/event"

	"cwtch.im/cwtch/peer"

	"cwtch.im/cwtch/model"
	"cwtch.im/cwtch/model/attr"
	"cwtch.im/cwtch/model/constants"
	"git.openprivacy.ca/openprivacy/log"
)

type hybridGroupEvent interface {
	EventType() GroupEventType
}

const ManagedGroupOpen = "managed-group-open"

type GroupEventType int

const (
	MemberGroupIDKey   = "member_group_id_key"
	MemberMessageIDKey = "member_group_messge_id"
)

const (
	AddMember       = GroupEventType(0x1000)
	RemoveMember    = GroupEventType(0x2000)
	RotateKey       = GroupEventType(0x3000)
	NewMessage      = GroupEventType(0x4000)
	NewClearMessage = GroupEventType(0x5000)
	SyncRequest     = GroupEventType(0x6000)
	MemberList      = GroupEventType(0x7000)

	ChangeMemberRole = GroupEventType(0x8000)
	ChangeGroupMode  = GroupEventType(0x9000)
)

type AddMemberEvent struct {
	Handle string `json:"h"`
}

func (e AddMemberEvent) EventType() GroupEventType {
	return AddMember
}

type ChangeGroupModeEvent struct {
	GroupMode model.GroupMode `json:"a"`
	Additive  bool
}

func (e ChangeGroupModeEvent) EventType() GroupEventType {
	return ChangeGroupMode
}

type ChangeMemberRoleEvent struct {
	Handle  string     `json:"h"`
	NewRole model.Role `json:"r"`
}

func (e ChangeMemberRoleEvent) EventType() GroupEventType {
	return ChangeMemberRole
}

type RemoveMemberEvent struct {
	Handle string `json:"h"`
}

func (e RemoveMemberEvent) EventType() GroupEventType {
	return RemoveMember
}

type RotateKeyEvent struct {
	Key []byte `json:"k"`
}

func (e RotateKeyEvent) EventType() GroupEventType {
	return RotateKey
}

type NewMessageEvent struct {
	EncryptedHybridGroupMessage []byte `json:"m"`
}

func (e NewMessageEvent) EventType() GroupEventType {
	return NewMessage
}

type NewClearMessageEvent struct {
	HybridGroupMessage []byte `json:"m"` // overlay message
}

func (e NewClearMessageEvent) EventType() GroupEventType {
	return NewClearMessage
}

type MemberListEvent struct {
	Members   model.MemberList `json:"m"`
	GroupMode model.GroupMode  `json:"g"`
}

func (e MemberListEvent) EventType() GroupEventType {
	return MemberList
}

type SyncRequestMessage struct {
	// a map of MemberGroupID: MemberMessageID
	LastSeen map[int]int `json:"l"`
}

func (e SyncRequestMessage) EventType() GroupEventType {
	return SyncRequest
}

// This file contains code for the Hybrid Group / Managed Group types..
type HybridGroupMessage struct {
	GroupID         string         `json:"g"`
	Author          string         `json:"a"` // the authors cwtch address
	MemberMessageID uint32         `json:"m"`
	MessageBody     string         `json:"b"`
	Sent            uint64         `json:"t"` // milliseconds since epoch
	Signature       []byte         `json:"s"` // of json-encoded content (including empty sig)
	EventType       GroupEventType `json:"e"`
}

// AuthenticateMessage returns true if the Author of the message produced the Signature over the message
func AuthenticateGroupMessage(groupID string, ci *model.Conversation, message HybridGroupMessage) bool {
	messageCopy := message
	messageCopy.Signature = []byte{}
	// Otherwise we derive the public key from the sender and check it against that.
	decodedPub, err := base32.StdEncoding.DecodeString(strings.ToUpper(message.Author))
	if err == nil {
		data, err := json.Marshal(messageCopy)
		if err == nil && len(decodedPub) >= 32 {
			if ed25519.Verify(decodedPub[:32], data, message.Signature) {
				if groupID == message.GroupID {
					// Check that the author is a member of the group
					if _, err := CheckACL(message.Author, ci); err == nil {
						// NOTE at this point we have verified that message.Author is base32 encoded valid public key
						mmid, exists := ci.GetIntAttribute(attr.LocalScope, attr.ConversationZone, fmt.Sprintf("%s::%s", message.Author, MemberMessageIDKey))
						if exists && uint32(mmid) == message.MemberMessageID {
							return false // attested to message identifier is incorrect, could be missing messages from this member...
						}
						return true
					} else if groupID == message.Author {
						// host can always send messages to the group...
						// this is needed so the host can initiate connections / invite members etc.
						mmid, exists := ci.GetIntAttribute(attr.LocalScope, attr.ConversationZone, fmt.Sprintf("%s::%s", message.Author, MemberMessageIDKey))
						if exists && uint32(mmid) == message.MemberMessageID {
							return false // attested to message identifier is incorrect, could be missing messages from this member...
						}
						return true
					} else {
						log.Errorf("group acl attestation did not match %s", err)
						return false
					}
				} else {
					log.Errorf("group attestation did not match %s", groupID, message)
					return false
				}
			}
		}
	}
	log.Errorf("invalid signature on message from %s", message)

	return false
}

func MayJoin(handle string, group *model.Conversation) bool {
	ac, inGroup := group.ACL[handle]
	if inGroup {
		return !ac.Blocked
	}
	return group.GroupMode().IsSet(model.OpenGroup)
}

func CheckACL(handle string, group *model.Conversation) (*model.AccessControl, error) {
	// If this is a closed group. Check if we have an ACL entry for this member
	// If we don't OR that member has been blocked, then close the connection.

	if ac, inGroup := group.ACL[handle]; !inGroup || ac.Blocked {
		log.Infof("ACL Check Failed: %v %v %v", handle, ac, inGroup)
		return nil, fmt.Errorf("peer is not a member of this group")
	} else {
		effectiveAC := ac.AssignedRole.ComputeEffectiveACL(group.GroupMode())
		return &effectiveAC, nil
	}
}

func SendMessageToManagedGroup(profile peer.CwtchPeer, conversation int, groupID string, message hybridGroupEvent) (int, error) {
	id, ojson, err := PrepMessageToManagedGroup(profile, groupID, conversation, message)
	if err != nil {
		return -1, err
	}
	_, err = profile.SendMessage(conversation, ojson)
	return id, err
}

// PrepMessageToManagedGroup additional bookkeeping steps for Hybrid Groups
func PrepMessageToManagedGroup(profile peer.CwtchPeer, groupID string, conversation int, message hybridGroupEvent) (int, string, error) {
	mmidkey := attr.LocalScope.ConstructScopedZonedPath(attr.ConversationZone.ConstructZonedPath(MemberMessageIDKey))
	mmid, err := profile.GetConversationAttributeInt(conversation, mmidkey)
	if err != nil {
		mmid = 0 // first message
	}

	mmid += 1

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

	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

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

	ojson, err := json.Marshal(&model.MessageWrapper{
		Overlay: model.OverlayManageGroupEvent,
		Data:    string(hjson),
	})
	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...
	if ncme, ok := message.(*NewClearMessageEvent); ok {
		newmmidstr := strconv.Itoa(mmid)
		attr := model.Attributes{MemberMessageIDKey: newmmidstr, constants.AttrAuthor: profile.GetOnion(), constants.AttrAck: event.False, constants.AttrSentTimestamp: time.Now().Format(time.RFC3339Nano)}
		id, err := profile.InternalInsertMessage(conversation, 0, hgm.Author, string(ncme.HybridGroupMessage), attr, hgm.Signature)
		return id, string(ojson), err
	}
	return -1, string(ojson), nil
}
