/*! @file GTMAppAuthFetcherAuthorization.m
    @brief GTMAppAuth SDK
    @copyright
        Copyright 2016 Google Inc.
    @copydetails
        Licensed under the Apache License, Version 2.0 (the "License");
        you may not use this file except in compliance with the License.
        You may obtain a copy of the License at

        http://www.apache.org/licenses/LICENSE-2.0

        Unless required by applicable law or agreed to in writing, software
        distributed under the License is distributed on an "AS IS" BASIS,
        WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
        See the License for the specific language governing permissions and
        limitations under the License.
 */

#import "GTMAppAuth/Sources/Public/GTMAppAuth/GTMAppAuthFetcherAuthorization.h"

#if SWIFT_PACKAGE || GTMAPPAUTH_USE_MODULAR_IMPORT
@import AppAuthCore;
#elif GTMAPPAUTH_USER_IMPORTS
#import "AppAuthCore.h"
#else
#import <AppAuth/AppAuthCore.h>
#endif

#define GTMOAuth2AssertValidSelector GTMBridgeAssertValidSelector

/*! @brief Provides a template implementation for init-family methods which have been marked as
        NS_UNAVILABLE. Stops the compiler from giving a warning when it's the super class'
        designated initializer, and gives callers useful feedback telling them what the
        new designated initializer is.
    @remarks Takes a SEL as a parameter instead of a string so that we get compiler warnings if the
        designated initializer's signature changes.
    @param designatedInitializer A SEL referencing the designated initializer.
 */
#define GTM_UNAVAILABLE_USE_INITIALIZER(designatedInitializer) { \
  NSString *reason = [NSString stringWithFormat:@"Called: %@\nDesignated Initializer:%@", \
                                                NSStringFromSelector(_cmd), \
                                                NSStringFromSelector(designatedInitializer)]; \
  @throw [NSException exceptionWithName:@"Attempt to call unavailable initializer." \
                                 reason:reason \
                               userInfo:nil]; \
}

/*! @brief Key used to encode the @c authState property for @c NSSecureCoding.
 */
static NSString *const kAuthStateKey = @"authState";

/*! @brief Key used to encode the @c serviceProvider property for @c NSSecureCoding.
 */
static NSString *const kServiceProviderKey = @"serviceProvider";

/*! @brief Key used to encode the @c userID property for @c NSSecureCoding.
 */
static NSString *const kUserIDKey = @"userID";

/*! @brief Key used to encode the @c userEmail property for @c NSSecureCoding.
 */
static NSString *const kUserEmailKey = @"userEmail";

/*! @brief Key used to encode the @c userEmailIsVerified property for @c NSSecureCoding.
 */
static NSString *const kUserEmailIsVerifiedKey = @"userEmailIsVerified";

NSString *const GTMAppAuthFetcherAuthorizationErrorDomain =
    @"kGTMAppAuthFetcherAuthorizationErrorDomain";
NSString *const GTMAppAuthFetcherAuthorizationErrorRequestKey = @"request";

/*! @brief Internal wrapper class for requests needing authorization and their callbacks.
    @discusssion Used to abstract away the detail of whether a callback or block is used.
 */
@interface GTMAppAuthFetcherAuthorizationArgs : NSObject

/*! @brief The request to authorize.
 *  @discussion Not copied, as we are mutating the request.
 */
@property (nonatomic, strong) NSMutableURLRequest *request;

/*! @brief The delegate on which @c selector is called on completion.
 */
@property (nonatomic, weak) id delegate;

/*! @brief The selector called on the @c delegate object on completion.
 */
@property (nonatomic) SEL selector;

/*! @brief The completion block when the block option was used.
 */
@property (nonatomic, strong) GTMAppAuthFetcherAuthorizationCompletion completionHandler;

/*! @brief The error that happened during token refresh (if any).
 */
@property (nonatomic, strong) NSError *error;

+ (GTMAppAuthFetcherAuthorizationArgs *)argsWithRequest:(NSMutableURLRequest *)req
             delegate:(id)delegate
             selector:(SEL)selector
    completionHandler:(GTMAppAuthFetcherAuthorizationCompletion)completionHandler;

@end

@implementation GTMAppAuthFetcherAuthorizationArgs

@synthesize request = _request;
@synthesize delegate = _delegate;
@synthesize selector = _selector;
@synthesize completionHandler = _completionHandler;
@synthesize error = _error;

+ (GTMAppAuthFetcherAuthorizationArgs *)argsWithRequest:(NSMutableURLRequest *)req
             delegate:(id)delegate
             selector:(SEL)selector
    completionHandler:(GTMAppAuthFetcherAuthorizationCompletion)completionHandler {
  GTMAppAuthFetcherAuthorizationArgs *obj;
  obj = [[GTMAppAuthFetcherAuthorizationArgs alloc] init];
  obj.request = req;
  obj.delegate = delegate;
  obj.selector = selector;
  obj.completionHandler = completionHandler;
  return obj;
}

