package dualwrite

import (
	"context"
	"fmt"
	"strconv"
	"time"

	"go.opentelemetry.io/otel"

	claims "github.com/grafana/authlib/types"

	"github.com/grafana/grafana/pkg/infra/db"
	"github.com/grafana/grafana/pkg/infra/log"
	"github.com/grafana/grafana/pkg/infra/serverlock"
	"github.com/grafana/grafana/pkg/services/authz/zanzana"
	"github.com/grafana/grafana/pkg/services/featuremgmt"
	"github.com/grafana/grafana/pkg/services/folder"
	"github.com/grafana/grafana/pkg/services/org"
	"github.com/grafana/grafana/pkg/setting"
)

var tracer = otel.Tracer("github.com/grafana/grafana/pkg/accesscontrol/migrator")

// ZanzanaReconciler is a component to reconcile RBAC permissions to zanzana.
// We should rewrite the migration after we have "migrated" all possible actions
// into our schema.
type ZanzanaReconciler struct {
	cfg      *setting.Cfg
	log      log.Logger
	features featuremgmt.FeatureToggles
	store    db.DB
	client   zanzana.Client
	lock     *serverlock.ServerLockService
	// reconcilers are migrations that tries to reconcile the state of grafana db to zanzana store.
	// These are run periodically to try to maintain a consistent state.
	reconcilers []resourceReconciler
}

func ProvideZanzanaReconciler(cfg *setting.Cfg, features featuremgmt.FeatureToggles, client zanzana.Client, store db.DB, lock *serverlock.ServerLockService, folderService folder.Service) *ZanzanaReconciler {
	zanzanaReconciler := &ZanzanaReconciler{
		cfg:      cfg,
		log:      log.New("zanzana.reconciler"),
		features: features,
		client:   client,
		lock:     lock,
		store:    store,
		reconcilers: []resourceReconciler{
			newResourceReconciler(
				"team memberships",
				teamMembershipCollector(store),
				zanzanaCollector([]string{zanzana.RelationTeamMember, zanzana.RelationTeamAdmin}),
				client,
			),
			newResourceReconciler(
				"folder tree",
				folderTreeCollector(folderService),
				zanzanaCollector([]string{zanzana.RelationParent}),
				client,
			),
			newResourceReconciler(
				"managed folder permissions",
				managedPermissionsCollector(store, zanzana.KindFolders),
				zanzanaCollector(zanzana.RelationsFolder),
				client,
			),
			newResourceReconciler(
				"managed dashboard permissions",
				managedPermissionsCollector(store, zanzana.KindDashboards),
				zanzanaCollector(zanzana.RelationsResouce),
				client,
			),
			newResourceReconciler(
				"role permissions",
				rolePermissionsCollector(store),
				zanzanaCollector(zanzana.RelationsFolder),
				client,
			),
			newResourceReconciler(
				"basic role bindings",
				basicRoleBindingsCollector(store),
				zanzanaCollector([]string{zanzana.RelationAssignee}),
				client,
			),
			newResourceReconciler(
				"team role bindings",
				teamRoleBindingsCollector(store),
				zanzanaCollector([]string{zanzana.RelationAssignee}),
				client,
			),
			newResourceReconciler(
				"user role bindings",
				userRoleBindingsCollector(store),
				zanzanaCollector([]string{zanzana.RelationAssignee}),
				client,
			),
			newResourceReconciler(
				"fixed role pemissions",
				fixedRolePermissionsCollector(store),
				zanzanaCollector(zanzana.RelationsFolder),
				client,
			),
		},
	}

	if cfg.Anonymous.Enabled {
		zanzanaReconciler.reconcilers = append(zanzanaReconciler.reconcilers,
			newResourceReconciler(
				"anonymous role binding",
				anonymousRoleBindingsCollector(cfg, store),
				zanzanaCollector([]string{zanzana.RelationAssignee}),
				client,
			),
		)
	}

	return zanzanaReconciler
}

// Run implements registry.BackgroundService
func (r *ZanzanaReconciler) Run(ctx context.Context) error {
	if r.features.IsEnabledGlobally(featuremgmt.FlagZanzana) {
		return r.Reconcile(ctx)
	}
	return nil
}

// Reconcile schedules as job that will run and reconcile resources between
// legacy access control and zanzana.
func (r *ZanzanaReconciler) Reconcile(ctx context.Context) error {
	r.reconcile(ctx)

	// FIXME:
	// We should be a bit graceful about reconciliations so we are not hammering dbs
	ticker := time.NewTicker(r.cfg.RBAC.ZanzanaReconciliationInterval)
	for {
		select {
		case <-ticker.C:
			r.reconcile(ctx)
		case <-ctx.Done():
			return ctx.Err()
		}
	}
}

func (r *ZanzanaReconciler) reconcile(ctx context.Context) {
	run := func(ctx context.Context, namespace string) {
		now := time.Now()
		r.log.Debug("Started reconciliation")

		for _, reconciler := range r.reconcilers {
			r.log.Debug("Performing zanzana reconciliation", "reconciler", reconciler.name)
			if err := reconciler.reconcile(ctx, namespace); err != nil {
				r.log.Warn("Failed to perform reconciliation for resource", "err", err)
			}
		}
		r.log.Debug("Finished reconciliation", "elapsed", time.Since(now))
	}

	var namespaces []string
	if r.cfg.StackID != "" {
		id, err := strconv.ParseInt(r.cfg.StackID, 10, 64)
		if err != nil {
			r.log.Error("cannot perform reconciliation, malformed stack id", "id", r.cfg.StackID, "err", err)
			return
		}

		namespaces = []string{claims.CloudNamespaceFormatter(id)}
	} else {
		ids, err := r.getOrgs(ctx)
		if err != nil {
			r.log.Error("cannot perform reconciliation, failed to fetch orgs", "err", err)
			return
		}

		for _, id := range ids {
			namespaces = append(namespaces, claims.OrgNamespaceFormatter(id))
		}
	}

	if r.lock == nil {
		for _, ns := range namespaces {
			run(ctx, ns)
		}
		return
	}

	// We ignore the error for now
	err := r.lock.LockExecuteAndRelease(ctx, "zanzana-reconciliation", 10*time.Hour, func(ctx context.Context) {
		for _, ns := range namespaces {
			run(ctx, ns)
		}
	})
	if err != nil {
		r.log.Error("Error performing zanzana reconciliation", "error", err)
	}
}

func (r *ZanzanaReconciler) getOrgs(ctx context.Context) ([]int64, error) {
	orgs := make([]int64, 0)
	err := r.store.WithDbSession(ctx, func(sess *db.Session) error {
		q := "SELECT id FROM org"
		if err := sess.SQL(q).Find(&orgs); err != nil {
			return err
		}
		return nil
	})
	if err != nil {
		return nil, err
	}
	return orgs, nil
}

func getOrgByName(ctx context.Context, store db.DB, name string) (*org.Org, error) {
	var orga org.Org
	err := store.WithDbSession(ctx, func(dbSession *db.Session) error {
		exists, err := dbSession.Where("name=?", name).Get(&orga)
		if err != nil {
			return err
		}
		if !exists {
			return fmt.Errorf("org does not exist: %s", name)
		}
		return nil
	})

	if err != nil {
		return nil, err
	}
	return &orga, nil
}
