package store

import (
	"context"
	"fmt"
	"sync"

	"github.com/grafana/grafana-plugin-sdk-go/data"

	"github.com/grafana/grafana/pkg/infra/filestorage"
	ac "github.com/grafana/grafana/pkg/services/accesscontrol"
)

type nestedTree struct {
	rootsByOrgId map[int64][]storageRuntime
	lookup       map[int64]map[string]storageRuntime

	orgInitMutex          sync.Mutex
	initializeOrgStorages func(orgId int64) []storageRuntime
}

var (
	_ storageTree = (*nestedTree)(nil)
)

func asNameToFileStorageMap(storages []storageRuntime) map[string]storageRuntime {
	lookup := make(map[string]storageRuntime)
	for _, storage := range storages {
		isUnderContentRoot := storage.Meta().Config.UnderContentRoot
		prefix := storage.Meta().Config.Prefix
		if !isUnderContentRoot {
			lookup[prefix] = storage
		} else {
			lookup[fmt.Sprintf("%s/%s", RootContent, prefix)] = storage
		}
	}
	return lookup
}

func (t *nestedTree) init() {
	t.orgInitMutex.Lock()
	defer t.orgInitMutex.Unlock()

	t.lookup = make(map[int64]map[string]storageRuntime, len(t.rootsByOrgId))

	for orgId, storages := range t.rootsByOrgId {
		t.lookup[orgId] = asNameToFileStorageMap(storages)
	}
}

func (t *nestedTree) assureOrgIsInitialized(orgId int64) {
	t.orgInitMutex.Lock()
	defer t.orgInitMutex.Unlock()
	if _, ok := t.rootsByOrgId[orgId]; !ok {
		orgStorages := t.initializeOrgStorages(orgId)
		t.rootsByOrgId[orgId] = orgStorages
		t.lookup[orgId] = asNameToFileStorageMap(orgStorages)
	}
}

func (t *nestedTree) getRoot(orgId int64, path string) (storageRuntime, string) {
	t.assureOrgIsInitialized(orgId)

	if path == "" {
		return nil, ""
	}

	rootKey, path := splitFirstSegment(path)
	root, ok := t.lookup[orgId][rootKey]
	if ok && root != nil {
		if root.Meta().Config.Prefix == RootContent && path != "" && path != "/" {
			mountedKey, nestedPath := splitFirstSegment(path)
			nestedLookupKey := rootKey + filestorage.Delimiter + mountedKey
			nestedRoot, nestedOk := t.lookup[orgId][nestedLookupKey]

			if nestedOk && nestedRoot != nil {
				return nestedRoot, filestorage.Delimiter + nestedPath
			}

			if orgId != ac.GlobalOrgID {
				globalRoot, globalOk := t.lookup[ac.GlobalOrgID][nestedLookupKey]
				if globalOk && globalRoot != nil {
					return globalRoot, filestorage.Delimiter + nestedPath
				}
			}
		}

		return root, filestorage.Delimiter + path
	}

	if orgId != ac.GlobalOrgID {
		globalRoot, ok := t.lookup[ac.GlobalOrgID][rootKey]
		if ok && globalRoot != nil {
			if globalRoot.Meta().Config.Prefix == RootContent && path != "" && path != "/" {
				mountedKey, nestedPath := splitFirstSegment(path)
				nestedLookupKey := rootKey + filestorage.Delimiter + mountedKey
				nestedRoot, nestedOk := t.lookup[orgId][nestedLookupKey]

				if nestedOk && nestedRoot != nil {
					return nestedRoot, filestorage.Delimiter + nestedPath
				}

				globalNestedRoot, globalOk := t.lookup[ac.GlobalOrgID][nestedLookupKey]
				if globalOk && globalNestedRoot != nil {
					return globalNestedRoot, filestorage.Delimiter + nestedPath
				}
			}

			return globalRoot, filestorage.Delimiter + path
		}
	}

	return nil, path // not found or not ready
}

func (t *nestedTree) GetFile(ctx context.Context, orgId int64, path string) (*filestorage.File, error) {
	if path == "" {
		return nil, nil // not found
	}
	root, path := t.getRoot(orgId, path)
	if root == nil {
		return nil, nil // not found (or not ready)
	}
	store := root.Store()
	if store == nil {
		return nil, fmt.Errorf("store not ready")
	}
	file, _, err := store.Get(ctx, path, nil)
	return file, err
}

func filterStoragesUnderContentRoot(storages []storageRuntime) []storageRuntime {
	out := make([]storageRuntime, 0)
	for _, s := range storages {
		if s.Meta().Config.UnderContentRoot {
			out = append(out, s)
		}
	}
	return out
}

