package authz

import (
	"context"
	"errors"
	"fmt"
	"net/http"
	"time"

	"github.com/fullstorydev/grpchan/inprocgrpc"
	grpcAuth "github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/auth"
	"github.com/prometheus/client_golang/prometheus"
	"github.com/prometheus/client_golang/prometheus/promauto"
	"go.opentelemetry.io/otel/trace"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials"
	"google.golang.org/grpc/credentials/insecure"
	"k8s.io/client-go/rest"

	authnlib "github.com/grafana/authlib/authn"
	authzlib "github.com/grafana/authlib/authz"
	authzv1 "github.com/grafana/authlib/authz/proto/v1"
	"github.com/grafana/authlib/cache"
	authlib "github.com/grafana/authlib/types"
	"github.com/grafana/dskit/middleware"

	"github.com/grafana/grafana/pkg/infra/db"
	"github.com/grafana/grafana/pkg/infra/log"
	"github.com/grafana/grafana/pkg/infra/tracing"
	"github.com/grafana/grafana/pkg/registry/apis/iam/legacy"
	"github.com/grafana/grafana/pkg/services/accesscontrol"
	"github.com/grafana/grafana/pkg/services/apiserver"
	"github.com/grafana/grafana/pkg/services/authz/rbac"
	"github.com/grafana/grafana/pkg/services/authz/rbac/store"
	"github.com/grafana/grafana/pkg/services/authz/zanzana"
	zClient "github.com/grafana/grafana/pkg/services/authz/zanzana/client"
	"github.com/grafana/grafana/pkg/services/featuremgmt"
	"github.com/grafana/grafana/pkg/services/grpcserver"
	"github.com/grafana/grafana/pkg/setting"
	"github.com/grafana/grafana/pkg/storage/legacysql"
)

// AuthzServiceAudience is the audience for the authz service.
const AuthzServiceAudience = "authzService"

// ProvideAuthZClient provides an AuthZ client and creates the AuthZ service.
func ProvideAuthZClient(
	cfg *setting.Cfg,
	features featuremgmt.FeatureToggles,
	grpcServer grpcserver.Provider,
	tracer tracing.Tracer,
	reg prometheus.Registerer,
	db db.DB,
	acService accesscontrol.Service,
	zanzanaClient zanzana.Client,
	restConfig apiserver.RestConfigProvider,
) (authlib.AccessClient, error) {
	//nolint:staticcheck // not yet migrated to OpenFeature
	zanzanaEnabled := features.IsEnabledGlobally(featuremgmt.FlagZanzana)

	authCfg, err := readAuthzClientSettings(cfg)
	if err != nil {
		return nil, err
	}

	//nolint:staticcheck // not yet migrated to OpenFeature
	if !features.IsEnabledGlobally(featuremgmt.FlagAuthZGRPCServer) && authCfg.mode == clientModeCloud {
		return nil, errors.New("authZGRPCServer feature toggle is required for cloud and grpc mode")
	}

	//nolint:staticcheck // not yet migrated to OpenFeature
	if zanzanaEnabled && features.IsEnabledGlobally(featuremgmt.FlagZanzanaNoLegacyClient) {
		return zanzanaClient, nil
	}

	// Provisioning uses mode 4 (read+write only to unified storage)
	// For G12 launch, we can disable caching for this and find a more scalable solution soon
	// most likely this would involve passing the RV (timestamp!) in each check method
	//nolint:staticcheck // not yet migrated to OpenFeature
	if features.IsEnabledGlobally(featuremgmt.FlagProvisioning) {
		authCfg.cacheTTL = 0
	}

	switch authCfg.mode {
	case clientModeCloud:
		rbacClient, err := newRemoteRBACClient(authCfg, tracer, reg)
		if zanzanaEnabled {
			return zClient.WithShadowClient(rbacClient, zanzanaClient, reg)
		}
		return rbacClient, err
	default:
		sql := legacysql.NewDatabaseProvider(db)

		rbacSettings := rbac.Settings{CacheTTL: authCfg.cacheTTL}
		if cfg != nil {
			rbacSettings.AnonOrgRole = cfg.Anonymous.OrgRole
		}

		// Register the server
		server := rbac.NewService(
			sql,
			// When running in-proc we get a injection cycle between
			// authz client, resource client and apiserver so we need to use
			// package level function to get rest config
			store.NewAPIFolderStore(tracer, reg, restConfig.GetRestConfig),
			legacy.NewLegacySQLStores(sql),
			store.NewUnionPermissionStore(
				store.NewStaticPermissionStore(acService),
				store.NewSQLPermissionStore(sql, tracer),
			),
			log.New("authz-grpc-server"),
			tracer,
			reg,
			cache.NewLocalCache(cache.Config{Expiry: 5 * time.Minute, CleanupInterval: 10 * time.Minute}),
			rbacSettings,
		)

		channel := &inprocgrpc.Channel{}
		channel.WithServerUnaryInterceptor(grpcAuth.UnaryServerInterceptor(func(ctx context.Context) (context.Context, error) {
			ctx = authlib.WithAuthInfo(ctx, authnlib.NewAccessTokenAuthInfo(authnlib.Claims[authnlib.AccessTokenClaims]{
				Rest: authnlib.AccessTokenClaims{
					Namespace: "*",
				},
			}))
			return ctx, nil
		}))
		authzv1.RegisterAuthzServiceServer(channel, server)
		rbacClient := authzlib.NewClient(
			channel,
			authzlib.WithCacheClientOption(&NoopCache{}),
			authzlib.WithTracerClientOption(tracer),
		)

		if zanzanaEnabled {
			return zClient.WithShadowClient(rbacClient, zanzanaClient, reg)
		}

		return rbacClient, nil
	}
}

