/*
 * Copyright 2014-2021 Firestack, all rights reserved.
 */
import * as soap from 'soap';
import { getMoTypeByName, iidParse } from './managed.objects';
import { VsphereDatacenter } from './vsphere';
import { ref, xsi, paramFilter, parseObjects } from './soap.parser';
import { ix, promise } from 'ts-basis';

export class VsphereCaller extends ix.Entity {
  $dc: VsphereDatacenter;

  topError: any = null;
  topErrorBody: string = '';
  topErrorMessage: string = '';
  topErrorDetail: string = '';
  errors: {data: any, t: number}[] = [];

  client: soap.Client;
  serviceContent: any;
  containerView: any;

  inventoryData: any;
  inventoryRaw: any;

  inventoryStubsData: any;
  inventoryStubsRaw: any;

  options = { forever: true, timeout: 30000 };

  constructor($dc: VsphereDatacenter, client) {
    super('vcenter-api');
    this.$dc = $dc;
    this.client = client;
    $dc.lifecycle.manage(this);
  }

  resetData() {
    this.serviceContent = null;
    this.containerView = null;
    this.inventoryData = null;
    this.inventoryRaw = null;
    this.inventoryStubsData = null;
    this.inventoryStubsRaw = null;
  }

  errorCodeHandle(message: string) {
    const dc = this.$dc;
    const has = part => message.indexOf(part) >= 0;
    if (has('ManagedObjectNotFoundFault')) {
      if (has('vim.view.ContainerView:session') &&
          has('has already been deleted or has not been completely created')) {
        // refetch default container view
        this.getDefaultContainerView(true);
        dc.emitEvent(dc.sessionNotValid$, { type: 'not_found' });
        return;
      } else {
        const moValue = message.split('</obj>')[0].split('>').pop();
        const iid = iidParse(moValue);
        dc.emitEvent(dc.entryNotFound$, { iid, value: moValue });
        return;
      }
    } else if (has('Unknown mo value session[')) {
      this.getDefaultContainerView(true);
      dc.emitEvent(dc.sessionNotValid$, { type: 'unknown_mo' });
      return;
    }
    return 'YES_DEBUG_OUTPUT';
  }

  resolveError(error, resolve) {
    this.topError = error;
    if (error.body) {
      this.topErrorBody = error.body;
      try {
        if (error.body.indexOf('<faultstring>') >= 0) {
          this.topErrorMessage = error.body.split('<faultstring>')[1].split('</faultstring>')[0];
          this.topErrorDetail = error.body.split('<detail>')[1].split('</detail>')[0];
          let doOutput = 'YES_DEBUG_OUTPUT';
          if (this.topErrorDetail) { doOutput = this.errorCodeHandle(this.topErrorDetail); }
          if (doOutput) { this.$dc.debugOutput(new Error(this.topErrorDetail)); }
        } else {
          let handled = false;
          if (error instanceof Error) {
            if (error.message === 'ESOCKETTIMEDOUT') {
              handled = true;
              this.$dc.debugOutput('request.js error code: ESOCKETTIMEDOUT');
            }
          } else {

          }
          if (!handled) {
            this.$dc.ix.pushError(new Error(`Unknown error: ${JSON.stringify(error)}`), 2);
          }
        }
      } catch (e) { this.$dc.ix.pushError(e); }
    } else {
      if (error instanceof Error) {
        this.$dc.ix.pushError(error, 2);
      } else {
        this.$dc.ix.pushError(new Error(`Unknown error: ${JSON.stringify(error)}`), 2);
      }
    }
    resolve(null);
  }

  async getServiceContent(): Promise<any> {
    if (!this.serviceContent) {
      this.serviceContent = promise<any>(async resolve => {
        this.client.VimService.VimPort.RetrieveServiceContent(paramFilter({
          _this: 'ServiceInstance'
        }), async (e, r) => {
          if (e) { return this.resolveError(e, resolve); }
          if (!r) { return this.resolveError(new Error('Emtpy response'), resolve); }
          this.serviceContent = r.returnval;
          this.$dc.debugOutput(`fetched service content`);
          return resolve(r.returnval);
        }, this.options);
      });
    }
    return await Promise.resolve(this.serviceContent);
  }

  async getDefaultContainerView(noCache = false) {
    if (!this.containerView || noCache) {
      this.containerView = promise(async resolve => {
        if (!await this.getServiceContent()) { return resolve(null); }
        this.client.VimService.VimPort.CreateContainerView(paramFilter({
          _this: ref(this.serviceContent.viewManager),
          container: ref(this.serviceContent.rootFolder),
          recursive: true
        }), async (e, r) => {
          if (e) { return this.resolveError(e, resolve); }
          if (!r) { return this.resolveError(new Error('Emtpy response'), resolve); }
          this.containerView = r.returnval;
          this.$dc.debugOutput(`fetched default container view (${this.containerView.$value})`);
          return resolve(r.returnval);
        }, this.options);
      });
    }
    return await Promise.resolve(this.containerView);
  }

