package selectors

import (
	"context"
	"fmt"
	"strconv"

	"github.com/alcionai/clues"

	"github.com/alcionai/corso/src/internal/common/dttm"
	"github.com/alcionai/corso/src/pkg/backup/details"
	"github.com/alcionai/corso/src/pkg/fault"
	"github.com/alcionai/corso/src/pkg/filters"
	"github.com/alcionai/corso/src/pkg/path"
)

// ---------------------------------------------------------------------------
// Selectors
// ---------------------------------------------------------------------------

type (
	// exchange provides an api for selecting
	// data scopes applicable to the Exchange service.
	exchange struct {
		Selector
	}

	// ExchangeBackup provides an api for selecting
	// data scopes applicable to the Exchange service,
	// plus backup-specific methods.
	ExchangeBackup struct {
		exchange
	}

	// ExchangeRestore provides an api for selecting
	// data scopes applicable to the Exchange service,
	// plus restore-specific methods.
	ExchangeRestore struct {
		exchange
	}
)

var (
	_ Reducer        = &ExchangeRestore{}
	_ pathCategorier = &ExchangeRestore{}
)

// NewExchange produces a new Selector with the service set to ServiceExchange.
func NewExchangeBackup(users []string) *ExchangeBackup {
	src := ExchangeBackup{
		exchange{
			newSelector(ServiceExchange, users),
		},
	}

	return &src
}

// ToExchangeBackup transforms the generic selector into an ExchangeBackup.
// Errors if the service defined by the selector is not ServiceExchange.
func (s Selector) ToExchangeBackup() (*ExchangeBackup, error) {
	if s.Service != ServiceExchange {
		return nil, badCastErr(ServiceExchange, s.Service)
	}

	src := ExchangeBackup{exchange{s}}

	return &src, nil
}

func (s ExchangeBackup) SplitByResourceOwner(users []string) []ExchangeBackup {
	sels := splitByResourceOwner[ExchangeScope](s.Selector, users, ExchangeUser)

	ss := make([]ExchangeBackup, 0, len(sels))
	for _, sel := range sels {
		ss = append(ss, ExchangeBackup{exchange{sel}})
	}

	return ss
}

// NewExchangeRestore produces a new Selector with the service set to ServiceExchange.
func NewExchangeRestore(users []string) *ExchangeRestore {
	src := ExchangeRestore{
		exchange{
			newSelector(ServiceExchange, users),
		},
	}

	return &src
}

// ToExchangeRestore transforms the generic selector into an ExchangeRestore.
// Errors if the service defined by the selector is not ServiceExchange.
func (s Selector) ToExchangeRestore() (*ExchangeRestore, error) {
	if s.Service != ServiceExchange {
		return nil, badCastErr(ServiceExchange, s.Service)
	}

	src := ExchangeRestore{exchange{s}}

	return &src, nil
}

func (sr ExchangeRestore) SplitByResourceOwner(users []string) []ExchangeRestore {
	sels := splitByResourceOwner[ExchangeScope](sr.Selector, users, ExchangeUser)

	ss := make([]ExchangeRestore, 0, len(sels))
	for _, sel := range sels {
		ss = append(ss, ExchangeRestore{exchange{sel}})
	}

	return ss
}

// PathCategories produces the aggregation of discrete users described by each type of scope.
func (s exchange) PathCategories() selectorPathCategories {
	return selectorPathCategories{
		Excludes: pathCategoriesIn[ExchangeScope, exchangeCategory](s.Excludes),
		Filters:  pathCategoriesIn[ExchangeScope, exchangeCategory](s.Filters),
		Includes: pathCategoriesIn[ExchangeScope, exchangeCategory](s.Includes),
	}
}

// ---------------------------------------------------------------------------
// Stringers and Concealers
// ---------------------------------------------------------------------------

func (s ExchangeScope) Conceal() string             { return conceal(s) }
func (s ExchangeScope) Format(fs fmt.State, r rune) { format(s, fs, r) }
func (s ExchangeScope) String() string              { return conceal(s) }
func (s ExchangeScope) PlainString() string         { return plainString(s) }

// -------------------
// Exclude/Includes

