package chat

import (
	"encoding/hex"
	"errors"
	"fmt"
	"math"
	"sort"
	"strings"
	"time"

	"github.com/keybase/client/go/chat/globals"
	"github.com/keybase/client/go/chat/storage"
	"github.com/keybase/client/go/chat/types"
	"github.com/keybase/client/go/chat/utils"
	"github.com/keybase/client/go/libkb"
	"github.com/keybase/client/go/protocol/chat1"
	"github.com/keybase/client/go/protocol/gregor1"
	"github.com/keybase/client/go/protocol/keybase1"
	"github.com/keybase/client/go/teams"
	"golang.org/x/net/context"
)

type Helper struct {
	globals.Contextified
	utils.DebugLabeler

	ri func() chat1.RemoteInterface
}

var _ (libkb.ChatHelper) = (*Helper)(nil)

func NewHelper(g *globals.Context, ri func() chat1.RemoteInterface) *Helper {
	return &Helper{
		Contextified: globals.NewContextified(g),
		DebugLabeler: utils.NewDebugLabeler(g.ExternalG(), "Helper", false),
		ri:           ri,
	}
}

func (h *Helper) NewConversation(ctx context.Context, uid gregor1.UID, tlfName string,
	topicName *string, topicType chat1.TopicType, membersType chat1.ConversationMembersType,
	vis keybase1.TLFVisibility) (chat1.ConversationLocal, bool, error) {
	return NewConversation(ctx, h.G(), uid, tlfName, topicName,
		topicType, membersType, vis, nil, h.ri, NewConvFindExistingNormal)
}

func (h *Helper) NewConversationSkipFindExisting(ctx context.Context, uid gregor1.UID, tlfName string,
	topicName *string, topicType chat1.TopicType, membersType chat1.ConversationMembersType,
	vis keybase1.TLFVisibility) (chat1.ConversationLocal, bool, error) {
	return NewConversation(ctx, h.G(), uid, tlfName, topicName,
		topicType, membersType, vis, nil, h.ri, NewConvFindExistingSkip)
}

func (h *Helper) NewConversationWithMemberSourceConv(ctx context.Context, uid gregor1.UID, tlfName string,
	topicName *string, topicType chat1.TopicType, membersType chat1.ConversationMembersType,
	vis keybase1.TLFVisibility, retentionPolicy *chat1.RetentionPolicy,
	memberSourceConv *chat1.ConversationID) (chat1.ConversationLocal, bool, error) {
	return NewConversationWithMemberSourceConv(ctx, h.G(), uid, tlfName, topicName,
		topicType, membersType, vis, nil, h.ri, NewConvFindExistingNormal, retentionPolicy, memberSourceConv)
}

func (h *Helper) SendTextByID(ctx context.Context, convID chat1.ConversationID,
	tlfName string, text string, vis keybase1.TLFVisibility) error {
	return h.SendMsgByID(ctx, convID, tlfName, chat1.NewMessageBodyWithText(chat1.MessageText{
		Body: text,
	}), chat1.MessageType_TEXT, vis)
}

func (h *Helper) SendMsgByID(ctx context.Context, convID chat1.ConversationID, tlfName string,
	body chat1.MessageBody, msgType chat1.MessageType, vis keybase1.TLFVisibility) error {
	boxer := NewBoxer(h.G())
	sender := NewBlockingSender(h.G(), boxer, h.ri)
	public := vis == keybase1.TLFVisibility_PUBLIC
	msg := chat1.MessagePlaintext{
		ClientHeader: chat1.MessageClientHeader{
			TlfName:     tlfName,
			TlfPublic:   public,
			MessageType: msgType,
		},
		MessageBody: body,
	}
	_, _, err := sender.Send(ctx, convID, msg, 0, nil, nil, nil)
	return err
}

func (h *Helper) SendTextByIDNonblock(ctx context.Context, convID chat1.ConversationID,
	tlfName string, text string, outboxID *chat1.OutboxID, replyTo *chat1.MessageID) (chat1.OutboxID, error) {
	return h.SendMsgByIDNonblock(ctx, convID, tlfName, chat1.NewMessageBodyWithText(chat1.MessageText{
		Body: text,
	}), chat1.MessageType_TEXT, outboxID, replyTo)
}

func (h *Helper) SendMsgByIDNonblock(ctx context.Context, convID chat1.ConversationID,
	tlfName string, body chat1.MessageBody, msgType chat1.MessageType, inOutboxID *chat1.OutboxID,
	replyTo *chat1.MessageID) (chat1.OutboxID, error) {
	boxer := NewBoxer(h.G())
	baseSender := NewBlockingSender(h.G(), boxer, h.ri)
	sender := NewNonblockingSender(h.G(), baseSender)
	msg := chat1.MessagePlaintext{
		ClientHeader: chat1.MessageClientHeader{
			TlfName:     tlfName,
			MessageType: msgType,
		},
		MessageBody: body,
	}
	prepareOpts := chat1.SenderPrepareOptions{
		ReplyTo: replyTo,
	}
	outboxID, _, err := sender.Send(ctx, convID, msg, 0, inOutboxID, nil, &prepareOpts)
	return outboxID, err
}

func (h *Helper) DeleteMsg(ctx context.Context, convID chat1.ConversationID, tlfName string,
	msgID chat1.MessageID) error {
	boxer := NewBoxer(h.G())
	sender := NewBlockingSender(h.G(), boxer, h.ri)
	msg := chat1.MessagePlaintext{
		ClientHeader: chat1.MessageClientHeader{
			TlfName:     tlfName,
			MessageType: chat1.MessageType_DELETE,
			Supersedes:  msgID,
		},
	}
	_, _, err := sender.Send(ctx, convID, msg, 0, nil, nil, nil)
	return err
}

func (h *Helper) DeleteMsgNonblock(ctx context.Context, convID chat1.ConversationID, tlfName string,
	msgID chat1.MessageID) error {
	boxer := NewBoxer(h.G())
	sender := NewNonblockingSender(h.G(), NewBlockingSender(h.G(), boxer, h.ri))
	msg := chat1.MessagePlaintext{
		ClientHeader: chat1.MessageClientHeader{
			TlfName:     tlfName,
			MessageType: chat1.MessageType_DELETE,
			Supersedes:  msgID,
		},
	}
	_, _, err := sender.Send(ctx, convID, msg, 0, nil, nil, nil)
	return err
}

func (h *Helper) SendTextByName(ctx context.Context, name string, topicName *string,
	membersType chat1.ConversationMembersType, ident keybase1.TLFIdentifyBehavior, text string) error {
	boxer := NewBoxer(h.G())
	sender := NewBlockingSender(h.G(), boxer, h.ri)
	helper := newSendHelper(h.G(), name, topicName, membersType, ident, sender, h.ri)
	_, _, err := helper.SendText(ctx, text, nil)
	return err
}

func (h *Helper) SendMsgByName(ctx context.Context, name string, topicName *string,
	membersType chat1.ConversationMembersType, ident keybase1.TLFIdentifyBehavior, body chat1.MessageBody,
	msgType chat1.MessageType) error {
	boxer := NewBoxer(h.G())
	sender := NewBlockingSender(h.G(), boxer, h.ri)
	helper := newSendHelper(h.G(), name, topicName, membersType, ident, sender, h.ri)
	_, _, err := helper.SendBody(ctx, body, msgType, nil)
	return err
}

func (h *Helper) SendTextByNameNonblock(ctx context.Context, name string, topicName *string,
	membersType chat1.ConversationMembersType, ident keybase1.TLFIdentifyBehavior, text string,
	inOutboxID *chat1.OutboxID) (chat1.OutboxID, error) {
	boxer := NewBoxer(h.G())
	baseSender := NewBlockingSender(h.G(), boxer, h.ri)
	sender := NewNonblockingSender(h.G(), baseSender)
	helper := newSendHelper(h.G(), name, topicName, membersType, ident, sender, h.ri)
	outboxID, _, err := helper.SendText(ctx, text, inOutboxID)
	return outboxID, err
}

