package g8

import (
	"sync"
)

// AuthorizationService is the service that manages client/token registry and client fallback as well as the service
// that determines whether a token meets the specific requirements to be authorized by a Gate or not.
type AuthorizationService struct {
	clients        map[string]*Client
	clientProvider *ClientProvider

	mutex sync.RWMutex
}

// NewAuthorizationService creates a new AuthorizationService
func NewAuthorizationService() *AuthorizationService {
	return &AuthorizationService{
		clients: make(map[string]*Client),
	}
}

// WithToken is used to specify a single token for which authorization will be granted
//
// The client that will be created from this token will have access to all handlers that are not protected with a
// specific permission.
//
// In other words, if you were to do the following:
//
//	gate := g8.New().WithAuthorizationService(g8.NewAuthorizationService().WithToken("12345"))
//
// The following handler would be accessible with the token 12345:
//
//	router.Handle("/1st-handler", gate.Protect(yourHandler))
//
// But not this one would not be accessible with the token 12345:
//
//	router.Handle("/2nd-handler", gate.ProtectWithPermissions(yourOtherHandler, []string{"admin"}))
//
// Calling this function multiple times will add multiple clients, though you may want to use WithTokens instead
// if you plan to add multiple clients
//
// If you wish to configure advanced permissions, consider using WithClient instead.
func (authorizationService *AuthorizationService) WithToken(token string) *AuthorizationService {
	authorizationService.mutex.Lock()
	authorizationService.clients[token] = NewClient(token)
	authorizationService.mutex.Unlock()
	return authorizationService
}

// WithTokens is used to specify a slice of tokens for which authorization will be granted
func (authorizationService *AuthorizationService) WithTokens(tokens []string) *AuthorizationService {
	authorizationService.mutex.Lock()
	for _, token := range tokens {
		authorizationService.clients[token] = NewClient(token)
	}
	authorizationService.mutex.Unlock()
	return authorizationService
}

// WithClient is used to specify a single client for which authorization will be granted
//
// When compared to WithToken, the advantage of using this function is that you may specify the client's
// permissions and thus, be a lot more granular with what endpoint a token has access to.
//
// In other words, if you were to do the following:
//
//	gate := g8.New().WithAuthorizationService(g8.NewAuthorizationService().WithClient(g8.NewClient("12345").WithPermission("mod")))
//
// The following handlers would be accessible with the token 12345:
//
//	router.Handle("/1st-handler", gate.ProtectWithPermissions(yourHandler, []string{"mod"}))
//	router.Handle("/2nd-handler", gate.Protect(yourOtherHandler))
//
// But not this one, because the user does not have the permission "admin":
//
//	router.Handle("/3rd-handler", gate.ProtectWithPermissions(yetAnotherHandler, []string{"admin"}))
//
// Calling this function multiple times will add multiple clients, though you may want to use WithClients instead
// if you plan to add multiple clients
func (authorizationService *AuthorizationService) WithClient(client *Client) *AuthorizationService {
	authorizationService.mutex.Lock()
	authorizationService.clients[client.Token] = client
	authorizationService.mutex.Unlock()
	return authorizationService
}

// WithClients is used to specify a slice of clients for which authorization will be granted
func (authorizationService *AuthorizationService) WithClients(clients []*Client) *AuthorizationService {
	authorizationService.mutex.Lock()
	for _, client := range clients {
		authorizationService.clients[client.Token] = client
	}
	authorizationService.mutex.Unlock()
	return authorizationService
}

// WithClientProvider allows specifying a custom provider to fetch clients by token.
//
// For example, you can use it to fallback to making a call in your database when a request is made with a token that
// hasn't been specified via WithToken, WithTokens, WithClient or WithClients.
func (authorizationService *AuthorizationService) WithClientProvider(provider *ClientProvider) *AuthorizationService {
	authorizationService.clientProvider = provider
	return authorizationService
}

// Authorize checks whether a client with a given token exists and has the permissions required.
//
// If permissionsRequired is nil or empty and a client with the given token exists, said client will have access to all
// handlers that are not protected by a given permission.
//
// Returns the client is authorized (or nil if no client was authorized), as well as whether the token is authorized
func (authorizationService *AuthorizationService) Authorize(token string, permissionsRequired []string) (client *Client, authorized bool) {
	if len(token) == 0 {
		return nil, false
	}
	authorizationService.mutex.RLock()
	client, _ = authorizationService.clients[token]
	authorizationService.mutex.RUnlock()
	// If there's no clients with the given token directly stored in the AuthorizationService, fall back to the
	// client provider, if there's one configured.
	if client == nil && authorizationService.clientProvider != nil {
		client = authorizationService.clientProvider.GetClientByToken(token)
	}
	if client != nil && client.HasPermissions(permissionsRequired) {
		// If the client has the required permissions, return true and the client
		return client, true
	}
	return nil, false
}
