package g8

import (
	"context"
	"net/http"
	"strings"
)

const (
	// AuthorizationHeader is the header in which g8 looks for the authorization bearer token
	AuthorizationHeader = "Authorization"

	// DefaultUnauthorizedResponseBody is the default response body returned if a request was sent with a missing or invalid token
	DefaultUnauthorizedResponseBody = "token is missing or invalid"

	// DefaultTooManyRequestsResponseBody is the default response body returned if a request exceeded the allowed rate limit
	DefaultTooManyRequestsResponseBody = "too many requests"

	// TokenContextKey is the key used to store the client's token in the context.
	TokenContextKey = "g8.token"

	// DataContextKey is the key used to store the client's data in the context.
	DataContextKey = "g8.data"
)

// Gate is lock to the front door of your API, letting only those you allow through.
type Gate struct {
	authorizationService     *AuthorizationService
	unauthorizedResponseBody []byte

	customTokenExtractorFunc func(request *http.Request) string

	rateLimiter                 *RateLimiter
	tooManyRequestsResponseBody []byte
}

// Deprecated: use New instead.
func NewGate(authorizationService *AuthorizationService) *Gate {
	return &Gate{
		authorizationService:        authorizationService,
		unauthorizedResponseBody:    []byte(DefaultUnauthorizedResponseBody),
		tooManyRequestsResponseBody: []byte(DefaultTooManyRequestsResponseBody),
	}
}

// New creates a new Gate.
func New() *Gate {
	return &Gate{
		unauthorizedResponseBody:    []byte(DefaultUnauthorizedResponseBody),
		tooManyRequestsResponseBody: []byte(DefaultTooManyRequestsResponseBody),
	}
}

// WithAuthorizationService sets the authorization service to use.
//
// If there is no authorization service, Gate will not enforce authorization.
func (gate *Gate) WithAuthorizationService(authorizationService *AuthorizationService) *Gate {
	gate.authorizationService = authorizationService
	return gate
}

// WithCustomUnauthorizedResponseBody sets a custom response body when Gate determines that a request must be blocked
func (gate *Gate) WithCustomUnauthorizedResponseBody(unauthorizedResponseBody []byte) *Gate {
	gate.unauthorizedResponseBody = unauthorizedResponseBody
	return gate
}

// WithCustomTokenExtractor allows the specification of a custom function to extract a token from a request.
// If a custom token extractor is not specified, the token will be extracted from the Authorization header.
//
// For instance, if you're using a session cookie, you can extract the token from the cookie like so:
//
//	authorizationService := g8.NewAuthorizationService()
//	customTokenExtractorFunc := func(request *http.Request) string {
//		sessionCookie, err := request.Cookie("session")
//		if err != nil {
//			return ""
//		}
//		return sessionCookie.Value
//	}
//	gate := g8.New().WithAuthorizationService(authorizationService).WithCustomTokenExtractor(customTokenExtractorFunc)
//
// You would normally use this with a client provider that matches whatever need you have.
// For example, if you're using a session cookie, your client provider would retrieve the user from the session ID
// extracted by this custom token extractor.
//
// Note that for the sake of convenience, the token extracted from the request is passed the protected handlers request
// context under the key TokenContextKey. This is especially useful if the token is in fact a session ID.
func (gate *Gate) WithCustomTokenExtractor(customTokenExtractorFunc func(request *http.Request) string) *Gate {
	gate.customTokenExtractorFunc = customTokenExtractorFunc
	return gate
}

// WithRateLimit adds rate limiting to the Gate
//
// If you just want to use a gate for rate limiting purposes:
//
//	gate := g8.New().WithRateLimit(50)
func (gate *Gate) WithRateLimit(maximumRequestsPerSecond int) *Gate {
	gate.rateLimiter = NewRateLimiter(maximumRequestsPerSecond)
	return gate
}

// Protect secures a handler, requiring requests going through to have a valid Authorization Bearer token.
// Unlike ProtectWithPermissions, Protect will allow access to any registered tokens, regardless of their permissions
// or lack thereof.
//
// Example:
//
//	gate := g8.New().WithAuthorizationService(g8.NewAuthorizationService().WithToken("token"))
//	router := http.NewServeMux()
//	// Without protection
//	router.Handle("/handle", yourHandler)
//	// With protection
//	router.Handle("/handle", gate.Protect(yourHandler))
//
// The token extracted from the request is passed to the handlerFunc request context under the key TokenContextKey
func (gate *Gate) Protect(handler http.Handler) http.Handler {
	return gate.ProtectWithPermissions(handler, nil)
}

// ProtectWithPermissions secures a handler, requiring requests going through to have a valid Authorization Bearer token
// as well as a slice of permissions that must be met.
//
// Example:
//
//	gate := g8.New().WithAuthorizationService(g8.NewAuthorizationService().WithClient(g8.NewClient("token").WithPermission("ADMIN")))
//	router := http.NewServeMux()
//	// Without protection
//	router.Handle("/handle", yourHandler)
//	// With protection
//	router.Handle("/handle", gate.ProtectWithPermissions(yourHandler, []string{"admin"}))
//
// The token extracted from the request is passed to the handlerFunc request context under the key TokenContextKey
func (gate *Gate) ProtectWithPermissions(handler http.Handler, permissions []string) http.Handler {
	return gate.ProtectFuncWithPermissions(func(writer http.ResponseWriter, request *http.Request) {
		handler.ServeHTTP(writer, request)
	}, permissions)
}

