/*****************************************************************************\
|  █████  █████ ██    █ █████ █████   ████  ██      ████   █████ █████  ███ ® |
| ██      █     ███   █ █     ██  ██ ██  ██ ██      ██  █ ██   ██  █   █      |
| ██  ███ ████  █ ██  █ ████  █████  ██████ ██      ████   █   █   █    ██    |
| ██   ██ █     █  ██ █ █     ██  ██ ██  ██ ██      ██  █ ██   ██  █      █   |
|  █████  █████ █   ███ █████ ██  ██ ██  ██ █████   ████   █████   █   ███    |
|                                                                             |
| General Bots Copyright (c) pragmatismo.cloud. All rights reserved.         |
| Licensed under the AGPL-3.0.                                                |
|                                                                             |
| According to our dual licensing model, this program can be used either      |
| under the terms of the GNU Affero General Public License, version 3,        |
| or under a proprietary license.                                             |
|                                                                             |
| The texts of the GNU Affero General Public License with an additional       |
| permission and of our proprietary license can be found at and               |
| in the LICENSE file you have received along with this program.              |
|                                                                             |
| This program is distributed in the hope that it will be useful,             |
| but WITHOUT ANY WARRANTY, without even the implied warranty of              |
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the                |
| GNU Affero General Public License for more details.                         |
|                                                                             |
| "General Bots" is a registered trademark of pragmatismo.cloud.             |
| The licensing of the program under the AGPLv3 does not imply a              |
| trademark license. Therefore any rights, title and interest in              |
| our trademarks remain entirely with us.                                     |
|                                                                             |
\*****************************************************************************/

/**
 * @fileoverview General Bots server core.
 */

'use strict';

import urlJoin from 'url-join';
import { HttpMethods, ServiceClient, TokenCredentials, WebResource } from '@azure/ms-rest-js';
import { CognitiveServicesManagementClient } from '@azure/arm-cognitiveservices';
import { ResourceManagementClient } from '@azure/arm-resources';
import { SubscriptionClient } from '@azure/arm-subscriptions';
import { SearchManagementClient } from '@azure/arm-search';
import { Server, SqlManagementClient } from '@azure/arm-sql';
import { WebSiteManagementClient } from '@azure/arm-appservice';
import { AppServicePlan, Site, SiteLogsConfig, SiteSourceControl } from '@azure/arm-appservice';
import { GBLog, IGBInstallationDeployer, IGBInstance, IGBDeployer, IGBCoreService } from 'botlib';
import { GBAdminService } from '../../../packages/admin.gbapp/services/GBAdminService.js';
import { GBCorePackage } from '../../../packages/core.gbapp/index.js';
import { GBConfigService } from '../../../packages/core.gbapp/services/GBConfigService.js';
import { GBDeployer } from '../../../packages/core.gbapp/services/GBDeployer.js';
import { Account } from '@azure/arm-cognitiveservices';
import MicrosoftGraph from '@microsoft/microsoft-graph-client';
import { Spinner } from 'cli-spinner';
import * as publicIp from 'public-ip';
import { AccessToken, TokenCredential } from '@azure/core-auth';
import { GBUtil } from '../../../src/util.js';
import { GBLogEx } from '../../core.gbapp/services/GBLogEx.js';

const WebSiteResponseTimeout = 900;
const iconUrl = 'https://github.com/pragmatismo-io/BotServer/blob/master/docs/images/generalbots-logo-squared.png';
/**
 * Deployer for Microsoft cloud.
 */
export class AzureDeployerService implements IGBInstallationDeployer {
  public apiVersion = '2017-12-01';
  public defaultEndPoint = 'http://localhost:4242';
  public instance: IGBInstance;
  public cloud: ResourceManagementClient;
  public webSiteClient: WebSiteManagementClient;
  public storageClient: SqlManagementClient;
  public cognitiveClient: CognitiveServicesManagementClient;
  public searchClient: SearchManagementClient;
  public provider = 'Microsoft.BotService';
  public subscriptionClient: SubscriptionClient;
  public accessToken: string;
  public location: string;
  public subscriptionId: string;
  public farmName: any;
  public deployer: IGBDeployer;
  public core: IGBCoreService;
  private freeTier: boolean;

  public async runSearch(instance: IGBInstance) {
    await this.deployer.rebuildIndex(instance, this.getKBSearchSchema(instance.searchIndex));
  }