func (h *Helper) SendMsgByNameNonblock(ctx context.Context, name string, topicName *string,
	membersType chat1.ConversationMembersType, ident keybase1.TLFIdentifyBehavior, body chat1.MessageBody,
	msgType chat1.MessageType, inOutboxID *chat1.OutboxID) (chat1.OutboxID, error) {
	boxer := NewBoxer(h.G())
	baseSender := NewBlockingSender(h.G(), boxer, h.ri)
	sender := NewNonblockingSender(h.G(), baseSender)
	helper := newSendHelper(h.G(), name, topicName, membersType, ident, sender, h.ri)
	outboxID, _, err := helper.SendBody(ctx, body, msgType, inOutboxID)
	return outboxID, err
}

func (h *Helper) FindConversations(ctx context.Context,
	name string, topicName *string,
	topicType chat1.TopicType, membersType chat1.ConversationMembersType, vis keybase1.TLFVisibility) ([]chat1.ConversationLocal, error) {
	kuid, err := CurrentUID(h.G())
	if err != nil {
		return nil, err
	}
	uid := gregor1.UID(kuid.ToBytes())

	oneChat := true
	var tname string
	if topicName != nil {
		tname = utils.SanitizeTopicName(*topicName)
	}
	convs, err := FindConversations(ctx, h.G(), h.DebugLabeler, types.InboxSourceDataSourceAll, h.ri, uid,
		name, topicType, membersType, vis, tname, &oneChat)
	return convs, err
}

func (h *Helper) FindConversationsByID(ctx context.Context, convIDs []chat1.ConversationID) ([]chat1.ConversationLocal, error) {
	kuid, err := CurrentUID(h.G())
	if err != nil {
		return nil, err
	}
	uid := gregor1.UID(kuid.ToBytes())
	query := &chat1.GetInboxLocalQuery{
		ConvIDs: convIDs,
	}
	inbox, _, err := h.G().InboxSource.Read(ctx, uid, types.ConversationLocalizerBlocking,
		types.InboxSourceDataSourceAll, nil, query)
	if err != nil {
		return nil, err
	}
	return inbox.Convs, nil
}

// GetChannelTopicName gets the name of a team channel even if it's not in the inbox.
func (h *Helper) GetChannelTopicName(ctx context.Context, teamID keybase1.TeamID,
	topicType chat1.TopicType, convID chat1.ConversationID) (topicName string, err error) {
	defer h.Trace(ctx, &err, "ChatHelper.GetChannelTopicName")()
	h.Debug(ctx, "for teamID:%v convID:%v", teamID.String(), convID.String())
	kuid, err := CurrentUID(h.G())
	if err != nil {
		return topicName, err
	}
	uid := gregor1.UID(kuid.ToBytes())
	tlfID, err := chat1.TeamIDToTLFID(teamID)
	if err != nil {
		return topicName, err
	}
	query := &chat1.GetInboxLocalQuery{
		ConvIDs: []chat1.ConversationID{convID},
	}
	inbox, _, err := h.G().InboxSource.Read(ctx, uid, types.ConversationLocalizerBlocking,
		types.InboxSourceDataSourceAll, nil, query)
	if err != nil {
		return topicName, err
	}
	h.Debug(ctx, "found inbox convs: %v", len(inbox.Convs))
	for _, conv := range inbox.Convs {
		if conv.GetConvID().Eq(convID) && conv.GetMembersType() == chat1.ConversationMembersType_TEAM {
			return conv.Info.TopicName, nil
		}
	}
	// Fallback to TeamChannelSource
	h.Debug(ctx, "using TeamChannelSource")
	topicName, err = h.G().TeamChannelSource.GetChannelTopicName(ctx, uid, tlfID, topicType, convID)
	return topicName, err
}

func (h *Helper) UpgradeKBFSToImpteam(ctx context.Context, tlfName string, tlfID chat1.TLFID, public bool) (err error) {
	ctx = globals.ChatCtx(ctx, h.G(), keybase1.TLFIdentifyBehavior_CHAT_GUI, nil, NewCachingIdentifyNotifier(h.G()))
	defer h.Trace(ctx, &err, "ChatHelper.UpgradeKBFSToImpteam(%s,%s,%v)",
		tlfID, tlfName, public)()
	var cryptKeys []keybase1.CryptKey
	nis := NewKBFSNameInfoSource(h.G())
	keys, err := nis.AllCryptKeys(ctx, tlfName, public)
	if err != nil {
		return err
	}
	for _, key := range keys[chat1.ConversationMembersType_KBFS] {
		cryptKeys = append(cryptKeys, keybase1.CryptKey{
			KeyGeneration: key.Generation(),
			Key:           key.Material(),
		})
	}
	ni, err := nis.LookupID(ctx, tlfName, public)
	if err != nil {
		return err
	}

	tlfName = ni.CanonicalName
	h.Debug(ctx, "UpgradeKBFSToImpteam: upgrading: TlfName: %s TLFID: %s public: %v keys: %d",
		tlfName, tlfID, public, len(cryptKeys))
	return teams.UpgradeTLFIDToImpteam(ctx, h.G().ExternalG(), tlfName, keybase1.TLFID(tlfID.String()),
		public, keybase1.TeamApplication_CHAT, cryptKeys)
}

func (h *Helper) GetMessages(ctx context.Context, uid gregor1.UID, convID chat1.ConversationID,
	msgIDs []chat1.MessageID, resolveSupersedes bool, reason *chat1.GetThreadReason) ([]chat1.MessageUnboxed, error) {
	return h.G().ConvSource.GetMessages(ctx, convID, uid, msgIDs, reason, nil, resolveSupersedes)
}

func (h *Helper) GetMessage(ctx context.Context, uid gregor1.UID, convID chat1.ConversationID,
	msgID chat1.MessageID, resolveSupersedes bool, reason *chat1.GetThreadReason) (chat1.MessageUnboxed, error) {
	return h.G().ConvSource.GetMessage(ctx, convID, uid, msgID, reason, nil, resolveSupersedes)
}

func (h *Helper) UserReacjis(ctx context.Context, uid gregor1.UID) keybase1.UserReacjis {
	return storage.NewReacjiStore(h.G()).UserReacjis(ctx, uid)
}

func (h *Helper) JourneycardTimeTravel(ctx context.Context, uid gregor1.UID, duration time.Duration) (int, int, error) {
	j, ok := h.G().JourneyCardManager.(*JourneyCardManager)
	if !ok {
		return 0, 0, fmt.Errorf("could not get JourneyCardManager")
	}
	return j.TimeTravel(ctx, uid, duration)
}

func (h *Helper) JourneycardResetAllConvs(ctx context.Context, uid gregor1.UID) error {
	j, ok := h.G().JourneyCardManager.(*JourneyCardManager)
	if !ok {
		return fmt.Errorf("could not get JourneyCardManager")
	}
	return j.ResetAllConvs(ctx, uid)
}

func (h *Helper) JourneycardDebugState(ctx context.Context, uid gregor1.UID, teamID keybase1.TeamID) (string, error) {
	j, ok := h.G().JourneyCardManager.(*JourneyCardManager)
	if !ok {
		return "", fmt.Errorf("could not get JourneyCardManager")
	}
	return j.DebugState(ctx, uid, teamID)
}

// InTeam gives a best effort to answer team membership based on the current state of the inbox cache
func (h *Helper) InTeam(ctx context.Context, uid gregor1.UID, teamID keybase1.TeamID) (bool, error) {
	tlfID := chat1.TLFID(teamID.ToBytes())
	ibox, err := h.G().InboxSource.ReadUnverified(ctx, uid, types.InboxSourceDataSourceLocalOnly,
		&chat1.GetInboxQuery{
			TlfID:            &tlfID,
			MemberStatus:     []chat1.ConversationMemberStatus{chat1.ConversationMemberStatus_ACTIVE},
			AllowUnseenQuery: true,
		})
	if err != nil {
		return false, err
	}
	return len(ibox.ConvsUnverified) > 0, nil
}