// ProtectWithPermission does the same thing as ProtectWithPermissions, but for a single permission instead of a
// slice of permissions
//
// See ProtectWithPermissions for further documentation
func (gate *Gate) ProtectWithPermission(handler http.Handler, permission string) http.Handler {
	return gate.ProtectFuncWithPermissions(func(writer http.ResponseWriter, request *http.Request) {
		handler.ServeHTTP(writer, request)
	}, []string{permission})
}

// ProtectFunc secures a handlerFunc, requiring requests going through to have a valid Authorization Bearer token.
// Unlike ProtectFuncWithPermissions, ProtectFunc will allow access to any registered tokens, regardless of their
// permissions or lack thereof.
//
// Example:
//
//	gate := g8.New().WithAuthorizationService(g8.NewAuthorizationService().WithToken("token"))
//	router := http.NewServeMux()
//	// Without protection
//	router.HandleFunc("/handle", yourHandlerFunc)
//	// With protection
//	router.HandleFunc("/handle", gate.ProtectFunc(yourHandlerFunc))
//
// The token extracted from the request is passed to the handlerFunc request context under the key TokenContextKey
func (gate *Gate) ProtectFunc(handlerFunc http.HandlerFunc) http.HandlerFunc {
	return gate.ProtectFuncWithPermissions(handlerFunc, nil)
}

// ProtectFuncWithPermissions secures a handler, requiring requests going through to have a valid Authorization Bearer
// token as well as a slice of permissions that must be met.
//
// Example:
//
//	gate := g8.New().WithAuthorizationService(g8.NewAuthorizationService().WithClient(g8.NewClient("token").WithPermission("admin")))
//	router := http.NewServeMux()
//	// Without protection
//	router.HandleFunc("/handle", yourHandlerFunc)
//	// With protection
//	router.HandleFunc("/handle", gate.ProtectFuncWithPermissions(yourHandlerFunc, []string{"admin"}))
//
// The token extracted from the request is passed to the handlerFunc request context under the key TokenContextKey
func (gate *Gate) ProtectFuncWithPermissions(handlerFunc http.HandlerFunc, permissions []string) http.HandlerFunc {
	return func(writer http.ResponseWriter, request *http.Request) {
		if gate.rateLimiter != nil {
			if !gate.rateLimiter.Try() {
				writer.WriteHeader(http.StatusTooManyRequests)
				_, _ = writer.Write(gate.tooManyRequestsResponseBody)
				return
			}
		}
		if gate.authorizationService != nil {
			token := gate.ExtractTokenFromRequest(request)
			if client, authorized := gate.authorizationService.Authorize(token, permissions); !authorized {
				writer.WriteHeader(http.StatusUnauthorized)
				_, _ = writer.Write(gate.unauthorizedResponseBody)
				return
			} else {
				request = request.WithContext(context.WithValue(request.Context(), TokenContextKey, token))
				if client != nil && client.Data != nil {
					request = request.WithContext(context.WithValue(request.Context(), DataContextKey, client.Data))
				}
			}
		}
		handlerFunc(writer, request)
	}
}

// ProtectFuncWithPermission does the same thing as ProtectFuncWithPermissions, but for a single permission instead of a
// slice of permissions
//
// See ProtectFuncWithPermissions for further documentation
func (gate *Gate) ProtectFuncWithPermission(handlerFunc http.HandlerFunc, permission string) http.HandlerFunc {
	return gate.ProtectFuncWithPermissions(handlerFunc, []string{permission})
}

// ExtractTokenFromRequest extracts a token from a request.
//
// By default, it extracts the bearer token from the AuthorizationHeader, but if a customTokenExtractorFunc is defined,
// it will use that instead.
//
// Note that this method is internally used by Protect, ProtectWithPermission, ProtectFunc and
// ProtectFuncWithPermissions, but it is exposed in case you need to use it directly.
func (gate *Gate) ExtractTokenFromRequest(request *http.Request) string {
	if gate.customTokenExtractorFunc != nil {
		// A custom token extractor function is defined, so we'll use it instead of the default token extraction logic
		return gate.customTokenExtractorFunc(request)
	}
	return strings.TrimPrefix(request.Header.Get(AuthorizationHeader), "Bearer ")
}

// PermissionMiddleware is a middleware that behaves like ProtectWithPermission, but it is meant to be used
// as a middleware for libraries that support such a feature.
//
// For instance, if you are using github.com/gorilla/mux, you can use PermissionMiddleware like so:
//
//	router := mux.NewRouter()
//	router.Use(gate.PermissionMiddleware("admin"))
//	router.Handle("/admin/handle", adminHandler)
//
// If you do not want to protect a router with a specific permission, you can use Gate.Protect instead.
func (gate *Gate) PermissionMiddleware(permissions ...string) func(http.Handler) http.Handler {
	return func(next http.Handler) http.Handler {
		return gate.ProtectWithPermissions(next, permissions)
	}
}
