import {AbstractDAO} from './AbstractDAO';
import {EventDAO} from './EventDAO';

import {Filter} from '../api/Filter';
import {IHasHTTP} from '../api/IHasHTTP';
import {IHash} from '../internal/IHash';
import {IOnmsHTTP} from '../api/IOnmsHTTP';
import {OnmsError} from '../api/OnmsError';
import {OnmsHTTPOptions} from '../api/OnmsHTTPOptions';
import {OnmsResult} from '../api/OnmsResult';
import {SearchProperty} from '../api/SearchProperty';

import {OnmsAlarm} from '../model/OnmsAlarm';
import {OnmsAlarmSummary} from '../model/OnmsAlarmSummary';
import {AlarmTypes} from '../model/OnmsAlarmType';
import {OnmsParm} from '../model/OnmsParm';
import {OnmsServiceType} from '../model/OnmsServiceType';
import {Severities} from '../model/OnmsSeverity';
import {OnmsTroubleTicketState, TroubleTicketStates} from '../model/OnmsTroubleTicketState';
import {OnmsMemo} from '../model/OnmsMemo';

import {log, catDao} from '../api/Log';
import {Category} from 'typescript-logging';

/** @hidden */
const cat = new Category('alarms', catDao);

/**
 * Data access for [[OnmsAlarm]] objects.
 * @module AlarmDAO
 */
export class AlarmDAO extends AbstractDAO<number, OnmsAlarm> {
  /**
   * an event DAO to be used for creating events attached to alarms from API/JSON data.
   * @hidden
   */
  private eventDao: EventDAO;

  constructor(impl: IHasHTTP | IOnmsHTTP) {
    super(impl);
    this.eventDao = new EventDAO(impl);
  }

  /**
   * Fetch an alarm.
   *
   * @version ReST v1+
   * @param {number} id - The alarm's ID.
   * @return An [[OnmsAlarm]].
   */
  public async get(id: number): Promise<OnmsAlarm> {
    return this.getOptions().then((opts) => {
        return this.http.get(this.pathToAlarmsEndpoint() + '/' + id, opts).then((result) => {
            return this.fromData(result.data);
        });
    });
  }

  /**
   * Find matching alarms.
   *
   * @version ReST v1+
   * @param {Filter} filter - The filter to use when querying.
   * @return An array of [[OnmsAlarm]] objects.
   */
  public async find(filter?: Filter): Promise<OnmsAlarm[]> {
    return this.getOptions(filter).then((opts) => {
        return this.http.get(this.pathToAlarmsEndpoint(), opts).then((result) => {
            const data = this.getData(result);
            if (!Array.isArray(data)) {
              if (!data) {
                return [] as OnmsAlarm[];
              }
              throw new OnmsError('Expected an array of alarms but got "' + (typeof data) + '" instead.');
            }
            return data.map((alarmData) => {
                return this.fromData(alarmData);
            });
        });
    });
  }

    /**
     * Extracts the data from an HTTP Request result.
     *
     * @param result the HTTP Request result.
     * @returns An array of [[OnmsAlarm]] objects.
     */
  public getData(result: any): OnmsAlarm[] {
      let data = result.data;

      if (data !== null && this.getCount(data) > 0 && data.alarm) {
        data = data.alarm;
      } else {
        data = [];
      }

      if (!Array.isArray(data)) {
        if (data.id) {
          data = [data];
        } else {
          throw new OnmsError('Expected an array of alarms but got "' + (typeof data) + '" instead.');
        }
      }
      return data;
  }

  /**
   * Acknowledge an alarm.
   *
   * @version ReST v1+
   * @param {number|OnmsAlarm} id - The [[OnmsAlarm]] or alarm ID.
   * @param {string=} user - The user to ack the alarm as.
   *                  (Only administrators have the right to do this.)
   */
  public async acknowledge(alarm: number|OnmsAlarm, user?: string): Promise<void> {
    const alarmId = (typeof(alarm) === 'number' ? alarm : alarm.id);
    const parameters = {} as IHash<string>;
    parameters.ack = 'true';
    if (user !== undefined) {
      parameters.ackUser = user;
    }
    return this.put(this.pathToAlarmsEndpoint() + '/' + alarmId, parameters);
  }

  /**
   * Un-acknowledge an alarm.
   *
   * @version ReST v1+
   * @param {number|OnmsAlarm} alarm - The [[OnmsAlarm]] or alarm ID.
   */
  public async unacknowledge(alarm: number|OnmsAlarm, user?: string): Promise<void> {
    const alarmId = (typeof(alarm) === 'number' ? alarm : alarm.id);
    const parameters = {} as IHash<string>;
    parameters.ack = 'false';
    if (user !== undefined) {
      parameters.ackUser = user;
    }
    return this.put(this.pathToAlarmsEndpoint() + '/' + alarmId, parameters);
  }