type sendHelper struct {
	utils.DebugLabeler

	name        string
	membersType chat1.ConversationMembersType
	ident       keybase1.TLFIdentifyBehavior
	sender      types.Sender
	ri          func() chat1.RemoteInterface

	topicName *string
	convID    chat1.ConversationID
	triple    chat1.ConversationIDTriple

	globals.Contextified
}

func newSendHelper(g *globals.Context, name string, topicName *string,
	membersType chat1.ConversationMembersType, ident keybase1.TLFIdentifyBehavior, sender types.Sender,
	ri func() chat1.RemoteInterface) *sendHelper {
	return &sendHelper{
		Contextified: globals.NewContextified(g),
		DebugLabeler: utils.NewDebugLabeler(g.ExternalG(), "sendHelper", false),
		name:         name,
		topicName:    topicName,
		membersType:  membersType,
		ident:        ident,
		sender:       sender,
		ri:           ri,
	}
}

func (s *sendHelper) SendText(ctx context.Context, text string, outboxID *chat1.OutboxID) (chat1.OutboxID, *chat1.MessageBoxed, error) {
	body := chat1.NewMessageBodyWithText(chat1.MessageText{Body: text})
	return s.SendBody(ctx, body, chat1.MessageType_TEXT, outboxID)
}

func (s *sendHelper) SendBody(ctx context.Context, body chat1.MessageBody, mtype chat1.MessageType,
	outboxID *chat1.OutboxID) (chat1.OutboxID, *chat1.MessageBoxed, error) {
	ctx = globals.ChatCtx(ctx, s.G(), s.ident, nil, NewCachingIdentifyNotifier(s.G()))
	if err := s.conversation(ctx); err != nil {
		return chat1.OutboxID{}, nil, err
	}
	return s.deliver(ctx, body, mtype, outboxID)
}

func (s *sendHelper) conversation(ctx context.Context) error {
	kuid, err := CurrentUID(s.G())
	if err != nil {
		return err
	}
	uid := gregor1.UID(kuid.ToBytes())
	conv, _, err := NewConversation(ctx, s.G(), uid, s.name, s.topicName,
		chat1.TopicType_CHAT, s.membersType, keybase1.TLFVisibility_PRIVATE, nil, s.remoteInterface,
		NewConvFindExistingNormal)
	if err != nil {
		return err
	}
	s.convID = conv.GetConvID()
	s.triple = conv.Info.Triple
	s.name = conv.Info.TlfName
	return nil
}

func (s *sendHelper) deliver(ctx context.Context, body chat1.MessageBody, mtype chat1.MessageType,
	outboxID *chat1.OutboxID) (chat1.OutboxID, *chat1.MessageBoxed, error) {
	msg := chat1.MessagePlaintext{
		ClientHeader: chat1.MessageClientHeader{
			Conv:        s.triple,
			TlfName:     s.name,
			MessageType: mtype,
		},
		MessageBody: body,
	}
	return s.sender.Send(ctx, s.convID, msg, 0, outboxID, nil, nil)
}

func (s *sendHelper) remoteInterface() chat1.RemoteInterface {
	return s.ri()
}

func CurrentUID(g *globals.Context) (keybase1.UID, error) {
	uid := g.Env.GetUID()
	if uid.IsNil() {
		return "", libkb.LoginRequiredError{}
	}
	return uid, nil
}

type recentConversationParticipants struct {
	globals.Contextified
	utils.DebugLabeler
}

func newRecentConversationParticipants(g *globals.Context) *recentConversationParticipants {
	return &recentConversationParticipants{
		Contextified: globals.NewContextified(g),
		DebugLabeler: utils.NewDebugLabeler(g.ExternalG(), "recentConversationParticipants", false),
	}
}

func (r *recentConversationParticipants) getActiveScore(ctx context.Context, conv chat1.Conversation) float64 {
	mtime := conv.GetMtime()
	diff := time.Since(mtime.Time())
	weeksAgo := diff.Seconds() / (time.Hour.Seconds() * 24 * 7)
	val := 10.0 - math.Pow(1.6, weeksAgo)
	if val < 1.0 {
		val = 1.0
	}
	return val
}

func (r *recentConversationParticipants) get(ctx context.Context, myUID gregor1.UID) (res []gregor1.UID, err error) {
	_, convs, err := storage.NewInbox(r.G()).ReadAll(ctx, myUID, true)
	if err != nil {
		if _, ok := err.(storage.MissError); ok {
			r.Debug(ctx, "get: no inbox, returning blank results")
			return nil, nil
		}
		return nil, err
	}

	r.Debug(ctx, "get: convs: %d", len(convs))
	m := make(map[string]float64, len(convs))
	for _, conv := range convs {
		if conv.Conv.Metadata.Status == chat1.ConversationStatus_BLOCKED ||
			conv.Conv.Metadata.Status == chat1.ConversationStatus_REPORTED {
			continue
		}
		for _, uid := range conv.Conv.Metadata.ActiveList {
			if uid.Eq(myUID) {
				continue
			}
			m[uid.String()] += r.getActiveScore(ctx, conv.Conv)
		}
	}
	for suid := range m {
		uid, _ := hex.DecodeString(suid)
		res = append(res, gregor1.UID(uid))
	}

	// Sort by the most appearances in the active lists
	sort.Slice(res, func(i, j int) bool {
		return m[res[i].String()] > m[res[j].String()]
	})
	return res, nil
}

func RecentConversationParticipants(ctx context.Context, g *globals.Context, myUID gregor1.UID) ([]gregor1.UID, error) {
	ctx = globals.ChatCtx(ctx, g, keybase1.TLFIdentifyBehavior_CHAT_GUI, nil, NewCachingIdentifyNotifier(g))
	return newRecentConversationParticipants(g).get(ctx, myUID)
}

func PresentConversationLocalWithFetchRetry(ctx context.Context, g *globals.Context,
	uid gregor1.UID, conv chat1.ConversationLocal, partMode utils.PresentParticipantsMode) (pc *chat1.InboxUIItem) {
	shouldPresent := true
	if conv.Error != nil {
		// If we get a transient failure, add this to the retrier queue
		if conv.Error.Typ == chat1.ConversationErrorType_TRANSIENT {
			g.FetchRetrier.Failure(ctx, uid,
				NewConversationRetry(g, conv.GetConvID(), &conv.Info.Triple.Tlfid, InboxLoad))
		} else {
			// If this is a permanent error, then we don't send anything to the frontend yet.
			shouldPresent = false
		}
	}
	if shouldPresent {
		pc = new(chat1.InboxUIItem)
		*pc = utils.PresentConversationLocal(ctx, g, uid, conv, partMode)
	}
	return pc
}

func GetTopicNameState(ctx context.Context, g *globals.Context, debugger utils.DebugLabeler,
	convs []chat1.ConversationLocal,
	uid gregor1.UID, tlfID chat1.TLFID, topicType chat1.TopicType,
	membersType chat1.ConversationMembersType) (res chat1.TopicNameState, err error) {

	var pairs chat1.ConversationIDMessageIDPairs
	sort.Sort(utils.ConvLocalByConvID(convs))
	for _, conv := range convs {
		msg, err := conv.GetMaxMessage(chat1.MessageType_METADATA)
		if err != nil {
			debugger.Debug(ctx, "GetTopicNameState: unable to get maxmessage: convID: %v, %v", conv.GetConvID(), err)
			continue
		}
		pairs.Pairs = append(pairs.Pairs, chat1.ConversationIDMessageIDPair{
			ConvID: conv.GetConvID(),
			MsgID:  msg.GetMessageID(),
		})
	}

	if res, err = utils.CreateTopicNameState(pairs); err != nil {
		debugger.Debug(ctx, "GetTopicNameState: failed to create topic name state: %v", err)
		return res, err
	}

	return res, nil
}

