package stub

import (
	"encoding/json"
	"fmt"
	"strings"

	"github.com/alcionai/clues"
	"github.com/google/uuid"

	odmetadata "github.com/alcionai/corso/src/internal/m365/collection/drive/metadata"
	odConsts "github.com/alcionai/corso/src/internal/m365/service/onedrive/consts"
	m365Stub "github.com/alcionai/corso/src/internal/m365/stub"
	"github.com/alcionai/corso/src/internal/version"
	"github.com/alcionai/corso/src/pkg/backup/details"
	"github.com/alcionai/corso/src/pkg/path"
	"github.com/alcionai/corso/src/pkg/services/m365/api/graph/metadata"
)

// For any version post this(inclusive), we expect to be using IDs for
// permission instead of email
const versionPermissionSwitchedToID = version.OneDrive4DirIncludesPermissions

func getMetadata(fileName string, meta MetaData, permUseID bool) odmetadata.Metadata {
	if meta.SharingMode != odmetadata.SharingModeCustom {
		return odmetadata.Metadata{
			FileName:    fileName,
			SharingMode: meta.SharingMode,
		}
	}

	testMeta := odmetadata.Metadata{FileName: fileName}

	if len(meta.Perms.User) != 0 {
		// In case of permissions, the id will usually be same for same
		// user/role combo unless deleted and readded, but we have to do
		// this as we only have two users of which one is already taken.
		id := uuid.NewString()
		uperm := odmetadata.Permission{ID: id, Roles: meta.Perms.Roles}

		uperm.Email = meta.Perms.User
		if permUseID {
			uperm.EntityID = meta.Perms.EntityID
		}

		testMeta.Permissions = []odmetadata.Permission{uperm}
	}

	if len(meta.LinkShares) != 0 {
		for _, ls := range meta.LinkShares {
			eids := []string{}
			for _, id := range ls.Entities {
				eids = append(eids, id.ID)
			}

			id := strings.Join(eids, "-") + ls.Scope + ls.Type

			roles := []string{}
			if ls.Type == "edit" {
				roles = []string{"write"}
			} else if ls.Type == "view" {
				roles = []string{"read"}
			}

			ls := odmetadata.LinkShare{
				ID: id, // id is required for mapping from parent
				Link: odmetadata.LinkShareLink{
					Scope:  ls.Scope,
					Type:   ls.Type,
					WebURL: id,
				},
				Entities: ls.Entities,
				Roles:    roles,
			}

			testMeta.LinkShares = append(testMeta.LinkShares, ls)
		}
	}

	return testMeta
}

type PermData struct {
	User     string // user is only for older versions
	EntityID string
	Roles    []string
}

type LinkShareData struct {
	Entities []odmetadata.Entity
	Scope    string
	Type     string
}

type MetaData struct {
	SharingMode odmetadata.SharingMode
	Perms       PermData
	LinkShares  []LinkShareData
}

type ItemData struct {
	Name string
	Data []byte
	Meta MetaData
}

type ColInfo struct {
	PathElements []string
	Files        []ItemData
	Folders      []ItemData
	Meta         MetaData
}

type collection struct {
	Service       path.ServiceType
	PathElements  []string
	Items         []m365Stub.ItemInfo
	Aux           []m365Stub.ItemInfo
	BackupVersion int
}

func (c collection) ColInfo() m365Stub.ColInfo {
	cat := path.FilesCategory
	if c.Service == path.SharePointService {
		cat = path.LibrariesCategory
	}

	return m365Stub.ColInfo{
		PathElements: c.PathElements,
		Category:     cat,
		Items:        c.Items,
		AuxItems:     c.Aux,
	}
}

func NewCollection(
	service path.ServiceType,
	PathElements []string,
	backupVersion int,
) *collection {
	return &collection{
		Service:       service,
		PathElements:  PathElements,
		BackupVersion: backupVersion,
	}
}

func DataForInfo(
	service path.ServiceType,
	cols []ColInfo,
	backupVersion int,
) ([]m365Stub.ColInfo, error) {
	var (
		res []m365Stub.ColInfo
		err error
	)

	for _, c := range cols {
		onedriveCol := NewCollection(service, c.PathElements, backupVersion)

		for _, f := range c.Files {
			_, err = onedriveCol.withFile(f.Name, f.Data, f.Meta)
			if err != nil {
				return res, err
			}
		}

		for _, d := range c.Folders {
			_, err = onedriveCol.withFolder(d.Name, d.Meta)
			if err != nil {
				return res, err
			}
		}

		_, err = onedriveCol.withPermissions(c.Meta)
		if err != nil {
			return res, err
		}

		res = append(res, onedriveCol.ColInfo())
	}

	return res, nil
}

