import {AbstractDAO} from './AbstractDAO';

import {Filter} from '../api/Filter';
import {IHasHTTP} from '../api/IHasHTTP';
import {IOnmsHTTP} from '../api/IOnmsHTTP';
import {OnmsError} from '../api/OnmsError';

import {Util} from '../internal/Util';

import {OnmsCategory} from '../model/OnmsCategory';
import {OnmsCollectType} from '../model/OnmsCollectType';
import {OnmsIpInterface} from '../model/OnmsIpInterface';
import {OnmsManagedType} from '../model/OnmsManagedType';
import {OnmsMonitoredService} from '../model/OnmsMonitoredService';
import {OnmsNode} from '../model/OnmsNode';
import {OnmsNodeLabelSource} from '../model/OnmsNodeLabelSource';
import {OnmsNodeType} from '../model/OnmsNodeType';
import {OnmsPrimaryType} from '../model/OnmsPrimaryType';
import {OnmsServiceType} from '../model/OnmsServiceType';
import {OnmsServiceStatusType} from '../model/OnmsServiceStatusType';
import {OnmsSnmpInterface} from '../model/OnmsSnmpInterface';
import {OnmsSnmpStatusType, SnmpStatusTypes} from '../model/OnmsSnmpStatusType';
import {PhysAddr} from '../model/PhysAddr';

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

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

/**
 * Data access for [[OnmsNode]] objects.
 * @module NodeDAO
 */
export class NodeDAO extends AbstractDAO<number, OnmsNode> {
  constructor(impl: IHasHTTP | IOnmsHTTP) {
    super(impl);
  }

  /**
   * Get an node, given the node's ID.
   *
   * @param id - The node's ID.
   * @param recurse - Optionally fetch all sub-model objects. (ipInterface, etc.)
   */
  public async get(id: number, recurse = false): Promise<OnmsNode> {
    return this.getOptions().then((opts) => {
        return this.http.get(this.pathToNodesEndpoint() + '/' + id, opts).then((result) => {
            const node = this.fromData(result.data);

            if (recurse) {
                return this.fetch(node);
            } else {
                return node;
            }
        });
    });
  }

