// Copyright (c) All respective contributors to the Peridot Project. All rights reserved.
// Copyright (c) 2021-2022 Rocky Enterprise Software Foundation, Inc. All rights reserved.
// Copyright (c) 2021-2022 Ctrl IQ, Inc. All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice,
// this list of conditions and the following disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright notice,
// this list of conditions and the following disclaimer in the documentation
// and/or other materials provided with the distribution.
//
// 3. Neither the name of the copyright holder nor the names of its contributors
// may be used to endorse or promote products derived from this software without
// specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
// POSSIBILITY OF SUCH DAMAGE.

package utils

import (
	"context"
	"fmt"
	"github.com/go-openapi/runtime"
	"github.com/go-openapi/strfmt"
	"github.com/ory/hydra-client-go/client"
	"github.com/ory/hydra-client-go/client/admin"
	"github.com/ory/hydra-client-go/client/public"
	"github.com/sirupsen/logrus"
	"google.golang.org/grpc"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/metadata"
	"google.golang.org/grpc/status"
	"strings"
)

type InterceptorFunc func(ctx context.Context, req interface{}, usi *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error)
type ServerInterceptorFunc func(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error

type ContextUser struct {
	ID        string `json:"id"`
	AuthToken string `json:"authToken"`
	Name      string `json:"name"`
	Email     string `json:"email"`
}

// Finish chains all interceptors
func (a InterceptorFunc) Finish(ctx context.Context, req interface{}, usi *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
	return a(ctx, req, usi, handler)
}
func (a ServerInterceptorFunc) Finish(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
	return a(srv, ss, info, handler)
}

// EndInterceptor should be used in the end of an interceptor chain
func EndInterceptor(ctx context.Context, req interface{}, _ *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
	return handler(ctx, req)
}
func ServerEndInterceptor(srv interface{}, ss grpc.ServerStream, _ *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
	return handler(srv, ss)
}

func checkAuth(ctx context.Context, hydraSDK *client.OryHydra, hydraAdmin *client.OryHydra) (context.Context, error) {
	// fetch metadata from grpc
	meta, ok := metadata.FromIncomingContext(ctx)
	if !ok {
		return ctx, status.Error(codes.InvalidArgument, "invalid request sent")
	}

	// get authorization header
	authHeader := meta["authorization"]
	if len(authHeader) == 0 {
		return ctx, status.Error(codes.InvalidArgument, "empty authorization header")
	}

	// verify that the authorization header contains a Bearer token
	authToken := strings.SplitN(authHeader[0], " ", 2)
	if len(authToken) != 2 || authToken[0] != "Bearer" {
		return ctx, status.Error(codes.InvalidArgument, "invalid authorization token")
	}

	userInfo, err := hydraSDK.Public.Userinfo(
		&public.UserinfoParams{
			Context: ctx,
		},
		runtime.ClientAuthInfoWriterFunc(func(request runtime.ClientRequest, registry strfmt.Registry) error {
			return request.SetHeaderParam("Authorization", authHeader[0])
		}),
	)
	if err != nil {
		return ctx, err
	}
	if userInfo.Payload.Sub == "" && hydraAdmin != nil {
		introspect, err := hydraAdmin.Admin.IntrospectOAuth2Token(
			&admin.IntrospectOAuth2TokenParams{
				Context: ctx,
				Token:   authToken[1],
			},
		)
		if err != nil {
			logrus.Errorf("error introspecting token: %s", err)
			return ctx, status.Errorf(codes.Unauthenticated, "invalid authorization token")
		}

		userInfo.Payload.Sub = introspect.Payload.ClientID
		userInfo.Payload.Name = introspect.Payload.Sub
		userInfo.Payload.Email = fmt.Sprintf("%s@%s", introspect.Payload.Sub, "serviceaccount.resf.org")
	}
	if userInfo.Payload.Sub == "" {
		return ctx, status.Errorf(codes.Unauthenticated, "invalid authorization token")
	}

	// supply subject and token to further requests
	pairs := metadata.Pairs("x-user-id", userInfo.Payload.Sub, "x-user-name", userInfo.Payload.Name, "x-user-email", userInfo.Payload.Email, "x-auth-token", authToken[1])
	ctx = metadata.NewIncomingContext(ctx, metadata.Join(meta, pairs))

	return ctx, nil
}

// AuthInterceptor requires OAuth2 authentication for all routes except listed
func AuthInterceptor(hydraSDK *client.OryHydra, hydraAdminSDK *client.OryHydra, excludedMethods []string, enforce bool, next InterceptorFunc) InterceptorFunc {
	return func(ctx context.Context, req interface{}, usi *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
		// skip authentication for excluded methods
		if !StrContains(usi.FullMethod, excludedMethods) {
			var err error
			if ctx, err = checkAuth(ctx, hydraSDK, hydraAdminSDK); err != nil {
				if enforce {
					return nil, err
				}
			}
		}

		return next(ctx, req, usi, handler)
	}
}

type serverStream struct {
	grpc.ServerStream
	ctx context.Context
}

func (ss *serverStream) Context() context.Context {
	return ss.ctx
}
func ServerAuthInterceptor(hydraSDK *client.OryHydra, hydraAdminSDK *client.OryHydra, excludedMethods []string, enforce bool, next ServerInterceptorFunc) ServerInterceptorFunc {
	return func(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
		newStream := serverStream{
			ServerStream: ss,
			ctx:          ss.Context(),
		}
		// skip authentication for excluded methods
		if !StrContains(info.FullMethod, excludedMethods) {
			var ctx context.Context
			var err error
			if ctx, err = checkAuth(ss.Context(), hydraSDK, hydraAdminSDK); err != nil {
				if enforce {
					return err
				}
			}
			if ctx != nil {
				newStream.ctx = ctx
			}
		}

		return next(srv, &newStream, info, handler)
	}
}

func UserFromContext(ctx context.Context) (*ContextUser, error) {
	meta, ok := metadata.FromIncomingContext(ctx)
	if !ok {
		return nil, status.Error(codes.InvalidArgument, "invalid request sent")
	}

	uid := meta["x-user-id"]
	if len(uid) == 0 {
		return nil, status.Error(codes.Unauthenticated, "no user id")
	}

	authTokens := meta["x-auth-token"]
	if len(authTokens) == 0 {
		return nil, status.Error(codes.Unauthenticated, "no auth token")
	}

	var name string
	if names := meta["x-user-name"]; len(names) > 0 {
		name = names[0]
	}
	var email string
	if emails := meta["x-user-email"]; len(emails) > 0 {
		email = emails[0]
	}

	return &ContextUser{
		ID:        uid[0],
		AuthToken: authTokens[0],
		Name:      name,
		Email:     email,
	}, nil
}
