package auth

import (
	"context"
	"crypto/tls"
	"crypto/x509"
	"net/url"

	"git.perx.ru/perxis/perxis-go/pkg/errors"
	kitgrpc "github.com/go-kit/kit/transport/grpc"
	"golang.org/x/oauth2/clientcredentials"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials"
	"google.golang.org/grpc/credentials/oauth"
	"google.golang.org/grpc/metadata"
)

const (
	OAuth2IdentityMetadata = "x-perxis-identity"
	TLSIdentityMetadata    = "x-forwarded-client-cert"
	AccessMetadata         = "x-perxis-access"

	AuthorizationMetadata = "authorization"
)

func GRPCToContext(factory *PrincipalFactory) kitgrpc.ServerRequestFunc {
	return func(ctx context.Context, md metadata.MD) context.Context {
		if identity := md.Get(TLSIdentityMetadata); len(identity) > 0 {
			return WithPrincipal(ctx, factory.Principal(identity[0]))
		}

		if identity := md.Get(OAuth2IdentityMetadata); len(identity) > 0 {
			return WithPrincipal(ctx, factory.Principal(identity[0]))
		}

		if identity := md.Get(AuthorizationMetadata); len(identity) > 0 {
			return WithPrincipal(ctx, factory.Principal(identity[0]))
		}

		if access := md.Get(AccessMetadata); len(access) > 0 {
			return WithPrincipal(ctx, factory.System())
		}

		return WithPrincipal(ctx, factory.Anonymous())
	}
}

func ContextToGRPC() kitgrpc.ClientRequestFunc {
	return func(ctx context.Context, md *metadata.MD) context.Context {
		p := GetPrincipal(ctx)

		switch p := p.(type) {
		case *UserPrincipal:
			if p.GetIdentity(ctx) != "" {
				(*md)[OAuth2IdentityMetadata] = []string{p.GetIdentity(ctx)}
			}
		case *ClientPrincipal:
			if ident := p.GetIdentity(ctx); ident != nil {
				switch {
				case ident.OAuthClientID != "":
					(*md)[OAuth2IdentityMetadata] = []string{ident.OAuthClientID + "@clients"}
				case ident.TLSSubject != "":
					(*md)[TLSIdentityMetadata] = []string{ident.TLSSubject}
				case ident.APIKey != "":
					(*md)[AuthorizationMetadata] = []string{"API-Key " + ident.APIKey}

				}
			}
		case *SystemPrincipal:
			(*md)[AccessMetadata] = []string{p.GetID(ctx)}
		}

		return ctx
	}
}

// PrincipalServerInterceptor - grpc-интерсептор, который используется для получения данных принципала из grpc-метаданы и добавления в контекст ”. В случае, если
// сервис не использует проверку прав 'Principal' к системе, в параметрах передается пустой объект '&PrincipalFactory{}'
func PrincipalServerInterceptor(factory *PrincipalFactory) grpc.UnaryServerInterceptor {
	return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
		if md, ok := metadata.FromIncomingContext(ctx); ok {
			ctx = GRPCToContext(factory)(ctx, md)
		}
		return handler(ctx, req)
	}
}

// PrincipalClientInterceptor - grpc-интерсептор, который используется для получения данных принципала. В случае, если
// сервис не использует проверку прав 'Principal' к системе, в параметрах передается пустой объект '&PrincipalFactory{}'
func PrincipalClientInterceptor() grpc.UnaryClientInterceptor {
	return func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
		md, ok := metadata.FromOutgoingContext(ctx)
		if !ok {
			md = metadata.MD{}
		}
		ctx = metadata.NewOutgoingContext(ContextToGRPC()(ctx, &md), md)
		return invoker(ctx, method, req, reply, cc, opts...)
	}
}

func AddAccessInterceptor(id string) grpc.UnaryClientInterceptor {
	return func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
		ctx = metadata.AppendToOutgoingContext(ctx, AccessMetadata, id)
		return invoker(ctx, method, req, reply, cc, opts...)
	}
}

func AddAuthorizationInterceptor(auth string) grpc.UnaryClientInterceptor {
	return func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
		ctx = metadata.AppendToOutgoingContext(ctx, "authorization", auth)
		return invoker(ctx, method, req, reply, cc, opts...)
	}
}

// AddAPIKeyInterceptor возвращает опции для создания grpc-соединения с передачей API-ключа при каждом запросе
func AddAPIKeyInterceptor(key string) grpc.UnaryClientInterceptor {
	return AddAuthorizationInterceptor("API-Key " + key)
}

// WithOAuth2Credentials возвращает опции для создания grpc-соединения с аутентификацией oauth2. Переданный контекст
// будет использован для каждого запроса токена, поэтому он не может быть с таймаутом.
func WithOAuth2Credentials(ctx context.Context, tokenURL, clientID, clientSecret, audience string) grpc.DialOption {
	conf := &clientcredentials.Config{
		TokenURL:       tokenURL,
		ClientID:       clientID,
		ClientSecret:   clientSecret,
		EndpointParams: url.Values{"audience": {audience}},
	}
	return grpc.WithPerRPCCredentials(oauth.TokenSource{TokenSource: conf.TokenSource(ctx)})
}

// WithTLSCredentials возвращает опции для создания grpc-соединения с TLS-сертификатами
func WithTLSCredentials(ctx context.Context, cert, cacert, key []byte) (grpc.DialOption, error) {
	certPool := x509.NewCertPool()
	if !certPool.AppendCertsFromPEM(cacert) {
		return nil, errors.New("CA certificate not loaded")
	}
	clientCert, err := tls.X509KeyPair(cert, key)
	if err != nil {
		return nil, err
	}
	return grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{Certificates: []tls.Certificate{clientCert}, RootCAs: certPool})), nil
}