// Copyright 2015-present 650 Industries. All rights reserved.

#import <EXDevLauncher/EXDevLauncherManifestParser.h>
#import <EXDevLauncher/EXDevLauncherController.h>

#if __has_include(<EXDevLauncher/EXDevLauncher-Swift.h>)
// For cocoapods framework, the generated swift header will be inside EXDevLauncher module
#import <EXDevLauncher/EXDevLauncher-Swift.h>
#else
#import <EXDevLauncher-Swift.h>
#endif

@import EXManifests;

typedef void (^CompletionHandler)(NSData *data, NSURLResponse *response);

@interface EXDevLauncherManifestParser ()

@property (strong, nonatomic) NSURL *url;
@property (nonatomic, strong) NSString *installationID;
@property (weak, nonatomic) NSURLSession *session;
@property (nonatomic, assign) NSTimeInterval requestTimeout;

@end

@implementation EXDevLauncherManifestParser


- (instancetype)initWithURL:(NSURL *)url
             installationID:(NSString *)installationID
                    session:(NSURLSession *)session
             requestTimeout:(NSTimeInterval)requestTimeout
{
  if (self = [super init]) {
    self.url = url;
    self.installationID = installationID;
    self.session = session;
    self.requestTimeout = requestTimeout;
  }
  return self;
}

- (void)isManifestURLWithCompletion:(IsManifestURL)completion
                            onError:(OnManifestError)onError
{
  [self _fetch:@"HEAD" onError:onError completionHandler:^(NSData *data, NSURLResponse *response) {
    NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;

    // published projects should respond unsuccessfully to HEAD requests sent with no headers
    if (httpResponse.statusCode < 200 || httpResponse.statusCode >= 300) {
      completion(YES);
      return;
    }

    NSDictionary *headers = [httpResponse allHeaderFields];
    if (headers[@"Exponent-Server"]) {
      completion(YES);
      return;
    }

    NSString *contentType = headers[@"Content-Type"];
    if (contentType && ![contentType hasPrefix:@"text/html"] && ![contentType containsString:@"/javascript"]) {
      completion(YES);
      return;
    }

    completion(NO);
  }];
}

- (void)tryToParseManifest:(OnManifestParsed)onParsed
                   onError:(OnManifestError)onError
{
  [self _fetch:@"GET" onError:onError completionHandler:^(NSData *data, NSURLResponse *response) {
    if ([response isKindOfClass:[NSHTTPURLResponse class]]) {
      NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
      if (httpResponse.statusCode < 200 || httpResponse.statusCode >= 300) {
        NSString *message = @"Failed to open app.\n\nIf you are trying to load the app from a development server, check your network connectivity and make sure you can access the server from your device.\n\nIf you are trying to open a published project, install a compatible version of expo-updates and follow all setup and integration steps.";
        onError([NSError errorWithDomain:@"DevelopmentClient" code:1 userInfo:@{NSLocalizedDescriptionKey: message}]);
        return;
      }
    }
    NSError *error;
    NSMutableDictionary *jsonObject = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableContainers error:&error];
    if (!jsonObject) {
      NSMutableDictionary *details = [NSMutableDictionary dictionary];
      details[NSLocalizedDescriptionKey] = [NSString stringWithFormat:@"Couldn't parse the manifest. %@", (error ? error.localizedDescription : @"")];
      if (error) {
        details[NSUnderlyingErrorKey] = error;
      }
      onError([[NSError alloc] initWithDomain:@"DevelopmentClient" code:1 userInfo:details]);
      return;
    }
    [self _resolveManifestUrls:jsonObject];
    EXManifestsManifest *manifest = [EXManifestsManifestFactory manifestForManifestJSON:jsonObject];
    onParsed(manifest);
  }];
}


- (void)_fetch:(NSString *)method onError:(OnManifestError)onError completionHandler:(CompletionHandler)completionHandler
{
  NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:self.url];
  [request setHTTPMethod:method];
  [request setValue:@"ios" forHTTPHeaderField:@"expo-platform"];
  [request setValue:@"application/expo+json,application/json" forHTTPHeaderField:@"accept"];
  // Dev-launcher infrastructure, not app traffic. Without this the reachability probe wins
  // `slowest` on nearly every dev launch and skews the launch metrics summary. Mirrors
  // `NetworkRequestTaskSwizzling.internalHeaderName` in expo-app-metrics. Unlike Android, the
  // iOS side can't strip the header once the task exists, so it reaches the server the user
  // pointed the dev client at.
  [request setValue:@"1" forHTTPHeaderField:@"Expo-AppMetrics-Skip"];
  [self _setForwardedHeadersOnRequest:request];
  [request setTimeoutInterval:self.requestTimeout];
  if (self.installationID) {
    [request setValue:self.installationID forHTTPHeaderField:@"Expo-Dev-Client-ID"];
  }
  NSURLSessionDataTask *dataTask = [self.session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
    if (!error) {
      completionHandler(data, response);
    } else if (onError) {
      onError(error);
    }
  }];
  [dataTask resume];
}

- (void)_setForwardedHeadersOnRequest:(NSMutableURLRequest *)request
{
  NSURLComponents *components = [NSURLComponents componentsWithURL:self.url resolvingAgainstBaseURL:NO];
  if (!components.host || !components.scheme) {
    return;
  }

  NSString *authority = components.port ? [NSString stringWithFormat:@"%@:%@", components.host, components.port] : components.host;
  [request setValue:[NSString stringWithFormat:@"host=\"%@\";proto=%@", authority, components.scheme] forHTTPHeaderField:@"Forwarded"];
  [request setValue:authority forHTTPHeaderField:@"X-Forwarded-Host"];
  [request setValue:components.scheme forHTTPHeaderField:@"X-Forwarded-Proto"];
}

- (void)_resolveManifestUrls:(NSMutableDictionary *)manifestJson
{
  NSString *bundleUrl = manifestJson[@"bundleUrl"];
  if (bundleUrl) {
    manifestJson[@"bundleUrl"] = [self _resolveUrlString:bundleUrl];
  }

  NSMutableDictionary *launchAsset = manifestJson[@"launchAsset"];
  if (launchAsset[@"url"]) {
    launchAsset[@"url"] = [self _resolveUrlString:launchAsset[@"url"]];
  }

  NSArray *assets = manifestJson[@"assets"];
  for (NSMutableDictionary *asset in assets) {
    if (asset[@"url"]) {
      asset[@"url"] = [self _resolveUrlString:asset[@"url"]];
    }
  }
}

- (NSString *)_resolveUrlString:(NSString *)urlString
{
  NSURL *resolvedUrl = [NSURL URLWithString:urlString relativeToURL:self.url];
  return resolvedUrl.absoluteURL.absoluteString ?: urlString;
}

@end