  /** Search for nodes, given an optional filter. */
  public async find(filter?: Filter): Promise<OnmsNode[]> {
    return this.getOptions(filter).then((opts) => {
        return this.http.get(this.pathToNodesEndpoint(), opts).then((result) => {
            let data = result.data;

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

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

  /** Given a node, fetch all the sub-model objects for that node. (ipInterfaces, snmpInterfaces, etc.) */
  public async fetch(node: OnmsNode): Promise<OnmsNode> {
    return this.snmpInterfaces(node).then((si) => {
      node.snmpInterfaces = si;
      si.forEach((iface) => {
        iface.node = node;
      });
      return this.ipInterfaces(node).then((ifaces) => {
        node.ipInterfaces = ifaces;
        ifaces.forEach((iface) => {
          iface.node = node;
        });
        return Promise.all(ifaces.map((iface) => {
          return this.services(node, iface).then((services) => {
            iface.services = services;
            services.forEach((service) => {
              service.node = node;
              service.ipInterface = iface;
            });
          });
        })).then(() => {
          return node;
        });
      });
    });
  }

  /** Given a node, get the IP interfaces for that node. */
  public async ipInterfaces(node: number | OnmsNode, filter?: Filter): Promise<OnmsIpInterface[]> {
      if (node instanceof OnmsNode) {
          node = node.id;
      }
      return this.getOptions(filter).then((opts) => {
        return this.http.get(this.pathToNodesEndpoint() + '/' + node + '/ipinterfaces', opts).then((result) => {
            let data = result.data;

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

            if (!Array.isArray(data)) {
                if (data.nodeId) {
                    data = [data];
                } else {
                    throw new OnmsError('Expected an array of IP interfaces but got "' + (typeof data) + '" instead.');
                }
            }
            return data.map((ifaceData) => {
                return this.fromIpInterfaceData(ifaceData);
            });
        });
    });
  }

  /** Given a node, get the SNMP interfaces for that node. */
  public async snmpInterfaces(node: number | OnmsNode, filter?: Filter): Promise<OnmsSnmpInterface[]> {
    return this.getOptions(filter).then((opts) => {
        if (node instanceof OnmsNode) {
            node = node.id;
        }
        return this.http.get(this.pathToNodesEndpoint() + '/' + node + '/snmpinterfaces', opts).then((result) => {
            let data = result.data;

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

            if (!Array.isArray(data)) {
                if (data.ifName) {
                    data = [data];
                } else {
                    throw new OnmsError('Expected an array of SNMP interfaces but got "'
                        + (typeof data) + '" instead.');
                }
            }
            return data.map((ifaceData) => {
                return this.fromSnmpData(ifaceData);
            });
        });
    });
  }

  /** Given a node, get the IP interfaces for that node. */
  public async services(
    node: number | OnmsNode,
    ipInterface: string | OnmsIpInterface,
    filter?: Filter,
  ): Promise<OnmsMonitoredService[]> {

    return this.getOptions(filter).then((opts) => {
        if (node instanceof OnmsNode) {
            node = node.id;
        }
        if (ipInterface instanceof OnmsIpInterface && ipInterface.ipAddress) {
            ipInterface = ipInterface.ipAddress.address;
        }
        const url = this.pathToNodesEndpoint() + '/' + node + '/ipinterfaces/' + ipInterface + '/services';
        return this.http.get(url, opts).then((result) => {
            let data = result.data;

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

            if (!Array.isArray(data)) {
                if (data.lastGood) {
                    data = [data];
                } else {
                    throw new OnmsError('Expected an array of services but got "' + (typeof data) + '" instead.');
                }
            }
            return data.map((ifaceData) => {
                return this.fromServiceData(ifaceData);
            });
        });
    });
  }

  /**
   * Create a node object from a JSON object.
   * @hidden
   */
  public fromData(data: any): OnmsNode {
    const node = new OnmsNode();

    node.id = this.toNumber(data.id);
    node.label = data.label;
    node.location = data.location;
    node.foreignSource = data.foreignSource || undefined;
    node.foreignId = data.foreignId || undefined;
    node.sysContact = data.sysContact;
    node.sysDescription = data.sysDescription;
    node.sysLocation = data.sysLocation;
    node.sysName = data.sysName;
    node.sysObjectId = data.sysObjectId;

    if (data.labelSource) {
      node.labelSource = OnmsNodeLabelSource.forId(data.labelSource);
    }
    if (data.createTime) {
      node.createTime = this.toDate(data.createTime);
    }
    if (data.lastCapsdPoll) {
      node.lastCapsdPoll = this.toDate(data.lastCapsdPoll);
    }
    if (data.type) {
      node.type = OnmsNodeType.forId(data.type);
    }

    node.categories = [];
    if (data.categories) {
      node.categories = data.categories.map((c) => {
        return OnmsCategory.for(c.id, c.name);
      });
    }

    for (const key in data.assetRecord) {
      if (data.assetRecord.hasOwnProperty(key)
        && data.assetRecord[key] !== null
        && data.assetRecord[key] !== undefined) {
        node.assets[key] = data.assetRecord[key];
      }
    }

    return node;
  }

  /**
   * create an IP interface object from a JSON object
   * @hidden
   */
  public fromIpInterfaceData(data: any): OnmsIpInterface {
    const iface = new OnmsIpInterface();

    iface.id = this.toNumber(data.id);
    iface.hostname = data.hostName || data.hostname;
    iface.ipAddress = Util.toIPAddress(data.ipAddress);
    iface.isManaged = OnmsManagedType.forId(data.isManaged);
    iface.lastCapsdPoll = this.toDate(data.lastCapsdPoll);
    iface.snmpPrimary = OnmsPrimaryType.forId(data.snmpPrimary);

    if (data.snmpInterface && data.snmpInterface.id) {
      iface.snmpInterfaceId = this.toNumber(data.snmpInterface.id);
    }

    return iface;
  }

  /**
   * create an SNMP interface object from a JSON object
   * @hidden
   */
  public fromSnmpData(data: any): OnmsSnmpInterface {
    const iface = new OnmsSnmpInterface();

    iface.id = this.toNumber(data.id);
    iface.ifIndex = this.toNumber(data.ifIndex);
    iface.ifDescr = data.ifDescr;
    iface.ifType = this.toNumber(data.ifType);
    iface.ifName = data.ifName;
    iface.ifSpeed = this.toNumber(data.ifSpeed);
    iface.ifAdminStatus = OnmsSnmpStatusType.forId(this.toNumber(data.ifAdminStatus));
    iface.ifOperStatus = OnmsSnmpStatusType.forId(this.toNumber(data.ifOperStatus));
    iface.ifAlias = data.ifAlias;
    iface.lastCapsdPoll = this.toDate(data.lastCapsdPoll);
    iface.collect = OnmsCollectType.forId(data.collectFlag);
    iface.poll = data.poll;
    iface.lastSnmpPoll = this.toDate(data.lastSnmpPoll);

    if (data.physAddr) {
      iface.physAddr = new PhysAddr(data.physAddr);
    }

    return iface;
  }

  /**
   * create a monitored service object from a JSON object
   * @hidden
   */
  public fromServiceData(data: any): OnmsMonitoredService {
    const service = new OnmsMonitoredService();

    service.id = this.toNumber(data.id);
    service.lastFail = this.toDate(data.lastFail);
    service.lastGood = this.toDate(data.lastGood);

    if (data.serviceType) {
      service.type = OnmsServiceType.for(data.serviceType.id, data.serviceType.name);
    }
    if (data.status) {
      service.status = OnmsServiceStatusType.forId(data.status);
    }

    return service;
  }

  /**
   * The path to the node search properties endpoint.
   */
  protected searchPropertyPath(): string {
    if (this.getApiVersion() < 2) {
      throw new OnmsError('Search properties are not supported in Node ReSTv1.');
    }
    return this.pathToNodesEndpoint() + '/properties';
  }

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