package resourcepermission

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"iter"
	"sync"

	apierrors "k8s.io/apimachinery/pkg/api/errors"
	"k8s.io/apimachinery/pkg/runtime/schema"
	"k8s.io/apiserver/pkg/endpoints/request"

	"github.com/grafana/authlib/types"

	"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1"
	"github.com/grafana/grafana/pkg/infra/log"
	"github.com/grafana/grafana/pkg/registry/apis/iam/common"
	idStore "github.com/grafana/grafana/pkg/registry/apis/iam/legacy"
	"github.com/grafana/grafana/pkg/services/sqlstore/session"
	"github.com/grafana/grafana/pkg/storage/legacysql"
	"github.com/grafana/grafana/pkg/storage/unified/resource"
	"github.com/grafana/grafana/pkg/storage/unified/resourcepb"
)

var (
	_ resource.StorageBackend = &ResourcePermSqlBackend{}
)

type ResourcePermSqlBackend struct {
	dbProvider    legacysql.LegacyDatabaseProvider
	identityStore IdentityStore
	logger        log.Logger

	mappers        map[schema.GroupResource]Mapper // group/resource -> rbac mapper
	reverseMappers map[string]schema.GroupResource // rbac kind -> group/resource

	subscribers []chan *resource.WrittenEvent
	mutex       sync.Mutex
}

func ProvideStorageBackend(dbProvider legacysql.LegacyDatabaseProvider) *ResourcePermSqlBackend {
	return &ResourcePermSqlBackend{
		dbProvider:    dbProvider,
		identityStore: idStore.NewLegacySQLStores(dbProvider),
		logger:        log.New("resourceperm_storage_backend"),

		mappers: map[schema.GroupResource]Mapper{
			{Group: "folder.grafana.app", Resource: "folders"}:       NewMapper("folders", defaultLevels),
			{Group: "dashboard.grafana.app", Resource: "dashboards"}: NewMapper("dashboards", defaultLevels),
		},
		reverseMappers: map[string]schema.GroupResource{
			"folders":    {Group: "folder.grafana.app", Resource: "folders"},
			"dashboards": {Group: "dashboard.grafana.app", Resource: "dashboards"},
		},

		subscribers: make([]chan *resource.WrittenEvent, 0),
		mutex:       sync.Mutex{},
	}
}

func (s *ResourcePermSqlBackend) GetResourceStats(ctx context.Context, nsr resource.NamespacedResource, minCount int) ([]resource.ResourceStats, error) {
	return []resource.ResourceStats{}, errNotImplemented
}

func (s *ResourcePermSqlBackend) ListHistory(context.Context, *resourcepb.ListRequest, func(resource.ListIterator) error) (int64, error) {
	return 0, errNotImplemented
}

func (s *ResourcePermSqlBackend) ListIterator(ctx context.Context, req *resourcepb.ListRequest, callback func(resource.ListIterator) error) (int64, error) {
	opts := req.Options
	if opts == nil || opts.Key == nil || opts.Key.Namespace == "" {
		return 0, apierrors.NewBadRequest("list requires a valid namespace")
	}
	ns, err := types.ParseNamespace(opts.Key.Namespace)
	if err != nil {
		return 0, err
	}
	if ns.OrgID <= 0 {
		return 0, apierrors.NewBadRequest(errInvalidNamespace.Error())
	}

	if req.ResourceVersion != 0 {
		return 0, apierrors.NewBadRequest("list with explicit resourceVersion is not supported by this storage backend")
	}

	limit := req.Limit
	if limit <= 0 {
		limit = 50
	}

	token, err := readContinueToken(req.NextPageToken)
	if err != nil {
		return 0, apierrors.NewBadRequest(fmt.Sprintf("invalid continue token: %v", err))
	}

	pagination := &common.Pagination{
		Limit:    limit + 1, // fetch one more to determine if there is a next page
		Continue: token.offset,
	}

	ctx = request.WithNamespace(ctx, ns.Value)
	dbHelper, err := s.dbProvider(ctx)
	if err != nil {
		logger := s.logger.FromContext(ctx)
		logger.Error("Failed to get database helper", "error", err)
		return 0, apierrors.NewInternalError(errDatabaseHelper)
	}

	iterator, err := s.newRoleIterator(ctx, dbHelper, ns, pagination)
	if iterator != nil {
		defer func() {
			_ = iterator.Close()
		}()
	}
	if err != nil {
		return 0, apierrors.NewInternalError(err)
	}

	err = callback(iterator)
	if err != nil {
		return 0, apierrors.NewInternalError(err)
	}

	return s.latestUpdate(ctx, dbHelper, ns), nil
}