func FindConversations(ctx context.Context, g *globals.Context, debugger utils.DebugLabeler,
	dataSource types.InboxSourceDataSourceTyp, ri func() chat1.RemoteInterface, uid gregor1.UID,
	tlfName string, topicType chat1.TopicType,
	membersTypeIn chat1.ConversationMembersType, vis keybase1.TLFVisibility, topicName string,
	oneChatPerTLF *bool) (res []chat1.ConversationLocal, err error) {

	findConvosWithMembersType := func(membersType chat1.ConversationMembersType) (res []chat1.ConversationLocal, err error) {
		// Don't look for KBFS conversations anymore, they have mostly been converted, and it is better
		// to just not search for them than to create a double conversation. Make an exception for
		// public conversations.
		if g.GetEnv().GetChatMemberType() != "kbfs" && membersType == chat1.ConversationMembersType_KBFS &&
			vis == keybase1.TLFVisibility_PRIVATE {
			return nil, nil
		}
		// Make sure team topic name makes sense
		if topicName == "" && membersType == chat1.ConversationMembersType_TEAM {
			topicName = globals.DefaultTeamTopic
		}

		// Attempt to resolve any sbs convs incase the team already exists.
		var nameInfo *types.NameInfo
		if strings.Contains(tlfName, "@") || strings.Contains(tlfName, ":") {
			// Fetch the TLF ID from specified name
			if info, err := CreateNameInfoSource(ctx, g, membersType).LookupID(ctx, tlfName, vis == keybase1.TLFVisibility_PUBLIC); err == nil {
				nameInfo = &info
				tlfName = nameInfo.CanonicalName
			}
		}

		query := &chat1.GetInboxLocalQuery{
			Name: &chat1.NameQuery{
				Name:        tlfName,
				MembersType: membersType,
			},
			TlfVisibility:     &vis,
			TopicName:         &topicName,
			TopicType:         &topicType,
			OneChatTypePerTLF: oneChatPerTLF,
		}

		inbox, _, err := g.InboxSource.Read(ctx, uid, types.ConversationLocalizerBlocking, dataSource, nil,
			query)
		if err != nil {
			acceptableErr := false
			// if we fail to load the team for some kind of rekey reason, treat as a complete miss
			if _, ok := IsRekeyError(err); ok {
				acceptableErr = true
			}
			// don't error out if the TLF name is just unknown, treat it as a complete miss
			if _, ok := err.(UnknownTLFNameError); ok {
				acceptableErr = true
			}
			if !acceptableErr {
				return res, err
			}
			inbox.Convs = nil
		}

		// If we have inbox hits, return those
		if len(inbox.Convs) > 0 {
			debugger.Debug(ctx, "FindConversations: found conversations in inbox: tlfName: %s num: %d",
				tlfName, len(inbox.Convs))
			res = inbox.Convs
		} else if membersType == chat1.ConversationMembersType_TEAM {
			// If this is a team chat that we are looking for, then let's try searching all
			// chats on the team to see if any match the arguments before giving up.
			// No need to worry (yet) about conflicting with public code path, since there
			// are not any public team chats.

			// Fetch the TLF ID from specified name
			if nameInfo == nil {
				info, err := CreateNameInfoSource(ctx, g, membersType).LookupID(ctx, tlfName, false)
				if err != nil {
					debugger.Debug(ctx, "FindConversations: failed to get TLFID from name: %s", err.Error())
					return res, err
				}
				nameInfo = &info
			}
			tlfConvs, err := g.TeamChannelSource.GetChannelsFull(ctx, uid, nameInfo.ID, topicType)
			if err != nil {
				debugger.Debug(ctx, "FindConversations: failed to list TLF conversations: %s", err.Error())
				return res, err
			}

			for _, tlfConv := range tlfConvs {
				if tlfConv.Info.TopicName == topicName {
					res = append(res, tlfConv)
				}
			}
			if len(res) > 0 {
				debugger.Debug(ctx, "FindConversations: found team channels: num: %d", len(res))
			}
		} else if vis == keybase1.TLFVisibility_PUBLIC {
			debugger.Debug(ctx, "FindConversations: no conversations found in inbox, trying public chats")

			// Check for offline and return an error
			if g.InboxSource.IsOffline(ctx) {
				return res, OfflineError{}
			}

			// If we miss the inbox, and we are looking for a public TLF, let's try and find
			// any conversation that matches
			nameInfo, err := GetInboxQueryNameInfo(ctx, g, query)
			if err != nil {
				return res, err
			}

			// Call into gregor to try and find some public convs
			pubConvs, err := ri().GetPublicConversations(ctx, chat1.GetPublicConversationsArg{
				TlfID:            nameInfo.ID,
				TopicType:        topicType,
				SummarizeMaxMsgs: true,
			})
			if err != nil {
				return res, err
			}

			// Localize the convs (if any)
			if len(pubConvs.Conversations) > 0 {
				convsLocal, _, err := g.InboxSource.Localize(ctx, uid,
					utils.RemoteConvs(pubConvs.Conversations), types.ConversationLocalizerBlocking)
				if err != nil {
					return res, err
				}

				// Search for conversations that match the topic name
				for _, convLocal := range convsLocal {
					if convLocal.Error != nil {
						debugger.Debug(ctx, "FindConversations: skipping convID: %s localization failure: %s",
							convLocal.GetConvID(), convLocal.Error.Message)
						continue
					}
					if convLocal.Info.TopicName == topicName &&
						convLocal.Info.TLFNameExpanded() == nameInfo.CanonicalName {
						debugger.Debug(ctx, "FindConversations: found matching public conv: id: %s topicName: %s",
							convLocal.GetConvID(), topicName)
						res = append(res, convLocal)
					}
				}
			}

		}
		return res, nil
	}

	attempts := make(map[chat1.ConversationMembersType]bool)
	mt := membersTypeIn
L:
	for {
		var ierr error
		attempts[mt] = true
		res, ierr = findConvosWithMembersType(mt)
		if ierr != nil || len(res) == 0 {
			if ierr != nil {
				debugger.Debug(ctx, "FindConversations: fail reason: %s mt: %v", ierr, mt)
			} else {
				debugger.Debug(ctx, "FindConversations: fail reason: no convs mt: %v", mt)
			}
			var newMT chat1.ConversationMembersType
			switch mt {
			case chat1.ConversationMembersType_TEAM:
				err = ierr
				debugger.Debug(ctx, "FindConversations: failed with team, aborting")
				break L
			case chat1.ConversationMembersType_IMPTEAMUPGRADE:
				if !attempts[chat1.ConversationMembersType_IMPTEAMNATIVE] {
					newMT = chat1.ConversationMembersType_IMPTEAMNATIVE
					// Only set the error if the members type is the same as what was passed in
					err = ierr
				} else {
					newMT = chat1.ConversationMembersType_KBFS
				}
			case chat1.ConversationMembersType_IMPTEAMNATIVE:
				if !attempts[chat1.ConversationMembersType_IMPTEAMUPGRADE] {
					newMT = chat1.ConversationMembersType_IMPTEAMUPGRADE
					// Only set the error if the members type is the same as what was passed in
					err = ierr
				} else {
					newMT = chat1.ConversationMembersType_KBFS
				}
			case chat1.ConversationMembersType_KBFS:
				debugger.Debug(ctx, "FindConversations: failed with KBFS, aborting")
				// We don't want to return random errors from KBFS if we are falling back to it,
				// just return no conversations and call it a day
				if membersTypeIn == chat1.ConversationMembersType_KBFS {
					err = ierr
				}
				break L
			}
			debugger.Debug(ctx,
				"FindConversations: failing to find anything for %v, trying again for %v", mt, newMT)
			mt = newMT
		} else {
			debugger.Debug(ctx, "FindConversations: success with mt: %v", mt)
			break L
		}
	}
	return res, err
}

