// Copyright 2021 Google LLC
//
// 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 "GoogleSignIn/Sources/GIDEMMErrorHandler.h"

#import <UIKit/UIKit.h>

#import "GoogleSignIn/Sources/GIDSignInStrings.h"

NS_ASSUME_NONNULL_BEGIN

// The error key in the server response.
static NSString *const kErrorKey = @"error";

// Error strings in the server response.
static NSString *const kGeneralErrorPrefix = @"emm_";
static NSString *const kScreenlockRequiredError = @"emm_passcode_required";
static NSString *const kAppVerificationRequiredErrorPrefix = @"emm_app_verification_required";

// Optional separator between error prefix and the payload.
static NSString *const kErrorPayloadSeparator = @":";

// A list for recognized error codes.
typedef enum {
  ErrorCodeNone = 0,
  ErrorCodeDeviceNotCompliant,
  ErrorCodeScreenlockRequired,
  ErrorCodeAppVerificationRequired,
} ErrorCode;

@implementation GIDEMMErrorHandler {
  // Whether or not a dialog is pending user interaction.
  BOOL _pendingDialog;
}

+ (instancetype)sharedInstance {
  static dispatch_once_t once;
  static GIDEMMErrorHandler *sharedInstance;
  dispatch_once(&once, ^{
    sharedInstance = [[self alloc] init];
  });
  return sharedInstance;
}