// Exclude appends the provided scopes to the selector's exclusion set.
// Every Exclusion scope applies globally, affecting all inclusion scopes.
// Data is excluded if it matches ANY exclusion (of the same data category).
//
// All parts of the scope must match for data to be exclucded.
// Ex: Mail(u1, f1, m1) => only excludes mail if it is owned by user u1,
// located in folder f1, and ID'd as m1.  MailSender(foo) => only excludes
// mail whose sender is foo.  Use selectors.Any() to wildcard a scope value.
// No value will match if selectors.None() is provided.
//
// Group-level scopes will automatically apply the Any() wildcard to
// child properties.
// ex: User(u1) automatically cascades to all mail, events, and contacts,
// therefore it is the same as selecting all of the following:
// Mail(u1, Any(), Any()), Event(u1, Any()), Contacts(u1, Any(), Any())
func (s *exchange) Exclude(scopes ...[]ExchangeScope) {
	s.Excludes = appendScopes(s.Excludes, scopes...)
}

// Filter appends the provided scopes to the selector's filters set.
// A selector with >0 filters and 0 inclusions will include any data
// that passes all filters.
// A selector with >0 filters and >0 inclusions will reduce the
// inclusion set to only the data that passes all filters.
// Data is retained if it passes ALL filters (of the same data category).
//
// All parts of the scope must match for data to pass the filter.
// Ex: Mail(u1, f1, m1) => only passes mail that is owned by user u1,
// located in folder f1, and ID'd as m1.  MailSender(foo) => only passes
// mail whose sender is foo.  Use selectors.Any() to wildcard a scope value.
// No value will match if selectors.None() is provided.
//
// Group-level scopes will automatically apply the Any() wildcard to
// child properties.
// ex: User(u1) automatically cascades to all mail, events, and contacts,
// therefore it is the same as selecting all of the following:
// Mail(u1, Any(), Any()), Event(u1, Any()), Contacts(u1, Any(), Any())
func (s *exchange) Filter(scopes ...[]ExchangeScope) {
	s.Filters = appendScopes(s.Filters, scopes...)
}

// Include appends the provided scopes to the selector's inclusion set.
// Data is included if it matches ANY inclusion.
// The inclusion set is later filtered (all included data must pass ALL
// filters) and excluded (all included data must not match ANY exclusion).
// Data is included if it matches ANY inclusion (of the same data category).
//
// All parts of the scope must match for data to be included.
// Ex: Mail(u1, f1, m1) => only includes mail if it is owned by user u1,
// located in folder f1, and ID'd as m1.  MailSender(foo) => only includes
// mail whose sender is foo.  Use selectors.Any() to wildcard a scope value.
// No value will match if selectors.None() is provided.
//
// Group-level scopes will automatically apply the Any() wildcard to
// child properties.
// ex: User(u1) automatically cascades to all mail, events, and contacts,
// therefore it is the same as selecting all of the following:
// Mail(u1, Any(), Any()), Event(u1, Any()), Contacts(u1, Any(), Any())
func (s *exchange) Include(scopes ...[]ExchangeScope) {
	s.Includes = appendScopes(s.Includes, scopes...)
}

// Scopes retrieves the list of exchangeScopes in the selector.
func (s *exchange) Scopes() []ExchangeScope {
	return scopes[ExchangeScope](s.Selector)
}

type ExchangeItemScopeConstructor func([]string, []string, ...option) []ExchangeScope

// -------------------
// Scope Factories

// Contacts produces one or more exchange contact scopes.
// If any slice contains selectors.Any, that slice is reduced to [selectors.Any]
// If any slice contains selectors.None, that slice is reduced to [selectors.None]
// If any slice is empty, it defaults to [selectors.None]
// options are only applied to the folder scopes.
func (s *exchange) Contacts(folders, contacts []string, opts ...option) []ExchangeScope {
	scopes := []ExchangeScope{}

	scopes = append(
		scopes,
		makeScope[ExchangeScope](ExchangeContact, contacts, defaultItemOptions(s.Cfg)...).
			set(ExchangeContactFolder, folders, opts...))

	return scopes
}

// Contactfolders produces one or more exchange contact folder scopes.
// If any slice contains selectors.Any, that slice is reduced to [selectors.Any]
// If any slice contains selectors.None, that slice is reduced to [selectors.None]
// If any slice is empty, it defaults to [selectors.None]
// options are only applied to the folder scopes.
func (s *exchange) ContactFolders(folders []string, opts ...option) []ExchangeScope {
	var (
		scopes = []ExchangeScope{}
		os     = append([]option{pathComparator()}, opts...)
	)

	scopes = append(
		scopes,
		makeScope[ExchangeScope](ExchangeContactFolder, folders, os...))

	return scopes
}