  public static async createInstance(deployer: GBDeployer, freeTier: boolean = true): Promise<AzureDeployerService> {
    const username = GBConfigService.get('CLOUD_USERNAME');
    const password = GBConfigService.get('CLOUD_PASSWORD');
    const subscriptionId = GBConfigService.get('CLOUD_SUBSCRIPTIONID');
    return await this.createInstanceWithCredentials(deployer, freeTier, subscriptionId, username, password);
  }

  public static async createInstanceWithADALCredentials(deployer: GBDeployer, freeTier: boolean = true,
    subscriptionId: string, credentials): Promise<AzureDeployerService> {
    const service = new AzureDeployerService();

    service.core = deployer.core;
    service.deployer = deployer;
    service.freeTier = freeTier;

    const token = credentials['tokenCache']._entries[0];

    await service.initServices(token.accessToken, token.expiresOn, subscriptionId);

    return service;
  }

  public static async createInstanceWithCredentials(deployer: GBDeployer, freeTier: boolean = true,
    subscriptionId: string, username: string, password: string): Promise<AzureDeployerService> {
    const credentials = await GBAdminService.getADALCredentialsFromUsername(username, password);
    return await this.createInstanceWithADALCredentials(deployer, freeTier, subscriptionId, credentials);
  }

  private static createRequestObject(url: string, accessToken: string, verb: HttpMethods, body: string) {
    const req = new WebResource();
    req.method = verb;
    req.url = url;
    req.headers.set('Content-Type', 'application/json');
    req.headers.set('accept-language', '*');
    req.headers.set('Authorization', `Bearer ${accessToken}`);
    req.body = body;

    return req;
  }

  public async getSubscriptions(credentials) {
    const subscriptionClient = new SubscriptionClient(credentials);

    return subscriptionClient.subscriptions.list();
  }

  public getKBSearchSchema(indexName: any) {
    return {
      name: indexName,
      fields: [
        {
          name: 'questionId',
          type: 'Edm.String',
          searchable: false,
          filterable: false,
          retrievable: true,
          sortable: false,
          facetable: false,
          key: true
        },
        {
          name: 'subject1',
          type: 'Edm.String',
          searchable: true,
          filterable: false,
          retrievable: false,
          sortable: false,
          facetable: false,
          key: false
        },
        {
          name: 'subject2',
          type: 'Edm.String',
          searchable: true,
          filterable: false,
          retrievable: false,
          sortable: false,
          facetable: false,
          key: false
        },
        {
          name: 'subject3',
          type: 'Edm.String',
          searchable: true,
          filterable: false,
          retrievable: false,
          sortable: false,
          facetable: false,
          key: false
        },
        {
          name: 'subject4',
          type: 'Edm.String',
          searchable: true,
          filterable: false,
          retrievable: false,
          sortable: false,
          facetable: false,
          key: false
        },
        {
          name: 'content',
          type: 'Edm.String',
          searchable: true,
          filterable: false,
          retrievable: false,
          sortable: false,
          facetable: false,
          key: false
        },
        {
          name: 'answerId',
          type: 'Edm.Int32',
          searchable: false,
          filterable: false,
          retrievable: true,
          sortable: false,
          facetable: false,
          key: false
        },
        {
          name: 'instanceId',
          type: 'Edm.Int32',
          searchable: false,
          filterable: true,
          retrievable: true,
          sortable: false,
          facetable: false,
          key: false
        },
        {
          name: 'skipIndex',
          type: 'Edm.Boolean',
          searchable: false,
          filterable: true,
          retrievable: true,
          sortable: false,
          facetable: false,
          key: false
        },
        {
          name: 'packageId',
          type: 'Edm.Int32',
          searchable: false,
          filterable: true,
          retrievable: true,
          sortable: false,
          facetable: false,
          key: false
        }
      ],
      scoringProfiles: [],
      defaultScoringProfile: undefined,
      corsOptions: undefined
    };
  }

  public async botExists(botId: string) {
    const baseUrl = `https://management.azure.com/`;
    const username = GBConfigService.get('CLOUD_USERNAME');
    const password = GBConfigService.get('CLOUD_PASSWORD');

    const accessToken = await GBAdminService.getADALTokenFromUsername(username, password);
    const httpClient = new ServiceClient();

    const query = `providers/${this.provider}/checkNameAvailability/Action?api-version=${this.apiVersion}`;

    const url = urlJoin(baseUrl, query);
    const body = {
      name: botId,
      type: 'botServices'
    };

    const req = AzureDeployerService.createRequestObject(url, accessToken, 'POST', JSON.stringify(body));
    const res = await httpClient.sendRequest(req);

    return !res.parsedBody.valid;
  }