func (s *ResourcePermSqlBackend) ListModifiedSince(ctx context.Context, key resource.NamespacedResource, sinceRv int64) (int64, iter.Seq2[*resource.ModifiedResource, error]) {
	return 0, func(yield func(*resource.ModifiedResource, error) bool) {
		yield(nil, errNotImplemented)
	}
}

func (s *ResourcePermSqlBackend) ReadResource(ctx context.Context, req *resourcepb.ReadRequest) *resource.BackendReadResponse {
	rsp := &resource.BackendReadResponse{Key: req.GetKey()}

	ns, err := types.ParseNamespace(req.Key.Namespace)
	if err != nil {
		rsp.Error = resource.AsErrorResult(apierrors.NewBadRequest(err.Error()))
		return rsp
	}
	if ns.OrgID <= 0 {
		rsp.Error = resource.AsErrorResult(apierrors.NewBadRequest(errInvalidNamespace.Error()))
		return rsp
	}

	if req.ResourceVersion > 0 {
		rsp.Error = resource.AsErrorResult(apierrors.NewBadRequest("resourceVersion is not supported"))
		return rsp
	}

	ctx = request.WithNamespace(ctx, ns.Value)
	dbHelper, err := s.dbProvider(ctx)
	if err != nil {
		// Hide the error from the user, but log it
		logger := s.logger.FromContext(ctx)
		logger.Error("Failed to get database helper", "error", err)
		return rsp
	}

	var resourcePermission *v0alpha1.ResourcePermission
	err = dbHelper.DB.GetSqlxSession().WithTransaction(ctx, func(tx *session.SessionTx) error {
		resourcePermission, err = s.getResourcePermission(ctx, dbHelper, tx, ns, req.Key.Name)
		return err
	})

	if err != nil {
		rsp.Error = resource.AsErrorResult(err)
		return rsp
	}

	rsp.ResourceVersion = resourcePermission.GetUpdateTimestamp().UnixMilli()
	resourcePermission.Namespace = ns.Value // ensure namespace is set, this is required when existing and new resources are compared for updates
	rsp.Value, err = json.Marshal(resourcePermission)
	if err != nil {
		rsp.Error = resource.AsErrorResult(err)
		return rsp
	}

	return rsp
}

func (s *ResourcePermSqlBackend) WatchWriteEvents(ctx context.Context) (<-chan *resource.WrittenEvent, error) {
	stream := make(chan *resource.WrittenEvent, 10)
	return stream, nil
}

func isValidKey(key *resourcepb.ResourceKey, requireName bool) error {
	gr := v0alpha1.ResourcePermissionInfo.GroupResource()
	if key.Group != gr.Group {
		return fmt.Errorf("expecting group (%s != %s)", key.Group, gr.Group)
	}
	if key.Resource != gr.Resource {
		return fmt.Errorf("expecting resource (%s != %s)", key.Resource, gr.Resource)
	}
	if requireName && key.Name == "" {
		return fmt.Errorf("expecting name (uid): %w", errInvalidName)
	}
	return nil
}

func getResourcePermissionFromEvent(event resource.WriteEvent) (*v0alpha1.ResourcePermission, error) {
	obj, ok := event.Object.GetRuntimeObject()
	if ok && obj != nil {
		resourcePermission, ok := obj.(*v0alpha1.ResourcePermission)
		if ok {
			return resourcePermission, nil
		}
	}
	resourcePermission := &v0alpha1.ResourcePermission{}
	err := json.Unmarshal(event.Value, resourcePermission)
	return resourcePermission, err
}