@end

@implementation GTMAppAuthFetcherAuthorization {
  /*! @brief Array of requests pending authorization headers.
   */
  NSMutableArray<GTMAppAuthFetcherAuthorizationArgs *> *_authorizationQueue;
}

@synthesize authState = _authState;
@synthesize serviceProvider = _serviceProvider;
@synthesize userID = _userID;
@synthesize userEmailIsVerified = _userEmailIsVerified;

// GTMFetcherAuthorizationProtocol doesn't specify atomic/nonatomic for these properties.
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wimplicit-atomic-properties"
@synthesize userEmail = _userEmail;
@synthesize shouldAuthorizeAllRequests = _shouldAuthorizeAllRequests;
@synthesize fetcherService = _fetcherService;
#pragma clang diagnostic pop

#pragma mark - Initializers

// Ignore warning about not calling the designated initializer.
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-designated-initializers"
- (instancetype)init
    GTM_UNAVAILABLE_USE_INITIALIZER(@selector(initWithAuthState:));
#pragma clang diagnostic pop

- (instancetype)initWithAuthState:(OIDAuthState *)authState {
  return [self initWithAuthState:authState
                 serviceProvider:nil
                          userID:nil
                       userEmail:nil
             userEmailIsVerified:nil];
}

- (instancetype)initWithAuthState:(OIDAuthState *)authState
                           serviceProvider:(nullable NSString *)serviceProvider
                                    userID:(nullable NSString *)userID
                                 userEmail:(nullable NSString *)userEmail
                       userEmailIsVerified:(nullable NSString *)userEmailIsVerified {
  self = [super init];
  if (self) {
    _authState = authState;
    _authorizationQueue = [[NSMutableArray alloc] init];

    _serviceProvider = [serviceProvider copy];
    _userID = [userID copy];
    _userEmail = [userEmail copy];
    _userEmailIsVerified = [userEmailIsVerified copy];

    // Decodes the ID Token locally to extract the email address.
    NSString *idToken = _authState.lastTokenResponse.idToken
        ? : _authState.lastAuthorizationResponse.idToken;
    if (idToken) {
      NSDictionary *claimsDictionary = [[OIDIDToken alloc] initWithIDTokenString:idToken].claims;
      if (claimsDictionary) {
        _userEmail = (NSString *)[claimsDictionary[@"email"] copy];
        _userEmailIsVerified = [(NSNumber *)claimsDictionary[@"email_verified"] stringValue];
        _userID = [claimsDictionary[@"sub"] copy];
      }
    }
  }
  return self;
}

# pragma mark - Convenience

#if !GTM_APPAUTH_SKIP_GOOGLE_SUPPORT
+ (OIDServiceConfiguration *)configurationForGoogle {
  NSURL *authorizationEndpoint =
      [NSURL URLWithString:@"https://accounts.google.com/o/oauth2/v2/auth"];
  NSURL *tokenEndpoint =
      [NSURL URLWithString:@"https://www.googleapis.com/oauth2/v4/token"];

  OIDServiceConfiguration *configuration =
      [[OIDServiceConfiguration alloc] initWithAuthorizationEndpoint:authorizationEndpoint
                                                       tokenEndpoint:tokenEndpoint];
  return configuration;
}
#endif // !GTM_APPAUTH_SKIP_GOOGLE_SUPPORT

#pragma mark - Authorizing Requests

/*! @brief Internal routine common to delegate and block invocations to queue requests while
        fresh tokens are obtained.
 */
- (void)authorizeRequestArgs:(GTMAppAuthFetcherAuthorizationArgs *)args {
  // Adds requests to queue.
  @synchronized(_authorizationQueue) {
    [_authorizationQueue addObject:args];
  }

  NSDictionary<NSString *, NSString *> *additionalRefreshParameters = _tokenRefreshDelegate ?
      [_tokenRefreshDelegate additionalRefreshParameters:self] : nil;

  // Obtains fresh tokens from AppAuth.
  [_authState performActionWithFreshTokens:^(NSString *_Nullable accessToken,
                                             NSString *_Nullable idToken,
                                             NSError *_Nullable error) {
    // Processes queue.
    @synchronized(self->_authorizationQueue) {
      for (GTMAppAuthFetcherAuthorizationArgs *fetcherArgs in self->_authorizationQueue) {
        [self authorizeRequestImmediateArgs:fetcherArgs accessToken:accessToken error:error];
      }
      [self->_authorizationQueue removeAllObjects];
    }
  }
               additionalRefreshParameters:additionalRefreshParameters];
}