- (BOOL)handleErrorFromResponse:(NSDictionary<NSString *, id> *)response
                     completion:(void (^)(void))completion {
  ErrorCode errorCode = ErrorCodeNone;
  NSURL *appVerificationURL;
  @synchronized(self) {  // for accessing _pendingDialog
    if (!_pendingDialog && [UIAlertController class] &&
        [response isKindOfClass:[NSDictionary class]]) {
      id errorValue = response[kErrorKey];
      if ([errorValue isEqual:kScreenlockRequiredError]) {
        errorCode = ErrorCodeScreenlockRequired;
      } else if ([errorValue hasPrefix:kAppVerificationRequiredErrorPrefix]) {
        errorCode = ErrorCodeAppVerificationRequired;
        NSString *appVerificationString =
            [errorValue substringFromIndex:kAppVerificationRequiredErrorPrefix.length];
        if ([appVerificationString hasPrefix:kErrorPayloadSeparator]) {
          appVerificationString =
              [appVerificationString substringFromIndex:kErrorPayloadSeparator.length];
        }
        appVerificationString = [appVerificationString
            stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
        if (appVerificationString.length) {
          appVerificationURL = [NSURL URLWithString:appVerificationString];
        }
      } else if ([errorValue hasPrefix:kGeneralErrorPrefix]) {
        errorCode = ErrorCodeDeviceNotCompliant;
      }
      if (errorCode) {
        _pendingDialog = YES;
      }
    }
  }
  if (!errorCode) {
    completion();
    return NO;
  }
  // All UI must happen in the main thread.
  dispatch_async(dispatch_get_main_queue(), ^() {
    UIWindow *keyWindow = [UIApplication sharedApplication].keyWindow;
    CGRect keyWindowBounds = CGRectIsEmpty(keyWindow.bounds) ?
        keyWindow.bounds : [UIScreen mainScreen].bounds;
    UIWindow *alertWindow = [[UIWindow alloc] initWithFrame:keyWindowBounds];
    alertWindow.backgroundColor = [UIColor clearColor];
    alertWindow.rootViewController = [[UIViewController alloc] init];
    alertWindow.rootViewController.view.backgroundColor = [UIColor clearColor];
    alertWindow.windowLevel = UIWindowLevelAlert;
    [alertWindow makeKeyAndVisible];
    void (^finish)(void) = ^{
      alertWindow.hidden = YES;
      alertWindow.rootViewController = nil;
      [keyWindow makeKeyAndVisible];
      self->_pendingDialog = NO;
      completion();
    };
    UIAlertController *alert;
    switch (errorCode) {
      case ErrorCodeNone:
        break;
      case ErrorCodeScreenlockRequired:
        alert = [self passcodeRequiredAlertWithCompletion:finish];
        break;
      case ErrorCodeAppVerificationRequired:
        alert = [self appVerificationRequiredAlertWithURL:appVerificationURL completion:finish];
        break;
      case ErrorCodeDeviceNotCompliant:
        alert = [self deviceNotCompliantAlertWithCompletion:finish];
        break;
    }
    if (alert) {
      [alertWindow.rootViewController presentViewController:alert animated:YES completion:nil];
    } else {
      // Should not happen but just in case.
      finish();
    }
  });
  return YES;
}

#pragma mark - Alerts

// Returns an alert controller for device not compliant error.
- (UIAlertController *)deviceNotCompliantAlertWithCompletion:(void (^)(void))completion {
  UIAlertController *alert =
      [UIAlertController alertControllerWithTitle:[self unableToAccessString]
                                          message:[self deviceNotCompliantString]
                                   preferredStyle:UIAlertControllerStyleAlert];
  [alert addAction:[UIAlertAction actionWithTitle:[self okayString]
                                            style:UIAlertActionStyleDefault
                                          handler:^(UIAlertAction *action) {
    completion();
  }]];
  return alert;
};

// Returns an alert controller for passcode required error.
- (UIAlertController *)passcodeRequiredAlertWithCompletion:(void (^)(void))completion {
  UIAlertController *alert =
      [UIAlertController alertControllerWithTitle:[self unableToAccessString]
                                          message:[self passcodeRequiredString]
                                   preferredStyle:UIAlertControllerStyleAlert];
  BOOL canOpenSettings = YES;
  if ([[UIDevice currentDevice].systemVersion hasPrefix:@"10."]) {
     // In iOS 10, `UIApplicationOpenSettingsURLString` fails to open the Settings app if the
     // opening app does not have Setting bundle.
    NSString* mainBundlePath = [[NSBundle mainBundle] resourcePath];
    NSString* settingsBundlePath = [mainBundlePath
        stringByAppendingPathComponent:@"Settings.bundle"];
    if (![NSBundle bundleWithPath:settingsBundlePath]) {
      canOpenSettings = NO;
    }
  }
  if (canOpenSettings) {
    [alert addAction:[UIAlertAction actionWithTitle:[self cancelString]
                                              style:UIAlertActionStyleCancel
                                            handler:^(UIAlertAction *action) {
      completion();
    }]];
    [alert addAction:[UIAlertAction actionWithTitle:[self settingsString]
                                              style:UIAlertActionStyleDefault
                                            handler:^(UIAlertAction *action) {
      completion();
      [[UIApplication sharedApplication]
          openURL:[NSURL URLWithString:UIApplicationOpenSettingsURLString]];
    }]];
  } else {
    [alert addAction:[UIAlertAction actionWithTitle:[self okayString]
                                              style:UIAlertActionStyleCancel
                                            handler:^(UIAlertAction *action) {
      completion();
    }]];
  }
  return alert;
};

// Returns an alert controller for app verification required error.
- (UIAlertController *)appVerificationRequiredAlertWithURL:(nullable NSURL *)url
                                                completion:(void (^)(void))completion {
  UIAlertController *alert;
  if (url) {
    // If the URL is provided, prompt user to open this URL or cancel.
    alert = [UIAlertController alertControllerWithTitle:[self appVerificationTitleString]
                                                message:[self appVerificationTextString]
                                     preferredStyle:UIAlertControllerStyleAlert];
    [alert addAction:[UIAlertAction actionWithTitle:[self cancelString]
                                              style:UIAlertActionStyleCancel
                                            handler:^(UIAlertAction *action) {
      completion();
    }]];
    [alert addAction:[UIAlertAction actionWithTitle:[self appVerificationActionString]
                                              style:UIAlertActionStyleDefault
                                            handler:^(UIAlertAction *action) {
      completion();
      [[UIApplication sharedApplication] openURL:url];
    }]];
  } else {
    // If the URL is not provided, simple let user acknowledge the issue. This is not supposed to
    // happen but just to fail gracefully.
    alert = [UIAlertController alertControllerWithTitle:[self unableToAccessString]
                                                message:[self appVerificationTextString]
                                         preferredStyle:UIAlertControllerStyleAlert];
    [alert addAction:[UIAlertAction actionWithTitle:[self okayString]
                                              style:UIAlertActionStyleDefault
                                            handler:^(UIAlertAction *action) {
      completion();
    }]];
  }
  return alert;
}

#pragma mark - Localization

// The English version of the strings are used as back-up in case the bundle resource is missing
// from the third-party app. Please keep them in sync with the strings in the bundle.

// Returns a localized string for unable to access the account.
- (NSString *)unableToAccessString {
  return [GIDSignInStrings localizedStringForKey:@"EmmErrorTitle"
                                            text:@"Unable to sign in to account"];
}

// Returns a localized string for device passcode required error.
- (NSString *)passcodeRequiredString {
  NSString *defaultText =
      @"Your administrator requires you to set a passcode on this device to access this account. "
      "Please set a passcode and try again.";
  return [GIDSignInStrings localizedStringForKey:@"EmmPasscodeRequired" text:defaultText];
}

// Returns a localized string for app verification error dialog title.
- (NSString *)appVerificationTitleString {
  return [GIDSignInStrings localizedStringForKey:@"EmmConnectTitle"
                                            text:@"Connect with Device Policy App?"];
}

// Returns a localized string for app verification error dialog message.
- (NSString *)appVerificationTextString {
  NSString *defaultText = @"In order to protect your organization's data, "
      "you must connect with the Device Policy app before logging in.";
  return [GIDSignInStrings localizedStringForKey:@"EmmConnectText" text:defaultText];
}

// Returns a localized string for app verification error dialog action button label.
- (NSString *)appVerificationActionString {
  return [GIDSignInStrings localizedStringForKey:@"EmmConnectLabel" text:@"Connect"];
}

// Returns a localized string for general device non-compliance error.
- (NSString *)deviceNotCompliantString {
  NSString *defaultText =
      @"The device is not compliant with the security policy set by your administrator.";
  return [GIDSignInStrings localizedStringForKey:@"EmmGeneralError" text:defaultText];
}

// Returns a localized string for "Settings".
- (NSString *)settingsString {
  return [GIDSignInStrings localizedStringForKey:@"SettingsAppName" text:@"Settings"];
}

// Returns a localized string for "OK".
- (NSString *)okayString {
  return [GIDSignInStrings localizedStringForKey:@"OK" text:@"OK"];
}

// Returns a localized string for "Cancel".
- (NSString *)cancelString {
  return [GIDSignInStrings localizedStringForKey:@"Cancel" text:@"Cancel"];
}

@end

NS_ASSUME_NONNULL_END