// Post a join or leave message. Must be called when the user is in the conv.
// Uses a blocking sender.
func postJoinLeave(ctx context.Context, g *globals.Context, ri func() chat1.RemoteInterface, uid gregor1.UID,
	convID chat1.ConversationID, body chat1.MessageBody) (err error) {
	typ, err := body.MessageType()
	if err != nil {
		return fmt.Errorf("message type for postJoinLeave: %v", err)
	}
	switch typ {
	case chat1.MessageType_JOIN, chat1.MessageType_LEAVE:
	// good
	default:
		return fmt.Errorf("invalid message type for postJoinLeave: %v", typ)
	}

	// Get the conversation from the inbox.
	query := chat1.GetInboxLocalQuery{
		ConvIDs: []chat1.ConversationID{convID},
	}
	ib, _, err := g.InboxSource.Read(ctx, uid, types.ConversationLocalizerBlocking,
		types.InboxSourceDataSourceAll, nil, &query)
	if err != nil {
		return fmt.Errorf("inbox read error: %s", err)
	}
	if len(ib.Convs) != 1 {
		return fmt.Errorf("post join/leave: found %d conversations", len(ib.Convs))
	}

	conv := ib.Convs[0]
	if conv.GetTopicType() != chat1.TopicType_CHAT {
		// only post these in chat convs
		return nil
	}
	plaintext := chat1.MessagePlaintext{
		ClientHeader: chat1.MessageClientHeader{
			Conv:         conv.Info.Triple,
			TlfName:      conv.Info.TlfName,
			TlfPublic:    conv.Info.Visibility == keybase1.TLFVisibility_PUBLIC,
			MessageType:  typ,
			Supersedes:   chat1.MessageID(0),
			Deletes:      nil,
			Prev:         nil, // Filled by Sender
			Sender:       nil, // Filled by Sender
			SenderDevice: nil, // Filled by Sender
			MerkleRoot:   nil, // Filled by Boxer
			OutboxID:     nil,
			OutboxInfo:   nil,
		},
		MessageBody: body,
	}

	// Send with a blocking sender
	sender := NewBlockingSender(g, NewBoxer(g), ri)
	_, _, err = sender.Send(ctx, convID, plaintext, 0, nil, nil, nil)
	return err
}

func (h *Helper) JoinConversationByID(ctx context.Context, uid gregor1.UID, convID chat1.ConversationID) (err error) {
	defer h.Trace(ctx, &err, "ChatHelper.JoinConversationByID")()
	return JoinConversation(ctx, h.G(), h.DebugLabeler, h.ri, uid, convID)
}

func JoinConversation(ctx context.Context, g *globals.Context, debugger utils.DebugLabeler,
	ri func() chat1.RemoteInterface, uid gregor1.UID, convID chat1.ConversationID) (err error) {
	if err := g.ConvSource.AcquireConversationLock(ctx, uid, convID); err != nil {
		return err
	}
	defer g.ConvSource.ReleaseConversationLock(ctx, uid, convID)

	if alreadyIn, err := g.InboxSource.IsMember(ctx, uid, convID); err != nil {
		// charge forward anyway
		debugger.Debug(ctx, "JoinConversation: IsMember err: %v", err)
	} else if alreadyIn {
		return nil
	}

	if _, err = ri().JoinConversation(ctx, convID); err != nil {
		debugger.Debug(ctx, "JoinConversation: failed to join conversation: %v", err)
		return err
	}

	if _, err = g.InboxSource.MembershipUpdate(ctx, uid, 0, []chat1.ConversationMember{
		{
			Uid:    uid,
			ConvID: convID,
		},
	}, nil, nil, nil, nil); err != nil {
		debugger.Debug(ctx, "JoinConversation: failed to apply membership update: %v", err)
	}
	// Send a message to the channel after joining
	joinMessageBody := chat1.NewMessageBodyWithJoin(chat1.MessageJoin{})
	debugger.Debug(ctx, "JoinConversation: sending join message to: %s", convID)
	if err := postJoinLeave(ctx, g, ri, uid, convID, joinMessageBody); err != nil {
		debugger.Debug(ctx, "JoinConversation: posting join-conv message failed: %v", err)
		// ignore the error
	}
	return nil
}

func (h *Helper) JoinConversationByName(ctx context.Context, uid gregor1.UID, tlfName, topicName string,
	topicType chat1.TopicType, vis keybase1.TLFVisibility) (err error) {
	defer h.Trace(ctx, &err, "ChatHelper.JoinConversationByName")()
	return JoinConversationByName(ctx, h.G(), h.DebugLabeler, h.ri, uid, tlfName, topicName, topicType, vis)
}

func JoinConversationByName(ctx context.Context, g *globals.Context, debugger utils.DebugLabeler,
	ri func() chat1.RemoteInterface, uid gregor1.UID, tlfName, topicName string, topicType chat1.TopicType,
	vis keybase1.TLFVisibility) (err error) {
	// Fetch the TLF ID from specified name
	nameInfo, err := CreateNameInfoSource(ctx, g, chat1.ConversationMembersType_TEAM).LookupID(ctx,
		tlfName, vis == keybase1.TLFVisibility_PUBLIC)
	if err != nil {
		debugger.Debug(ctx, "JoinConversationByName: failed to get TLFID from name: %s", err.Error())
		return err
	}

	// List all the conversations on the team
	convs, err := g.TeamChannelSource.GetChannelsFull(ctx, uid, nameInfo.ID, topicType)
	if err != nil {
		return err
	}
	var convID chat1.ConversationID
	for _, conv := range convs {
		convTopicName := conv.Info.TopicName
		if convTopicName != "" && convTopicName == topicName {
			convID = conv.GetConvID()
		}
	}
	if convID.IsNil() {
		return fmt.Errorf("no topic name %s exists on specified team", topicName)
	}
	if err = JoinConversation(ctx, g, debugger, ri, uid, convID); err != nil {
		return err
	}
	return nil
}

func (h *Helper) LeaveConversation(ctx context.Context, uid gregor1.UID, convID chat1.ConversationID) (err error) {
	defer h.Trace(ctx, &err, "ChatHelper.LeaveConversation")()
	return LeaveConversation(ctx, h.G(), h.DebugLabeler, h.ri, uid, convID)
}

func LeaveConversation(ctx context.Context, g *globals.Context, debugger utils.DebugLabeler,
	ri func() chat1.RemoteInterface, uid gregor1.UID, convID chat1.ConversationID) (err error) {
	alreadyIn, err := g.InboxSource.IsMember(ctx, uid, convID)
	if err != nil {
		debugger.Debug(ctx, "LeaveConversation: IsMember err: %s", err.Error())
		// Pretend we're in.
		alreadyIn = true
	}

	// Send a message to the channel to leave the conversation
	if alreadyIn {
		leaveMessageBody := chat1.NewMessageBodyWithLeave(chat1.MessageLeave{})
		err := postJoinLeave(ctx, g, ri, uid, convID, leaveMessageBody)
		if err != nil {
			debugger.Debug(ctx, "LeaveConversation: posting leave-conv message failed: %v", err)
			return err
		}
	} else {
		_, err = ri().LeaveConversation(ctx, convID)
		if err != nil {
			debugger.Debug(ctx, "LeaveConversation: failed to leave conversation as a non-member: %s", err)
			return err
		}
	}

	return nil
}

func PreviewConversation(ctx context.Context, g *globals.Context, debugger utils.DebugLabeler,
	ri func() chat1.RemoteInterface, uid gregor1.UID, convID chat1.ConversationID) (res chat1.ConversationLocal, err error) {
	alreadyIn, err := g.InboxSource.IsMember(ctx, uid, convID)
	if err != nil {
		debugger.Debug(ctx, "PreviewConversation: IsMember err: %s", err.Error())
		// Assume we aren't in, server will reject us otherwise.
		alreadyIn = false
	}
	if alreadyIn {
		debugger.Debug(ctx, "PreviewConversation: already in the conversation, no need to preview")
		return utils.GetVerifiedConv(ctx, g, uid, convID, types.InboxSourceDataSourceAll)
	}

	if _, err = ri().PreviewConversation(ctx, convID); err != nil {
		debugger.Debug(ctx, "PreviewConversation: failed to preview conversation: %s", err.Error())
		return res, err
	}
	return utils.GetVerifiedConv(ctx, g, uid, convID, types.InboxSourceDataSourceRemoteOnly)
}

