package stub

import (
	"bytes"
	"io"

	"golang.org/x/exp/maps"

	"github.com/alcionai/corso/src/internal/data"
	exchMock "github.com/alcionai/corso/src/internal/m365/exchange/mock"
	"github.com/alcionai/corso/src/internal/m365/mock"
	"github.com/alcionai/corso/src/internal/m365/onedrive/metadata"
	"github.com/alcionai/corso/src/internal/m365/resource"
	"github.com/alcionai/corso/src/pkg/control"
	"github.com/alcionai/corso/src/pkg/path"
)

type ColInfo struct {
	// Elements (in order) for the path representing this collection. Should
	// only contain elements after the prefix that corso uses for the path. For
	// example, a collection for the Inbox folder in exchange mail would just be
	// "Inbox".
	PathElements []string
	Category     path.CategoryType
	Items        []ItemInfo
	// auxItems are items that can be retrieved with Fetch but won't be returned
	// by Items(). These files do not directly participate in comparisosn at the
	// end of a test.
	AuxItems []ItemInfo
}

type ItemInfo struct {
	// lookupKey is a string that can be used to find this data from a set of
	// other data in the same collection. This key should be something that will
	// be the same before and after restoring the item in M365 and may not be
	// the M365 ID. When restoring items out of place, the item is assigned a
	// new ID making it unsuitable for a lookup key.
	LookupKey string
	Name      string
	Data      []byte
}

type ConfigInfo struct {
	Opts           control.Options
	Resource       resource.Category
	Service        path.ServiceType
	Tenant         string
	ResourceOwners []string
	RestoreCfg     control.RestoreConfig
}

func GetCollectionsAndExpected(
	config ConfigInfo,
	testCollections []ColInfo,
	backupVersion int,
) (int, int, []data.RestoreCollection, map[string]map[string][]byte, error) {
	var (
		collections     []data.RestoreCollection
		expectedData    = map[string]map[string][]byte{}
		totalItems      = 0
		totalKopiaItems = 0
	)

	for _, owner := range config.ResourceOwners {
		numItems, kopiaItems, ownerCollections, userExpectedData, err := CollectionsForInfo(
			config.Service,
			config.Tenant,
			owner,
			config.RestoreCfg,
			testCollections,
			backupVersion)
		if err != nil {
			return totalItems, totalKopiaItems, collections, expectedData, err
		}

		collections = append(collections, ownerCollections...)
		totalItems += numItems
		totalKopiaItems += kopiaItems

		maps.Copy(expectedData, userExpectedData)
	}

	return totalItems, totalKopiaItems, collections, expectedData, nil
}

func CollectionsForInfo(
	service path.ServiceType,
	tenant, user string,
	restoreCfg control.RestoreConfig,
	allInfo []ColInfo,
	backupVersion int,
) (int, int, []data.RestoreCollection, map[string]map[string][]byte, error) {
	var (
		collections  = make([]data.RestoreCollection, 0, len(allInfo))
		expectedData = make(map[string]map[string][]byte, len(allInfo))
		totalItems   = 0
		kopiaEntries = 0
	)

	for _, info := range allInfo {
		pth, err := path.Build(
			tenant,
			user,
			service,
			info.Category,
			false,
			info.PathElements...)
		if err != nil {
			return totalItems, kopiaEntries, collections, expectedData, err
		}

		mc := exchMock.NewCollection(pth, pth, len(info.Items))

		baseDestPath, err := backupOutputPathFromRestore(restoreCfg, pth)
		if err != nil {
			return totalItems, kopiaEntries, collections, expectedData, err
		}

		baseExpected := expectedData[baseDestPath.String()]
		if baseExpected == nil {
			expectedData[baseDestPath.String()] = make(map[string][]byte, len(info.Items))
			baseExpected = expectedData[baseDestPath.String()]
		}

		for i := 0; i < len(info.Items); i++ {
			mc.Names[i] = info.Items[i].Name
			mc.Data[i] = info.Items[i].Data

			baseExpected[info.Items[i].LookupKey] = info.Items[i].Data

			// We do not count metadata files against item count
			if backupVersion > 0 &&
				(service == path.OneDriveService || service == path.SharePointService) &&
				metadata.HasMetaSuffix(info.Items[i].Name) {
				continue
			}

			totalItems++
		}

		c := mock.RestoreCollection{
			Collection: mc,
			AuxItems:   map[string]data.Stream{},
		}

		for _, aux := range info.AuxItems {
			c.AuxItems[aux.Name] = &exchMock.Data{
				ID:     aux.Name,
				Reader: io.NopCloser(bytes.NewReader(aux.Data)),
			}
		}

		collections = append(collections, c)
		kopiaEntries += len(info.Items)
	}

	return totalItems, kopiaEntries, collections, expectedData, nil
}

// backupOutputPathFromRestore returns a path.Path denoting the location in
// kopia the data will be placed at. The location is a data-type specific
// combination of the location the data was recently restored to and where the
// data was originally in the hierarchy.
func backupOutputPathFromRestore(
	restoreCfg control.RestoreConfig,
	inputPath path.Path,
) (path.Path, error) {
	base := []string{restoreCfg.Location}

	// OneDrive has leading information like the drive ID.
	if inputPath.Service() == path.OneDriveService || inputPath.Service() == path.SharePointService {
		folders := inputPath.Folders()
		base = append(append([]string{}, folders[:3]...), restoreCfg.Location)

		if len(folders) > 3 {
			base = append(base, folders[3:]...)
		}
	}

	if inputPath.Service() == path.ExchangeService && inputPath.Category() == path.EmailCategory {
		base = append(base, inputPath.Folders()...)
	}

	return path.Build(
		inputPath.Tenant(),
		inputPath.ResourceOwner(),
		inputPath.Service(),
		inputPath.Category(),
		false,
		base...)
}