// Events produces one or more exchange event scopes.
// If any slice contains selectors.Any, that slice is reduced to [selectors.Any]
// If any slice contains selectors.None, that slice is reduced to [selectors.None]
// If any slice is empty, it defaults to [selectors.None]
// options are only applied to the folder scopes.
func (s *exchange) Events(calendars, events []string, opts ...option) []ExchangeScope {
	scopes := []ExchangeScope{}

	scopes = append(
		scopes,
		makeScope[ExchangeScope](ExchangeEvent, events, defaultItemOptions(s.Cfg)...).
			set(ExchangeEventCalendar, calendars, opts...))

	return scopes
}

// EventCalendars produces one or more exchange event calendar scopes.
// Calendars act as folders to contain Events
// If any slice contains selectors.Any, that slice is reduced to [selectors.Any]
// If any slice contains selectors.None, that slice is reduced to [selectors.None]
// If any slice is empty, it defaults to [selectors.None]
// options are only applied to the folder scopes.
func (s *exchange) EventCalendars(events []string, opts ...option) []ExchangeScope {
	var (
		scopes = []ExchangeScope{}
		os     = append([]option{pathComparator()}, opts...)
	)

	scopes = append(
		scopes,
		makeScope[ExchangeScope](ExchangeEventCalendar, events, os...))

	return scopes
}

// Produces one or more mail scopes.
// If any slice contains selectors.Any, that slice is reduced to [selectors.Any]
// If any slice contains selectors.None, that slice is reduced to [selectors.None]
// If any slice is empty, it defaults to [selectors.None]
// options are only applied to the folder scopes.
func (s *exchange) Mails(folders, mails []string, opts ...option) []ExchangeScope {
	scopes := []ExchangeScope{}

	scopes = append(
		scopes,
		makeScope[ExchangeScope](ExchangeMail, mails, defaultItemOptions(s.Cfg)...).
			set(ExchangeMailFolder, folders, opts...))

	return scopes
}

// Produces one or more exchange mail folder scopes.
// If any slice contains selectors.Any, that slice is reduced to [selectors.Any]
// If any slice contains selectors.None, that slice is reduced to [selectors.None]
// If any slice is empty, it defaults to [selectors.None]
// options are only applied to the folder scopes.
func (s *exchange) MailFolders(folders []string, opts ...option) []ExchangeScope {
	var (
		scopes = []ExchangeScope{}
		os     = append([]option{pathComparator()}, opts...)
	)

	scopes = append(
		scopes,
		makeScope[ExchangeScope](ExchangeMailFolder, folders, os...))

	return scopes
}

// Retrieves all exchange data.
// Each user id generates three scopes, one for each data type: contact, event, and mail.
// If any slice contains selectors.Any, that slice is reduced to [selectors.Any]
// If any slice contains selectors.None, that slice is reduced to [selectors.None]
// If any slice is empty, it defaults to [selectors.None]
func (s *exchange) AllData() []ExchangeScope {
	scopes := []ExchangeScope{}

	scopes = append(scopes,
		makeScope[ExchangeScope](ExchangeContactFolder, Any()),
		makeScope[ExchangeScope](ExchangeEventCalendar, Any()),
		makeScope[ExchangeScope](ExchangeMailFolder, Any()))

	return scopes
}

// -------------------
// Info Factories

// ContactName produces one or more exchange contact name info scopes.
// Matches any contact whose name contains the provided string.
// If any slice contains selectors.Any, that slice is reduced to [selectors.Any]
// If any slice contains selectors.None, that slice is reduced to [selectors.None]
// If any slice is empty, it defaults to [selectors.None]
func (sr *ExchangeRestore) ContactName(senderID string) []ExchangeScope {
	return []ExchangeScope{
		makeInfoScope[ExchangeScope](
			ExchangeContact,
			ExchangeInfoContactName,
			[]string{senderID},
			filters.In),
	}
}