// ProvideStandaloneAuthZClient provides a standalone AuthZ client, without registering the AuthZ service.
// You need to provide a remote address in the configuration
func ProvideStandaloneAuthZClient(
	cfg *setting.Cfg, features featuremgmt.FeatureToggles, tracer trace.Tracer, reg prometheus.Registerer,
) (authlib.AccessClient, error) {
	//nolint:staticcheck // not yet migrated to OpenFeature
	if !features.IsEnabledGlobally(featuremgmt.FlagAuthZGRPCServer) {
		return nil, nil
	}

	//nolint:staticcheck // not yet migrated to OpenFeature
	zanzanaEnabled := features.IsEnabledGlobally(featuremgmt.FlagZanzana)

	zanzanaClient, err := ProvideStandaloneZanzanaClient(cfg, features)
	if err != nil {
		return nil, err
	}

	//nolint:staticcheck // not yet migrated to OpenFeature
	if zanzanaEnabled && features.IsEnabledGlobally(featuremgmt.FlagZanzanaNoLegacyClient) {
		return zanzanaClient, nil
	}

	authCfg, err := readAuthzClientSettings(cfg)
	if err != nil {
		return nil, err
	}

	remoteRBACClient, err := newRemoteRBACClient(authCfg, tracer, reg)
	if err != nil {
		return nil, err
	}

	if zanzanaEnabled {
		return zClient.WithShadowClient(remoteRBACClient, zanzanaClient, reg)
	}

	return remoteRBACClient, nil
}

func newRemoteRBACClient(clientCfg *authzClientSettings, tracer trace.Tracer, reg prometheus.Registerer) (authlib.AccessClient, error) {
	tokenClient, err := authnlib.NewTokenExchangeClient(authnlib.TokenExchangeConfig{
		Token:            clientCfg.token,
		TokenExchangeURL: clientCfg.tokenExchangeURL,
	})
	if err != nil {
		return nil, fmt.Errorf("failed to initialize token exchange client: %w", err)
	}

	transportCreds := insecure.NewCredentials()
	if clientCfg.certFile != "" {
		transportCreds, err = credentials.NewClientTLSFromFile(clientCfg.certFile, "")
		if err != nil {
			return nil, fmt.Errorf("failed to load TLS credentials: %w", err)
		}
	}

	authzRequestDuration := promauto.With(reg).NewHistogramVec(prometheus.HistogramOpts{
		Name:                            "authz_server_client_request_duration_seconds",
		Help:                            "Time spent executing requests to authz server.",
		NativeHistogramBucketFactor:     1.1,
		NativeHistogramMaxBucketNumber:  160,
		NativeHistogramMinResetDuration: time.Hour,
	}, []string{"operation", "status_code"})

	unaryInterceptors, streamInterceptors := instrument(authzRequestDuration, middleware.ReportGRPCStatusOption)

	opts := []grpc.DialOption{
		grpc.WithTransportCredentials(transportCreds),
		grpc.WithPerRPCCredentials(
			NewGRPCTokenAuth(AuthzServiceAudience, clientCfg.tokenNamespace, tokenClient),
		),
		grpc.WithChainUnaryInterceptor(unaryInterceptors...),
		grpc.WithChainStreamInterceptor(streamInterceptors...),
	}

	// // if we serve the client as a load balancer
	if clientCfg.loadBalancingEnabled {
		// Use round_robin to balances requests more evenly over the available Grafana replicas.
		opts = append(opts, grpc.WithDefaultServiceConfig(`{"loadBalancingPolicy": "round_robin"}`))

		// Disable looking up service config from TXT DNS records.
		// This reduces the number of requests made to the DNS servers.
		opts = append(opts, grpc.WithDisableServiceConfig())
	}

	conn, err := grpc.NewClient(clientCfg.remoteAddress, opts...)
	if err != nil {
		return nil, fmt.Errorf("failed to create authz client to remote server: %w", err)
	}

	// Client side cache
	var authzCache cache.Cache = &NoopCache{}
	if clientCfg.cacheTTL != 0 {
		authzCache = cache.NewLocalCache(cache.Config{
			Expiry:          clientCfg.cacheTTL,
			CleanupInterval: 2 * time.Minute,
		})
	}

	client := authzlib.NewClient(conn, authzlib.WithCacheClientOption(authzCache), authzlib.WithTracerClientOption(tracer))

	return client, nil
}