  public async updateBotProxy(botId: string, group: string, endpoint: string) {
    if (!await this.botExists(botId)) {
      GBLog.error(`Bot ${botId} does not exist on cloud.`);

      return;
    }
    const baseUrl = `https://management.azure.com/`;
    const username = GBConfigService.get('CLOUD_USERNAME');
    const password = GBConfigService.get('CLOUD_PASSWORD');
    const subscriptionId = GBConfigService.get('CLOUD_SUBSCRIPTIONID');

    const accessToken = await GBAdminService.getADALTokenFromUsername(username, password);
    const httpClient = new ServiceClient();

    const parameters = {
      properties: {
        endpoint: endpoint
      }
    };

    const query = `subscriptions/${subscriptionId}/resourceGroups/${group}/providers/${this.provider}/botServices/${botId}?api-version=${this.apiVersion}`;
    const url = urlJoin(baseUrl, query);
    const req = AzureDeployerService.createRequestObject(url, accessToken, 'PATCH', JSON.stringify(parameters));
    const res = await httpClient.sendRequest(req);
    // CHECK
    if (!JSON.parse(res.bodyAsText).id) {
      throw res.bodyAsText;
    }
    GBLogEx.info(0, `Bot proxy updated at: ${endpoint}.`);
  }

  public async updateBot(botId: string, group: string, name: string, description: string, endpoint: string) {
    const baseUrl = `https://management.azure.com/`;
    const username = GBConfigService.get('CLOUD_USERNAME');
    const password = GBConfigService.get('CLOUD_PASSWORD');
    const subscriptionId = GBConfigService.get('CLOUD_SUBSCRIPTIONID');

    const accessToken = await GBAdminService.getADALTokenFromUsername(username, password);
    const httpClient = new ServiceClient();

    const parameters = {
      properties: {
        description: `${description}`,
        displayName: name,
        endpoint: endpoint,
        iconUrl: iconUrl
      }
    };

    const query = `subscriptions/${subscriptionId}/resourceGroups/${group}/providers/${this.provider}/botServices/${botId}?api-version=${this.apiVersion}`;
    const url = urlJoin(baseUrl, query);
    const req = AzureDeployerService.createRequestObject(url, accessToken, 'PATCH', JSON.stringify(parameters));
    const res = await httpClient.sendRequest(req);
    // CHECK
    if (!JSON.parse(res.bodyAsText).id) {
      throw res.bodyAsText;
    }
    GBLogEx.info(0, `Bot updated at: ${endpoint}.`);
  }

  public async deleteBot(botId: string, group: string) {
    const baseUrl = `https://management.azure.com/`;
    const username = GBConfigService.get('CLOUD_USERNAME');
    const password = GBConfigService.get('CLOUD_PASSWORD');
    const subscriptionId = GBConfigService.get('CLOUD_SUBSCRIPTIONID');

    const accessToken = await GBAdminService.getADALTokenFromUsername(username, password);
    const httpClient = new ServiceClient();

    const query = `subscriptions/${subscriptionId}/resourceGroups/${group}/providers/${this.provider}/botServices/${botId}?api-version=${this.apiVersion}`;
    const url = urlJoin(baseUrl, query);
    const req = AzureDeployerService.createRequestObject(url, accessToken, 'DELETE', undefined);
    const res = await httpClient.sendRequest(req);

    if (res.bodyAsText !== '') {
      throw res.bodyAsText;
    }
    GBLogEx.info(0, `Bot ${botId} was deleted from the provider.`);
  }

  public async openStorageFirewall(groupName: string, serverName: string) {
    const subscriptionId = GBConfigService.get('CLOUD_SUBSCRIPTIONID');

    const ip = await publicIp.publicIpv4();
    let params = {
      startIpAddress: ip,
      endIpAddress: ip
    };
    await this.storageClient.firewallRules.createOrUpdate(groupName, serverName, 'gb', params);
  }

  public async deployFarm(
    proxyAddress: string,
    instance: IGBInstance,
    credentials: any,
    subscriptionId: string
  ): Promise<IGBInstance> {
    return await this.deployFarm2(proxyAddress,
      instance,
      credentials,
      subscriptionId);
  }