func (t *nestedTree) getStorages(orgId int64) []storageRuntime {
	globalStorages := make([]storageRuntime, 0)
	globalStorages = append(globalStorages, t.rootsByOrgId[ac.GlobalOrgID]...)

	if orgId == ac.GlobalOrgID {
		return append(make([]storageRuntime, 0), globalStorages...)
	}

	orgPrefixes := make(map[string]bool)
	storages := make([]storageRuntime, 0)

	for _, s := range t.rootsByOrgId[orgId] {
		storages = append(storages, s)
		orgPrefixes[s.Meta().Config.Prefix] = true
	}

	for _, s := range globalStorages {
		// prefer org-specific storage over global with the same prefix
		if ok := orgPrefixes[s.Meta().Config.Prefix]; !ok {
			storages = append(storages, s)
		}
	}

	return storages
}

func (t *nestedTree) ListFolder(ctx context.Context, orgId int64, path string, maxFiles int, accessFilter filestorage.PathFilter) (*StorageListFrame, error) {
	if path == "" || path == "/" {
		t.assureOrgIsInitialized(orgId)

		idx := 0

		storages := t.getStorages(orgId)
		count := len(storages)
		grafanaStorageLogger.Info("Listing root folder", "path", path, "storageCount", len(storages))

		names := data.NewFieldFromFieldType(data.FieldTypeString, count)
		title := data.NewFieldFromFieldType(data.FieldTypeString, count)
		descr := data.NewFieldFromFieldType(data.FieldTypeString, count)
		mtype := data.NewFieldFromFieldType(data.FieldTypeString, count)
		title.Name = titleListFrameField
		names.Name = nameListFrameField
		descr.Name = descriptionListFrameField
		mtype.Name = mediaTypeListFrameField
		for _, f := range storages {
			meta := f.Meta()
			names.Set(idx, meta.Config.Prefix)
			title.Set(idx, meta.Config.Name)
			descr.Set(idx, meta.Config.Description)
			mtype.Set(idx, "directory")
			idx++
		}

		frame := data.NewFrame("", names, title, descr, mtype)
		frame.SetMeta(&data.FrameMeta{
			Type: data.FrameTypeDirectoryListing,
		})
		return &StorageListFrame{frame}, nil
	}

	root, path := t.getRoot(orgId, path)
	if root == nil {
		return nil, nil // not found (or not ready)
	}

	var storages []storageRuntime
	if root.Meta().Config.Prefix == RootContent && (path == "" || path == "/") {
		storages = filterStoragesUnderContentRoot(t.getStorages(orgId))
	}
	grafanaStorageLogger.Info("Listing folder", "path", path, "storageCount", len(storages), "root", root.Meta().Config.Prefix)

	store := root.Store()
	if store == nil {
		return nil, fmt.Errorf("store not ready")
	}

	pathFilter := accessFilter
	if root.Meta().Config.Prefix == RootContent && len(storages) > 0 {
		// create a PathFilter that will filter out folders that are "shadowed" by the mounted storages
		pathFilter = filestorage.NewAndPathFilter(
			accessFilter,
			t.createPathFilterForContentRoot(storages),
		)
	}

	listResponse, err := store.List(ctx, path,
		&filestorage.Paging{
			Limit: maxFiles,
		},
		&filestorage.ListOptions{
			Recursive:   false,
			WithFolders: true,
			WithFiles:   true,
			Filter:      pathFilter,
		})

	if err != nil {
		return nil, err
	}

	count := len(listResponse.Files) + len(storages)
	names := data.NewFieldFromFieldType(data.FieldTypeString, count)
	mtype := data.NewFieldFromFieldType(data.FieldTypeString, count)
	fsize := data.NewFieldFromFieldType(data.FieldTypeInt64, count)
	names.Name = nameListFrameField
	mtype.Name = mediaTypeListFrameField
	fsize.Name = sizeListFrameField
	fsize.Config = &data.FieldConfig{
		Unit: "bytes",
	}

	idx := 0
	for _, s := range storages {
		names.Set(idx, s.Meta().Config.Prefix)
		mtype.Set(idx, filestorage.DirectoryMimeType)
		fsize.Set(idx, int64(0))
		idx++
	}

	for _, f := range listResponse.Files {
		names.Set(idx, f.Name)
		mtype.Set(idx, f.MimeType)
		fsize.Set(idx, f.Size)
		idx++
	}

	frame := data.NewFrame("", names, mtype, fsize)
	frame.SetMeta(&data.FrameMeta{
		Type: data.FrameTypeDirectoryListing,
		Custom: map[string]any{
			"HasMore": listResponse.HasMore,
		},
	})
	return &StorageListFrame{frame}, nil
}

func (t *nestedTree) createPathFilterForContentRoot(storages []storageRuntime) filestorage.PathFilter {
	disallowedPrefixes := make([]string, 0)
	disallowedPaths := make([]string, 0)

	for _, s := range storages {
		path := filestorage.Delimiter + s.Meta().Config.Prefix
		disallowedPaths = append(disallowedPaths, path)
		disallowedPrefixes = append(disallowedPrefixes, path+filestorage.Delimiter)
	}

	grafanaStorageLogger.Info("Created a path filter for the content root", "disallowedPrefixes", disallowedPrefixes, "disallowedPaths", disallowedPaths)
	return filestorage.NewPathFilter(nil, nil, disallowedPrefixes, disallowedPaths)
}