// EventSubject produces one or more exchange event subject info scopes.
// Matches any event where the event subject contains one of the provided strings.
// If any slice contains selectors.Any, that slice is reduced to [selectors.Any]
// If any slice contains selectors.None, that slice is reduced to [selectors.None]
// If any slice is empty, it defaults to [selectors.None]
func (sr *ExchangeRestore) EventOrganizer(organizer string) []ExchangeScope {
	return []ExchangeScope{
		makeInfoScope[ExchangeScope](
			ExchangeEvent,
			ExchangeInfoEventOrganizer,
			[]string{organizer},
			filters.In),
	}
}

// EventRecurs produces one or more exchange event recurrence info scopes.
// Matches any event if the comparator flag matches the event recurrence flag.
// If any slice contains selectors.Any, that slice is reduced to [selectors.Any]
// If any slice contains selectors.None, that slice is reduced to [selectors.None]
// If any slice is empty, it defaults to [selectors.None]
func (sr *ExchangeRestore) EventRecurs(recurs string) []ExchangeScope {
	return []ExchangeScope{
		makeInfoScope[ExchangeScope](
			ExchangeEvent,
			ExchangeInfoEventRecurs,
			[]string{recurs},
			filters.Equal),
	}
}

// EventStartsAfter produces an exchange event starts-after info scope.
// Matches any event where the start time is after the timestring.
// If the input equals selectors.Any, the scope will match all times.
// If the input is empty or selectors.None, the scope will always fail comparisons.
func (sr *ExchangeRestore) EventStartsAfter(timeStrings string) []ExchangeScope {
	return []ExchangeScope{
		makeInfoScope[ExchangeScope](
			ExchangeEvent,
			ExchangeInfoEventStartsAfter,
			[]string{timeStrings},
			filters.Less),
	}
}

// EventStartsBefore produces an exchange event starts-before info scope.
// Matches any event where the start time is before the timestring.
// If the input equals selectors.Any, the scope will match all times.
// If the input is empty or selectors.None, the scope will always fail comparisons.
func (sr *ExchangeRestore) EventStartsBefore(timeStrings string) []ExchangeScope {
	return []ExchangeScope{
		makeInfoScope[ExchangeScope](
			ExchangeEvent,
			ExchangeInfoEventStartsBefore,
			[]string{timeStrings},
			filters.Greater),
	}
}

// EventSubject produces one or more exchange event subject info scopes.
// Matches any event where the event subject contains one of the provided strings.
// If any slice contains selectors.Any, that slice is reduced to [selectors.Any]
// If any slice contains selectors.None, that slice is reduced to [selectors.None]
// If any slice is empty, it defaults to [selectors.None]
func (sr *ExchangeRestore) EventSubject(subject string) []ExchangeScope {
	return []ExchangeScope{
		makeInfoScope[ExchangeScope](
			ExchangeEvent,
			ExchangeInfoEventSubject,
			[]string{subject},
			filters.In),
	}
}

// MailReceivedAfter produces an exchange mail received-after info scope.
// Matches any mail which was received after the timestring.
// If the input equals selectors.Any, the scope will match all times.
// If the input is empty or selectors.None, the scope will always fail comparisons.
func (sr *ExchangeRestore) MailReceivedAfter(timeStrings string) []ExchangeScope {
	return []ExchangeScope{
		makeInfoScope[ExchangeScope](
			ExchangeMail,
			ExchangeInfoMailReceivedAfter,
			[]string{timeStrings},
			filters.Less),
	}
}

// MailReceivedBefore produces an exchange mail received-before info scope.
// Matches any mail which was received before the timestring.
// If the input equals selectors.Any, the scope will match all times.
// If the input is empty or selectors.None, the scope will always fail comparisons.
func (sr *ExchangeRestore) MailReceivedBefore(timeStrings string) []ExchangeScope {
	return []ExchangeScope{
		makeInfoScope[ExchangeScope](
			ExchangeMail,
			ExchangeInfoMailReceivedBefore,
			[]string{timeStrings},
			filters.Greater),
	}
}

// MailSender produces one or more exchange mail sender info scopes.
// Matches any mail whose sender contains one of the provided strings.
// If any slice contains selectors.Any, that slice is reduced to [selectors.Any]
// If any slice contains selectors.None, that slice is reduced to [selectors.None]
// If any slice is empty, it defaults to [selectors.None]
func (sr *ExchangeRestore) MailSender(sender string) []ExchangeScope {
	return []ExchangeScope{
		makeInfoScope[ExchangeScope](
			ExchangeMail,
			ExchangeInfoMailSender,
			[]string{sender},
			filters.In),
	}
}

