// Copyright 2019 Keybase, Inc. All rights reserved. Use of
// this source code is governed by the included BSD license.

package contacts

import (
	"errors"
	"strings"

	"github.com/keybase/client/go/externals"
	"github.com/keybase/client/go/libkb"
	"github.com/keybase/client/go/protocol/keybase1"
)

func AssertionFromComponent(actx libkb.AssertionContext, c keybase1.ContactComponent, coercedValue string) (string, error) {
	key := c.AssertionType()
	var value string
	if coercedValue != "" {
		value = coercedValue
	} else {
		value = c.ValueString()
	}
	if key == "phone" {
		// ContactComponent has the PhoneNumber type which is E164 phone
		// number starting with `+`, we need to remove all non-digits for
		// the assertion.
		value = keybase1.PhoneNumberToAssertionValue(value)
	} else {
		value = strings.ToLower(strings.TrimSpace(value))
	}
	if key == "" || value == "" {
		return "", errors.New("invalid variant value in contact component")
	}
	ret, err := libkb.ParseAssertionURLKeyValue(actx, key, value, true /* strict */)
	if err != nil {
		return "", err
	}
	return ret.String(), nil
}

// fillResolvedUserInfo takes uidSet and processed contact list and fill the
// following info (in place) for resolved contacts:
// - usernames and full names,
// - follow status (are we following the user or not),
// - service summaries.
func fillResolvedUserInfo(mctx libkb.MetaContext, provider ContactsProvider, uidSet map[keybase1.UID]struct{},
	contacts []keybase1.ProcessedContact) {

	uidList := make([]keybase1.UID, 0, len(uidSet))
	for uid := range uidSet {
		uidList = append(uidList, uid)
	}

	// Uidmap everything to get Keybase usernames and full names.
	usernames, err := provider.FindUsernames(mctx, uidList)
	if err != nil {
		mctx.Warning("Unable to find usernames for contacts: %s", err)
		usernames = make(map[keybase1.UID]ContactUsernameAndFullName)
	}

	// Get tracking info and set "Following" field for contacts.
	following, err := provider.FindFollowing(mctx, uidList)
	if err != nil {
		mctx.Warning("Unable to find tracking info for contacts: %s", err)
		following = make(map[keybase1.UID]bool)
	}

	// Get service maps
	serviceMaps, err := provider.FindServiceMaps(mctx, uidList)
	if err != nil {
		mctx.Warning("Unable to get service maps for contacts: %s", err)
		serviceMaps = make(map[keybase1.UID]libkb.UserServiceSummary)
	}

	for i := range contacts {
		v := &contacts[i]
		if v.Resolved {
			if unamePkg, found := usernames[v.Uid]; found {
				v.Username = unamePkg.Username
				v.FullName = unamePkg.Fullname
			}
			if follow, found := following[v.Uid]; found {
				v.Following = follow
			}
			if smap, found := serviceMaps[v.Uid]; found && len(smap) > 0 {
				v.ServiceMap = make(map[string]string, len(smap))
				for service, username := range smap {
					v.ServiceMap[service] = username
				}
			}
		}
	}
}

