package resources

import (
	"context"
	"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/apimachinery/pkg/runtime/schema"

	provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1"
	provisioningv0alpha1 "github.com/grafana/grafana/apps/provisioning/pkg/generated/clientset/versioned/typed/provisioning/v0alpha1"
	"github.com/grafana/grafana/apps/provisioning/pkg/repository"
	"github.com/grafana/grafana/apps/provisioning/pkg/safepath"
	"github.com/grafana/grafana/pkg/apimachinery/utils"
)

//go:generate mockery --name RepositoryResourcesFactory --structname MockRepositoryResourcesFactory --inpackage --filename repository_resources_factory_mock.go --with-expecter
type RepositoryResourcesFactory interface {
	Client(ctx context.Context, repo repository.ReaderWriter, opts ...RepositoryResourcesOption) (RepositoryResources, error)
}

//go:generate mockery --name RepositoryResources --structname MockRepositoryResources --inpackage --filename repository_resources_mock.go --with-expecter
type RepositoryResources interface {
	// Folders
	SetTree(tree FolderTree)
	EnsureFolderPathExist(ctx context.Context, filePath, ref string, opts ...EnsurePathOption) (parent string, err error)
	EnsureFolderExists(ctx context.Context, folder Folder, parentID string) error
	EnsureFolderTreeExists(ctx context.Context, ref, path string, tree FolderTree, fn func(folder Folder, created bool, err error) error) error
	RemoveFolderFromTree(folderID string)
	RemoveFolder(ctx context.Context, folderName string) error
	RenameFolderPath(ctx context.Context, previousPath, previousRef, newPath, newRef string, opts ...EnsurePathOption) (string, error)
	// File from Resource
	WriteResourceFileFromObject(ctx context.Context, obj *unstructured.Unstructured, options WriteOptions) (string, error)
	// Resource from file
	WriteResourceFromFile(ctx context.Context, path, ref string) (string, schema.GroupVersionKind, error)
	ReplaceResourceFromFile(ctx context.Context, path, ref string, oldName string, oldGVR schema.GroupVersionResource) (string, schema.GroupVersionKind, error)
	ReplaceResourceFromFileByRef(ctx context.Context, path, ref, previousRef string) (string, schema.GroupVersionKind, error)
	RemoveResourceFromFile(ctx context.Context, path, ref string) (string, string, schema.GroupVersionKind, error)
	FindResourcePath(ctx context.Context, name string, gvk schema.GroupVersionKind) (string, error)
	RenameResourceFile(ctx context.Context, path, previousRef, newPath, newRef string, folderOpts ...EnsurePathOption) (string, string, schema.GroupVersionKind, error)
	// Stats
	Stats(ctx context.Context) (*provisioning.ResourceStats, error)
	List(ctx context.Context) (*provisioning.ResourceList, error)
}

type repositoryResourcesFactory struct {
	parsers               ParserFactory
	clients               ClientFactory
	lister                ResourceLister
	folderMetadataEnabled bool
	folderAPIVersion      string
}

type RepositoryResourcesOption func(*repositoryResourcesOptions)

type repositoryResourcesOptions struct {
	folderManagerOptions []FolderManagerOption
}

func WithFolderManagerOptions(opts ...FolderManagerOption) RepositoryResourcesOption {
	return func(cfg *repositoryResourcesOptions) {
		cfg.folderManagerOptions = append(cfg.folderManagerOptions, opts...)
	}
}

type repositoryResources struct {
	*FolderManager
	*ResourcesManager
	lister    ResourceLister
	namespace string
	repoName  string
}

func (r *repositoryResources) Stats(ctx context.Context) (*provisioning.ResourceStats, error) {
	return r.lister.Stats(ctx, r.namespace, r.repoName)
}

func (r *repositoryResources) List(ctx context.Context) (*provisioning.ResourceList, error) {
	return r.lister.List(ctx, r.namespace, r.repoName)
}