// MailSubject produces one or more exchange mail subject line info scopes.
// Matches any mail whose subject contains one of the provided strings.
// If any slice contains selectors.Any, that slice is reduced to [selectors.Any]
// If any slice contains selectors.None, that slice is reduced to [selectors.None]
// If any slice is empty, it defaults to [selectors.None]
func (sr *ExchangeRestore) MailSubject(subject string) []ExchangeScope {
	return []ExchangeScope{
		makeInfoScope[ExchangeScope](
			ExchangeMail,
			ExchangeInfoMailSubject,
			[]string{subject},
			filters.In),
	}
}

// ---------------------------------------------------------------------------
// Categories
// ---------------------------------------------------------------------------

// exchangeCategory enumerates the type of the lowest level
// of data specified by the scope.
type exchangeCategory string

// interface compliance checks
var _ categorizer = ExchangeCategoryUnknown

const (
	ExchangeCategoryUnknown exchangeCategory = ""

	// types of data identified by exchange
	ExchangeContact       exchangeCategory = "ExchangeContact"
	ExchangeContactFolder exchangeCategory = "ExchangeContactFolder"
	ExchangeEvent         exchangeCategory = "ExchangeEvent"
	ExchangeEventCalendar exchangeCategory = "ExchangeEventCalendar"
	ExchangeMail          exchangeCategory = "ExchangeMail"
	ExchangeMailFolder    exchangeCategory = "ExchangeMailFolder"
	ExchangeUser          exchangeCategory = "ExchangeUser"

	// data contained within details.ItemInfo
	ExchangeInfoMailSender         exchangeCategory = "ExchangeInfoMailSender"
	ExchangeInfoMailSubject        exchangeCategory = "ExchangeInfoMailSubject"
	ExchangeInfoMailReceivedAfter  exchangeCategory = "ExchangeInfoMailReceivedAfter"
	ExchangeInfoMailReceivedBefore exchangeCategory = "ExchangeInfoMailReceivedBefore"
	ExchangeInfoContactName        exchangeCategory = "ExchangeInfoContactName"
	ExchangeInfoEventOrganizer     exchangeCategory = "ExchangeInfoEventOrganizer"
	ExchangeInfoEventRecurs        exchangeCategory = "ExchangeInfoEventRecurs"
	ExchangeInfoEventStartsAfter   exchangeCategory = "ExchangeInfoEventStartsAfter"
	ExchangeInfoEventStartsBefore  exchangeCategory = "ExchangeInfoEventStartsBefore"
	ExchangeInfoEventSubject       exchangeCategory = "ExchangeInfoEventSubject"
)

// exchangeLeafProperties describes common metadata of the leaf categories
var exchangeLeafProperties = map[categorizer]leafProperty{
	ExchangeContact: {
		pathKeys: []categorizer{ExchangeContactFolder, ExchangeContact},
		pathType: path.ContactsCategory,
	},
	ExchangeEvent: {
		pathKeys: []categorizer{ExchangeEventCalendar, ExchangeEvent},
		pathType: path.EventsCategory,
	},
	ExchangeMail: {
		pathKeys: []categorizer{ExchangeMailFolder, ExchangeMail},
		pathType: path.EmailCategory,
	},
	ExchangeUser: { // the root category must be represented, even though it isn't a leaf
		pathKeys: []categorizer{ExchangeUser},
		pathType: path.UnknownCategory,
	},
}

func (ec exchangeCategory) String() string {
	return string(ec)
}