func (s *ResourcePermSqlBackend) WriteEvent(ctx context.Context, event resource.WriteEvent) (rv int64, err error) {
	ns, err := types.ParseNamespace(event.Key.Namespace)
	if err != nil {
		return 0, err
	}
	if ns.OrgID <= 0 {
		return 0, apierrors.NewBadRequest("write requires a valid namespace")
	}

	if err := isValidKey(event.Key, true); err != nil {
		return 0, apierrors.NewBadRequest(fmt.Sprintf("invalid key %q: %v", event.Key, err.Error()))
	}

	ctx = request.WithNamespace(ctx, ns.Value)
	dbHelper, err := s.dbProvider(ctx)
	if err != nil {
		// Hide the error from the user, but log it
		logger := s.logger.FromContext(ctx)
		logger.Error("Failed to get database helper", "error", err)
		return 0, errDatabaseHelper
	}

	grn, err := splitResourceName(event.Key.Name)
	if err != nil {
		return 0, apierrors.NewBadRequest(fmt.Sprintf("invalid resource name %q: %v", event.Key.Name, err.Error()))
	}

	mapper, err := s.getResourceMapper(grn.Group, grn.Resource)
	if err != nil {
		return 0, apierrors.NewBadRequest(fmt.Sprintf("invalid group/resource in resource name %q: %v", event.Key.Name, err.Error()))
	}

	if grn.Name == "" {
		return 0, fmt.Errorf("resource name cannot be empty: %w", errInvalidName)
	}

	switch event.Type {
	case resourcepb.WatchEvent_DELETED:
		err = s.deleteResourcePermission(ctx, dbHelper, ns, event.Key.Name)
	case resourcepb.WatchEvent_ADDED, resourcepb.WatchEvent_MODIFIED:
		{
			var v0resourceperm *v0alpha1.ResourcePermission
			v0resourceperm, err = getResourcePermissionFromEvent(event)
			if err != nil {
				return 0, apierrors.NewBadRequest(fmt.Sprintf("invalid resource permission in event: %v", err))
			}

			if v0resourceperm.Name != event.Key.Name {
				return 0, apierrors.NewBadRequest(
					fmt.Sprintf("resource permission name %q != %q: %v", event.Key.Name, v0resourceperm.Name, errNameMismatch.Error()),
				)
			}
			if v0resourceperm.Namespace != ns.Value {
				return 0, apierrors.NewBadRequest(
					fmt.Sprintf("namespace %q != %q: %v", ns.Value, v0resourceperm.Namespace, errNamespaceMismatch.Error()),
				)
			}

			if event.Type == resourcepb.WatchEvent_ADDED {
				rv, err = s.createResourcePermission(ctx, dbHelper, ns, mapper, grn, v0resourceperm)
				if err != nil && errors.Is(err, errConflict) {
					return 0, apierrors.NewConflict(v0alpha1.ResourcePermissionInfo.GroupResource(), event.Key.Name, err)
				}
			} else {
				rv, err = s.updateResourcePermission(ctx, dbHelper, ns, mapper, grn, v0resourceperm)
				if errors.Is(err, errNotFound) {
					return 0, apierrors.NewNotFound(v0alpha1.ResourcePermissionInfo.GroupResource(), event.Key.Name)
				}
			}

			if err != nil {
				if errors.Is(err, errInvalidSpec) || errors.Is(err, errInvalidName) {
					return 0, apierrors.NewBadRequest(err.Error())
				}
				return 0, err
			}
		}
	default:
		return 0, apierrors.NewMethodNotSupported(v0alpha1.ResourcePermissionInfo.GroupResource(), event.Type.String())
	}

	return rv, err
}

func (s *ResourcePermSqlBackend) GetResourceLastImportTimes(ctx context.Context) iter.Seq2[resource.ResourceLastImportTime, error] {
	return func(yield func(resource.ResourceLastImportTime, error) bool) {
		yield(resource.ResourceLastImportTime{}, errNotImplemented)
	}
}