func RemoveFromConversation(ctx context.Context, g *globals.Context, debugger utils.DebugLabeler,
	ri func() chat1.RemoteInterface, convID chat1.ConversationID, usernames []string) (err error) {
	users := make([]gregor1.UID, len(usernames))
	for i, username := range usernames {
		uid, err := g.GetUPAKLoader().LookupUID(ctx, libkb.NewNormalizedUsername(username))
		if err != nil {
			return fmt.Errorf("error resolving user %s: %s", username, err)
		}
		users[i] = uid.ToBytes()
	}

	_, err = ri().RemoveFromConversation(ctx, chat1.RemoveFromConversationArg{
		ConvID: convID,
		Users:  users,
	})
	return err
}

type NewConvFindExistingMode int

const (
	NewConvFindExistingNormal NewConvFindExistingMode = iota
	NewConvFindExistingSkip
)

func NewConversation(ctx context.Context, g *globals.Context, uid gregor1.UID, tlfName string,
	topicName *string, topicType chat1.TopicType, membersType chat1.ConversationMembersType,
	vis keybase1.TLFVisibility, knownTopicID *chat1.TopicID, ri func() chat1.RemoteInterface,
	findExistingMode NewConvFindExistingMode) (chat1.ConversationLocal, bool, error) {
	return NewConversationWithMemberSourceConv(ctx, g, uid, tlfName, topicName, topicType, membersType, vis,
		knownTopicID, ri, findExistingMode, nil, nil)
}

func NewConversationWithMemberSourceConv(ctx context.Context, g *globals.Context, uid gregor1.UID,
	tlfName string, topicName *string, topicType chat1.TopicType, membersType chat1.ConversationMembersType,
	vis keybase1.TLFVisibility, knownTopicID *chat1.TopicID, ri func() chat1.RemoteInterface,
	findExistingMode NewConvFindExistingMode, retentionPolicy *chat1.RetentionPolicy,
	memberSourceConv *chat1.ConversationID) (chat1.ConversationLocal, bool, error) {
	defer utils.SuspendComponent(ctx, g, g.ConvLoader)()
	helper := newNewConversationHelper(g, uid, tlfName, topicName, topicType, membersType, vis,
		ri, findExistingMode, retentionPolicy, memberSourceConv, knownTopicID)
	return helper.create(ctx)
}

type newConversationHelper struct {
	globals.Contextified
	utils.DebugLabeler

	uid              gregor1.UID
	tlfName          string
	topicName        *string
	topicType        chat1.TopicType
	topicID          *chat1.TopicID
	membersType      chat1.ConversationMembersType
	memberSourceConv *chat1.ConversationID
	vis              keybase1.TLFVisibility
	ri               func() chat1.RemoteInterface
	findExistingMode NewConvFindExistingMode
	retentionPolicy  *chat1.RetentionPolicy
}

func newNewConversationHelper(g *globals.Context, uid gregor1.UID, tlfName string, topicName *string,
	topicType chat1.TopicType, membersType chat1.ConversationMembersType, vis keybase1.TLFVisibility,
	ri func() chat1.RemoteInterface, findExistingMode NewConvFindExistingMode,
	retentionPolicy *chat1.RetentionPolicy, memberSourceConv *chat1.ConversationID,
	knownTopicID *chat1.TopicID) *newConversationHelper {
	return &newConversationHelper{
		Contextified:     globals.NewContextified(g),
		DebugLabeler:     utils.NewDebugLabeler(g.ExternalG(), "newConversationHelper", false),
		uid:              uid,
		tlfName:          utils.AddUserToTLFName(g, tlfName, vis, membersType),
		topicName:        topicName,
		topicType:        topicType,
		membersType:      membersType,
		memberSourceConv: memberSourceConv,
		vis:              vis,
		ri:               ri,
		findExistingMode: findExistingMode,
		retentionPolicy:  retentionPolicy,
		topicID:          knownTopicID,
	}
}

func (n *newConversationHelper) findExisting(ctx context.Context, tlfID chat1.TLFID, topicName string,
	dataSource types.InboxSourceDataSourceTyp) (res []chat1.ConversationLocal, err error) {
	switch n.findExistingMode {
	case NewConvFindExistingNormal:
		ib, _, err := n.G().InboxSource.Read(ctx, n.uid, types.ConversationLocalizerBlocking,
			dataSource, nil, &chat1.GetInboxLocalQuery{
				Name: &chat1.NameQuery{
					Name:        n.tlfName,
					TlfID:       &tlfID,
					MembersType: n.membersType,
				},
				MemberStatus:  chat1.AllConversationMemberStatuses(),
				TlfVisibility: &n.vis,
				TopicName:     &topicName,
				TopicType:     &n.topicType,
			})
		if err != nil {
			return res, err
		}
		return ib.Convs, nil
	case NewConvFindExistingSkip:
		return nil, nil
	}
	return nil, nil
}

func (n *newConversationHelper) getNameInfo(ctx context.Context) (res types.NameInfo, err error) {
	isPublic := n.vis == keybase1.TLFVisibility_PUBLIC
	switch n.membersType {
	case chat1.ConversationMembersType_KBFS, chat1.ConversationMembersType_TEAM,
		chat1.ConversationMembersType_IMPTEAMUPGRADE:
		return CreateNameInfoSource(ctx, n.G(), n.membersType).LookupID(ctx, n.tlfName, isPublic)
	case chat1.ConversationMembersType_IMPTEAMNATIVE:
		// NameInfoSource interface doesn't allow us to quickly lookup and create at the same time,
		// so let's just do this manually here. Note: this will allow a user to dup impteamupgrade
		// convs with unresolved assertions in them, the server can catch any normal convs being duped.
		if override, _ := globals.CtxOverrideNameInfoSource(ctx); override != nil {
			return override.LookupID(ctx, n.tlfName, isPublic)
		}
		team, _, impTeamName, err := teams.LookupOrCreateImplicitTeam(ctx, n.G().ExternalG(), n.tlfName,
			isPublic)
		if err != nil {
			return res, err
		}
		return types.NameInfo{
			ID:            chat1.TLFID(team.ID.ToBytes()),
			CanonicalName: impTeamName.String(),
		}, nil
	}
	return res, errors.New("unknown members type")
}

func (n *newConversationHelper) findExistingViaInboxSearch(ctx context.Context, searchTopicName string) *chat1.ConversationLocal {
	query := utils.StripUsernameFromConvName(n.tlfName, n.G().GetEnv().GetUsername().String())
	n.Debug(ctx, "findExistingViaInboxSearch: looking for: %s", query)
	convs, err := n.G().InboxSource.Search(ctx, n.uid, query, 0, types.InboxSourceSearchEmptyModeAll)
	if err != nil {
		n.Debug(ctx, "findExistingViaInboxSearch: failed to perform inbox search: %s", err)
		return nil
	}
	if len(convs) == 0 {
		n.Debug(ctx, "findExistingViaInboxSearch: no convs found from search")
		return nil
	}

	convsLocal, _, err := n.G().InboxSource.Localize(ctx, n.uid, convs, types.ConversationLocalizerBlocking)
	if err != nil {
		n.Debug(ctx, "findExistingViaInboxSearch: failed to localize: %s", err)
		return nil
	}
	searchIsPublic := n.vis == keybase1.TLFVisibility_PUBLIC
	for _, conv := range convsLocal {
		convName := conv.Info.TlfName
		if conv.Error != nil {
			convName = conv.Error.UnverifiedTLFName
		}
		convName = utils.StripUsernameFromConvName(convName, n.G().GetEnv().GetUsername().String())
		n.Debug(ctx, "findExistingViaInboxSearch: candidate: %s", convName)
		if convName == query && conv.GetTopicType() == n.topicType &&
			conv.GetTopicName() == searchTopicName && conv.GetMembersType() == n.membersType &&
			conv.IsPublic() == searchIsPublic {
			n.Debug(ctx, "findExistingViaInboxSearch: found conv match: %s id: %s", conv.Info.TlfName,
				conv.GetConvID())
			return &conv
		}
	}
	n.Debug(ctx, "findExistingViaInboxSearch: no convs found with exact match")
	return nil
}

