package driveish

import (
	"context"

	"github.com/alcionai/clues"
	"github.com/microsoftgraph/msgraph-sdk-go/models"
	"golang.org/x/exp/slices"

	"github.com/alcionai/corso/src/cmd/sanity_test/common"
	"github.com/alcionai/corso/src/internal/common/ptr"
	"github.com/alcionai/corso/src/pkg/services/m365/api"
)

const (
	expandPermissions = "expand_permissions"
	owner             = "owner"
)

// sanitree population will grab a superset of data in the drive.
// this increases the chance that we'll run into a race collision with
// the cleanup script.  Sometimes that's okay (deleting old data that
// isn't scrutinized in the test), other times it's not.  We mark whether
// that's okay to do or not by specifying the folders being
// scrutinized for the test.  Any errors within those folders should cause
// a fatal exit.  Errors outside of those folders get ignored.
//
// since we're using folder names, mustPopulateFolders will
// work best (ie: have the fewest collisions/side-effects) if the folder
// names are very specific.  Standard sanity tests should include timestamps,
// which should help ensure that.  Be warned if you try to use it with
// a more generic name: unintended effects could occur.
func populateSanitree(
	ctx context.Context,
	ac api.Client,
	driveID string,
	mustPopulateFolders []string,
) *common.Sanitree[models.DriveItemable, models.DriveItemable] {
	common.Infof(ctx, "building sanitree for drive: %s", driveID)

	root, err := ac.Drives().GetRootFolder(ctx, driveID)
	if err != nil {
		common.Fatal(ctx, "getting drive root folder", err)
	}

	rootName := ptr.Val(root.GetName())

	stree := &common.Sanitree[models.DriveItemable, models.DriveItemable]{
		Self:     root,
		ID:       ptr.Val(root.GetId()),
		Name:     rootName,
		Leaves:   map[string]*common.Sanileaf[models.DriveItemable, models.DriveItemable]{},
		Children: map[string]*common.Sanitree[models.DriveItemable, models.DriveItemable]{},
	}

	recursivelyBuildTree(
		ctx,
		ac,
		driveID,
		stree.Name+"/",
		mustPopulateFolders,
		slices.Contains(mustPopulateFolders, rootName),
		stree)

	return stree
}

func recursivelyBuildTree(
	ctx context.Context,
	ac api.Client,
	driveID string,
	location string,
	mustPopulateFolders []string,
	isChildOfFolderRequiringNoErrors bool,
	stree *common.Sanitree[models.DriveItemable, models.DriveItemable],
) {
	common.Debugf(ctx, "adding: %s", location)

	children, err := ac.Drives().GetFolderChildren(ctx, driveID, stree.ID)
	if err != nil {
		if isChildOfFolderRequiringNoErrors {
			common.Fatal(ctx, "getting drive children by id", err)
		}

		common.Infof(
			ctx,
			"ignoring error getting children in directory %q because it is not within directory set %v\nerror: %s\n%+v",
			location,
			mustPopulateFolders,
			err.Error(),
			clues.ToCore(err))

		return
	}

	for _, driveItem := range children {
		var (
			itemID   = ptr.Val(driveItem.GetId())
			itemName = ptr.Val(driveItem.GetName())
		)

		if driveItem.GetFolder() != nil {
			// currently we don't restore blank folders.
			// skip permission check for empty folders
			if ptr.Val(driveItem.GetFolder().GetChildCount()) == 0 {
				common.Infof(ctx, "skipped empty folder: %s%s", location, itemName)
				continue
			}

			cannotAllowErrors := isChildOfFolderRequiringNoErrors ||
				slices.Contains(mustPopulateFolders, itemName)

			branch := &common.Sanitree[models.DriveItemable, models.DriveItemable]{
				Parent: stree,
				Self:   driveItem,
				ID:     itemID,
				Name:   itemName,
				Expand: map[string]any{
					expandPermissions: permissionIn(ctx, ac, driveID, itemID, cannotAllowErrors),
				},
				Leaves:   map[string]*common.Sanileaf[models.DriveItemable, models.DriveItemable]{},
				Children: map[string]*common.Sanitree[models.DriveItemable, models.DriveItemable]{},
			}

			stree.Children[itemName] = branch

			recursivelyBuildTree(
				ctx,
				ac,
				driveID,
				location+branch.Name+"/",
				mustPopulateFolders,
				cannotAllowErrors,
				branch)
		}

		if driveItem.GetFile() != nil {
			stree.CountLeaves++
			stree.Leaves[itemName] = &common.Sanileaf[models.DriveItemable, models.DriveItemable]{
				Parent: stree,
				Self:   driveItem,
				ID:     itemID,
				Name:   itemName,
				Size:   ptr.Val(driveItem.GetSize()),
			}
		}
	}
}
