package inline

import (
	"context"
	"crypto/tls"
	"crypto/x509"
	"fmt"
	"os"

	"github.com/fullstorydev/grpchan"
	authnlib "github.com/grafana/authlib/authn"
	"github.com/grafana/authlib/types"
	"go.opentelemetry.io/otel/trace"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials"
	"google.golang.org/grpc/credentials/insecure"

	inlinev1beta1 "github.com/grafana/grafana/apps/secret/inline/v1beta1"
	secretv1beta1 "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1"
	"github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1"
	"github.com/grafana/grafana/pkg/registry/apis/secret/contracts"
)

type GRPCInlineClient struct {
	conn           *grpc.ClientConn
	tracer         trace.Tracer
	tokenExchanger authnlib.TokenExchanger
}

var _ contracts.InlineSecureValueSupport = &GRPCInlineClient{}

type TLSConfig struct {
	UseTLS             bool
	CAFile             string
	ServerName         string
	InsecureSkipVerify bool
}

func NewGRPCInlineClient(tokenExchanger authnlib.TokenExchanger, tracer trace.Tracer, address string, tlsConfig TLSConfig, clientLoadBalancingEnabled bool) (*GRPCInlineClient, error) {
	var opts []grpc.DialOption
	if tlsConfig.UseTLS {
		creds, err := createTLSCredentials(tlsConfig)
		if err != nil {
			return nil, fmt.Errorf("failed to setup TLS: %w", err)
		}

		opts = append(opts, grpc.WithTransportCredentials(creds))
	} else {
		opts = append(opts, grpc.WithTransportCredentials(insecure.NewCredentials()))
	}

	if clientLoadBalancingEnabled {
		// Use round_robin to balances requests more evenly over the available 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(address, opts...)
	if err != nil {
		return nil, fmt.Errorf("failed to connect to grpc server at %s: %w", address, err)
	}

	return &GRPCInlineClient{
		conn:           conn,
		tracer:         tracer,
		tokenExchanger: tokenExchanger,
	}, nil
}

func createTLSCredentials(config TLSConfig) (credentials.TransportCredentials, error) {
	tlsConfig := &tls.Config{}

	if config.CAFile != "" {
		caCert, err := os.ReadFile(config.CAFile)
		if err != nil {
			return nil, fmt.Errorf("failed to read CA: %w", err)
		}

		caCertPool := x509.NewCertPool()
		if !caCertPool.AppendCertsFromPEM(caCert) {
			return nil, fmt.Errorf("failed to append CA")
		}
		tlsConfig.RootCAs = caCertPool
	}

	if config.ServerName != "" {
		tlsConfig.ServerName = config.ServerName
	}

	if config.InsecureSkipVerify {
		tlsConfig.InsecureSkipVerify = true
	}

	return credentials.NewTLS(tlsConfig), nil
}

// Close will close the underlying gRPC connection. After it is closed, the client cannot be used anymore.
func (g *GRPCInlineClient) Close() error {
	if g.conn != nil {
		return g.conn.Close()
	}
	return nil
}

func (g *GRPCInlineClient) CanReference(ctx context.Context, owner v0alpha1.ObjectReference, names ...string) error {
	client, err := g.getClient(owner.Namespace)
	if err != nil {
		return err
	}

	req := &inlinev1beta1.CanReferenceRequest{
		Owner: &inlinev1beta1.ObjectReference{
			ApiGroup:   owner.APIGroup,
			ApiVersion: owner.APIVersion,
			Kind:       owner.Kind,
			Namespace:  owner.Namespace,
			Name:       owner.Name,
		},
		Names: names,
	}

	_, err = client.CanReference(ctx, req)
	return err
}

func (g *GRPCInlineClient) CreateInline(ctx context.Context, owner v0alpha1.ObjectReference, value v0alpha1.RawSecureValue) (string, error) {
	client, err := g.getClient(owner.Namespace)
	if err != nil {
		return "", err
	}

	if value.IsZero() {
		return "", fmt.Errorf("empty value provided for CreateInline")
	}

	req := &inlinev1beta1.CreateInlineRequest{
		Owner: &inlinev1beta1.ObjectReference{
			ApiGroup:   owner.APIGroup,
			ApiVersion: owner.APIVersion,
			Kind:       owner.Kind,
			Namespace:  owner.Namespace,
			Name:       owner.Name,
		},
		Value: value.DangerouslyExposeAndConsumeValue(),
	}

	resp, err := client.CreateInline(ctx, req)
	if err != nil {
		return "", err
	}

	return resp.GetName(), nil
}

func (g *GRPCInlineClient) DeleteWhenOwnedByResource(ctx context.Context, owner v0alpha1.ObjectReference, names ...string) error {
	client, err := g.getClient(owner.Namespace)
	if err != nil {
		return err
	}

	req := &inlinev1beta1.DeleteWhenOwnedByResourceRequest{
		Owner: &inlinev1beta1.ObjectReference{
			ApiGroup:   owner.APIGroup,
			ApiVersion: owner.APIVersion,
			Kind:       owner.Kind,
			Namespace:  owner.Namespace,
			Name:       owner.Name,
		},
		Names: names,
	}

	_, err = client.DeleteWhenOwnedByResource(ctx, req)
	return err
}

func (g *GRPCInlineClient) getClient(namespace string) (inlinev1beta1.InlineSecureValueServiceClient, error) {
	_, err := types.ParseNamespace(namespace)
	if err != nil {
		return nil, err
	}

	tokenExchangerInterceptor := authnlib.NewGrpcClientInterceptor(
		g.tokenExchanger,
		authnlib.WithClientInterceptorTracer(g.tracer),
		authnlib.WithClientInterceptorNamespace(namespace),
		authnlib.WithClientInterceptorAudience([]string{secretv1beta1.APIGroup}),
	)

	clientConn := grpchan.InterceptClientConn(
		g.conn,
		tokenExchangerInterceptor.UnaryClientInterceptor,
		tokenExchangerInterceptor.StreamClientInterceptor,
	)

	return inlinev1beta1.NewInlineSecureValueServiceClient(clientConn), nil
}
