/*
 * Copyright 2019 Google
 *
 * 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 "GoogleUtilities/Environment/Public/GoogleUtilities/GULKeychainStorage.h"
#import <Security/Security.h>

#if __has_include(<FBLPromises/FBLPromises.h>)
#import <FBLPromises/FBLPromises.h>
#else
#import "FBLPromises.h"
#endif

#import "GoogleUtilities/Environment/Public/GoogleUtilities/GULKeychainUtils.h"
#import "GoogleUtilities/Environment/Public/GoogleUtilities/GULSecureCoding.h"

@interface GULKeychainStorage ()
@property(nonatomic, readonly) dispatch_queue_t keychainQueue;
@property(nonatomic, readonly) dispatch_queue_t inMemoryCacheQueue;
@property(nonatomic, readonly) NSString *service;
@property(nonatomic, readonly) NSCache<NSString *, id<NSSecureCoding>> *inMemoryCache;
@end

@implementation GULKeychainStorage

- (instancetype)initWithService:(NSString *)service {
  NSCache *cache = [[NSCache alloc] init];
  // Cache up to 5 installations.
  cache.countLimit = 5;
  return [self initWithService:service cache:cache];
}

- (instancetype)initWithService:(NSString *)service cache:(NSCache *)cache {
  self = [super init];
  if (self) {
    _keychainQueue =
        dispatch_queue_create("com.gul.KeychainStorage.Keychain", DISPATCH_QUEUE_SERIAL);
    _inMemoryCacheQueue =
        dispatch_queue_create("com.gul.KeychainStorage.InMemoryCache", DISPATCH_QUEUE_SERIAL);
    _service = [service copy];
    _inMemoryCache = cache;
  }
  return self;
}

#pragma mark - Public

- (FBLPromise<id<NSSecureCoding>> *)getObjectForKey:(NSString *)key
                                        objectClass:(Class)objectClass
                                        accessGroup:(nullable NSString *)accessGroup {
  return [FBLPromise onQueue:self.inMemoryCacheQueue
                          do:^id _Nullable {
                            // Return cached object or fail otherwise.
                            id object = [self.inMemoryCache objectForKey:key];
                            return object
                                       ?: [[NSError alloc]
                                              initWithDomain:FBLPromiseErrorDomain
                                                        code:FBLPromiseErrorCodeValidationFailure
                                                    userInfo:nil];
                          }]
      .recover(^id _Nullable(NSError *error) {
        // Look for the object in the keychain.
        return [self getObjectFromKeychainForKey:key
                                     objectClass:objectClass
                                     accessGroup:accessGroup];
      });
}

- (FBLPromise<NSNull *> *)setObject:(id<NSSecureCoding>)object
                             forKey:(NSString *)key
                        accessGroup:(nullable NSString *)accessGroup {
  return [FBLPromise onQueue:self.inMemoryCacheQueue
                          do:^id _Nullable {
                            // Save to the in-memory cache first.
                            [self.inMemoryCache setObject:object forKey:[key copy]];
                            return [NSNull null];
                          }]
      .thenOn(self.keychainQueue, ^id(id result) {
        // Then store the object to the keychain.
        NSDictionary *query = [self keychainQueryWithKey:key accessGroup:accessGroup];
        NSError *error;
        NSData *encodedObject = [GULSecureCoding archivedDataWithRootObject:object error:&error];
        if (!encodedObject) {
          return error;
        }

        if (![GULKeychainUtils setItem:encodedObject withQuery:query error:&error]) {
          return error;
        }

        return [NSNull null];
      });
}

- (FBLPromise<NSNull *> *)removeObjectForKey:(NSString *)key
                                 accessGroup:(nullable NSString *)accessGroup {
  return [FBLPromise onQueue:self.inMemoryCacheQueue
                          do:^id _Nullable {
                            [self.inMemoryCache removeObjectForKey:key];
                            return nil;
                          }]
      .thenOn(self.keychainQueue, ^id(id result) {
        NSDictionary *query = [self keychainQueryWithKey:key accessGroup:accessGroup];

        NSError *error;
        if (![GULKeychainUtils removeItemWithQuery:query error:&error]) {
          return error;
        }

        return [NSNull null];
      });
}

#pragma mark - Private

- (FBLPromise<id<NSSecureCoding>> *)getObjectFromKeychainForKey:(NSString *)key
                                                    objectClass:(Class)objectClass
                                                    accessGroup:(nullable NSString *)accessGroup {
  // Look for the object in the keychain.
  return [FBLPromise
             onQueue:self.keychainQueue
                  do:^id {
                    NSDictionary *query = [self keychainQueryWithKey:key accessGroup:accessGroup];
                    NSError *error;
                    NSData *encodedObject = [GULKeychainUtils getItemWithQuery:query error:&error];

                    if (error) {
                      return error;
                    }
                    if (!encodedObject) {
                      return nil;
                    }
                    id object = [GULSecureCoding unarchivedObjectOfClass:objectClass
                                                                fromData:encodedObject
                                                                   error:&error];
                    if (error) {
                      return error;
                    }

                    return object;
                  }]
      .thenOn(self.inMemoryCacheQueue,
              ^id<NSSecureCoding> _Nullable(id<NSSecureCoding> _Nullable object) {
                // Save object to the in-memory cache if exists and return the object.
                if (object) {
                  [self.inMemoryCache setObject:object forKey:[key copy]];
                }
                return object;
              });
}

- (void)resetInMemoryCache {
  [self.inMemoryCache removeAllObjects];
}

#pragma mark - Keychain

- (NSMutableDictionary<NSString *, id> *)keychainQueryWithKey:(NSString *)key
                                                  accessGroup:(nullable NSString *)accessGroup {
  NSMutableDictionary<NSString *, id> *query = [NSMutableDictionary dictionary];

  query[(__bridge NSString *)kSecClass] = (__bridge NSString *)kSecClassGenericPassword;
  query[(__bridge NSString *)kSecAttrService] = self.service;
  query[(__bridge NSString *)kSecAttrAccount] = key;

  if (accessGroup) {
    query[(__bridge NSString *)kSecAttrAccessGroup] = accessGroup;
  }

  if (@available(iOS 13.0, macOS 10.15, macCatalyst 13.0, tvOS 13.0, watchOS 6.0, *)) {
    // Ensures that the keychain query behaves the same across all platforms.
    // See go/firebase-macos-keychain-popups for details.
    query[(__bridge id)kSecUseDataProtectionKeychain] = (__bridge id)kCFBooleanTrue;
  }

#if TARGET_OS_OSX
  if (self.keychainRef) {
    query[(__bridge NSString *)kSecUseKeychain] = (__bridge id)(self.keychainRef);
    query[(__bridge NSString *)kSecMatchSearchList] = @[ (__bridge id)(self.keychainRef) ];
  }
#endif  // TARGET_OS_OSX

  return query;
}

@end
