package resources

import (
	"context"
	"errors"
	"fmt"

	apierrors "k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
	"k8s.io/client-go/dynamic"

	folders "github.com/grafana/grafana/apps/folder/pkg/apis/folder/v1beta1"
	"github.com/grafana/grafana/apps/provisioning/pkg/repository"
	"github.com/grafana/grafana/apps/provisioning/pkg/safepath"
	"github.com/grafana/grafana/pkg/apimachinery/identity"
	"github.com/grafana/grafana/pkg/apimachinery/utils"
	"github.com/grafana/grafana/pkg/services/dashboards"
)

const MaxNumberOfFolders = 10000

type FolderManager struct {
	repo   repository.ReaderWriter
	tree   FolderTree
	client dynamic.ResourceInterface
}

func NewFolderManager(repo repository.ReaderWriter, client dynamic.ResourceInterface, lookup FolderTree) *FolderManager {
	return &FolderManager{
		repo:   repo,
		tree:   lookup,
		client: client,
	}
}

func (fm *FolderManager) Client() dynamic.ResourceInterface {
	return fm.client
}

func (fm *FolderManager) Tree() FolderTree {
	return fm.tree
}

func (fm *FolderManager) SetTree(tree FolderTree) {
	fm.tree = tree
}

// EnsureFoldersExist creates the folder structure in the cluster.
func (fm *FolderManager) EnsureFolderPathExist(ctx context.Context, filePath string) (parent string, err error) {
	cfg := fm.repo.Config()
	parent = RootFolder(cfg)

	dir := filePath
	if !safepath.IsDir(filePath) {
		dir = safepath.Dir(filePath)
	}

	if dir == "" {
		return parent, nil
	}

	f := ParseFolder(dir, cfg.Name)
	if fm.tree.In(f.ID) {
		return f.ID, nil
	}

	err = safepath.Walk(ctx, f.Path, func(ctx context.Context, traverse string) error {
		f := ParseFolder(traverse, cfg.GetName())
		if fm.tree.In(f.ID) {
			parent = f.ID
			return nil
		}

		if err := fm.EnsureFolderExists(ctx, f, parent); err != nil {
			return fmt.Errorf("ensure folder exists: %w", err)
		}

		fm.tree.Add(f, parent)
		parent = f.ID
		return nil
	})

	if err != nil {
		return "", err
	}

	return f.ID, nil
}

// EnsureFolderExists creates the folder if it doesn't exist.
// If the folder already exists:
// - it will error if the folder is not owned by this repository
func (fm *FolderManager) EnsureFolderExists(ctx context.Context, folder Folder, parent string) error {
	cfg := fm.repo.Config()
	obj, err := fm.client.Get(ctx, folder.ID, metav1.GetOptions{})
	if err == nil {
		current, ok := obj.GetAnnotations()[utils.AnnoKeyManagerIdentity]
		if !ok {
			return fmt.Errorf("target folder is not managed by a repository")
		}
		if current != cfg.Name {
			return fmt.Errorf("target folder is managed by a different repository (%s)", current)
		}
		return nil
	} else if !apierrors.IsNotFound(err) {
		return fmt.Errorf("failed to check if folder exists: %w", err)
	}

	// Always use the provisioning identity when writing
	ctx, _, err = identity.WithProvisioningIdentity(ctx, cfg.GetNamespace())
	if err != nil {
		return fmt.Errorf("unable to use provisioning identity %w", err)
	}

	obj = &unstructured.Unstructured{
		Object: map[string]interface{}{
			"spec": map[string]any{
				"title": folder.Title,
			},
		},
	}
	obj.SetAPIVersion(folders.APIVERSION)
	obj.SetKind(folders.FolderResourceInfo.GroupVersionKind().Kind)
	obj.SetNamespace(cfg.GetNamespace())
	obj.SetName(folder.ID)

	meta, err := utils.MetaAccessor(obj)
	if err != nil {
		return fmt.Errorf("create meta accessor for the object: %w", err)
	}

	if parent != "" {
		meta.SetFolder(parent)
	} else {
		meta.SetAnnotation(utils.AnnoKeyGrantPermissions, utils.AnnoGrantPermissionsDefault)
	}
	meta.SetManagerProperties(utils.ManagerProperties{
		Kind:     utils.ManagerKindRepo,
		Identity: cfg.GetName(),
	})
	meta.SetSourceProperties(utils.SourceProperties{
		Path: folder.Path,
	})

	if _, err := fm.client.Create(ctx, obj, metav1.CreateOptions{}); err != nil {
		// there is a potential race here where two syncs can be triggered
		// if we try to create and there is an error, check if it is from another sync
		// job for this repo that created it
		if apierrors.IsAlreadyExists(err) || err.Error() == dashboards.ErrFolderVersionMismatch.Error() {
			obj, err2 := fm.client.Get(ctx, folder.ID, metav1.GetOptions{})
			if err2 != nil {
				return fmt.Errorf("failed to get folder: %w", err2)
			} else if obj == nil {
				return fmt.Errorf("failed to create folder: %w", err)
			}

			current, ok := obj.GetAnnotations()[utils.AnnoKeyManagerIdentity]
			if !ok {
				return fmt.Errorf("target folder is not managed by a repository")
			}
			if current != cfg.Name {
				return fmt.Errorf("target folder is managed by a different repository (%s)", current)
			}

			return nil
		}

		return fmt.Errorf("failed to create folder: %w", err)
	}
	return nil
}

func (fm *FolderManager) GetFolder(ctx context.Context, name string) (*unstructured.Unstructured, error) {
	return fm.client.Get(ctx, name, metav1.GetOptions{})
}

func (fm *FolderManager) RemoveFolder(ctx context.Context, name string) error {
	return fm.client.Delete(ctx, name, metav1.DeleteOptions{})
}

// ReplicateTree replicates the folder tree to the repository.
// The function fn is called for each folder.
// If the folder already exists, the function is called with created set to false.
// If the folder is created, the function is called with created set to true.
func (fm *FolderManager) EnsureFolderTreeExists(ctx context.Context, ref, path string, tree FolderTree, fn func(folder Folder, created bool, err error) error) error {
	return tree.Walk(ctx, func(ctx context.Context, folder Folder, parent string) error {
		p := folder.Path
		if path != "" {
			p = safepath.Join(path, p)
		}
		if !safepath.IsDir(p) {
			p = p + "/" // trailing slash indicates folder
		}

		_, err := fm.repo.Read(ctx, p, ref)
		if err != nil && (!errors.Is(err, repository.ErrFileNotFound) && !apierrors.IsNotFound(err)) {
			return fn(folder, false, fmt.Errorf("check if folder exists before writing: %w", err))
		} else if err == nil {
			return fn(folder, false, nil)
		}

		msg := fmt.Sprintf("Add folder %s", p)
		if err := fm.repo.Create(ctx, p, ref, nil, msg); err != nil {
			return fn(folder, true, fmt.Errorf("write folder in repo: %w", err))
		}
		// Add it to the existing tree
		fm.tree.Add(folder, parent)

		return fn(folder, true, nil)
	})
}
