/*
 * Copyright 2014-2021 Firestack, all rights reserved.
 */

import { IncomingHttpHeaders } from 'http';
import * as https from 'https';

import * as cookie from 'cookie';
import * as _ from 'lodash';
import * as soap from 'soap';
import * as uuid from 'uuid';

import { VsphereCaller } from './vsphere.caller';
import { ix } from 'ts-basis';
import { CustomHttpClient } from './soap.request';
import { VsphereDatacenter } from './vsphere';

type SoapMethodCallback<Params, Result> = (params: Params, callback: (
  err: Error | null,
  result: { returnval: Result; } | null,
  rawResponse: string,
  soapHeaders: Record<string, string>,
  rawRequest: string
) => void) => void;

export class VsphereConnection extends ix.Entity {
  readonly connId = uuid.v4();

  api: VsphereCaller;
  client: soap.Client;
  traffic = { up: 0, down: 0, baseOverhead: 200 };

  constructor(
    public $dc: VsphereDatacenter,
    public readonly url: string,
    public readonly options: soap.IOptions,
    private readonly connectionAllowed?: boolean
  ) {
    super('vcenter-conn');
    $dc.lifecycle.manage(this);
  }

  /**
   * Executes a SOAP command
   * @returns result and headers
   */
  async exec<Params, Result>(command: string, params: Params): Promise<{ result: Result; headers: IncomingHttpHeaders; }> {
    // remove "connection" members to avoid circular references
    const paramsClone = this.sanitizeParams(_.cloneDeep(params));
    // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
    const callback = this.client.VimService.VimPort[command] as SoapMethodCallback<Params, Result>;
    const result = await new Promise<Result>((resolve, reject) => {
      callback(paramsClone as unknown as Params, (err, res) => {
        if (err) {
          return reject(err);
        }
        if (res === null) {
          return reject(new Error('No result returned from vSphere'));
        }
        resolve(res.returnval);
      });
    });
    return { result, headers: this.client.lastResponseHeaders ?? {} };
  }

  async connect(): Promise<boolean> {
    this.$dc.debugOutput(`connecting... (connId=${this.connId})`);
    if (!this.connectionAllowed) {
      throw new Error(`Cannot connect to ${this.url}, connection not granted.`);
    }
    let url = this.url;
    if (!url.startsWith('https://') && !url.startsWith('http://')) {
      url = 'https://' + url;
    }
    if (!url.endsWith('/sdk/vimService.wsdl')) {
      if (url.endsWith('/')) {
        url += 'sdk/vimService.wsdl';
      } else {
        url += '/sdk/vimService.wsdl';
      }
    }
    return new Promise<boolean>(async resolve => {
      soap.createClientAsync(url, {
        endpoint: url,
        httpClient: new CustomHttpClient({
          sdkCache: this.$dc.vinfra.behavior.useSdkCache,
        }) as any,
      }, url).then(client => {
        this.client = client;
        this.client.setSecurity(new soap.ClientSSLSecurity(
          null, // key
          null, // cert
          null, // ca
          {
              strictSSL: true,
              forever: true,
              ca: https.globalAgent.options.ca
          }
        ));
        this.$dc.debugOutput(`client created (clientId=${this.connId})`);
        // tslint:disable-next-line: no-console
        this.client.on('request', (xml, eid) => {
          this.traffic.up += xml.length + this.traffic.baseOverhead;
        });
        this.client.on('response', (body, eid) => {
          if (body) {
            this.traffic.down += body.length + this.traffic.baseOverhead;
          }
          // console.log('res', body, eid);
        });
        this.client.on('soapError', (error, eid) => {
          if (error.body) {
            this.traffic.down += error.body.length + this.traffic.baseOverhead;
          }
          // console.log('soapE', eid);
        });
        this.api = new VsphereCaller(this.$dc, this.client);
        resolve(true);
      }).catch(e => {
        this.$dc.debugOutput(e);
        return resolve(false);
      });
    });
  }

  async login(username: string, password: string): Promise<boolean> {
    try {
      const res = await this.exec('Login', { _this: { $value: 'SessionManager' }, userName: username, password });
      // make sure we use VMWare's session cookie
      const header = res.headers['set-cookie'];
      if (header && header.length > 0) {
        const sessionId = cookie.parse(header[0]).vmware_soap_session;
        this.client.addHttpHeader('Cookie', cookie.serialize('vmware_soap_session', sessionId));
        return true;
      } else {
        return false;
      }
    } catch (err) {
      if (err instanceof Error && err.message.includes('Cannot complete login due to an incorrect user name or password.')) {
        throw new Error(`vSphere rejected login credentials for '${username}' target '${this.url}'`);
      }
      throw err;
    }
  }

  /** Recursively remove "connection" properties from an object */
  private sanitizeParams<Params>(params: Params): Params {
    (Object.keys(params) as (keyof Params)[]).forEach(key => {
      if (key === 'connection') {
        // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
        delete params[key];
      } else if (typeof params[key] === 'object') {
        this.sanitizeParams(params[key]);
      }
    });
    return params;
  }
}