  public async deployFarm2(
    proxyAddress: string,
    instance: IGBInstance,
    credentials: any,
    subscriptionId: string
  ) {
    const culture = 'en-us';

    const token = credentials['tokenCache']._entries[0];

    await this.initServices(token.accessToken, token.expiresOn, subscriptionId);

    const spinner = new Spinner('%s');
    spinner.start();
    spinner.setSpinnerString('|/-\\');
    let keys: any;
    const name = instance.botId;

    GBLogEx.info(0, `Enabling resource providers...`);

    await this.enableResourceProviders('Microsoft.CognitiveServices');
    await this.enableResourceProviders('Microsoft.BotService');
    await this.enableResourceProviders('Microsoft.Search');
    await this.enableResourceProviders('Microsoft.Web');
    await this.enableResourceProviders('Microsoft.Sql');

    GBLogEx.info(0, `Deploying Deploy Group (It may take a few minutes)...`);
    await this.createDeployGroup(name, instance.cloudLocation);

    let serverFarm;
    let serverName;

    if (process.env.DEPLOY_WEB) {
      GBLogEx.info(0, `Deploying Bot Server...`);
      serverFarm = await this.createHostingPlan(name, `${name}-server-plan`, instance.cloudLocation);
      serverName = `${name}-server`;
      await this.createServer(serverFarm.id, name, serverName, instance.cloudLocation);
    }

    GBLogEx.info(0, `Deploying Bot Storage...`);
    const administratorLogin = `sa${GBAdminService.getRndReadableIdentifier()}`;
    const administratorPassword = GBAdminService.getRndPassword();
    const storageServer = `${name.toLowerCase()}-storage-server`;
    const storageName = `${name}-storage`;
    await this.createStorageServer(
      name,
      storageServer,
      administratorLogin,
      administratorPassword,
      storageServer,
      instance.cloudLocation
    );
    await this.createStorage(name, storageServer, storageName, instance.cloudLocation);
    instance.storageUsername = administratorLogin;
    instance.storagePassword = administratorPassword;
    instance.storageName = storageName;
    instance.storageDialect = 'mssql';
    instance.storageServer = `${storageServer}.database.windows.net`;

    // TODO: Enable in .env
    // GBLogEx.info(min, `Deploying Search...`);
    // const searchName = `${name}-search`.toLowerCase();
    // await this.createSearch(name, searchName, instance.cloudLocation);
    // const searchKeys = await this.searchClient.adminKeys.get(name, searchName);
    // instance.searchHost = `${searchName}.search.windows.net`;
    // instance.searchIndex = 'azuresql-index';
    // instance.searchIndexer = 'azuresql-indexer';
    // instance.searchKey = searchKeys.primaryKey;

    // GBLogEx.info(min, `Deploying Speech...`);
    // const speech = await this.createSpeech(name, `${name}speech`, instance.cloudLocation);
    // keys = await this.cognitiveClient.accounts.listKeys(name, speech.name);
    // instance.speechEndpoint = speech.properties.endpoint;
    // instance.speechKey = keys.key1;

    // GBLogEx.info(min, `Deploying Text Analytics...`);
    // const textAnalytics = await this.createTextAnalytics(name, `${name}-textanalytics`, instance.cloudLocation);
    // instance.textAnalyticsEndpoint = textAnalytics.properties.endpoint.replace(`/text/analytics/v2.0`, '');

    GBLogEx.info(0, `Deploying SpellChecker...`);
    const spellChecker = await this.createSpellChecker(name, `${name}-spellchecker`);
    instance.spellcheckerEndpoint = spellChecker.properties.endpoint;

    // GBLogEx.info(min, `Deploying NLP...`);
    // const nlp = await this.createNLP(name, `${name}-nlp`, instance.cloudLocation);
    // const nlpa = await this.createNLPAuthoring(name, `${name}-nlpa`, instance.cloudLocation);
    // instance.nlpEndpoint = nlp.properties.endpoint;


    GBLogEx.info(0, `Deploying Bot...`);
    instance.botEndpoint = 'TODO: remove this column.';

    instance = await this.internalDeployBot(
      instance,
      this.accessToken,
      name,
      name,
      name,
      'General BootBot',
      `${proxyAddress}/api/messages/${name}`,
      'global',
      instance.nlpAppId,
      instance.nlpKey,
      instance.marketplaceId,
      instance.marketplacePassword,
      instance.cloudSubscriptionId
    );

    GBLogEx.info(0, `Waiting one minute to finish NLP service and keys creation...`);
    await GBUtil.sleep(60000);
    // keys = await this.cognitiveClient.accounts.listKeys(name, textAnalytics.name);
    // instance.textAnalyticsKey = keys.key1;
    keys = await this.cognitiveClient.accounts.listKeys(name, spellChecker.name);
    instance.spellcheckerKey = keys.key1;
    // let authoringKeys = await this.cognitiveClient.accounts.listKeys(name, nlpa.name);
    // keys = await this.cognitiveClient.accounts.listKeys(name, nlp.name);
    // instance.nlpKey = keys.key1;

    // instance.nlpAuthoringKey = authoringKeys.key1;
    // const nlpAppId = await this.createNLPService(name, name, instance.cloudLocation, culture, instance.nlpAuthoringKey);
    // instance.nlpAppId = nlpAppId;

    if (process.env.DEPLOY_WEB) {
      GBLogEx.info(0, 'Updating server environment variables...');
      await this.updateWebisteConfig(name, serverName, serverFarm.id, instance);
    }
    spinner.stop();

    return instance;
  }