func (n *newConversationHelper) create(ctx context.Context) (res chat1.ConversationLocal, created bool, reserr error) {
	defer n.Trace(ctx, &reserr, "newConversationHelper")()
	// Handle a nil topic name with default values for the members type specified
	if n.topicName == nil {
		// We never want a blank topic name in team chats, always default to the default team name
		switch n.membersType {
		case chat1.ConversationMembersType_TEAM:
			n.topicName = &globals.DefaultTeamTopic
		default:
			// Nothing to do for other member types.
		}
	}

	var findConvsTopicName string
	if n.topicName != nil {
		findConvsTopicName = utils.SanitizeTopicName(*n.topicName)
	}
	info, err := n.getNameInfo(ctx)
	if err != nil {
		// If we failed this, just do a quick inbox search to see if we can find one with the same name.
		// This can happen if a user tries to create a conversation with the same person as a conversation
		// in which they are currently locked out due to reset.
		if conv := n.findExistingViaInboxSearch(ctx, findConvsTopicName); conv != nil {
			return *conv, false, nil
		}
		return res, false, err
	}
	n.tlfName = info.CanonicalName

	// Find any existing conversations that match this argument specifically. We need to do this check
	// here in the client since we can't see the topic name on the server.

	// NOTE: The CLI already does this. It is hard to move that code completely into the service, since
	// there is a ton of logic in there to try and present a nice looking menu to help out the
	// user and such. For the most part, the CLI just uses FindConversationsLocal though, so it
	// should hopefully just result in a bunch of cache hits on the second invocation.
	convs, err := n.findExisting(ctx, info.ID, findConvsTopicName, types.InboxSourceDataSourceAll)
	if err != nil {
		n.Debug(ctx, "error running findExisting: %s", err)
		convs = nil
	}
	// If we find one conversation, then just return it as if we created it.
	if len(convs) == 1 {
		// if we have a known topic ID, make sure we hit it
		if n.topicID == nil || n.topicID.Eq(convs[0].Info.Triple.TopicID) {
			n.Debug(ctx, "found previous conversation that matches, returning")
			return convs[0], false, nil
		}
	}

	if n.G().ExternalG().Env.GetChatMemberType() == "impteam" {
		// if KBFS, return an error. Need to use IMPTEAM now.
		if n.membersType == chat1.ConversationMembersType_KBFS {
			// let it slide in devel for tests
			if n.G().ExternalG().Env.GetRunMode() != libkb.DevelRunMode {
				n.Debug(ctx, "KBFS conversations deprecated; switching membersType from KBFS to IMPTEAM")
				n.membersType = chat1.ConversationMembersType_IMPTEAMNATIVE
			}
		}
	}

	n.Debug(ctx, "no matching previous conversation, proceeding to create new conv")
	triple := chat1.ConversationIDTriple{
		Tlfid:     info.ID,
		TopicType: n.topicType,
		TopicID:   make(chat1.TopicID, 16),
	}

	// If we get a ChatStalePreviousStateError we blow away in the box cache
	// once to allow the retry to get fresh data.
	clearedCache := false
	isPublic := n.vis == keybase1.TLFVisibility_PUBLIC
	for i := 0; i < 5; i++ {
		if n.topicID != nil {
			triple.TopicID = *n.topicID
		} else {
			triple.TopicID, err = utils.NewChatTopicID()
			if err != nil {
				return res, false, fmt.Errorf("error creating topic ID: %s", err)
			}
		}
		n.Debug(ctx, "attempt: %v [tlfID: %s topicType: %d topicID: %s name: %s public: %v mt: %v]",
			i, triple.Tlfid, triple.TopicType, triple.TopicID, info.CanonicalName, isPublic,
			n.membersType)
		firstMessageBoxed, topicNameState, err := n.makeFirstMessage(ctx, triple, info.CanonicalName,
			n.membersType, n.vis, n.topicName)
		switch err := err.(type) {
		case nil:
		case DuplicateTopicNameError:
			return err.Conv, false, nil
		default:
			return res, false, err
		}

		var ncrres chat1.NewConversationRemoteRes
		ncrres, reserr = n.ri().NewConversationRemote2(ctx, chat1.NewConversationRemote2Arg{
			IdTriple:         triple,
			TLFMessage:       *firstMessageBoxed,
			MembersType:      n.membersType,
			TopicNameState:   topicNameState,
			MemberSourceConv: n.memberSourceConv,
			RetentionPolicy:  n.retentionPolicy,
		})
		convID := ncrres.ConvID
		if reserr != nil {
			switch cerr := reserr.(type) {
			case libkb.ChatStalePreviousStateError:
				n.Debug(ctx, "stale topic name state, trying again")
				if !clearedCache {
					n.Debug(ctx, "Send: clearing inbox cache to retry stale previous state")
					err := n.G().InboxSource.Clear(ctx, n.uid, &types.ClearOpts{
						SendLocalAdminNotification: true,
						Reason:                     "received ChatStalePreviousStateError",
					})
					if err != nil {
						n.Debug(ctx, "Send: error clearing inbox: %+v", err)
					}
					clearedCache = true
				}
				continue
			case libkb.ChatConvExistsError:
				// This triple already exists.
				n.Debug(ctx, "conv exists: %v", cerr.ConvID)
				if n.topicID != nil {
					// if the topicID is hardcoded, just fail right away
					return res, false, reserr
				}
				if triple.TopicType != chat1.TopicType_CHAT ||
					n.membersType == chat1.ConversationMembersType_TEAM {
					// THIS CHECK IS FOR WHEN THE SERVER RETURNS THIS ERROR WHEN PREVENTING
					// MULTIPLE CHANNELS ON NON-TEAM CHATS. IT TRIES TO REDIRECT YOU TO THE CONV
					// THAT IS ALREADY THERE.
					//
					// Not a chat (or is a team) conversation. Multiples are fine. Just retry with a
					// different topic ID.
					continue
				}
				// A chat conversation already exists; just reuse it. See above comment.
				// Note that from this point on, TopicID is entirely the wrong value.
				convID = cerr.ConvID
			case libkb.ChatCollisionError:
				// The triple did not exist, but a collision occurred on convID. Retry with a different topic ID.
				n.Debug(ctx, "collision: %v", reserr)
				if n.topicID != nil {
					// if the topicID is hardcoded, just fail right away
					return res, false, reserr
				}
				continue
			case libkb.ChatClientError:
				// just make sure we can't find anything with FindConversations if we get this back
				topicName := ""
				if n.topicName != nil {
					topicName = *n.topicName
				}
				fcRes, err := FindConversations(ctx, n.G(), n.DebugLabeler, types.InboxSourceDataSourceAll,
					n.ri, n.uid, n.tlfName, n.topicType, n.membersType, n.vis, topicName, nil)
				if err != nil {
					n.Debug(ctx, "failed trying FindConversations after client error: %s", err)
					return res, false, reserr
				} else if len(fcRes) > 0 {
					convID = fcRes[0].GetConvID()
				} else {
					return res, false, reserr
				}
			case libkb.ChatNotInTeamError:
				if n.membersType == chat1.ConversationMembersType_TEAM {
					teamID, tmpErr := TLFIDToTeamID(triple.Tlfid)
					if tmpErr == nil && teamID.IsSubTeam() {
						n.Debug(ctx, "For tlf ID %s, inferring NotExplicitMemberOfSubteamError, from error: %s", triple.Tlfid, reserr.Error())
						return res, false, teams.NewNotExplicitMemberOfSubteamError()
					}
				}
				return res, false, fmt.Errorf("error creating conversation: %s", reserr)
			default:
				return res, false, fmt.Errorf("error creating conversation: %s", reserr)
			}
		}

		n.Debug(ctx, "established conv: %v", convID)

		// create succeeded; grabbing the conversation and returning
		ib, _, err := n.G().InboxSource.Read(ctx, n.uid, types.ConversationLocalizerBlocking,
			types.InboxSourceDataSourceRemoteOnly, nil,
			&chat1.GetInboxLocalQuery{
				ConvIDs: []chat1.ConversationID{convID},
			})
		if err != nil {
			return res, false, err
		}

		if len(ib.Convs) != 1 {
			return res, false,
				fmt.Errorf("newly created conversation fetch error: found %d conversations", len(ib.Convs))
		}
		res = ib.Convs[0]
		n.Debug(ctx, "fetched conv: %v mt: %v public: %v", res.GetConvID(), res.GetMembersType(),
			res.IsPublic())

		// Update inbox cache
		updateConv := ib.ConvsUnverified[0]
		if err = n.G().InboxSource.NewConversation(ctx, n.uid, 0, updateConv.Conv); err != nil {
			return res, false, err
		}

		if res.Error != nil {
			return res, false, errors.New(res.Error.Message)
		}

		// Send a message to the channel after joining.
		switch n.membersType {
		case chat1.ConversationMembersType_TEAM:
			// don't send join messages to #general
			if findConvsTopicName != globals.DefaultTeamTopic {
				joinMessageBody := chat1.NewMessageBodyWithJoin(chat1.MessageJoin{})
				if err := postJoinLeave(ctx, n.G(), n.ri, n.uid, convID, joinMessageBody); err != nil {
					n.Debug(ctx, "posting join-conv message failed: %v", err)
					// ignore the error
				}
			}
		default:
			// pass
		}

		// If we created a complex team in the process of creating this conversation, send a special
		// message into the general channel letting everyone know about the change.
		if ncrres.CreatedComplexTeam {
			subBody := chat1.NewMessageSystemWithComplexteam(chat1.MessageSystemComplexTeam{
				Team: n.tlfName,
			})
			body := chat1.NewMessageBodyWithSystem(subBody)
			if _, err := n.G().ChatHelper.SendMsgByNameNonblock(ctx, n.tlfName, &globals.DefaultTeamTopic,
				chat1.ConversationMembersType_TEAM, keybase1.TLFIdentifyBehavior_CHAT_GUI,
				body, chat1.MessageType_SYSTEM, nil); err != nil {
				n.Debug(ctx, "failed to send complex team intro message: %s", err)
			}
		}
		return res, true, nil
	}
	return res, false, reserr
}