  async getInventoryStubs(noCache = false) {
    if (!this.inventoryStubsData || noCache) {
      this.inventoryStubsData = promise(async resolve => {
        if (!await this.getDefaultContainerView()) { return resolve(null); }
        this.client.VimService.VimPort.RetrievePropertiesEx(paramFilter({
          _this: ref(this.serviceContent.propertyCollector),
          specSet: [
            xsi('PropertyFilterSpec', {
              propSet: [
                xsi('PropertySpec', { type: 'Folder', all: false, pathSet: ['value'] }),
                xsi('PropertySpec', { type: 'HostSystem', all: false, pathSet: ['value'] }),
                xsi('PropertySpec', { type: 'Datacenter', all: false, pathSet: ['value'] }),
                xsi('PropertySpec', { type: 'ComputeResource', all: false, pathSet: ['value'] }),
                xsi('PropertySpec', { type: 'Network', all: false, pathSet: ['value'] }),
                xsi('PropertySpec', { type: 'Datastore', all: false, pathSet: ['value'] }),
                xsi('PropertySpec', { type: 'VirtualMachine', all: false, pathSet: ['value'] }),
                xsi('PropertySpec', { type: 'ResourcePool', all: false, pathSet: ['value'] }),
                xsi('PropertySpec', { type: 'VirtualApp', all: false, pathSet: ['value'] }),
                xsi('PropertySpec', { type: 'DistributedVirtualSwitch', all: false, pathSet: ['value'] }),
              ],
              objectSet: [
                xsi('ObjectSpec', {
                  obj: ref(this.containerView), skip: true,
                  selectSet: [ xsi('TraversalSpec', { type: 'ContainerView', path: 'view', skip: false }) ]
                })
              ],
            })
          ],
          options: { maxObjects: 1000000 },
        }), async (e, r) => {
          if (e) { return this.resolveError(e, resolve); }
          if (!r) { return this.resolveError(new Error('Emtpy response'), resolve); }
          const pages = await this.getAllPages(r.returnval);
          if (!pages) { return resolve(null); }
          this.inventoryStubsRaw = pages.rawData;
          this.inventoryStubsData = pages.data;
          return resolve(this.inventoryStubsData);
        }, this.options);
      });
    }
    return await Promise.resolve(this.inventoryStubsData);
  }

  async getInventoryData(noCache = false) {
    if (!this.inventoryData || noCache) {
      this.inventoryData = promise(async resolve => {
        if (!await this.getDefaultContainerView()) { return resolve(null); }
        this.client.VimService.VimPort.RetrievePropertiesEx(paramFilter({
          _this: ref(this.serviceContent.propertyCollector),
          specSet: [
            xsi('PropertyFilterSpec', {
              propSet: [
                xsi('PropertySpec', { type: 'Folder', all: true }),
                xsi('PropertySpec', { type: 'HostSystem', all: true }),
                xsi('PropertySpec', { type: 'Datacenter', all: true }),
                xsi('PropertySpec', { type: 'ComputeResource', all: true }),
                xsi('PropertySpec', { type: 'Network', all: true }),
                xsi('PropertySpec', { type: 'Datastore', all: true }),
                xsi('PropertySpec', { type: 'VirtualMachine', all: true }),
                xsi('PropertySpec', { type: 'ResourcePool', all: true }),
                xsi('PropertySpec', { type: 'VirtualApp', all: true }),
                xsi('PropertySpec', { type: 'DistributedVirtualSwitch', all: true }),
              ],
              objectSet: [
                xsi('ObjectSpec', {
                  obj: ref(this.containerView), skip: true,
                  selectSet: [ xsi('TraversalSpec', { type: 'ContainerView', path: 'view', skip: false }) ]
                })
              ],
            })
          ],
          options: { maxObjects: 1000000 },
        }), async (e, r) => {
          if (e) { return this.resolveError(e, resolve); }
          if (!r) { return this.resolveError(new Error('Emtpy response'), resolve); }
          const pages = await this.getAllPages(r.returnval);
          if (!pages) { return resolve(null); }
          this.inventoryRaw = pages.rawData;
          this.inventoryData = pages.data;
          this.$dc.lockStubs();
          const allProms = [];
          for (const moData of this.inventoryData) {
            const moTypeName = moData.iid.split(':')[1];
            const moClass = getMoTypeByName(moTypeName);
            const mo = new moClass(this.$dc, moData);
            if (mo) { allProms.push(mo.$initPending); }
          }
          this.$dc.unlockStubs();
          this.$dc.inventoryPending = promise<boolean>(async invResolve => {
            await Promise.all(allProms);
            await this.$dc.postInventoryDataFetch();
            this.$dc.inventoryPending = false;
            invResolve(false);
          });
          return resolve(r.returnval.objects);
        }, this.options);
      });
    }
    return await Promise.resolve(this.inventoryData);
  }

