/*
	Licensed Material - Property of IBM

	(C) Copyright 2015,2016 IBM Corp.

	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.
*/


'use strict';

var moduleName = 'exportApp'; // - name for clarification during logging
//private modules
var promptUserForMissingInfo = require('./promptUserInput');
var serverSupport = require('mdo-server-support');
var profileIO = serverSupport.profileIO;
var serverDefaults = serverSupport.getServerDefaults;
var platformManager = require('platform-manager');
var strings = require('ibm-strings');
var messages = require('./externalizedStrings');
var services = require('./services');

//3rd party modules
var findup = require('findup-sync');
var nopt = require('nopt');
var fs = require('fs');
var path = require('path');
var log = require('npmlog');
var Q = require('q');
var urlFormatter = require('url');

// run iterator function over all elements in the array, ensuring
// that the asynchronous iterator function runs in array order
function mapSeries (arr, iterator, serverInfo, cordovaWindowsPlatforms, config) {
  // create a empty promise to start our series (so we can use `then`)
  var currentPromise = new Q();
  var promises = arr.map(function (el) {
    return currentPromise = currentPromise.then(function () {
      // execute the next function after the previous has resolved successfully
      return iterator(el, serverInfo, cordovaWindowsPlatforms, config);
    });
  });
  // group the results and return the group promise
  return Q.all(promises);
}

module.exports = function exportApp(args) {
    var d = Q.defer(),
        project = {},
        server,
        runtime,
        password,
        serverDetails;

    var opts = nopt({
        'password': String,
        'windows': String
    }, {
        '-p': '--password',
        '-w': '--windows'
    }, args, 0);

    password = opts.password ? opts.password : undefined;
    if(opts.argv.remain.length > 0) {
        server = opts.argv.remain[0];
        runtime = opts.argv.remain[1] ? opts.argv.remain[1] : undefined;
    }
    var mfpConfig = detectMfpConfig();
    //Process server options
    createServerProfileUsingConfig(server, mfpConfig)
    .then(function(retServer) {
      return serverSupport.getServerDetails(retServer, runtime, password);
    })
    .then(function (serverDetailsReturned) {      
        serverDetails = serverDetailsReturned;
        var path = serverDetails.serverProfile.path;
        if(typeof path !== 'undefined' && path.length > 1)
          serverDetails.serverProfile.fullURL = urlFormatter.format({protocol:(serverDetails.serverProfile.protocol || 'http') , hostname: serverDetails.serverProfile.host, port: serverDetails.serverProfile.port, pathname: path});
        else
          serverDetails.serverProfile.fullURL = urlFormatter.format({protocol:(serverDetails.serverProfile.protocol || 'http') , hostname: serverDetails.serverProfile.host, port: serverDetails.serverProfile.port});
        strings.info(strings.format(messages.exportingLocationMessage, serverDetails.serverProfile.fullURL, serverDetails.runtime));
        return platformManager.detectPlatforms();
    })
    .then(function (detectedProjectInfo) {
      project = detectedProjectInfo;
      var projects = [];
      //If we have a windows8 project then we need to split it and create two projects, one for the Windows folder and one for the WindowsPhone folder
      if(project.platforms[0].platform.indexOf('windows8') >= 0 && project.platforms.length > 1) {
        projects = splitWindows8Projects(project);
      } else {
        projects.push(project);
      }
      return mapSeries(projects, exportAppConfiguration, serverDetails, opts.windows, mfpConfig);

    })
    .then(function () {
      d.resolve();
    })
    .catch(function (err) {
      log.silly(moduleName, 'Failed to get details of exporting application. Reason: ' + err);
      if (err.description) {
        strings.fatal(err.description);
      } else {
        strings.fatal(strings.format(messages.cmdFailed, err));
      }      
      d.reject(err);       
    });

    return d.promise;
};