  public async deployToCloud(
    title: string,
    username: string,
    password: string,
    cloudLocation: string,
    authoringKey: string,
    appId: string,
    appPassword: string,
    subscriptionId: string
  ) {
    const instance = <IGBInstance>{};
    instance.state = 'active';
    instance.botId = title;
    instance.cloudUsername = username;
    instance.cloudPassword = password;
    instance.cloudSubscriptionId = subscriptionId;
    instance.cloudLocation = cloudLocation;
    instance.nlpAuthoringKey = authoringKey;
    instance.marketplaceId = appId;
    instance.marketplacePassword = appPassword;
    instance.adminPass = GBAdminService.getRndPassword();

    const credentials = await GBAdminService.getADALCredentialsFromUsername(username, password);
    // tslint:disable-next-line:no-http-string
    const url = `https://${instance.botId}.azurewebsites.net`;
    return await this.deployFarm(url, instance, credentials, subscriptionId);
  }

  /**
   * @see https://github.com/Azure/azure-rest-api-specs/blob/master/specification/botservice/resource-manager/Microsoft.BotService/preview/2017-12-01/botservice.json
   */
  public async internalDeployBot(
    instance,
    accessToken: string,
    botId: string,
    name: string,
    group,
    description: string,
    endpoint,
    location,
    nlpAppId,
    nlpKey: string,
    appId: string,
    appPassword: string,
    subscriptionId
  ): Promise<IGBInstance> {
    return new Promise(async (resolve, reject) => {
      const baseUrl = `https://management.azure.com/`;
      await this.registerProviders(subscriptionId, baseUrl, accessToken);

      instance.marketplaceId = appId;
      instance.marketplacePassword = appPassword;
      instance.engineName = GBCorePackage['CurrentEngineName'];

      const parameters = {
        location: location,
        sku: {
          name: this.freeTier ? 'F0' : 'S1'
        },
        name: botId,
        kind: 'bot',
        properties: {
          description: description,
          displayName: name,
          endpoint: endpoint,
          iconUrl: iconUrl,
          luisAppIds: [nlpAppId],
          luisKey: nlpKey,
          msaAppId: appId,
          msaAppPassword: appPassword
        }
      };

      const httpClient = new ServiceClient();
      let query = `subscriptions/${subscriptionId}/resourceGroups/${group}/providers/${this.provider}/botServices/${botId}?api-version=${this.apiVersion}`;
      let url = urlJoin(baseUrl, query);
      let req = AzureDeployerService.createRequestObject(url, accessToken, 'PUT', JSON.stringify(parameters));
      const res = await httpClient.sendRequest(req);
      if (!JSON.parse(res.bodyAsText).id) {
        reject(res.bodyAsText);

        return;
      }
      

      await GBUtil.sleep(30000);

      // MSFT has changed without warnings. 

      try {

        //tslint:disable-next-line:max-line-length.
        
        query = `subscriptions/${subscriptionId}/resourceGroups/${group}/providers/Microsoft.BotService/botServices/${botId}/channels/WebChatChannel/listChannelWithKeys?api-version=${this.apiVersion}`;
        url = urlJoin(baseUrl, query);
        req = AzureDeployerService.createRequestObject(url, accessToken, 'POST', JSON.stringify(parameters));
        const resChannel = await httpClient.sendRequest(req);
        const key = JSON.parse(resChannel.bodyAsText).properties.properties.sites[0].key;
        instance.webchatKey = key;

        resolve(instance);
      } catch (error) {
        reject(error);
      }
    });
  }

  public async syncBotServerRepository(group: string, name: string) {
    await this.webSiteClient.webApps.syncRepository(group, name);
  }