  /**
   * Escalate an alarm.
   *
   * @version ReST v1+
   * @param {number|OnsmAlarm} alarm - The [[OnmsAlarm]] or alarm ID.
   */
  public async escalate(alarm: number|OnmsAlarm): Promise<void> {
    const alarmId = (typeof(alarm) === 'number' ? alarm : alarm.id);
    const parameters = {} as IHash<string>;
    parameters.escalate = 'true';
    return this.put(this.pathToAlarmsEndpoint() + '/' + alarmId, parameters);
  }

  /**
   * Clear an alarm.
   *
   * @version ReST v1+
   * @param {number|OnmsAlarm} alarm - The [[OnmsAlarm]] or alarm ID.
   */
  public async clear(alarm: number|OnmsAlarm): Promise<void> {
    const alarmId = (typeof(alarm) === 'number' ? alarm : alarm.id);
    const parameters = {} as IHash<string>;
    parameters.clear = 'true';
    return this.put(this.pathToAlarmsEndpoint() + '/' + alarmId, parameters);
  }

  /**
   * Associate a ticket ID with the alarm.
   *
   * @version ReST v1+
   * @param {number|OnmsAlarm} alarm - The [[OnmsAlarm]] or alarm ID.
   * @param {string} ticketId - The ticket ID.
   */
  public async setTTicketId(alarm: number|OnmsAlarm, ticketId: string): Promise<void> {
    const alarmId = (typeof(alarm) === 'number' ? alarm : alarm.id);
    const parameters = {} as IHash<string>;
    parameters.ticketId = ticketId;
    return this.put(this.pathToAlarmsEndpoint() + '/' + alarmId, parameters);
  }

  /**
   * Update the state of the ticket associated with the alarm.
   *
   * @version ReST v1+
   * @param {number|OnmsAlarm} alarm - The [[OnmsAlarm]] or alarm ID.
   * @param {string} state - The ticket state.
   */
  public async setTTicketState(alarm: number|OnmsAlarm, state: OnmsTroubleTicketState): Promise<void> {
    const alarmId = (typeof(alarm) === 'number' ? alarm : alarm.id);
    const parameters = {} as IHash<string>;
    parameters.ticketState = state.label;
    return this.put(this.pathToAlarmsEndpoint() + '/' + alarmId, parameters);
  }

  /**
   * Create a trouble ticket for the specified alarm.
   *
   * @version ReST v2
   * @param {number|OnmsAlarm} alarm - The [[OnmsAlarm]] or alarm ID.
   */
  public async createTicket(alarm: number|OnmsAlarm): Promise<void> {
    if (this.getApiVersion() === 1) {
      throw new OnmsError('Create/Update/Close ticket is only available in OpenNMS ' +
        'versions that support the ReSTv2 API.');
    }

    const alarmId = (typeof(alarm) === 'number' ? alarm : alarm.id);
    const options = new OnmsHTTPOptions();
    options.headers.accept = 'text/plain';
    return this.http.post(this.pathToAlarmsEndpoint() + '/' + alarmId + '/ticket/create', options).then(() => {
      log.debug('Ticket creation pending.', cat);
    }).catch((err: OnmsResult<OnmsAlarm>) => {
      if (err.code === 501) {
        log.warn('Trouble ticketing is not enabled on ' + this.http.server.toString());
      }
      throw err;
    });
  }

  /**
   * Notify OpenNMS it should fetch updated ticket state for an alarm from the remote ticketing system.
   *
   * @version ReST v2
   * @param {number|OnmsAlarm} alarm - The [[OnmsAlarm]] or alarm ID.
   */
  public async triggerTicketUpdate(alarm: number|OnmsAlarm): Promise<void> {
    if (this.getApiVersion() === 1) {
      throw new OnmsError('Create/Update/Close ticket is only available in OpenNMS ' +
        'versions that support the ReSTv2 API.');
    }

    const alarmId = (typeof(alarm) === 'number' ? alarm : alarm.id);
    const options = new OnmsHTTPOptions();
    options.headers.accept = 'text/plain';
    return this.http.post(this.pathToAlarmsEndpoint() + '/' + alarmId + '/ticket/update', options).then(() => {
      log.debug('Ticket update pending.', cat);
    }).catch((err: OnmsResult<OnmsAlarm>) => {
      if (err.code === 501) {
        log.warn('Trouble ticketing is not enabled on ' + this.http.server.toString());
      }
      throw err;
    });
  }