function exportAppConfiguration(project, serverDetails, cordovaWindowsPlatforms, config) {

  var d = Q.defer();
  var appName;
  var appInfo;
  var registerPromises = [];
  var parsingDefer = Q.defer();

  //We have either multiple Cordova project that all use the config.xml to parse for info or we have only one platform (ios, android, etc) that will be at index 0.
  //Nested .then was needed because the error throw by parsePlatform had to be explicitly caught
  platformManager.parsePlatform(project.platforms[0].platformId, project.rootDir)
  .then(function (platformInfo) {

    parsingDefer.resolve(platformInfo);
  }, function () {
    log.silly(moduleName, 'Failed to parse info from project file.');
    parsingDefer.resolve(
      {
        'app.dotId': '',
        'app.version': '',
        'app.name': ''
    });
  });
  //Received back parsed information (if any) and are extracting the appName
  //Prompt the user if any information was missing in the parsed data, if all properties needed are present then it will
  //pass through the module
  parsingDefer.promise.then(function (platformInfo) {
    //Use platform info to try and not have to prompt user for info.
    appName = platformInfo['app.name'] !== '' ? platformInfo['app.name'] : project.platforms[0].appName;
    return promptUserForMissingInfo(platformInfo, project.platforms[0].platform);
  })
  //Received back the prompted data (if any). Delegates out to build/update the property file for current platform
  //Cordova -> updates config.xml, IOS -> builds/updates mfpclient.plist, Android&Windows -> builds/updates mfpclient.properties
  .then(function (retPromptedInfo) {
    var finalConfigJson = {};
    appInfo = retPromptedInfo;
    appInfo.name = appName;

    var filteredPlatforms = project.platforms;
    //If --windows flag was set then remove the Windows platform that were not specified by it
    if(cordovaWindowsPlatforms && project.isCordova) {
      filteredPlatforms = removeNonSpecifiedWindows(cordovaWindowsPlatforms, project.platforms);
    }
    //Call register for each platform
    filteredPlatforms.forEach(function (platform) {
      registerPromises.push(serverSupport.getApplication(serverDetails.serverProfile, appInfo['app.dotId'], platform.platform, appInfo['app.version']));
    });

    Q.allSettled(registerPromises)
    .then(function (results) {      
      var platforms = [];
      results.forEach(function (result) {
        if (result.state === "fulfilled") {
          if(result.value.descriptor) {
            platforms.push(result.value.descriptor);
          }
        } else {
          if (result.reason) {
            strings.warn(result.reason);
          }
        }
      });
      if (platforms.length > 0) {
        finalConfigJson.platforms = platforms;
      }
      return serverSupport.getConfidentialClients(serverDetails.serverProfile);
    }).then(function (result) {
      var clients = [];
      result.clients.forEach(function(client) {
        if (client.customconfclient) {
          delete client.customconfclient;
        }
        clients.push(client);
      }); 
      if (clients.length > 0) {
        finalConfigJson.confidentialClients = clients;
      }      
      return services.getServices(serverDetails.serverProfile, appInfo['app.dotId'])
    }).then(function (services) {
      if (services && services.length > 0) {
        finalConfigJson.services = services;
      }      
      if (!config.mfpConfigPath) {
        config.mfpConfigPath = path.join(process.cwd(), 'mfpconfig');
        fs.mkdirSync(config.mfpConfigPath);
      }
      fs.writeFileSync(path.join(config.mfpConfigPath, 'app_config.json'), JSON.stringify(finalConfigJson, null, 2));
      strings.success(strings.format(messages.exportedSuccess, appInfo.name));
      d.resolve();
    }).catch(function (err) {
      strings.fatal(err.description);
      d.reject(err);
    });
  })
  //Print out if any promises above had errors
  .catch(function (err) {
    strings.fatal(err.description);
    d.reject(err);
  });

  return d.promise;
}