  public async initServices(accessToken: string, expiresOnTimestamp, subscriptionId: string) {
    this.accessToken = accessToken;
    class AccessToken2 implements AccessToken {
      public expiresOnTimestamp: number;
      public token: string;
    }

    class StaticAccessToken implements TokenCredential {
      public getToken(): Promise<AccessToken> {
        return new Promise<AccessToken>(async (resolve, reject) => {
          const t = new AccessToken2();
          t.token = accessToken;
          t.expiresOnTimestamp = expiresOnTimestamp;
          resolve(t);
        });
      }
    }

    const token = new StaticAccessToken();

    this.cloud = new ResourceManagementClient(token, subscriptionId);
    this.webSiteClient = new WebSiteManagementClient(token, subscriptionId);
    this.storageClient = new SqlManagementClient(token, subscriptionId);
    this.cognitiveClient = new CognitiveServicesManagementClient(token, subscriptionId);
    this.searchClient = new SearchManagementClient(token, subscriptionId);
  }

  private async createStorageServer(
    group: string,
    name: string,
    administratorLogin: string,
    administratorPassword: string,
    serverName: string,
    location: string
  ) {
    const params = {
      location: location,
      administratorLogin: administratorLogin,
      administratorLoginPassword: administratorPassword,
      fullyQualifiedDomainName: serverName,
      requestOptions: { timeout: 60 * 1000 * 5 }
    };

    let database: Server;
    try {
      database = await this.storageClient.servers.beginCreateOrUpdateAndWait(group, name, params);
    } catch (error) {
      // Try again (MSFT issues).
      GBLogEx.info(0, 'Storage (server) creation failed. Retrying...');
      database = await this.storageClient.servers.beginCreateOrUpdateAndWait(group, name, params);
    }

    // AllowAllWindowsAzureIps must be created that way, so the Azure Search can
    // access SQL Database to index its contents.

    const paramsFirewall = {
      startIpAddress: '0.0.0.0',
      endIpAddress: '0.0.0.0'
    };
    await this.storageClient.firewallRules.createOrUpdate(group, name, 'AllowAllWindowsAzureIps', paramsFirewall);

    return database;
  }

  public async createApplication(token: string, name: string) {
      let client = MicrosoftGraph.Client.init({
        authProvider: done => {
          done(null, token);
        }
      });
      let app:any = {
        displayName: name,
        signInAudience: "AzureADandPersonalMicrosoftAccount",
      };

      const res = await client.api(`/applications`).post(app);

      return  res;
  }

  public async createApplicationSecret(token: string, appId: string) {
    return new Promise<string>((resolve, reject) => {
      let client = MicrosoftGraph.Client.init({
        authProvider: done => {
          done(null, token);
        }
      });
      const body = {
        passwordCredential: {
          displayName: 'General Bots Generated'
        }
      };

      client.api(`/applications/${appId}/addPassword`).post(body, (err, res) => {
        if (err) {
          reject(err);
        } else {
          resolve(res.secretText);
        }
      });
    });
  }

  private async registerProviders(subscriptionId: string, baseUrl: string, accessToken: string) {
    const query = `subscriptions/${subscriptionId}/providers/${this.provider}/register?api-version=2018-02-01`;
    const requestUrl = urlJoin(baseUrl, query);

    const req = new WebResource();
    req.method = 'POST';
    req.url = requestUrl;
    req.headers = <any>{};
    req.headers['Content-Type'] = 'application/json; charset=utf-8';
    req.headers['accept-language'] = '*';
    (req.headers as any).Authorization = `Bearer ${accessToken}`;
  }