func (c *collection) withFile(name string, fileData []byte, meta MetaData) (*collection, error) {
	switch c.BackupVersion {
	case 0:
		// Lookups will occur using the most recent version of things so we need
		// the embedded file name to match that.
		item, err := FileWithData(
			name,
			name+metadata.DataFileSuffix,
			fileData)
		if err != nil {
			return c, err
		}

		c.Items = append(c.Items, item)

		// v1-5, early metadata design
	case version.OneDrive1DataAndMetaFiles, 2, version.OneDrive3IsMetaMarker,
		version.OneDrive4DirIncludesPermissions, version.OneDrive5DirMetaNoName:
		items, err := FileWithData(
			name+metadata.DataFileSuffix,
			name+metadata.DataFileSuffix,
			fileData)
		if err != nil {
			return c, err
		}

		c.Items = append(c.Items, items)

		md, err := ItemWithMetadata(
			"",
			name+metadata.MetaFileSuffix,
			name+metadata.MetaFileSuffix,
			meta,
			c.BackupVersion >= versionPermissionSwitchedToID)
		if err != nil {
			return c, err
		}

		c.Items = append(c.Items, md)
		c.Aux = append(c.Aux, md)

		// v6+ current metadata design
	case version.OneDrive6NameInMeta, version.OneDrive7LocationRef,
		version.All8MigrateUserPNToID, version.Groups9Update:
		item, err := FileWithData(
			name+metadata.DataFileSuffix,
			name+metadata.DataFileSuffix,
			fileData)
		if err != nil {
			return c, err
		}

		c.Items = append(c.Items, item)

		md, err := ItemWithMetadata(
			name,
			name+metadata.MetaFileSuffix,
			name,
			meta,
			c.BackupVersion >= versionPermissionSwitchedToID)
		if err != nil {
			return c, err
		}

		c.Items = append(c.Items, md)
		c.Aux = append(c.Aux, md)

	default:
		return c, clues.New(fmt.Sprintf("bad backup version. version %d", c.BackupVersion))
	}

	return c, nil
}

func (c *collection) withFolder(name string, meta MetaData) (*collection, error) {
	switch c.BackupVersion {
	case 0, version.OneDrive4DirIncludesPermissions, version.OneDrive5DirMetaNoName,
		version.OneDrive6NameInMeta, version.OneDrive7LocationRef,
		version.All8MigrateUserPNToID, version.Groups9Update:
		return c, nil

	case version.OneDrive1DataAndMetaFiles, 2, version.OneDrive3IsMetaMarker:
		item, err := ItemWithMetadata(
			"",
			name+metadata.DirMetaFileSuffix,
			name+metadata.DirMetaFileSuffix,
			meta,
			c.BackupVersion >= versionPermissionSwitchedToID)

		c.Items = append(c.Items, item)

		if err != nil {
			return c, err
		}

	default:
		return c, clues.New(fmt.Sprintf("bad backup version.version %d", c.BackupVersion))
	}

	return c, nil
}

// withPermissions adds permissions to the folder represented by this
// onedriveCollection.
func (c *collection) withPermissions(meta MetaData) (*collection, error) {
	// These versions didn't store permissions for the folder or didn't store them
	// in the folder's collection.
	if c.BackupVersion < version.OneDrive4DirIncludesPermissions {
		return c, nil
	}

	name := c.PathElements[len(c.PathElements)-1]
	metaName := name

	if c.BackupVersion >= version.OneDrive5DirMetaNoName {
		// We switched to just .dirmeta for metadata file names.
		metaName = ""
	}

	if name == odConsts.RootPathDir {
		return c, nil
	}

	md, err := ItemWithMetadata(
		name,
		metaName+metadata.DirMetaFileSuffix,
		metaName+metadata.DirMetaFileSuffix,
		meta,
		c.BackupVersion >= versionPermissionSwitchedToID)
	if err != nil {
		return c, err
	}

	c.Items = append(c.Items, md)
	c.Aux = append(c.Aux, md)

	return c, err
}

type FileData struct {
	FileName string `json:"fileName,omitempty"`
	Data     []byte `json:"data,omitempty"`
}

func FileWithData(
	name, lookupKey string,
	fileData []byte,
) (m365Stub.ItemInfo, error) {
	content := FileData{
		FileName: lookupKey,
		Data:     fileData,
	}

	serialized, err := json.Marshal(content)
	if err != nil {
		return m365Stub.ItemInfo{}, clues.Stack(err)
	}

	return m365Stub.ItemInfo{
		Name:      name,
		Data:      serialized,
		LookupKey: lookupKey,
	}, nil
}

func ItemWithMetadata(
	fileName, itemID, lookupKey string,
	meta MetaData,
	permUseID bool,
) (m365Stub.ItemInfo, error) {
	testMeta := getMetadata(fileName, meta, permUseID)

	testMetaJSON, err := json.Marshal(testMeta)
	if err != nil {
		return m365Stub.ItemInfo{}, clues.Wrap(err, "marshalling metadata")
	}

	return m365Stub.ItemInfo{
		Name:      itemID,
		Data:      testMetaJSON,
		LookupKey: lookupKey,
	}, nil
}

func DriveItemInfo() details.ItemInfo {
	return details.ItemInfo{
		OneDrive: &details.OneDriveInfo{
			ItemType: details.OneDriveItem,
			ItemName: "test.txt",
			Size:     1,
		},
	}
}