// leafCat returns the leaf category of the receiver.
// If the receiver category has multiple leaves (ex: User) or no leaves,
// (ex: Unknown), the receiver itself is returned.
// If the receiver category is an info type (ex: ExchangeInfoMailSubject),
// returns the category covered by the info.
// Ex: ExchangeContactFolder.leafCat() => ExchangeContact
// Ex: ExchangeEvent.leafCat() => ExchangeEvent
// Ex: ExchangeUser.leafCat() => ExchangeUser
func (ec exchangeCategory) leafCat() categorizer {
	switch ec {
	case ExchangeContact, ExchangeContactFolder, ExchangeInfoContactName:
		return ExchangeContact

	case ExchangeEvent, ExchangeEventCalendar, ExchangeInfoEventOrganizer, ExchangeInfoEventRecurs,
		ExchangeInfoEventStartsAfter, ExchangeInfoEventStartsBefore, ExchangeInfoEventSubject:
		return ExchangeEvent

	case ExchangeMail, ExchangeMailFolder, ExchangeInfoMailReceivedAfter,
		ExchangeInfoMailReceivedBefore, ExchangeInfoMailSender, ExchangeInfoMailSubject:
		return ExchangeMail
	}

	return ec
}

// rootCat returns the root category type.
func (ec exchangeCategory) rootCat() categorizer {
	return ExchangeUser
}

// unknownCat returns the unknown category type.
func (ec exchangeCategory) unknownCat() categorizer {
	return ExchangeCategoryUnknown
}

// isUnion returns true if c is a user
func (ec exchangeCategory) isUnion() bool {
	return ec == ec.rootCat()
}

// isLeaf is true if the category is a mail, event, or contact category.
func (ec exchangeCategory) isLeaf() bool {
	return ec == ec.leafCat()
}

// pathValues transforms the two paths to maps of identified properties.
//
// Example:
// [tenantID, service, userPN, category, mailFolder, mailID]
// => {exchMailFolder: mailFolder, exchMail: mailID}
func (ec exchangeCategory) pathValues(
	repo path.Path,
	ent details.Entry,
	cfg Config,
) (map[categorizer][]string, error) {
	var folderCat, itemCat categorizer

	switch ec {
	case ExchangeContact:
		folderCat, itemCat = ExchangeContactFolder, ExchangeContact

	case ExchangeEvent:
		folderCat, itemCat = ExchangeEventCalendar, ExchangeEvent

	case ExchangeMail:
		folderCat, itemCat = ExchangeMailFolder, ExchangeMail

	default:
		return nil, clues.New("bad exchanageCategory").With("category", ec)
	}

	item := ent.ItemRef
	if len(item) == 0 {
		item = repo.Item()
	}

	items := []string{ent.ShortRef, item}

	// only include the item ID when the user is NOT matching
	// item names. Exchange data does not contain an item name,
	// only an ID, and we don't want to mix up the two.
	if cfg.OnlyMatchItemNames {
		items = []string{ent.ShortRef}
	}

	// Will hit the if-condition when we're at a top-level folder, but we'll get
	// the same result when we extract from the RepoRef.
	folder := ent.LocationRef
	if len(folder) == 0 {
		folder = repo.Folder(true)
	}

	result := map[categorizer][]string{
		folderCat: {folder},
		itemCat:   items,
	}

	return result, nil
}

// pathKeys returns the path keys recognized by the receiver's leaf type.
func (ec exchangeCategory) pathKeys() []categorizer {
	return exchangeLeafProperties[ec.leafCat()].pathKeys
}

// PathType converts the category's leaf type into the matching path.CategoryType.
func (ec exchangeCategory) PathType() path.CategoryType {
	return exchangeLeafProperties[ec.leafCat()].pathType
}

// ---------------------------------------------------------------------------
// Scopes
// ---------------------------------------------------------------------------

// ExchangeScope specifies the data available
// when interfacing with the Exchange service.
type ExchangeScope scope

// interface compliance checks
var _ scoper = &ExchangeScope{}

// Category describes the type of the data in scope.
func (s ExchangeScope) Category() exchangeCategory {
	return exchangeCategory(getCategory(s))
}

// categorizer type is a generic wrapper around Category.
// Primarily used by scopes.go to for abstract comparisons.
func (s ExchangeScope) categorizer() categorizer {
	return s.Category()
}

// Matches returns true if the category is included in the scope's
// data type, and the target string matches that category's comparator.
func (s ExchangeScope) Matches(cat exchangeCategory, target string) bool {
	return matches(s, cat, target)
}

// InfoCategory returns the category enum of the scope info.
// If the scope is not an info type, returns ExchangeUnknownCategory.
func (s ExchangeScope) InfoCategory() exchangeCategory {
	return exchangeCategory(getInfoCategory(s))
}

