package dualwrite

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

	"github.com/prometheus/client_golang/prometheus"
	"github.com/prometheus/client_golang/prometheus/promauto"
	"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/accesscontrol"
	"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")
var reconcilerLogger = log.New("zanzana.reconciler")

// 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
	metrics  struct {
		lastSuccess prometheus.Gauge
	}
	// 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, reg prometheus.Registerer) *ZanzanaReconciler {
	pageSize := cfg.ZanzanaServer.ReadPageSize

	zanzanaReconciler := &ZanzanaReconciler{
		cfg:      cfg,
		log:      reconcilerLogger,
		features: features,
		client:   client,
		lock:     lock,
		store:    store,
		reconcilers: []resourceReconciler{
			newResourceReconciler(
				"team memberships",
				teamMembershipCollector(store),
				zanzanaCollector([]string{zanzana.RelationTeamMember, zanzana.RelationTeamAdmin}, pageSize),
				client,
			),
			newResourceReconciler(
				"folder tree",
				folderTreeCollector(folderService),
				zanzanaCollector([]string{zanzana.RelationParent}, pageSize),
				client,
			),
			newResourceReconciler(
				"managed folder permissions",
				managedPermissionsCollector(store, zanzana.KindFolders),
				zanzanaCollector(zanzana.RelationsFolder, pageSize),
				client,
			),
			newResourceReconciler(
				"managed dashboard permissions",
				managedPermissionsCollector(store, zanzana.KindDashboards),
				zanzanaCollector(zanzana.RelationsResouce, pageSize),
				client,
			),
			newResourceReconciler(
				"role permissions",
				rolePermissionsCollector(store),
				zanzanaCollector(zanzana.RelationsFolder, pageSize),
				client,
			),
			newResourceReconciler(
				"basic role bindings",
				basicRoleBindingsCollector(store),
				zanzanaCollector([]string{zanzana.RelationAssignee}, pageSize),
				client,
			),
			newResourceReconciler(
				"role bindings",
				roleBindingsCollector(store),
				zanzanaCollector([]string{zanzana.RelationAssignee}, pageSize),
				client,
			),
		},
	}

	if reg != nil {
		zanzanaReconciler.metrics.lastSuccess = promauto.With(reg).NewGauge(prometheus.GaugeOpts{
			Name: "grafana_zanzana_reconcile_last_success_timestamp_seconds",
			Help: "Unix timestamp (seconds) when the Zanzana reconciler last completed a reconciliation cycle.",
		})
	}

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

	return zanzanaReconciler
}

// Run implements registry.BackgroundService
func (r *ZanzanaReconciler) Run(ctx context.Context) error {
	//nolint:staticcheck // not yet migrated to OpenFeature
	if !r.features.IsEnabledGlobally(featuremgmt.FlagZanzana) {
		return nil
	}

	if r.cfg.ZanzanaReconciler.Mode != setting.ZanzanaReconcilerModeLegacy {
		r.log.Info("Legacy RBAC reconciler is disabled",
			"mode", r.cfg.ZanzanaReconciler.Mode)
		return nil
	}

	return r.Reconcile(ctx)
}

// Reconcile schedules as job that will run and reconcile resources between
// legacy access control and zanzana.
func (r *ZanzanaReconciler) Reconcile(ctx context.Context) error {
	// Ensure we don't reconcile an empty/partial RBAC state before OSS has seeded basic role permissions.
	// This matters most during startup where fixed-role loading + basic-role permission refresh runs as another background service.
	r.waitForBasicRolesSeeded(ctx)
	r.reconcile(ctx)

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

func (r *ZanzanaReconciler) hasBasicRolePermissions(ctx context.Context) bool {
	var count int64
	// Basic role permissions are stored on "basic:%" roles in the global org (0).
	// In a fresh DB, this will be empty until fixed roles are registered and the basic role permission refresh runs.
	type row struct {
		Count int64 `xorm:"count"`
	}
	_ = r.store.WithDbSession(ctx, func(sess *db.Session) error {
		var rr row
		_, err := sess.SQL(
			`SELECT COUNT(*) AS count
			 FROM role INNER JOIN permission AS p ON p.role_id = role.id
			 WHERE role.org_id = ? AND role.name LIKE ?`,
			accesscontrol.GlobalOrgID,
			accesscontrol.BasicRolePrefix+"%",
		).Get(&rr)
		if err != nil {
			return err
		}
		count = rr.Count
		return nil
	})
	return count > 0
}

func (r *ZanzanaReconciler) waitForBasicRolesSeeded(ctx context.Context) {
	// Best-effort: don't block forever. If we can't observe basic roles, proceed anyway.
	const (
		maxWait  = 15 * time.Second
		interval = 1 * time.Second
	)

	deadline := time.NewTimer(maxWait)
	defer deadline.Stop()
	ticker := time.NewTicker(interval)
	defer ticker.Stop()

	for {
		if r.hasBasicRolePermissions(ctx) {
			return
		}
		select {
		case <-ctx.Done():
			return
		case <-deadline.C:
			return
		case <-ticker.C:
		}
	}
}

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

		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)
				ok = false
			}
		}
		r.log.Debug("Finished reconciliation", "elapsed", time.Since(now))
		return ok
	}

	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 {
		allOK := true
		for _, ns := range namespaces {
			if !run(ctx, ns) {
				allOK = false
			}
		}
		if r.metrics.lastSuccess != nil && allOK {
			r.metrics.lastSuccess.SetToCurrentTime()
		}
		return
	}

	// We ignore the error for now
	err := r.lock.LockExecuteAndRelease(ctx, "zanzana-reconciliation", 10*time.Hour, func(ctx context.Context) {
		allOK := true
		for _, ns := range namespaces {
			if !run(ctx, ns) {
				allOK = false
			}
		}
		if r.metrics.lastSuccess != nil && allOK {
			r.metrics.lastSuccess.SetToCurrentTime()
		}
	})
	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
}
