package api

import (
	"context"
	"fmt"

	"github.com/alcionai/clues"
	"github.com/microsoftgraph/msgraph-sdk-go/drives"
	"github.com/microsoftgraph/msgraph-sdk-go/models"
	"github.com/pkg/errors"

	"github.com/alcionai/corso/src/internal/common/ptr"
	"github.com/alcionai/corso/src/pkg/control"
	"github.com/alcionai/corso/src/pkg/errs/core"
	"github.com/alcionai/corso/src/pkg/services/m365/api/graph"
)

// ---------------------------------------------------------------------------
// controller
// ---------------------------------------------------------------------------

func (c Client) Drives() Drives {
	return Drives{c}
}

// Drives is an interface-compliant provider of the client.
type Drives struct {
	Client
}

// ---------------------------------------------------------------------------
// Folders
// ---------------------------------------------------------------------------

const (
	itemByPathRawURLFmt   = "https://graph.microsoft.com/v1.0/drives/%s/items/%s:/%s"
	createLinkShareURLFmt = "https://graph.microsoft.com/beta/drives/%s/items/%s/createLink"
)

var ErrFolderNotFound = clues.New("folder not found")

// GetFolderByName will lookup the specified folder by name within the parentFolderID folder.
func (c Drives) GetFolderByName(
	ctx context.Context,
	driveID, parentFolderID, folderName string,
) (models.DriveItemable, error) {
	// The `Children().Get()` API doesn't yet support $filter, so using that to find a folder
	// will be sub-optimal.
	// Instead, we leverage OneDrive path-based addressing -
	// https://learn.microsoft.com/en-us/graph/onedrive-addressing-driveitems#path-based-addressing
	// - which allows us to lookup an item by its path relative to the parent ID
	rawURL := fmt.Sprintf(itemByPathRawURLFmt, driveID, parentFolderID, folderName)
	builder := drives.NewItemItemsDriveItemItemRequestBuilder(rawURL, c.Stable.Adapter())

	foundItem, err := builder.Get(ctx, nil)
	if err != nil {
		if errors.Is(err, core.ErrNotFound) {
			err = clues.Stack(ErrFolderNotFound, err)
		}

		return nil, clues.Wrap(err, "getting folder")
	}

	// Check if the item found is a folder, fail the call if not
	if foundItem.GetFolder() == nil {
		return nil, clues.WrapWC(ctx, ErrFolderNotFound, "item is not a folder")
	}

	return foundItem, nil
}

func (c Drives) GetRootFolder(
	ctx context.Context,
	driveID string,
) (models.DriveItemable, error) {
	root, err := c.Stable.
		Client().
		Drives().
		ByDriveId(driveID).
		Root().
		Get(ctx, nil)
	if err != nil {
		return nil, clues.Wrap(err, "getting drive root")
	}

	return root, nil
}

// TODO: pagination controller needed for completion.
func (c Drives) GetFolderChildren(
	ctx context.Context,
	driveID, folderID string,
) ([]models.DriveItemable, error) {
	response, err := c.Stable.
		Client().
		Drives().
		ByDriveId(driveID).
		Items().
		ByDriveItemId(folderID).
		Children().
		Get(ctx, nil)
	if err != nil {
		return nil, clues.Wrap(err, "getting folder children")
	}

	return response.GetValue(), nil
}

// ---------------------------------------------------------------------------
// Items
// ---------------------------------------------------------------------------

// generic drive item getter
func (c Drives) GetItem(
	ctx context.Context,
	driveID, itemID string,
) (models.DriveItemable, error) {
	options := &drives.ItemItemsDriveItemItemRequestBuilderGetRequestConfiguration{
		QueryParameters: &drives.ItemItemsDriveItemItemRequestBuilderGetQueryParameters{
			// FIXME: accept a CallConfig instead of hardcoding the select.
			Select: DefaultDriveItemProps(),
		},
	}

	di, err := c.Stable.
		Client().
		Drives().
		ByDriveId(driveID).
		Items().
		ByDriveItemId(itemID).
		Get(ctx, options)

	return di, clues.Wrap(err, "getting item").OrNil()
}

func (c Drives) NewItemContentUpload(
	ctx context.Context,
	driveID, itemID string,
) (models.UploadSessionable, error) {
	session := drives.NewItemItemsItemCreateUploadSessionPostRequestBody()

	r, err := c.Stable.
		Client().
		Drives().
		ByDriveId(driveID).
		Items().
		ByDriveItemId(itemID).
		CreateUploadSession().
		Post(ctx, session, nil)

	return r, clues.Wrap(err, "uploading drive item").OrNil()
}

//nolint:lll
const itemChildrenRawURLFmt = "https://graph.microsoft.com/v1.0/drives/%s/items/%s/children?@microsoft.graph.conflictBehavior=%s"

const (
	conflictBehaviorFail    = "fail"
	conflictBehaviorRename  = "rename"
	conflictBehaviorReplace = "replace"
)