/*! @brief Adds authorization headers to the given request, using the supplied access token, or
        handles the error.
    @param args The request argument group to authorize.
    @param accessToken A currently valid access token.
    @param error If accessToken is nil, the error which caused the token to be unavailable.
    @return YES if the request was authorized with a valid access token.
 */
- (BOOL)authorizeRequestImmediateArgs:(GTMAppAuthFetcherAuthorizationArgs *)args
                          accessToken:(NSString *)accessToken
                                error:(NSError *)error {
  // This authorization entry point never attempts to refresh the access token,
  // but does call the completion routine

  NSMutableURLRequest *request = args.request;

  NSURL *requestURL = [request URL];
  NSString *scheme = [requestURL scheme];
  BOOL isAuthorizableRequest =
      !requestURL
      || (scheme && [scheme caseInsensitiveCompare:@"https"] == NSOrderedSame)
      || [requestURL isFileURL]
      || self.shouldAuthorizeAllRequests;
  if (!isAuthorizableRequest) {
    // Request is not https, a local file, or nil, so may be insecure
    //
    // The NSError will be created below
#if DEBUG
    NSLog(@"Cannot authorize request with scheme %@ (%@)", scheme, request);
#endif
  }

  // Get the access token.
  if (isAuthorizableRequest && accessToken && accessToken.length > 0) {
    if (request) {
      // Adds the authorization header to the request.
      NSString *value = [NSString stringWithFormat:@"%@ %@", @"Bearer", accessToken];
      [request setValue:value forHTTPHeaderField:@"Authorization"];
    }

    // We've authorized the request, even if the previous refresh
    // failed with an error
    args.error = nil;
  } else {
    NSMutableDictionary *userInfo = [error.userInfo mutableCopy];
    if (!userInfo) {
      userInfo = [[NSMutableDictionary alloc] init];
    }
    if (request) {
      userInfo[GTMAppAuthFetcherAuthorizationErrorRequestKey] = request;
    }

    if (!isAuthorizableRequest || !error) {
      args.error = [NSError errorWithDomain:GTMAppAuthFetcherAuthorizationErrorDomain
                                       code:GTMAppAuthFetcherAuthorizationErrorUnauthorizableRequest
                                   userInfo:userInfo];
    } else {
      // Passes through error domain & code from AppAuth, with additional userInfo args.
      args.error = [NSError errorWithDomain:error.domain
                                       code:error.code
                                   userInfo:userInfo];
    }
  }

  // Invoke any callbacks on the proper thread
  if (args.delegate || args.completionHandler) {
    // If the fetcher service provides a callback queue, we'll use that
    // (or if it's nil, we'll use the main thread) for callbacks.
    dispatch_queue_t callbackQueue = self.fetcherService.callbackQueue;
    if (!callbackQueue) {
      callbackQueue = dispatch_get_main_queue();
    }
    dispatch_async(callbackQueue, ^{
      [self invokeCallbackArgs:args];
    });
  }

  BOOL didAuth = (args.error == nil);
  return didAuth;
}

/*! @brief Invokes the callback for the given authorization argument group.
    @param args The request argument group to invoke following authorization or error.
 */
- (void)invokeCallbackArgs:(GTMAppAuthFetcherAuthorizationArgs *)args {
  NSError *error = args.error;
  id delegate = args.delegate;
  SEL sel = args.selector;

  // If the selector callback method exists, invokes the selector.
  if (delegate && sel) {
    NSMutableURLRequest *request = args.request;

    NSMethodSignature *sig = [delegate methodSignatureForSelector:sel];
    NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:sig];
    [invocation setSelector:sel];
    [invocation setTarget:delegate];
    GTMAppAuthFetcherAuthorization *authorization = self;
    [invocation setArgument:&authorization atIndex:2];
    [invocation setArgument:&request atIndex:3];
    [invocation setArgument:&error atIndex:4];
    [invocation invoke];
  }

  // If a callback block exists, executes the block.
  id handler = args.completionHandler;
  if (handler) {
    void (^authCompletionBlock)(NSError *) = handler;
    authCompletionBlock(error);
  }
}

#pragma mark - GTMFetcherAuthorizationProtocol

/*! @brief Authorizing with a callback selector.
    @discussion Selector has the signature:
        - (void)authentication:(GTMAppAuthFetcherAuthorization *)auth
                       request:(NSMutableURLRequest *)request
             finishedWithError:(NSError *)error;
 */