func RegisterRBACAuthZService(
	handler grpcserver.Provider,
	db legacysql.LegacyDatabaseProvider,
	tracer tracing.Tracer,
	reg prometheus.Registerer,
	cache cache.Cache,
	exchangeClient authnlib.TokenExchanger,
	cfg RBACServerSettings,
) {
	var folderStore store.FolderStore
	// FIXME: for now we default to using database read proxy for folders if the api url is not configured.
	// we should remove this and the sql implementation once we have verified that is works correctly
	if cfg.Folder.Host == "" {
		folderStore = store.NewSQLFolderStore(db, tracer)
	} else {
		folderStore = store.NewAPIFolderStore(tracer, reg, func(ctx context.Context) (*rest.Config, error) {
			return &rest.Config{
				Host: cfg.Folder.Host,
				WrapTransport: func(rt http.RoundTripper) http.RoundTripper {
					return &tokenExhangeRoundTripper{te: exchangeClient, rt: rt}
				},
				TLSClientConfig: rest.TLSClientConfig{
					Insecure: cfg.Folder.Insecure,
					CAFile:   cfg.Folder.CAFile,
				},
				QPS:   50,
				Burst: 100,
			}, nil
		})
	}

	server := rbac.NewService(
		db,
		folderStore,
		legacy.NewLegacySQLStores(db),
		store.NewSQLPermissionStore(db, tracer),
		log.New("authz-grpc-server"),
		tracer,
		reg,
		cache,
		rbac.Settings{CacheTTL: cfg.CacheTTL}, // anonymous org role can only be set in-proc
	)

	srv := handler.GetServer()
	authzv1.RegisterAuthzServiceServer(srv, server)
}

var _ http.RoundTripper = tokenExhangeRoundTripper{}

type tokenExhangeRoundTripper struct {
	te authnlib.TokenExchanger
	rt http.RoundTripper
}

func (t tokenExhangeRoundTripper) RoundTrip(r *http.Request) (*http.Response, error) {
	res, err := t.te.Exchange(r.Context(), authnlib.TokenExchangeRequest{
		Namespace: "*",
		Audiences: []string{"folder.grafana.app"},
	})

	if err != nil {
		return nil, fmt.Errorf("create access token: %w", err)
	}

	r.Header.Set("X-Access-Token", "Bearer "+res.Token)
	return t.rt.RoundTrip(r)
}

type NoopCache struct{}

func (lc *NoopCache) Get(ctx context.Context, key string) ([]byte, error) {
	return nil, cache.ErrNotFound
}

func (lc *NoopCache) Set(ctx context.Context, key string, data []byte, exp time.Duration) error {
	return nil
}

func (lc *NoopCache) Delete(ctx context.Context, key string) error {
	return nil
}

// instrument is the same as grpcclient.Instrument but without the middleware.ClientUserHeaderInterceptor,
// otgrpc.OpenTracingClientInterceptor, otgrpc.OpenTracingStreamClientInterceptor
// and middleware.StreamClientUserHeaderInterceptor as we don't need them.
func instrument(requestDuration *prometheus.HistogramVec, instrumentationLabelOptions ...middleware.InstrumentationOption) ([]grpc.UnaryClientInterceptor, []grpc.StreamClientInterceptor) {
	return []grpc.UnaryClientInterceptor{
			middleware.UnaryClientInstrumentInterceptor(requestDuration, instrumentationLabelOptions...),
		}, []grpc.StreamClientInterceptor{
			middleware.StreamClientInstrumentInterceptor(requestDuration, instrumentationLabelOptions...),
		}
}