//Function to split the native Windows8 projects, in a Windows8 project there can be a Windows folder and a WindowsPhone,
//These two folders are treated as seperate projects in 8.0, so we need to split them so register can run for each project type.
//This is not the same as Cordova because for Cordova they share a file that is parsed (config.xml) and they don't generate a mfpclient file
function splitWindows8Projects(project) {
  var projects = [];
  for(var i = 0; i < project.platforms.length; i++) {
    var projectObj = {};
    for(var key in project) {
      if(key !== 'platforms') {
        projectObj[key] = project[key];
      }
    }
    //Replace rootDir with path to specific windows folder (Windows or WindowsPhone)
    projectObj.rootDir = project.platforms[i].windowsEnvPath;
    projectObj.platforms = [];
    projectObj.platforms.push(project.platforms[i]);
    projects.push(projectObj);
  }

  return projects;
}

/**This function is for the --windows flag.  It removes the other windows platforms that weren't specified by the --windows flag
--windows takes a comma separated list, valid entries are windows,windows8,windowsphone8.
  Ex.  mfpdev app webupdate --windows windows,windows8  , this would cause the removal of the windowsphone8 platform
**/
function removeNonSpecifiedWindows(windowsPlats, platforms) {
  var filteredPlatforms = [];
  var invalid = false;

  var windowsPlatforms = windowsPlats.split(',');
  //If provided an invalid arg then just ignore the --windows command
  windowsPlatforms.forEach(function (windows) {
    if(windows !== 'windows8' && windows !== 'windowsphone8' && windows !== 'windows') {
      strings.warn(strings.format(messages.invalidWindowsArgument, windows));
      invalid = true;
    }
  });

  if(!invalid) {
    //Check for each platform if it is present in our --windows arguments or that it is not a Windows platform type
    platforms.forEach(function (platform) {
      if(windowsPlatforms.indexOf(platform.platform) >= 0 || platform.platform.indexOf('windows') < 0) {
        filteredPlatforms.push(platform);
      }
    });
    return filteredPlatforms;
  } else {
    return platforms;
  }
}

function createServerProfileUsingConfig(server, mfpConfig)  {
  var d = Q.defer();

  if (server) {
    d.resolve(server);
  } else {
    if (mfpConfig.serverJsonPath) {              
      var serverData = fs.readFileSync(mfpConfig.serverJsonPath);
      const serverJson = JSON.parse(serverData);
      if (serverJson.mfp && serverJson.mfp.name) {
        profileIO.getServerProfiles(false, false).then(function (profiles) {
          if (profiles.servers[serverJson.mfp.name] === undefined) {
            log.verbose(moduleName, 'Server profile ' + serverJson.mfp.name + ' not found.');
            const serverCred = serverJson.mfp.servercred.split(':');
            profiles.servers[serverJson.mfp.name] = {
              'protocol': serverJson.mfp.protocol,
              'host': serverJson.mfp.host,
              'port': serverJson.mfp.port,
              'path': serverDefaults.PATH,
              'login': serverCred[0],
              'password': Buffer.alloc(serverCred[1].length, serverCred[1]).toString('base64'),
              'contextRoot': serverDefaults.CONTEXT_ROOT,
              'timeout': serverDefaults.TIMEOUT_SECONDS
            }; 
            profileIO.writeServerProfiles(profiles).then(function () {
              log.verbose(moduleName, 'Server profile ' + serverJson.mfp.name + ' added successfully.');
              d.resolve(serverJson.mfp.name);
            }).fail(function (error) {
              d.reject(error);
            });                      
          } else {
            log.verbose(moduleName, 'Server profile ' + serverJson.mfp.name + ' already exists.');
            d.resolve(serverJson.mfp.name);
          }     
        }).catch(function (error) {
          d.reject(error);
        });
      } else {
        d.resolve(server);
      }
    } else {
      d.resolve(server);
    } 
  }

  return d.promise;
}

function detectMfpConfig() {
  const mfpConfig = {};
  var mfpConfigPath = findup('mfpconfig');
  if (mfpConfigPath) {
    mfpConfig.mfpConfigPath = mfpConfigPath;
    var serverJsonPath = path.join(mfpConfigPath, 'server_config.json');
    if (fs.existsSync(serverJsonPath)) {
      mfpConfig.serverJsonPath = serverJsonPath;
    }        
  }
  return mfpConfig;
}