package onedrive

import (
	"context"
	"strings"

	"github.com/alcionai/clues"

	"github.com/alcionai/corso/src/internal/data"
	"github.com/alcionai/corso/src/internal/m365/onedrive/metadata"
	"github.com/alcionai/corso/src/internal/version"
	"github.com/alcionai/corso/src/pkg/backup/details"
	"github.com/alcionai/corso/src/pkg/control"
	"github.com/alcionai/corso/src/pkg/export"
	"github.com/alcionai/corso/src/pkg/fault"
	"github.com/alcionai/corso/src/pkg/path"
)

var _ export.Collection = &exportCollection{}

// exportCollection is the implementation of export.ExportCollection for OneDrive
type exportCollection struct {
	// baseDir contains the path of the collection
	baseDir string

	// backingCollection is the restore collection from which we will
	// create the export collection.
	backingCollection data.RestoreCollection

	// backupVersion is the backupVersion of the backup this collection was part
	// of. This is required to figure out how to get the name of the
	// item.
	backupVersion int
}

func (ec exportCollection) BasePath() string {
	return ec.baseDir
}

func (ec exportCollection) Items(ctx context.Context) <-chan export.Item {
	ch := make(chan export.Item)
	go items(ctx, ec, ch)

	return ch
}

// items converts items in backing collection to export items
func items(ctx context.Context, ec exportCollection, ch chan<- export.Item) {
	defer close(ch)

	errs := fault.New(false)

	// There will only be a single item in the backingCollections
	// for OneDrive
	for item := range ec.backingCollection.Items(ctx, errs) {
		itemUUID := item.UUID()
		if isMetadataFile(itemUUID, ec.backupVersion) {
			continue
		}

		name, err := getItemName(ctx, itemUUID, ec.backupVersion, ec.backingCollection)

		ch <- export.Item{
			ID: itemUUID,
			Data: export.ItemData{
				Name: name,
				Body: item.ToReader(),
			},
			Error: err,
		}
	}

	eitems, erecovereable := errs.ItemsAndRecovered()

	// Return all the items that we failed to get from kopia at the end
	for _, err := range eitems {
		ch <- export.Item{
			ID:    err.ID,
			Error: &err,
		}
	}

	for _, ec := range erecovereable {
		ch <- export.Item{
			Error: ec,
		}
	}
}

// isMetadataFile is used to determine if a path corresponds to a
// metadata file.  This is OneDrive specific logic and depends on the
// version of the backup unlike metadata.IsMetadataFile which only has
// to be concerned about the current version.
func isMetadataFile(id string, backupVersion int) bool {
	if backupVersion < version.OneDrive1DataAndMetaFiles {
		return false
	}

	return strings.HasSuffix(id, metadata.MetaFileSuffix) ||
		strings.HasSuffix(id, metadata.DirMetaFileSuffix)
}

// getItemName is used to get the name of the item.
// How we get the name depends on the version of the backup.
func getItemName(
	ctx context.Context,
	id string,
	backupVersion int,
	fin data.FetchItemByNamer,
) (string, error) {
	if backupVersion < version.OneDrive1DataAndMetaFiles {
		return id, nil
	}

	if backupVersion < version.OneDrive5DirMetaNoName {
		return strings.TrimSuffix(id, metadata.DataFileSuffix), nil
	}

	if strings.HasSuffix(id, metadata.DataFileSuffix) {
		trimmedName := strings.TrimSuffix(id, metadata.DataFileSuffix)
		metaName := trimmedName + metadata.MetaFileSuffix

		meta, err := fetchAndReadMetadata(ctx, fin, metaName)
		if err != nil {
			return "", clues.Wrap(err, "getting metadata").WithClues(ctx)
		}

		return meta.FileName, nil
	}

	return "", clues.New("invalid item id").WithClues(ctx)
}

// ProduceExportCollections will create the export collections for the
// given restore collections.
func ProduceExportCollections(
	ctx context.Context,
	backupVersion int,
	exportCfg control.ExportConfig,
	opts control.Options,
	dcs []data.RestoreCollection,
	deets *details.Builder,
	errs *fault.Bus,
) ([]export.Collection, error) {
	var (
		el = errs.Local()
		ec = make([]export.Collection, 0, len(dcs))
	)

	for _, dc := range dcs {
		drivePath, err := path.ToDrivePath(dc.FullPath())
		if err != nil {
			return nil, clues.Wrap(err, "transforming path to drive path").WithClues(ctx)
		}

		baseDir := path.Builder{}.Append(drivePath.Folders...)

		ec = append(ec, exportCollection{
			baseDir:           baseDir.String(),
			backingCollection: dc,
			backupVersion:     backupVersion,
		})
	}

	return ec, el.Failure()
}