// IncludeCategory checks whether the scope includes a certain category of data.
// Ex: to check if the scope includes mail data:
// s.IncludesCategory(selector.ExchangeMail)
func (s ExchangeScope) IncludesCategory(cat exchangeCategory) bool {
	return categoryMatches(s.Category(), cat)
}

// returns true if the category is included in the scope's data type,
// and the value is set to Any().
func (s ExchangeScope) IsAny(cat exchangeCategory) bool {
	return isAnyTarget(s, cat)
}

// Get returns the data category in the scope.  If the scope
// contains all data types for a user, it'll return the
// ExchangeUser category.
func (s ExchangeScope) Get(cat exchangeCategory) []string {
	return getCatValue(s, cat)
}

// sets a value by category to the scope.  Only intended for internal use.
func (s ExchangeScope) set(cat exchangeCategory, v []string, opts ...option) ExchangeScope {
	os := []option{}
	if cat == ExchangeContactFolder || cat == ExchangeEventCalendar || cat == ExchangeMailFolder {
		os = append(os, pathComparator())
	}

	return set(s, cat, v, append(os, opts...)...)
}

// setDefaults ensures that contact folder, mail folder, and user category
// scopes all express `AnyTgt` for their child category types.
func (s ExchangeScope) setDefaults() {
	switch s.Category() {
	case ExchangeContactFolder:
		s[ExchangeContact.String()] = passAny

	case ExchangeEventCalendar:
		s[ExchangeEvent.String()] = passAny

	case ExchangeMailFolder:
		s[ExchangeMail.String()] = passAny

	case ExchangeUser:
		s[ExchangeContactFolder.String()] = passAny
		s[ExchangeContact.String()] = passAny
		s[ExchangeEvent.String()] = passAny
		s[ExchangeMailFolder.String()] = passAny
		s[ExchangeMail.String()] = passAny
	}
}

// ---------------------------------------------------------------------------
// Backup Details Filtering
// ---------------------------------------------------------------------------

// Reduce filters the entries in a details struct to only those that match the
// inclusions, filters, and exclusions in the selector.
func (s exchange) Reduce(
	ctx context.Context,
	deets *details.Details,
	errs *fault.Bus,
) *details.Details {
	return reduce[ExchangeScope](
		ctx,
		deets,
		s.Selector,
		map[path.CategoryType]exchangeCategory{
			path.ContactsCategory: ExchangeContact,
			path.EventsCategory:   ExchangeEvent,
			path.EmailCategory:    ExchangeMail,
		},
		errs)
}

// matchesInfo handles the standard behavior when comparing a scope and an ExchangeInfo
// returns true if the scope and info match for the provided category.
func (s ExchangeScope) matchesInfo(dii details.ItemInfo) bool {
	info := dii.Exchange
	if info == nil {
		return false
	}

	infoCat := s.InfoCategory()

	cfpc := categoryFromItemType(info.ItemType)
	if !typeAndCategoryMatches(infoCat, cfpc) {
		return false
	}

	i := ""

	switch infoCat {
	case ExchangeInfoContactName:
		i = info.ContactName
	case ExchangeInfoEventOrganizer:
		i = info.Organizer
	case ExchangeInfoEventRecurs:
		i = strconv.FormatBool(info.EventRecurs)
	case ExchangeInfoEventStartsAfter, ExchangeInfoEventStartsBefore:
		i = dttm.Format(info.EventStart)
	case ExchangeInfoEventSubject:
		i = info.Subject
	case ExchangeInfoMailSender:
		i = info.Sender
	case ExchangeInfoMailSubject:
		i = info.Subject
	case ExchangeInfoMailReceivedAfter, ExchangeInfoMailReceivedBefore:
		i = dttm.Format(info.Received)
	}

	return s.Matches(infoCat, i)
}

// categoryFromItemType interprets the category represented by the ExchangeInfo
// struct.  Since every ExchangeInfo can hold all exchange data info, the exact
// type that the struct represents must be compared using its ItemType prop.
func categoryFromItemType(pct details.ItemType) exchangeCategory {
	switch pct {
	case details.ExchangeContact:
		return ExchangeContact
	case details.ExchangeMail:
		return ExchangeMail
	case details.ExchangeEvent:
		return ExchangeEvent
	}

	return ExchangeCategoryUnknown
}
