'use strict';
const assert = require('assert');
const Promise = require('any-promise');
const ldap = require('ldapjs');

let count = 0;

module.exports = app => {
  app.ldapjs = createClient(app.config.ldapjs, app);
};

function createClient(config, app) {
  assert(config.url && config.dn && config.password,
    `[egg-ldapjs] 'url: ${config.url}', 'dn: ${config.dn}', 'password: ${config.password}' are required on config`);

  app.coreLogger.info('[egg-ldapjs] connecting %s@%s:%s/%s',
    config.dn, config.url);

  const LdapClient = function(options) {
    this.client = ldap.createClient(options);
  };

  function promisify(fn) {
    return function() {
      const client = this.client;
      const args = Array.prototype.slice.call(arguments);

      return new Promise(function(resolve, reject) {
        args.push(function(err, result) {
          if (err) reject(err);
          else resolve(result);
        });

        client[fn].apply(client, args);
      });
    };
  }

  [ 'bind', 'add', 'compare', 'del', 'exop', 'modify', 'modifyDN', 'unbind' ].forEach(function(fn) {
    LdapClient.prototype[fn] = promisify(fn);
  });

  LdapClient.prototype.destroy = function() { this.client.destroy(); };
  LdapClient.prototype._search = promisify('search');


  LdapClient.prototype.search = function(base, options, controls) {
    const client = this.client;

    return new Promise(function(resolve, reject) {
      const searchCallback = function(err, result) {
        const r = {
          entries: [],
          objEntries: [],
          references: [],
        };

        result.on('searchEntry', function(entry) {
          r.entries.push(entry);
          r.objEntries.push(entry.object);
        });

        result.on('searchReference', function(reference) {
          r.references.push(reference);
        });

        result.on('error', function(err) {
          reject(err);
        });

        result.on('end', function(result) {
          if (result.status === 0) {
            resolve(r);
          } else {
            reject(new Error('non-zero status code: ' + result.status));
          }
        });
      };

      const args = ([ base, options, controls, searchCallback ])
        .filter(function(x) { return typeof x !== 'undefined'; });

      client.search.apply(client, args);
    });
  };


  LdapClient.prototype.authenticate = function(base, cn, password) {
    const _this = this;

    return _this.bind('CN=' + cn + ',' + base, password).then(
      function() {
        return _this.search(base, { scope: 'sub', filter: '(cn=' + cn + ')' }).then(function(result) {
          return result.entries[0].object;
        });
      },
      function(err) {
        if (err.name === 'InvalidCredentialsError') {
          return null;
        }
        throw err;

      }
    );
  };


  LdapClient.prototype.authenticateUser = function(base, cn, password) {
    const dnRegex = new RegExp('^CN=([^,]+),' + base + '$');

    return this.authenticate(base, cn, password).then(function(result) {
      if (result) {
        let groups = [];

        if (result.memberOf) {
          groups = result.memberOf
            .map(function(x) { return (x.match(dnRegex) || [])[1]; })
            .filter(function(x) { return typeof x !== 'undefined'; });
        }

        return {
          email: result.userPrincipalName,
          name: result.displayName,
          groups,
        };

      }
      return null;

    });
  };


  const client = new LdapClient({ url: config.url });

  app.beforeStart(async () => {
    const result = await client.bind(config.dn, config.password);
    const index = count++;
    app.coreLogger.info(`[egg-ldapjs] instance[${index}] status OK, client ready ${result}`);
  });
  return client;
}