  /**
   * Close the ticket associated with the given alarm.
   *
   * @version ReST v2
   * @param {number|OnmsAlarm} alarm - The [[OnmsAlarm]] or alarm ID.
   */
  public async closeTicket(alarm: number|OnmsAlarm): Promise<void> {
    if (this.getApiVersion() === 1) {
      throw new OnmsError('Create/Update/Close ticket is only available in OpenNMS ' +
        'versions that support the ReSTv2 API.');
    }

    const alarmId = (typeof(alarm) === 'number' ? alarm : alarm.id);
    const options = new OnmsHTTPOptions();
    options.headers.accept = 'text/plain';
    return this.http.post(this.pathToAlarmsEndpoint() + '/' + alarmId + '/ticket/close', options).then(() => {
      log.debug('Ticket close pending.', cat);
    }).catch((err: OnmsResult<OnmsAlarm>) => {
      if (err.code === 501) {
        log.warn('Trouble ticketing is not enabled on ' + this.http.server.toString());
      }
      throw err;
    });
  }

  /**
   * Create or update the sticky memo associated with the alarm.
   *
   * @version ReST v2
   * @param {number|OnmsAlarm} alarm - The [[OnmsAlarm]] or alarm ID.
   * @param {string} body - The memo body
   * @param {string=} user - The user to update the memo as.
   *                  (Only administrators have the right to do this.)
   */
  public async saveStickyMemo(alarm: number|OnmsAlarm, body: string, user?: string): Promise<void> {
    return this.saveMemo('memo', alarm, body, user);
  }

  /**
   * Create or update the journal memo associated with the alarm.
   *
   * @version ReST v2
   * @param {number|OnmsAlarm} alarm - The [[OnmsAlarm]] or alarm ID.
   * @param {string} body - The memo body
   * @param {string=} user - The user to update the memo as.
   *                  (Only administrators have the right to do this.)
   */
  public async saveJournalMemo(alarm: number|OnmsAlarm, body: string, user?: string): Promise<void> {
    return this.saveMemo('journal', alarm, body, user);
  }

  /**
   * Delete the sticky memo ticket associated with the given alarm.
   *
   * @version ReST v2
   * @param {number|OnmsAlarm} alarm - The [[OnmsAlarm]] or alarm ID.
   */
  public async deleteStickyMemo(alarm: number|OnmsAlarm): Promise<void> {
    return this.deleteMemo('memo', alarm);
  }

  /**
   * Delete the journal memo ticket associated with the given alarm.
   *
   * @version ReST v2
   * @param {number|OnmsAlarm} alarm - The [[OnmsAlarm]] or alarm ID.
   */
  public async deleteJournalMemo(alarm: number|OnmsAlarm): Promise<void> {
    return this.deleteMemo('journal', alarm);
  }

  /**
   * Generate an alarm object from the given dictionary.
   * @hidden
   */
  public fromData(data: any) {
    const alarm = new OnmsAlarm();

    alarm.id = this.toNumber(data.id);
    alarm.count = data.count;
    alarm.ackUser = data.ackUser;
    alarm.uei = data.uei;
    alarm.description = data.description;
    alarm.firstEventTime = this.toDate(data.firstEventTime);
    alarm.lastEvent = this.eventDao.fromData(data.lastEvent);
    alarm.location = data.location;
    alarm.logMessage = data.logMessage;
    alarm.reductionKey = data.reductionKey;
    alarm.troubleTicket = data.troubleTicket;
    alarm.troubleTicketLink = data.troubleTicketLink;
    alarm.nodeId = this.toNumber(data.nodeId);
    alarm.nodeLabel = data.nodeLabel;
    alarm.suppressedBy = data.suppressedBy;
    alarm.operatorInstructions = data.operatorInstructions;

    if (data.ackTime) {
      alarm.ackTime = this.toDate(data.ackTime);
    }

    if (data.severity) {
      alarm.severity = Severities[data.severity];
    }

    if (data.type) {
      const type = this.toNumber(data.type);
      alarm.type = AlarmTypes[type];
    }

    if (typeof data.troubleTicketState !== 'undefined') {
        const troubleTicketKey = Object.keys(TroubleTicketStates).find((key) => {
            return TroubleTicketStates[key].id === data.troubleTicketState;
        });
        if (troubleTicketKey) {
            alarm.troubleTicketState = TroubleTicketStates[troubleTicketKey];
        }
    }

    if (data.serviceType) {
      const st = data.serviceType;
      alarm.service = OnmsServiceType.for(st.id, st.name);
    }

    if (data.suppressedTime) {
      alarm.suppressedTime = this.toDate(data.suppressedTime);
    }

    if (data.suppressedUntil) {
      alarm.suppressedUntil = this.toDate(data.suppressedUntil);
    }

    if (data.parameters) {
      let parms = data.parameters;
      if (parms.parameter) {
        parms = parms.parameter;
      }
      if (!Array.isArray(parms)) {
        parms = [parms];
      }
      alarm.parameters = [];

      for (let parm of parms) {
        parm = new OnmsParm(
          parm.name,
          parm.type,
          parm.value,
        );
        alarm.parameters.push(parm);
      }
    }

    alarm.relatedAlarms = data.relatedAlarms;

    alarm.managedObjectType = data.managedObjectType;
    alarm.managedObjectInstance = data.managedObjectInstance;

    alarm.sticky = this.toMemo(data.stickyMemo);
    alarm.journal = this.toMemo(data.reductionKeyMemo);

    alarm.detailsPage = this.getDetailsPage(alarm);

    alarm.affectedNodeCount = data.affectedNodeCount;

    return alarm;
  }