// FindResourcePath finds the repository file path for a resource by its name and GroupVersionKind
func (r *repositoryResources) FindResourcePath(ctx context.Context, name string, gvk schema.GroupVersionKind) (string, error) {
	// Use ForKind to get the dynamic client for this resource type
	client, gvr, err := r.clients.ForKind(ctx, gvk)
	if err != nil {
		return "", fmt.Errorf("get client for kind %s: %w", gvk.Kind, err)
	}

	// Get the specific resource by name using the dynamic client (already namespaced)
	obj, err := client.Get(ctx, name, metav1.GetOptions{})
	if err != nil {
		if apierrors.IsNotFound(err) {
			return "", fmt.Errorf("resource not found: %s/%s/%s", gvr.Group, gvr.Resource, name)
		}
		return "", fmt.Errorf("failed to get resource %s/%s/%s: %w", gvr.Group, gvr.Resource, name, err)
	}

	// Extract the source path from annotations
	annotations := obj.GetAnnotations()
	if annotations == nil {
		return "", fmt.Errorf("resource %s/%s/%s has no annotations", gvr.Group, gvr.Resource, name)
	}

	sourcePath, exists := annotations[utils.AnnoKeySourcePath]
	if !exists || sourcePath == "" {
		return "", fmt.Errorf("resource %s/%s/%s has no source path annotation", gvr.Group, gvr.Resource, name)
	}

	// For folder resources, ensure the path has a trailing slash for proper deletion
	if gvk.Kind == "Folder" && !safepath.IsDir(sourcePath) {
		sourcePath = sourcePath + "/"
	}

	return sourcePath, nil
}

func NewRepositoryResourcesFactory(parsers ParserFactory, clients ClientFactory, lister ResourceLister, folderMetadataEnabled bool, folderAPIVersion string) RepositoryResourcesFactory {
	return &repositoryResourcesFactory{
		parsers:               parsers,
		clients:               clients,
		lister:                lister,
		folderMetadataEnabled: folderMetadataEnabled,
		folderAPIVersion:      folderAPIVersion,
	}
}

func (r *repositoryResourcesFactory) Client(ctx context.Context, repo repository.ReaderWriter, opts ...RepositoryResourcesOption) (RepositoryResources, error) {
	clients, err := r.clients.Clients(ctx, repo.Config().Namespace)
	if err != nil {
		return nil, fmt.Errorf("create clients: %w", err)
	}

	folderClient, folderGVK, err := clients.Folder(ctx, r.folderAPIVersion)
	if err != nil {
		return nil, fmt.Errorf("create folder client: %w", err)
	}
	parser, err := r.parsers.GetParser(ctx, repo)
	if err != nil {
		return nil, fmt.Errorf("create parser: %w", err)
	}

	cfg := &repositoryResourcesOptions{}
	for _, opt := range opts {
		opt(cfg)
	}

	folderManagerOpts := append(cfg.folderManagerOptions, WithFolderMetadataEnabled(r.folderMetadataEnabled))
	folders := NewFolderManager(repo, folderClient, NewEmptyFolderTree(), folderGVK, folderManagerOpts...)
	resources := NewResourcesManager(repo, folders, parser, clients)

	return &repositoryResources{
		FolderManager:    folders,
		ResourcesManager: resources,
		lister:           r.lister,
		namespace:        repo.Config().Namespace,
		repoName:         repo.Config().Name,
	}, nil
}

type RepositoryGetter struct {
	factory repository.Factory
	client  provisioningv0alpha1.ProvisioningV0alpha1Interface
}

func NewRepositoryGetter(
	factory repository.Factory,
	client provisioningv0alpha1.ProvisioningV0alpha1Interface,
) *RepositoryGetter {
	return &RepositoryGetter{
		factory: factory,
		client:  client,
	}
}

func (r *RepositoryGetter) GetRepository(ctx context.Context, namespace, repoName string) (repository.Repository, error) {
	repo, err := r.client.Repositories(namespace).Get(ctx, repoName, metav1.GetOptions{})
	if err != nil {
		return nil, fmt.Errorf("get repository %q: %w", repoName, err)
	}

	return r.factory.Build(ctx, repo)
}