// ResolveContacts resolves contacts with cache for UI. See API documentation
// in phone_numbers.avdl
//
func ResolveContacts(mctx libkb.MetaContext, provider ContactsProvider, contacts []keybase1.Contact) (res []keybase1.ProcessedContact, err error) {

	if len(contacts) == 0 {
		mctx.Debug("`contacts` is empty, nothing to resolve")
		return res, nil
	}

	// Collect sets of email addresses and phones for provider lookup. Use sets
	// for deduplication.
	emailSet := make(map[keybase1.EmailAddress]struct{})
	phoneSet := make(map[keybase1.RawPhoneNumber]struct{})

	for _, contact := range contacts {
		for _, component := range contact.Components {
			if component.Email != nil {
				emailSet[*component.Email] = struct{}{}
			}
			if component.PhoneNumber != nil {
				phoneSet[*component.PhoneNumber] = struct{}{}
			}
		}
	}

	mctx.Debug("Going to look up %d emails and %d phone numbers using provider", len(emailSet), len(phoneSet))

	actx := externals.MakeStaticAssertionContext(mctx.Ctx())

	errorComponents := make(map[string]string)
	userUIDSet := make(map[keybase1.UID]struct{})

	// Discard duplicate components that come from contacts with the same
	// contact name and hold the same assertion. Will also skip same assertions
	// within one contact (duplicated components with same value and same or
	// different name)
	type contactAssertionPair struct {
		contactName    string
		componentValue string
	}
	contactAssertionsSeen := make(map[contactAssertionPair]struct{})

	if len(emailSet)+len(phoneSet) == 0 {
		// There is nothing to resolve.
		return res, nil
	}

	phones := make([]keybase1.RawPhoneNumber, 0, len(phoneSet))
	emails := make([]keybase1.EmailAddress, 0, len(emailSet))
	for phone := range phoneSet {
		phones = append(phones, phone)
	}
	for email := range emailSet {
		emails = append(emails, email)
	}
	providerRes, err := provider.LookupAll(mctx, emails, phones)
	if err != nil {
		return res, err
	}

	for contactIndex, contact := range contacts {
		var addLabel = len(contact.Components) > 1
		for _, component := range contact.Components {
			assertion, err := AssertionFromComponent(actx, component, "")
			if err != nil {
				mctx.Warning("Couldn't make assertion from component: %+v, %q: error: %s", component, component.ValueString(), err)
				continue
			}

			cvp := contactAssertionPair{contact.Name, assertion}
			if _, seen := contactAssertionsSeen[cvp]; seen {
				// Already seen the exact contact name and assertion.
				continue
			}

			if lookupRes, found := providerRes.FindComponent(component); found {
				if lookupRes.Error != "" {
					errorComponents[component.ValueString()] = lookupRes.Error
					mctx.Debug("Could not look up component: %+v, %q, error: %s", component, component.ValueString(), lookupRes.Error)
					continue
				}

				if lookupRes.Coerced != "" {
					// Create assertion again if server gave us coerced version.
					assertion, err = AssertionFromComponent(actx, component, lookupRes.Coerced)
					if err != nil {
						mctx.Warning("Couldn't make assertion from coerced value: %+v, %s: error: %s", component, lookupRes.Coerced, err)
						continue
					}
				}

				res = append(res, keybase1.ProcessedContact{
					ContactIndex: contactIndex,
					ContactName:  contact.Name,
					Component:    component,
					Resolved:     true,

					Uid: lookupRes.UID,
					// Rest of resolved user data is filled by `fillResolvedUserInfo`.

					Assertion: assertion,
				})

				userUIDSet[lookupRes.UID] = struct{}{}
			} else {
				res = append(res, keybase1.ProcessedContact{
					ContactIndex: contactIndex,
					ContactName:  contact.Name,
					Component:    component,
					Resolved:     false,

					DisplayName:  contact.Name,
					DisplayLabel: component.FormatDisplayLabel(addLabel),

					Assertion: assertion,
				})
			}

			// Mark as seen if we got this far.
			contactAssertionsSeen[cvp] = struct{}{}
		}
	}

	mctx.Debug("Got %d contact entries and %d resolved users", len(res), len(userUIDSet))

	if len(res) > 0 {
		fillResolvedUserInfo(mctx, provider, userUIDSet, res)

		// And now that we have Keybase names and following information, make a
		// decision about displayName and displayLabel.
		for i := range res {
			v := &res[i]
			if v.Resolved {
				v.DisplayName = v.Username
				switch {
				case v.Following && v.FullName != "":
					v.DisplayLabel = v.FullName
				case v.ContactName != "":
					v.DisplayLabel = v.ContactName
				default:
					v.DisplayLabel = v.Component.ValueString()
				}
			}
		}
	}

	return res, nil
}
