import * as libxmljs from 'libxmljs';
import { unescape } from 'lodash';
import * as Promise from 'bluebird';
import Util from '../lib/util'

const DIALOG_PREFIX = '(//Dialog|//dialog)';
const USER_PREFIX   = '(//User|//user)';

export type ParsedXML = any;

export interface IUserReason {
  code: string;
  label: string;
}

export interface IUserPayload {
  reason: IUserReason | null;
  agentState: string;
  lastChange: string;
  pendingState: string;
}

export interface IDialogInformation {
  dialogId: string;
  fromAddress: string;
  toAddress: string;
  type: string;
  start: string;
  lastChange: string;
  dialogState: string;
  participants: string[]
}

export interface IDialogPayload {
  dialog: boolean;
  dialogs?: IDialogInformation;
}

export interface IReasonPayload {
  category: string;
  code: string;
  label: string;
}

export default class XmlHelper {
  private _parsed: ParsedXML;

  constructor (xml) {
    xml = xml.replace(/\sxmlns[^"]+"[^"]+"/g, ''); // remove namespaces
    try {
      this._parsed = libxmljs.parseXml(unescape(xml)).root();
    } catch (e) {
      this._parsed = null;
      console.error('Could not parse XML', e, xml);
    }
  }

  isUser (): boolean {
    return !!this._parsed && !!this._parsed.get(USER_PREFIX);
  }

  isDialog (): boolean {
    return !!this._parsed && !!this._parsed.get(DIALOG_PREFIX);
  }

  userJson (): IUserPayload | null {
    if (!this.isUser()) {
      return null;
    }

    let xml       = this._parsed.get(USER_PREFIX);
    let reason    = xml.get('./reasonCode');
    let reasonObj = !reason ? null :
      {
        code:  XmlHelper._safelyGet('id',    { xml: reason })!,
        label: XmlHelper._safelyGet('label', { xml: reason })!
      };

    return {
      reason:       reasonObj,
      agentState:   XmlHelper._safelyGet('./state', { xml, callback: Util._sanitizeState })!,
      lastChange:   XmlHelper._safelyGet('./stateChangeTime', { xml })!,
      pendingState: XmlHelper._safelyGet('./pendingState', { xml })!
    };
  }

  reasonsJson (): Promise<IReasonPayload[]> {
    return this._promisedFind('/ReasonCodes/ReasonCode', this._parsed)
                .then(reasons => reasons.map(reason => {
                  let opts    = { xml: reason };
                  let getCode = { callback: data => data.substr(data.lastIndexOf('/') + 1) };

                  let code    = XmlHelper._safelyGet('./uri', Object.assign(opts, getCode));

                  return {
                    category: XmlHelper._safelyGet('./category', opts),
                    label:    XmlHelper._safelyGet('./label',    opts),
                    code
                  };
                }));
  }

  dialogJson (extension: string): IDialogPayload {
    if (!this.isDialog()) return { dialog: false };

    let dialogs = this._parsed.find(DIALOG_PREFIX).reduce((acc, dialog) => {
      let opts = { xml: dialog };
      let agent = dialog.get(`./participants/Participant[mediaAddress = '${extension}']`);
      let participants = dialog.find(`./participants/Participant[mediaAddress != '${extension}']`);

      participants = participants.filter((participant) => {
        return XmlHelper._safelyGet('./state', { xml: participant, callback: Util._sanitizeState }) !== 'DROPPED';
      }).map((participant) => {
        let opts = { xml: participant, callback: Util._formatPhone };
        return XmlHelper._safelyGet('./mediaAddress', opts);
      }, []);

      let dialogId: string = XmlHelper._safelyGet('./id', opts) as string;
      let phoneNumberOpts = Object.assign({}, opts, { callback: Util._formatPhone });

      acc[dialogId] = {
        dialogId,
        fromAddress:  XmlHelper._safelyGet('./fromAddress', phoneNumberOpts),
        toAddress:    XmlHelper._safelyGet('./toAddress', phoneNumberOpts),
        type:         XmlHelper._safelyGet('./mediaProperties/callType', opts),
        start:        XmlHelper._safelyGet('.//startTime', opts),
        lastChange:   XmlHelper._safelyGet('./stateChangeTime', { xml: agent }),
        dialogState:  XmlHelper._safelyGet('./state', { xml: agent, callback: Util._sanitizeState }),
        participants
      };

      return acc;
    }, {});

    return {
      dialog: true,
      dialogs
    };
  }

  _filterParticipants (participants: string[]): Promise<any[]> {
    let formatted = participants.filter((participant) => {
      return XmlHelper._safelyGet('./state', { xml: participant });
    }).map((filtered) => {
      return XmlHelper._promisedSafelyGet('./mediaAddress', { xml: filtered });
    });

    return Promise.all(formatted);
  }

  _promisedFind (xpath: string, root: ParsedXML): Promise<any> {
    return new Promise((resolve, reject) => {
      try {
        resolve(root.find(xpath));
      } catch (e) {
        reject(`Couldn't find xpath: ${e}`);
      }
    });
  }

  promisedDialogJson (extension): Promise<IDialogPayload> {
    if (!this.isDialog()) return Promise.resolve({ dialog: false });

    let resolveValue = this._promisedFind(DIALOG_PREFIX, this._parsed).map((dialog: any) => {
      let scope: IDialogInformation = {
        dialogId: '',
        fromAddress: '',
        toAddress: '',
        type: '',
        start: '',
        lastChange: '',
        dialogState: '',
        participants: []
      };

      let opts = { xml: dialog };

      return XmlHelper._promisedSafelyGet('./id', opts)
        .then((data) => {
          scope.dialogId = data;

          return this._promisedFind(`./participants/Participant[mediaAddress != '${extension}']`, dialog);
        })
        .then(this._filterParticipants)
        .then((formatted) => {
          scope.participants = formatted;
          return XmlHelper._promisedSafelyGet('./fromAddress', opts);
        })
        .then((data) => {
          scope.fromAddress = data;
          return XmlHelper._promisedSafelyGet('./toAddress', opts);
        })
        .then((data) => {
          scope.toAddress = data;

          let type = XmlHelper._safelyGet('./mediaProperties/callType', opts);
          let start = XmlHelper._safelyGet('.//startTime', opts);

          let agent = dialog.get(`./participants/Participant[mediaAddress = '${extension}']`);
          let lastChange = XmlHelper._safelyGet('./stateChangeTime', { xml: agent });
          let dialogState =  XmlHelper._safelyGet('./state', { xml: agent, callback: Util._sanitizeState });

          return {
            dialogId:     scope.dialogId,
            fromAddress:  scope.fromAddress,
            toAddress:    scope.toAddress,
            type,
            start,
            lastChange,
            dialogState,
            participants: scope.participants
          };
        }).catch(Util._promiseErrorHandler);
    });

    let promisedDialogs = Promise.all(resolveValue).then((allDialogs) => {
      return allDialogs.reduce((acc, val) => { acc[val.dialogId] = val; return acc; }, {});
    }).catch(Util._promiseErrorHandler);

    return promisedDialogs.then((data) => {
      return {
        dialog: true,
        dialogs: data
      };
    }).catch(Util._promiseErrorHandler);
  }

  static _safelyGet (xpath: string, opts: { xml: ParsedXML; callback?: (x: string | null) => string }): string | null {
    let xml = opts.xml;

    if (!xml) return null;

    let node = xml.get(xpath);
    let returnValue = node ? node.text() : null;

    return (opts && opts.callback) ? opts.callback(returnValue) : returnValue;
  }

  static _promisedSafelyGet (xpath: string, opts: { xml: ParsedXML }): Promise<any> {
    return new Promise((resolve, reject) => {
      let xml = opts.xml;

      if (!xml) reject('No XML object');

      let node = xml.get(xpath);

      return node ? resolve(node.text()) : reject('Node not found');
    });
  }

  // Doesn't support nested properties (yet)
  static objToXml (obj: { root: string; properties: Object }): string {
    let doc = new (libxmljs as any).Document();
    let rootNode = doc.node(obj.root);

    Object.keys(obj.properties).map((val) => {
      rootNode.node(val, obj.properties[val]);
    });

    return rootNode.toString();
  }
};
