/* eslint no-throw-literal: 0 */

import {coroutine, promisify} from 'bluebird'
import eventToPromise from 'event-to-promise'
import {createClient} from 'ldapjs'
import {escape} from 'ldapjs/lib/filters/escape'
import {readFileSync} from 'fs'

// ===================================================================

const VAR_RE = /\{\{([^}]+)\}\}/g
function evalFilter (filter, vars) {
  return filter.replace(VAR_RE, (_, name) => {
    const value = vars[name]

    if (value === undefined) {
      throw new Error('invalid variable: ' + name)
    }

    return escape(value)
  })
}

// ===================================================================

class AuthLdap {
  constructor (conf) {
    const clientOpts = {
      url: conf.uri,
      maxConnections: 5,
      tlsOptions: {}
    }

    {
      const {
        bind,
        checkCertificate = true,
        certificateAuthorities
      } = conf

      if (bind) {
        clientOpts.bindDN = bind.dn
        clientOpts.bindCredentials = bind.password
      }

      const {tlsOptions} = clientOpts

      tlsOptions.rejectUnauthorized = checkCertificate
      if (certificateAuthorities) {
        tlsOptions.ca = certificateAuthorities.map(path => readFileSync(path))
      }
    }

    const {base: searchBase} = conf
    const searchFilter = conf.filter || '(uid={{name}})'

    this._provider = coroutine(function * ({username, password}) {
      if (username === undefined || password === undefined) {
        throw null
      }

      const client = createClient(clientOpts)

      try {
        // Promisify some methods.
        const bind = promisify(client.bind, client)
        const search = promisify(client.search, client)

        // Bind if necessary.
        {
          const {bind: credentials} = conf
          if (credentials) {
            yield bind(credentials.dn, credentials.password)
          }
        }

        // Search for the user.
        const entries = []
        {
          const response = yield search(searchBase, {
            scope: 'sub',
            filter: evalFilter(searchFilter, {
              name: username
            })
          })

          response.on('searchEntry', entry => {
            entries.push(entry.json)
          })

          const {status} = yield eventToPromise(response, 'end')
          if (status) {
            throw new Error('unexpected search response status: ' + status)
          }
        }

        // Try to find an entry which can be bind with the given password.
        for (let entry of entries) {
          try {
            yield bind(entry.objectName, password)
            return { username }
          } catch (error) {}
        }

        throw null
      } finally {
        client.unbind()
      }
    })
  }

  load (xo) {
    xo.registerAuthenticationProvider(this._provider)
  }

  unload (xo) {
    xo.unregisterAuthenticationProvider(this._provider)
  }
}

// ===================================================================

export default (conf) => new AuthLdap(conf)

@@ -39,19 +39,8 @@
 rt %7B
-coroutine, 
 prom
@@ -1242,16 +1242,23 @@
  const %7B
+%0A      
 base: se
@@ -1269,26 +1269,23 @@
 Base
-%7D = conf%0A    const
+,%0A      filter:
  sea
@@ -1299,23 +1299,8 @@
 er =
- conf.filter %7C%7C
  '(u
@@ -1312,16 +1312,29 @@
 name%7D%7D)'
+%0A    %7D = conf
 %0A%0A    th
@@ -1352,26 +1352,22 @@
 r = 
-coroutine(
+async 
 function
  * (
@@ -1362,18 +1362,16 @@
 function
- *
  (%7Busern
@@ -1801,29 +1801,29 @@
             
-yield
+await
  bind(creden
@@ -1973,21 +1973,21 @@
 ponse = 
-yield
+await
  search(
@@ -2257,21 +2257,21 @@
 atus%7D = 
-yield
+await
  eventTo
@@ -2562,13 +2562,13 @@
     
-yield
+await
  bin
@@ -2743,17 +2743,16 @@
  %7D%0A    %7D
-)
 %0A  %7D%0A%0A  

Use more ES6/7 features.