func (n *newConversationHelper) makeFirstMessage(ctx context.Context, triple chat1.ConversationIDTriple,
	tlfName string, membersType chat1.ConversationMembersType, tlfVisibility keybase1.TLFVisibility,
	topicName *string) (*chat1.MessageBoxed, *chat1.TopicNameState, error) {
	var msg chat1.MessagePlaintext
	if topicName != nil {
		msg = chat1.MessagePlaintext{
			ClientHeader: chat1.MessageClientHeader{
				Conv:        triple,
				TlfName:     tlfName,
				TlfPublic:   tlfVisibility == keybase1.TLFVisibility_PUBLIC,
				MessageType: chat1.MessageType_METADATA,
				Prev:        nil, // TODO
				// Sender and SenderDevice filled by prepareMessageForRemote
			},
			MessageBody: chat1.NewMessageBodyWithMetadata(
				chat1.MessageConversationMetadata{
					ConversationTitle: *topicName,
				}),
		}
	} else {
		if membersType == chat1.ConversationMembersType_TEAM {
			return nil, nil, errors.New("team conversations require a topic name")
		}
		msg = chat1.MessagePlaintext{
			ClientHeader: chat1.MessageClientHeader{
				Conv:        triple,
				TlfName:     tlfName,
				TlfPublic:   tlfVisibility == keybase1.TLFVisibility_PUBLIC,
				MessageType: chat1.MessageType_TLFNAME,
				Prev:        nil, // TODO
				// Sender and SenderDevice filled by prepareMessageForRemote
			},
		}
	}
	opts := chat1.SenderPrepareOptions{
		SkipTopicNameState: n.findExistingMode == NewConvFindExistingSkip,
	}
	sender := NewBlockingSender(n.G(), NewBoxer(n.G()), n.ri)
	prepareRes, err := sender.Prepare(ctx, msg, membersType, nil, &opts)
	return &prepareRes.Boxed, prepareRes.TopicNameState, err
}

func CreateNameInfoSource(ctx context.Context, g *globals.Context, membersType chat1.ConversationMembersType) types.NameInfoSource {
	if override, _ := globals.CtxOverrideNameInfoSource(ctx); override != nil {
		return override
	}
	switch membersType {
	case chat1.ConversationMembersType_KBFS:
		return NewKBFSNameInfoSource(g)
	case chat1.ConversationMembersType_TEAM:
		return NewTeamsNameInfoSource(g)
	case chat1.ConversationMembersType_IMPTEAMNATIVE:
		return NewImplicitTeamsNameInfoSource(g, membersType)
	case chat1.ConversationMembersType_IMPTEAMUPGRADE:
		return NewImplicitTeamsNameInfoSource(g, membersType)
	}
	g.GetLog().CDebugf(ctx, "createNameInfoSource: unknown members type, using KBFS: %v", membersType)
	return NewKBFSNameInfoSource(g)
}

func (h *Helper) BulkAddToConv(ctx context.Context, uid gregor1.UID, convID chat1.ConversationID, usernames []string) error {
	if len(usernames) == 0 {
		return fmt.Errorf("Unable to BulkAddToConv, no users specified")
	}

	rc, err := utils.GetUnverifiedConv(ctx, h.G(), uid, convID, types.InboxSourceDataSourceAll)
	if err != nil {
		return err
	}
	conv := rc.Conv
	mt := conv.Metadata.MembersType
	switch mt {
	case chat1.ConversationMembersType_TEAM:
	default:
		return fmt.Errorf("BulkAddToConv only available to TEAM conversations. Found %v conv", mt)
	}

	boxer := NewBoxer(h.G())
	sender := NewBlockingSender(h.G(), boxer, h.ri)
	sendBulkAddToConv := func(ctx context.Context, sender *BlockingSender, usernames []string, convID chat1.ConversationID, info types.NameInfo) error {
		subBody := chat1.NewMessageSystemWithBulkaddtoconv(chat1.MessageSystemBulkAddToConv{
			Usernames: usernames,
		})
		body := chat1.NewMessageBodyWithSystem(subBody)
		msg := chat1.MessagePlaintext{
			ClientHeader: chat1.MessageClientHeader{
				TlfName:     info.CanonicalName,
				MessageType: chat1.MessageType_SYSTEM,
			},
			MessageBody: body,
		}
		status := chat1.ConversationMemberStatus_ACTIVE
		_, _, err = sender.Send(ctx, convID, msg, 0, nil, &chat1.SenderSendOptions{
			JoinMentionsAs: &status,
		}, nil)
		return err
	}

	info, err := CreateNameInfoSource(ctx, h.G(), mt).LookupName(
		ctx, conv.Metadata.IdTriple.Tlfid, conv.Metadata.Visibility == keybase1.TLFVisibility_PUBLIC, "")
	if err != nil {
		return err
	}
	// retry the add a few times to prevent races. Each time we remove members
	// that are already part of the conversation.
	toExclude := make(map[keybase1.UID]bool)
	for i := 0; i < 4 && len(usernames) > 0; i++ {
		h.Debug(ctx, "BulkAddToConv: trying to add %v", usernames)
		err = sendBulkAddToConv(ctx, sender, usernames, convID, info)
		switch e := err.(type) {
		case nil:
			return nil
		case libkb.ChatUsersAlreadyInConversationError:
			// remove the usernames which are already part of the conversation and retry
			for _, uid := range e.Uids {
				toExclude[uid] = true
			}
			var usernamesToRetry []string
			for _, username := range usernames {
				if !toExclude[libkb.UsernameToUID(username)] {
					usernamesToRetry = append(usernamesToRetry, username)
				}
			}
			usernames = usernamesToRetry
			if len(usernamesToRetry) == 0 {
				// don't let this bubble up if everyone is already in the channel
				err = nil
			}
		default:
			return e
		}
	}
	return err
}