  async getInventory(noCache = false) {
    this.$dc.debugOutput(`mapping inventory...`);
    await this.getInventoryData(noCache);
    await Promise.resolve(this.$dc.inventoryPending);
    const hostCount = Object.keys(this.$dc.inventory.hostSystem).length;
    const foldersCount = Object.keys(this.$dc.inventory.folder).length;
    const vmCount = Object.keys(this.$dc.inventory.virtualMachine).length;
    this.$dc.debugOutput(`finished mapping inventory (VMs=${vmCount}, Folders=${foldersCount}, Hosts=${hostCount})`);
    this.$dc.inventoryChangedLast = Date.now();
    this.$dc.preserializeInventory();
    return this.$dc;
  }

  async getManagedObjectData(target: any) {
    const stub = ref(target);
    const moType = stub.attributes.type;
    return await promise(async resolve => {
      if (!await this.getDefaultContainerView()) { return resolve(null); }
      this.client.VimService.VimPort.RetrievePropertiesEx(paramFilter({
        _this: ref(this.serviceContent.propertyCollector),
        specSet: [
          xsi('PropertyFilterSpec', {
            propSet: [ xsi('PropertySpec', { type: moType, all: true }), ],
            objectSet: [ xsi('ObjectSpec', { obj: stub }) ],
          })
        ],
        options: { maxObjects: 1 },
      }), async (e, r) => {
        if (e) { return this.resolveError(e, resolve); }
        if (!r) { return this.resolveError(new Error('Emtpy response'), resolve); }
        const targetData = (await parseObjects(r.returnval.objects))[0];
        return resolve(targetData);
      }, this.options);
    });
  }

  async prop(target, prop) {
    const targetStub = ref(target);
    let targetType = targetStub.attributes.type;
    if (!targetType) { targetType = target._type; }
    if (!targetType) {
      this.$dc.debugOutput(`unknown target type: ${JSON.stringify(targetStub)}`);
      return null;
    }
    return await promise(async resolve => {
      if (!await this.getDefaultContainerView()) { return resolve(null); }
      this.client.VimService.VimPort.RetrievePropertiesEx(paramFilter({
        _this: ref(this.serviceContent.propertyCollector),
        specSet: [
          xsi('PropertyFilterSpec', {
            propSet: [ xsi('PropertySpec', { type: targetType, all: [ prop ] }), ],
            objectSet: [ xsi('ObjectSpec', { obj: targetStub }), ],
          })
        ],
        options: { maxObjects: 1 },
      }), async (e, r) => {
        if (e) { return this.resolveError(e, resolve); }
        if (!r) { return this.resolveError(new Error('Emtpy response'), resolve); }
        const list = r.returnval.objects;
        if (!list || list.length === 0) { return null; }
        return resolve(r.returnval.objects);
      }, this.options);
    });
  }

  async propChain(target, ...props: string[]) {
    let prevTarget = target;
    for (const prop of props) {
      target = await this.prop(target, prop);
      if (target === null) {
        this.$dc.debugOutput(`Cannot fetch '${prop}' of prop chain '${props.join('.')}' on target ${JSON.stringify(ref(prevTarget))}`);
        return null;
      }
      prevTarget = target;
    }
    return target;
  }

  async getAllPages(returnVal: any, stepDelayMs: number = 100) {
    const objectsPagesRaw = [returnVal.objects];
    const objectsPages = [await parseObjects(returnVal.objects)];
    let token = returnVal.token;
    while (token) {
      await sleepMs(stepDelayMs);
      const page = await this.furtherRetrievePages(token);
      if (!page) { return null; }
      objectsPagesRaw.push(page.objects);
      objectsPages.push(await parseObjects(page.objects));
      if (page.token === token) { return null; }
      token = page.token;
    }
    const result = {
      rawData: [].concat(...objectsPagesRaw),
      data: [].concat(...objectsPages),
    };
    return result;
  }

  async furtherRetrievePages(furtherToken: string): Promise<{objects: any[]; token?: string}> {
    return await promise(async resolve => {
      if (!await this.getDefaultContainerView()) { return resolve(null); }
      this.client.VimService.VimPort.ContinueRetrievePropertiesEx(paramFilter({
        _this: ref(this.serviceContent.propertyCollector), token: furtherToken
      }), async (e, r) => {
        if (e) { return this.resolveError(e, resolve); }
        if (!r) { return this.resolveError(new Error('Emtpy response'), resolve); }
        resolve({ objects: r.returnval.objects as any[], token: r.returnval.token });
      }, this.options);
    });
  }

}

async function sleepMs(ms: number) {
  return promise<void>(resolve => setTimeout(resolve, ms));
}