  private async createNLPService(
    name: string,
    description: string,
    location: string,
    culture: string,
    authoringKey: string
  ) {
    const parameters = {
      name: name,
      description: description,
      culture: culture
    };

    const body = JSON.stringify(parameters);
    const apps = await this.makeNlpRequest(location, authoringKey, undefined, 'GET', 'apps');

    let app = null;
    if (apps.bodyAsText && apps.bodyAsText !== '[]') {
      const result = JSON.parse(apps.bodyAsText);
      if (result.error) {
        if (result.error.code !== '401') {
          throw new Error(result.error);
        }
      } else {
        app = result.filter(x => x.name === name)[0];
      }
    }
    let id: string;
    if (!app) {
      const res = await this.makeNlpRequest(location, authoringKey, body, 'POST', 'apps');
      id = res.bodyAsText;
    } else {
      id = app.id;
    }

    return id.replace(/\'/gi, '');
  }

  private async makeNlpRequest(
    location: string,
    authoringKey: string,
    body: string,
    method: HttpMethods,
    resource: string
  ) {
    const req = new WebResource();
    req.method = method;
    req.url = `https://${location}.api.cognitive.microsoft.com/luis/api/v2.0/${resource}`;
    req.headers.set('Content-Type', 'application/json');
    req.headers.set('accept-language', '*');
    req.headers.set('Ocp-Apim-Subscription-Key', authoringKey);
    req.body = body;
    const httpClient = new ServiceClient();

    return await httpClient.sendRequest(req);
  }

  public async refreshEntityList(location: string, nlpAppId: string, clEntityId: string, nlpKey: string, data: any) {
    const req = new WebResource();
    req.method = 'PUT';
    req.url = `https://${location}.api.cognitive.microsoft.com/luis/api/v2.0/apps/${nlpAppId}/versions/0.1/closedlists/${clEntityId}`;
    req.headers.set('Content-Type', 'application/json');
    req.headers.set('accept-language', '*');
    req.headers.set('Ocp-Apim-Subscription-Key', nlpKey);
    req.body = JSON.stringify(data);
    const httpClient = new ServiceClient();

    return await httpClient.sendRequest(req);
  }

  public async trainNLP(location: string, nlpAppId: string, nlpAuthoringKey: string) {
    const req = new WebResource();
    req.method = 'POST';
    req.url = `https://${location}.api.cognitive.microsoft.com/luis/api/v2.0/apps/${nlpAppId}/versions/0.1/train`;
    req.headers.set('Content-Type', 'application/json');
    req.headers.set('accept-language', '*');
    req.headers.set('Ocp-Apim-Subscription-Key', nlpAuthoringKey);
    const httpClient = new ServiceClient();

    return await httpClient.sendRequest(req);
  }

  public async publishNLP(location: string, nlpAppId: string, nlpAuthoringKey: string) {
    const body = {
      versionId: '0.1',
      isStaging: false,
      directVersionPublish: false
    };
    const req = new WebResource();
    req.method = 'POST';
    req.url = `https://${location}.api.cognitive.microsoft.com/luis/api/v2.0/apps/${nlpAppId}/publish`;
    req.headers.set('Content-Type', 'application/json');
    req.headers.set('accept-language', '*');
    req.headers.set('Ocp-Apim-Subscription-Key', nlpAuthoringKey);
    req.body = JSON.stringify(body);
    const httpClient = new ServiceClient();

    return await httpClient.sendRequest(req);
  }

  private async createSearch(group: string, name: string, location: string) {
    const params = {
      sku: {
        name: this.freeTier ? 'free' : 'standard'
      },
      location: location
    };

    return await this.searchClient.services.beginCreateOrUpdateAndWait(group, name, params as any);
  }

  private async createStorage(group: string, serverName: string, name: string, location: string) {
    const params = {
      sku: { name: 'Basic' },
      createMode: 'Default',
      location: location
    };

    let database;
    try {
      database = await this.storageClient.databases.beginCreateOrUpdateAndWait(group, serverName, name, params);
    } catch (error) {

      // Try again (MSFT issues).
      GBLogEx.info(0, 'Storage (database) creation failed. Retrying...');
      database = await this.storageClient.databases.beginCreateOrUpdateAndWait(group, serverName, name, params);
    }
    return database;
  }

  private async createCognitiveServices(group: string, name: string, location: string, kind: string): Promise<Account> {
    const params = {
      sku: {
        name: name
      },
      createMode: 'Default',
      location: location,
      kind: kind,
      properties: {}
    };

    if (kind === 'LUIS.Authoring' || kind === 'LUIS') {
      params.sku.name = this.freeTier ? 'F0' : 'S0';
    } else if (kind === 'TextAnalytics') {
      params.sku.name = this.freeTier ? 'F0' : 'S0';
    } else if (kind === 'Bing.SpellCheck.v7') {
      params.sku.name = this.freeTier ? 'S0' : 'S1';
    } else if (kind === 'CognitiveServices') {
      params.sku.name = 'S0';
    } else if (kind === 'SpeechServices') {
      params.sku.name = this.freeTier ? 'F0' : 'S0';
    }

    return await this.cognitiveClient.accounts.beginCreateAndWait(group, name, params);
  }

  private async createSpeech(group: string, name: string, location: string): Promise<Account> {
    return await this.createCognitiveServices(group, name, location, 'SpeechServices');
  }

  private async createNLP(group: string, name: string, location: string): Promise<Account> {
    return await this.createCognitiveServices(group, name, location, 'LUIS');
  }

  private async createNLPAuthoring(group: string, name: string, location: string): Promise<Account> {
    return await this.createCognitiveServices(group, name, location, 'LUIS.Authoring');
  }

  private async createSpellChecker(group: string, name: string): Promise<Account> {
    return await this.createCognitiveServices(group, name, 'westus', 'CognitiveServices');
  }

  private async createTextAnalytics(group: string, name: string, location: string): Promise<Account> {
    return await this.createCognitiveServices(group, name, location, 'TextAnalytics');
  }

  private async createDeployGroup(name: string, location: string) {
    const params = { location: location };

    return await this.cloud.resourceGroups.createOrUpdate(name, params);
  }

  private async enableResourceProviders(name: string) {
    const ret = await this.cloud.providers.get(name);
    if (ret.registrationState === 'NotRegistered') {
      await this.cloud.providers.register(name);
    }
  }

  private async createHostingPlan(group: string, name: string, location: string): Promise<AppServicePlan> {
    const params = {
      serverFarmWithRichSkuName: name,
      location: location,
      sku: {
        name: this.freeTier ? 'F1' : 'S1',
        capacity: 1,
        tier: this.freeTier ? 'Free' : 'Standard'
      }
    };

    return await this.webSiteClient.appServicePlans.beginCreateOrUpdateAndWait(group, name, params);
  }

  private async createServer(farmId: string, group: string, name: string, location: string) {
    let tryed = false;
    const create = async () => {
      const parameters: Site = {
        location: location,
        serverFarmId: farmId,

        siteConfig: {
          nodeVersion: GBAdminService.getNodeVersion(),
          detailedErrorLoggingEnabled: true,
          requestTracingEnabled: true
        }
      };
      const server = await this.webSiteClient.webApps.beginCreateOrUpdateAndWait(group, name, parameters);

      const siteLogsConfig: SiteLogsConfig = {
        applicationLogs: {
          fileSystem: { level: 'Error' }
        }
      };
      await this.webSiteClient.webApps.updateDiagnosticLogsConfig(group, name, siteLogsConfig);

      const souceControlConfig: SiteSourceControl = {
        repoUrl: 'https://github.com/GeneralBots/BotServer.git',
        branch: 'master',
        isManualIntegration: true,
        isMercurial: false,
        deploymentRollbackEnabled: false
      };

      await this.webSiteClient.webApps.beginCreateOrUpdateSourceControlAndWait(group, name, souceControlConfig);
      return server;
    };

    try {
      return await create();
    } catch (e) {
      if (!tryed) {
        tryed = true;
        GBLogEx.info(0, 'Retrying Deploying Bot Server...');
        try {
          return await create();
        } catch (error) {
          GBLogEx.info(0, 'Server creation failed at all on MSAzure, stopping...');
          throw error;
        }
      }
    }
  }

  private async updateWebisteConfig(group: string, name: string, serverFarmId: string, instance: IGBInstance) {
    const parameters: Site = {
      location: instance.cloudLocation,
      serverFarmId: serverFarmId,
      siteConfig: {
        appSettings: [
          { name: 'WEBSITES_CONTAINER_START_TIME_LIMIT', value: `${WebSiteResponseTimeout}` },
          { name: 'WEBSITE_NODE_DEFAULT_VERSION', value: GBAdminService.getNodeVersion() },
          { name: 'ADDITIONAL_DEPLOY_PATH', value: `` },
          { name: 'ADMIN_PASS', value: `${instance.adminPass}` },
          { name: 'BOT_ID', value: `${instance.botId}` },
          { name: 'CLOUD_SUBSCRIPTIONID', value: `${instance.cloudSubscriptionId}` },
          { name: 'CLOUD_LOCATION', value: `${instance.cloudLocation}` },
          { name: 'CLOUD_USERNAME', value: `${instance.cloudUsername}` },
          { name: 'CLOUD_PASSWORD', value: `${instance.cloudPassword}` },
          { name: 'MARKETPLACE_ID', value: `${instance.marketplaceId}` },
          { name: 'MARKETPLACE_SECRET', value: `${instance.marketplacePassword}` },
          { name: 'STORAGE_DIALECT', value: `${instance.storageDialect}` },
          { name: 'STORAGE_SERVER', value: `${instance.storageServer}.database.windows.net` },
          { name: 'STORAGE_NAME', value: `${instance.storageName}` },
          { name: 'STORAGE_USERNAME', value: `${instance.storageUsername}` },
          { name: 'STORAGE_PASSWORD', value: `${instance.storagePassword}` },
          { name: 'STORAGE_SYNC', value: `true` }
        ]
      }
    };

    return await this.webSiteClient.webApps.beginCreateOrUpdateAndWait(group, name, parameters);
  }
}