- (void)authorizeRequest:(NSMutableURLRequest *)request
                delegate:(id)delegate
       didFinishSelector:(SEL)sel {
  GTMOAuth2AssertValidSelector(delegate, sel,
                               @encode(GTMAppAuthFetcherAuthorization *),
                               @encode(NSMutableURLRequest *),
                               @encode(NSError *), 0);

  GTMAppAuthFetcherAuthorizationArgs *args;
  args = [GTMAppAuthFetcherAuthorizationArgs argsWithRequest:request
                                                    delegate:delegate
                                                    selector:sel
                                           completionHandler:nil];
  [self authorizeRequestArgs:args];
}

/*! @brief Removes all pending requests from the authorization queue.
 */
- (void)stopAuthorization {
  @synchronized(_authorizationQueue) {
    [_authorizationQueue removeAllObjects];
  }
}

/*! @brief Attempts to remove a specific pending requests from the authorization queue.
    @discussion Has no effect if the authorization already occurred.
 */
- (void)stopAuthorizationForRequest:(NSURLRequest *)request {
  @synchronized(_authorizationQueue) {
    NSUInteger argIndex = 0;
    BOOL found = NO;
    for (GTMAppAuthFetcherAuthorizationArgs *args in _authorizationQueue) {
      // Checks pointer equality with given request, don't want to match equivalent requests.
      if ([args request] == request) {
        found = YES;
        break;
      }
      argIndex++;
    }

    if (found) {
      [_authorizationQueue removeObjectAtIndex:argIndex];

      // If the queue is now empty, go ahead and stop the fetcher.
      if (_authorizationQueue.count == 0) {
        [self stopAuthorization];
      }
    }
  }
}

/*! @brief Returns YES if the given requests is in the pending authorization queue.
 */
- (BOOL)isAuthorizingRequest:(NSURLRequest *)request {
  BOOL wasFound = NO;
  @synchronized(_authorizationQueue) {
    for (GTMAppAuthFetcherAuthorizationArgs *args in _authorizationQueue) {
      // Checks pointer equality with given request, don't want to match equivalent requests.
      if ([args request] == request) {
        wasFound = YES;
        break;
      }
    }
  }
  return wasFound;
}

/*! @brief Returns YES if given request has an Authorization header.
 */
- (BOOL)isAuthorizedRequest:(NSURLRequest *)request {
  NSString *authStr = [request valueForHTTPHeaderField:@"Authorization"];
  return (authStr.length > 0);
}

/*! @brief Returns YES if the authorization state is currently valid.
    @discussion Note that the state can become invalid immediately due to an error on token refresh.
 */
- (BOOL)canAuthorize {
  return [_authState isAuthorized];
}

/*! @brief Authorizing with a completion block.
 */
- (void)authorizeRequest:(NSMutableURLRequest *)request
       completionHandler:(GTMAppAuthFetcherAuthorizationCompletion)handler {
  GTMAppAuthFetcherAuthorizationArgs *args =
  [GTMAppAuthFetcherAuthorizationArgs argsWithRequest:request
                                             delegate:nil
                                             selector:NULL
                                    completionHandler:handler];
  [self authorizeRequestArgs:args];
}

/*! @brief Forces a token refresh the next time a request is queued for authorization.
 */
- (BOOL)primeForRefresh {
  if (_authState.refreshToken == nil) {
    // Cannot refresh without a refresh token
    return NO;
  }
  [_authState setNeedsTokenRefresh];
  return YES;
}

#pragma mark - NSSecureCoding

+ (BOOL)supportsSecureCoding {
  return YES;
}

- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder {
  OIDAuthState *authState =
      [aDecoder decodeObjectOfClass:[OIDAuthState class] forKey:kAuthStateKey];
  NSString *serviceProvider =
      [aDecoder decodeObjectOfClass:[NSString class] forKey:kServiceProviderKey];
  NSString *userID = [aDecoder decodeObjectOfClass:[NSString class] forKey:kUserIDKey];
  NSString *userEmail = [aDecoder decodeObjectOfClass:[NSString class] forKey:kUserEmailKey];
  NSString *userEmailIsVerified =
      [aDecoder decodeObjectOfClass:[NSString class] forKey:kUserEmailIsVerifiedKey];

  self = [self initWithAuthState:authState
                 serviceProvider:serviceProvider
                           userID:userID
                        userEmail:userEmail
              userEmailIsVerified:userEmailIsVerified];
  return self;
}

- (void)encodeWithCoder:(NSCoder *)aCoder {
  [aCoder encodeObject:_authState forKey:kAuthStateKey];
  [aCoder encodeObject:_serviceProvider forKey:kServiceProviderKey];
  [aCoder encodeObject:_userID forKey:kUserIDKey];
  [aCoder encodeObject:_userEmail forKey:kUserEmailKey];
  [aCoder encodeObject:_userEmailIsVerified forKey:kUserEmailIsVerifiedKey];
}

@end
