import util = require("util");
import BluebirdPromise = require("bluebird");
import exceptions = require("./Exceptions");
import Dovehash = require("dovehash");
import ldapjs = require("ldapjs");

import { EventEmitter } from "events";
import { LdapConfiguration } from "./../../types/Configuration";
import { Ldapjs } from "../../types/Dependencies";
import { Winston } from "../../types/Dependencies";

interface SearchEntry {
  object: any;
}

export class LdapClient {
  private options: LdapConfiguration;
  private ldapjs: Ldapjs;
  private logger: Winston;
  private client: ldapjs.ClientAsync;

  constructor(options: LdapConfiguration, ldapjs: Ldapjs, logger: Winston) {
    this.options = options;
    this.ldapjs = ldapjs;
    this.logger = logger;

    this.connect();
  }

  connect(): void {
    const ldap_client = this.ldapjs.createClient({
      url: this.options.url,
      reconnect: true
    });

    ldap_client.on("error", function (err: Error) {
      console.error("LDAP Error:", err.message);
    });

    this.client = BluebirdPromise.promisifyAll(ldap_client) as ldapjs.ClientAsync;
  }

  private build_user_dn(username: string): string {
    let user_name_attr = this.options.user_name_attribute;
    // if not provided, default to cn
    if (!user_name_attr) user_name_attr = "cn";

    const additional_user_dn = this.options.additional_user_dn;
    const base_dn = this.options.base_dn;

    let user_dn = util.format("%s=%s", user_name_attr, username);
    if (additional_user_dn) user_dn += util.format(",%s", additional_user_dn);
    user_dn += util.format(",%s", base_dn);
    return user_dn;
  }

  bind(username: string, password: string): BluebirdPromise<void> {
    const user_dn = this.build_user_dn(username);

    this.logger.debug("LDAP: Bind user %s", user_dn);
    return this.client.bindAsync(user_dn, password)
      .error(function (err: Error) {
        throw new exceptions.LdapBindError(err.message);
      });
  }

  private search_in_ldap(base: string, query: ldapjs.SearchOptions): BluebirdPromise<any> {
    this.logger.debug("LDAP: Search for %s in %s", JSON.stringify(query), base);
    return new BluebirdPromise((resolve, reject) => {
      this.client.searchAsync(base, query)
        .then(function (res: EventEmitter) {
          const doc: SearchEntry[] = [];
          res.on("searchEntry", function (entry: SearchEntry) {
            doc.push(entry.object);
          });
          res.on("error", function (err: Error) {
            reject(new exceptions.LdapSearchError(err.message));
          });
          res.on("end", function () {
            resolve(doc);
          });
        })
        .catch(function (err: Error) {
          reject(new exceptions.LdapSearchError(err.message));
        });
    });
  }

  get_groups(username: string): BluebirdPromise<string[]> {
    const user_dn = this.build_user_dn(username);

    let group_name_attr = this.options.group_name_attribute;
    if (!group_name_attr) group_name_attr = "cn";

    const additional_group_dn = this.options.additional_group_dn;
    const base_dn = this.options.base_dn;

    let group_dn = base_dn;
    if (additional_group_dn)
      group_dn = util.format("%s,", additional_group_dn) + group_dn;

    const query = {
      scope: "sub",
      attributes: [group_name_attr],
      filter: "member=" + user_dn
    };

    const that = this;
    this.logger.debug("LDAP: get groups of user %s", username);
    return this.search_in_ldap(group_dn, query)
      .then(function (docs) {
        const groups = [];
        for (let i = 0; i < docs.length; ++i) {
          groups.push(docs[i].cn);
        }
        that.logger.debug("LDAP: got groups %s", groups);
        return BluebirdPromise.resolve(groups);
      });
  }

  get_emails(username: string): BluebirdPromise<string[]> {
    const that = this;
    const user_dn = this.build_user_dn(username);

    const query = {
      scope: "base",
      sizeLimit: 1,
      attributes: ["mail"]
    };

    this.logger.debug("LDAP: get emails of user %s", username);
    return this.search_in_ldap(user_dn, query)
      .then(function (docs) {
        const emails = [];
        for (let i = 0; i < docs.length; ++i) {
          if (typeof docs[i].mail === "string")
            emails.push(docs[i].mail);
          else {
            emails.concat(docs[i].mail);
          }
        }
        that.logger.debug("LDAP: got emails %s", emails);
        return BluebirdPromise.resolve(emails);
      });
  }

  update_password(username: string, new_password: string): BluebirdPromise<void> {
    const user_dn = this.build_user_dn(username);

    const encoded_password = Dovehash.encode("SSHA", new_password);
    const change = {
      operation: "replace",
      modification: {
        userPassword: encoded_password
      }
    };

    const that = this;
    this.logger.debug("LDAP: update password of user %s", username);

    this.logger.debug("LDAP: bind admin");
    return this.client.bindAsync(this.options.user, this.options.password)
      .then(function () {
        that.logger.debug("LDAP: modify password");
        return that.client.modifyAsync(user_dn, change);
      });
  }
}