// PostItemInContainer creates a new item in the specified folder
func (c Drives) PostItemInContainer(
	ctx context.Context,
	driveID, parentFolderID string,
	newItem models.DriveItemable,
	onCollision control.CollisionPolicy,
) (models.DriveItemable, error) {
	// graph api has no policy for Skip; instead we wrap the same-name failure
	// as a graph.ErrItemAlreadyExistsConflict.
	conflictBehavior := conflictBehaviorFail

	switch onCollision {
	case control.Replace:
		conflictBehavior = conflictBehaviorReplace
	case control.Copy:
		conflictBehavior = conflictBehaviorRename
	}

	// Graph SDK doesn't yet provide a POST method for `/children` so we set the `rawUrl` ourselves as recommended
	// here: https://github.com/microsoftgraph/msgraph-sdk-go/issues/155#issuecomment-1136254310
	rawURL := fmt.Sprintf(itemChildrenRawURLFmt, driveID, parentFolderID, conflictBehavior)
	builder := drives.NewItemItemsRequestBuilder(rawURL, c.Stable.Adapter())

	newItem, err := builder.Post(ctx, newItem, nil)
	if err != nil {
		return nil, clues.Wrap(err, "creating item in folder")
	}

	return newItem, nil
}

func (c Drives) PatchItem(
	ctx context.Context,
	driveID, itemID string,
	item models.DriveItemable,
) error {
	_, err := c.Stable.
		Client().
		Drives().
		ByDriveId(driveID).
		Items().
		ByDriveItemId(itemID).
		Patch(ctx, item, nil)

	return clues.Wrap(err, "patching drive item").OrNil()
}

func (c Drives) PutItemContent(
	ctx context.Context,
	driveID, itemID string,
	content []byte,
) error {
	_, err := c.Stable.
		Client().
		Drives().
		ByDriveId(driveID).
		Items().
		ByDriveItemId(itemID).
		Content().
		Put(ctx, content, nil)

	return clues.Wrap(err, "uploading drive item content").OrNil()
}

// deletes require unique http clients
// https://github.com/alcionai/corso/issues/2707
func (c Drives) DeleteItem(
	ctx context.Context,
	driveID, itemID string,
) error {
	// deletes require unique http clients
	// https://github.com/alcionai/corso/issues/2707
	srv, err := c.Service(c.counter)
	if err != nil {
		return clues.WrapWC(ctx, err, "creating adapter to delete item permission")
	}

	err = srv.
		Client().
		Drives().
		ByDriveId(driveID).
		Items().
		ByDriveItemId(itemID).
		Delete(ctx, nil)

	return clues.Wrap(err, "deleting item").With("item_id", itemID).OrNil()
}

// ---------------------------------------------------------------------------
// Permissions
// ---------------------------------------------------------------------------

func (c Drives) GetItemPermission(
	ctx context.Context,
	driveID, itemID string,
) (models.PermissionCollectionResponseable, error) {
	perm, err := c.Stable.
		Client().
		Drives().
		ByDriveId(driveID).
		Items().
		ByDriveItemId(itemID).
		Permissions().
		Get(ctx, nil)

	return perm, clues.Wrap(err, "getting item permissions").OrNil()
}

func (c Drives) PostItemPermissionUpdate(
	ctx context.Context,
	driveID, itemID string,
	body *drives.ItemItemsItemInvitePostRequestBody,
) (drives.ItemItemsItemInviteResponseable, error) {
	ctx = graph.ConsumeNTokens(ctx, graph.PermissionsLC)

	itm, err := c.Stable.
		Client().
		Drives().
		ByDriveId(driveID).
		Items().
		ByDriveItemId(itemID).
		Invite().
		Post(ctx, body, nil)

	return itm, clues.Wrap(err, "posting permissions").OrNil()
}

func (c Drives) DeleteItemPermission(
	ctx context.Context,
	driveID, itemID, permissionID string,
) error {
	// deletes require unique http clients
	// https://github.com/alcionai/corso/issues/2707
	srv, err := c.Service(c.counter)
	if err != nil {
		return clues.WrapWC(ctx, err, "creating adapter to delete item permission")
	}

	err = srv.
		Client().
		Drives().
		ByDriveId(driveID).
		Items().
		ByDriveItemId(itemID).
		Permissions().
		ByPermissionId(permissionID).
		Delete(graph.ConsumeNTokens(ctx, graph.PermissionsLC), nil)

	return clues.Wrap(err, "deleting drive item permission").OrNil()
}

func (c Drives) PostItemLinkShareUpdate(
	ctx context.Context,
	driveID, itemID string,
	body *drives.ItemItemsItemCreateLinkPostRequestBody,
) (models.Permissionable, error) {
	ctx = graph.ConsumeNTokens(ctx, graph.PermissionsLC)

	// We are using the beta version of the endpoint. This allows us
	// to add recipients in the same request as well as to make it not
	// send out and email for every link share the user gets added to.
	rawURL := fmt.Sprintf(createLinkShareURLFmt, driveID, itemID)
	builder := drives.NewItemItemsItemCreateLinkRequestBuilder(rawURL, c.Stable.Adapter())

	itm, err := builder.Post(ctx, body, nil)

	return itm, clues.Wrap(err, "creating link share").OrNil()
}

// ---------------------------------------------------------------------------
// helper funcs
// ---------------------------------------------------------------------------

// DriveItemCollisionKeyy constructs a key from the item name.
// collision keys are used to identify duplicate item conflicts for handling advanced restoration config.
func DriveItemCollisionKey(item models.DriveItemable) string {
	if item == nil {
		return ""
	}

	return ptr.Val(item.GetName())
}

// NewDriveItem initializes a `models.DriveItemable` with either a folder or file entry.
func NewDriveItem(name string, folder bool) *models.DriveItem {
	itemToCreate := models.NewDriveItem()
	itemToCreate.SetName(&name)

	if folder {
		itemToCreate.SetFolder(models.NewFolder())
	} else {
		itemToCreate.SetFile(models.NewFile())
	}

	return itemToCreate
}
