package exchange

import (
	"context"
	"fmt"

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

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

type attachmentPoster interface {
	PostSmallAttachment(
		ctx context.Context,
		userID, containerID, itemID string,
		body models.Attachmentable,
	) error
	PostLargeAttachment(
		ctx context.Context,
		userID, containerID, itemID, name string,
		content []byte,
	) (string, error)
}

const (
	// Use large attachment logic for attachments > 3MB
	// https://learn.microsoft.com/en-us/graph/outlook-large-attachments
	largeAttachmentSize           = 3 * 1024 * 1024
	fileAttachmentOdataValue      = "#microsoft.graph.fileAttachment"
	itemAttachmentOdataValue      = "#microsoft.graph.itemAttachment"
	referenceAttachmentOdataValue = "#microsoft.graph.referenceAttachment"
)

func attachmentType(attachment models.Attachmentable) models.AttachmentType {
	attachmentType := ptr.Val(attachment.GetOdataType())
	switch attachmentType {
	case fileAttachmentOdataValue:
		return models.FILE_ATTACHMENTTYPE
	case itemAttachmentOdataValue:
		return models.ITEM_ATTACHMENTTYPE
	case referenceAttachmentOdataValue:
		return models.REFERENCE_ATTACHMENTTYPE
	default:
		// Should not hit this but default to ITEM_ATTACHMENTTYPE
		// which will pick the default attachment upload mechanism
		return models.ITEM_ATTACHMENTTYPE
	}
}

// uploadAttachment will upload the specified message attachment to M365
func uploadAttachment(
	ctx context.Context,
	ap attachmentPoster,
	userID, containerID, parentItemID string,
	attachment models.Attachmentable,
) error {
	var (
		attachmentType = attachmentType(attachment)
		id             = ptr.Val(attachment.GetId())
		name           = ptr.Val(attachment.GetName())
		size           = ptr.Val(attachment.GetSize())
	)

	ctx = clues.Add(
		ctx,
		"attachment_size", size,
		"attachment_id", id,
		"attachment_name", clues.Hide(name),
		"attachment_type", attachmentType,
		"attachment_odata_type", ptr.Val(attachment.GetOdataType()),
		"attachment_outlook_odata_type", getOutlookOdataType(attachment),
		"parent_item_id", parentItemID)

	logger.Ctx(ctx).Debug("uploading attachment")

	// reference attachments that are inline() do not need to be recreated. The contents are part of the body.
	if attachmentType == models.REFERENCE_ATTACHMENTTYPE && ptr.Val(attachment.GetIsInline()) {
		logger.Ctx(ctx).Debug("skip uploading inline reference attachment")
		return nil
	}

	// item Attachments to be skipped until the completion of Issue #2353
	if attachmentType == models.ITEM_ATTACHMENTTYPE {
		a, err := toItemAttachment(attachment)
		if err != nil {
			logger.CtxErr(ctx, err).Info(fmt.Sprintf("item attachment type not supported: %v", attachmentType))
			return nil
		}

		attachment = a
	}

	// for file attachments sized >= 3MB
	if attachmentType == models.FILE_ATTACHMENTTYPE && size >= largeAttachmentSize {
		// We expect the entire attachment to fit in memory.
		// Max attachment size is 150MB.
		content, err := api.GetAttachmentContent(attachment)
		if err != nil {
			return clues.Wrap(err, "serializing attachment content").WithClues(ctx)
		}

		_, err = ap.PostLargeAttachment(ctx, userID, containerID, parentItemID, name, content)

		return err
	}

	// for all other attachments
	return ap.PostSmallAttachment(ctx, userID, containerID, parentItemID, attachment)
}

func getOutlookOdataType(query models.Attachmentable) string {
	attachment, ok := query.(models.ItemAttachmentable)
	if !ok {
		return ""
	}

	item := attachment.GetItem()
	if item == nil {
		return ""
	}

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