  /**
   * Generate a memo from the given dictionary.
   * @hidden
   */
  public toMemo(data: any): OnmsMemo {
    if (!data) {
      return null;
    }

    const memo = new OnmsMemo();
    memo.id = data.id;
    memo.author = data.author;
    memo.body = data.body;
    memo.created = this.toDate(data.created);
    memo.updated = this.toDate(data.updated);
    return memo;
  }

  /**
   * The path to the alarm search properties endpoint.
   */
  protected searchPropertyPath() {
    return this.pathToAlarmsEndpoint() + '/properties';
  }

  /**
   * Given an optional filter, generate an [[OnmsHTTPOptions]] object for DAO calls.
   * @hidden
   */
  protected async getOptions(filter?: Filter): Promise<OnmsHTTPOptions> {
    return super.getOptions(filter).then((options) => {
        // always use application/json for v2 calls
        if (this.getApiVersion() === 2) {
            options.headers.accept = 'application/json';
        }
        return options;
    });
  }

  /**
   * Call a PUT request in the format the alarm ack API expects.
   * @hidden
   */
  private async put(url: string, parameters = {} as IHash<string>): Promise<void> {
    return this.getOptions().then((opts) => {
        opts.headers['content-type'] = 'application/x-www-form-urlencoded';
        opts.headers.accept = null;
        opts.parameters = parameters;
        return this.http.put(url, opts).then((result) => {
            if (!result.isSuccess) {
                throw result;
            }
            return;
        });
    });
  }

  /**
   * Call a DELETE request in the format the alarm ack API expects.
   * @hidden
   */
  private async httpDelete(url: string, parameters = {} as IHash<string>): Promise<void> {
      return this.getOptions().then((opts) => {
          opts.headers['content-type'] = 'application/x-www-form-urlencoded';
          opts.headers.accept = null;
          opts.parameters = parameters;
          return this.http.httpDelete(url, opts).then((result) => {
              if (!result.isSuccess) {
                  throw result;
              }
              return;
          });
      });
  }

  /**
   * Get the path to the alarms endpoint for the appropriate API version.
   * @hidden
   */
  private pathToAlarmsEndpoint() {
    return this.getApiVersion() === 2 ? 'api/v2/alarms' : 'rest/alarms';
  }

  /**
   * Save a journal or sticky memo.
   * @hidden
   */
  private async saveMemo(type: string, alarm: number|OnmsAlarm, body: string, user?: string): Promise<void> {
    if (this.getApiVersion() === 1) {
      throw new OnmsError('Save/Delete memo is only available in OpenNMS ' +
          'versions that support the ReSTv2 API.');
    }

    const alarmId = (typeof(alarm) === 'number' ? alarm : alarm.id);
    const parameters = {} as IHash<string>;
    parameters.body = body;
    if (user !== undefined) {
      parameters.user = user;
    }
    return this.put(this.pathToAlarmsEndpoint() + '/' + alarmId + '/' + type, parameters);
  }

  /**
   * Delete a journal or sticky memo
   * @hidden
   */
  private async deleteMemo(type: string, alarm: number|OnmsAlarm): Promise<void> {
    if (this.getApiVersion() === 1) {
      throw new OnmsError('Save/Delete memo is only available in OpenNMS ' +
          'versions that support the ReSTv2 API.');
    }

    const alarmId = (typeof(alarm) === 'number' ? alarm : alarm.id);
    return this.httpDelete(this.pathToAlarmsEndpoint() + '/' + alarmId + '/' + type);
  }

  /**
   * Retrieves the URL to the details page for the given alarm.
   *
   * @param {number|OnmsAlarm} alarm - The [[OnmsAlarm]] or alarm ID.
   * @returns {URL} URL on the associated OpenNMS server for the alarm details page.
   */
  private getDetailsPage(alarm: number|OnmsAlarm): string {
      const alarmId = (typeof(alarm) === 'number' ? alarm : alarm.id);
      return this.http.server.resolveURL(`alarm/detail.htm`, {id: alarmId});
  }
}
