const Unit = require('libflitter/Unit')
const LDAP = require('ldapjs')
const Validator = require('email-validator')
const net = require('net')
const fs = require('fs')

// TODO support logging ALL ldap requests when in DEBUG, not just routed ones
// TODO need to support LDAP server auto-discovery/detection features

class LDAPServerUnit extends Unit {
    static get name() {
        return 'ldap_server'
    }

    static get services() {
        return [...super.services, 'configs', 'express', 'output']
    }

    /**
     * Get the standard format for LDAP DNs. Can be passed into
     * ldapjs/DN.format().
     * @returns {object}
     */
    standard_format() {
        return this.configs.get('ldap:server.format')
    }

    /**
     * Get the LDAP.js DN for the user auth base.
     * @returns {ldap/DN}
     */
    auth_dn() {
        return this.build_dn(this.config.schema.authentication_base)
    }

    group_dn() {
        return this.build_dn(this.config.schema.group_base)
    }

    machine_dn() {
        return this.build_dn(this.config.schema.machine_base)
    }

    machine_group_dn() {
        return this.build_dn(this.config.schema.machine_group_base)
    }

    sudo_dn() {
        return this.build_dn(this.config.schema.sudo_base)
    }

    /**
     * Get the anonymous DN.
     * @returns {ldap/DN}
     */
    anonymous() {
        return LDAP.parseDN('cn=anonymous')
    }

    /**
     * Returns true if the string is a valid e-mail address.
     *
     * @see https://stackoverflow.com/questions/46155/how-to-validate-an-email-address-in-javascript
     * @param {string} email
     * @returns {boolean}
     */
    validate_email(email) {
        return Validator.validate(email)
    }

    /**
     * Build an LDAP.js DN object from a set of string RDNs.
     * @param {...string} parts
     * @returns {ldap/DN}
     */
    build_dn(...parts) {
        parts = parts.flat()
        parts.push(this.config.schema.base_dc)
        return LDAP.parseDN(parts.join(','))
    }

    /**
     * Starts the LDAP server.
     * @param {module:libflitter/app/FlitterApp~FlitterApp} app
     * @returns {Promise<void>}
     */
    async go(app) {
        this.config = this.configs.get('ldap:server')
        const server_config = {}

        // If Flitter is configured to use an SSL certificate,
        // use it to enable LDAPS in the server.
        if ( this.config.ssl?.enable ) {
            this.output.info('Using configured SSL certificate. The LDAP server will require an ldaps:// connection.')
            server_config.certificate = fs.readFileSync(this.config.ssl.certificate)
            server_config.key = fs.readFileSync(this.config.ssl.key)
        } else if ( this.express.use_ssl() ) {
            this.output.info('Using configured SSL certificate. The LDAP server will require an ldaps:// connection.')
            server_config.certificate = await this.express.ssl_certificate()
            server_config.key = await this.express.ssl_key()
        }

        this.server = LDAP.createServer(server_config)

        if ( this.config.max_connections ) {
            this.server.maxConnections = this.config.max_connections
        }

        this.output.info(`Will listen on ${this.config.interface}:${this.config.port}`)
        if ( await this.port_free() ) {
            await new Promise((res, rej) => {
                this.server.listen(this.config.port, this.config.interface, (err) => {
                    this.output.success(`LDAP server listening on port ${this.config.port}...`)
                    res()
                })
            })
        } else {
            this.output.error(`LDAP server port ${this.config.port} is not available. The LDAP server was not started.`)
        }
    }

    async port_free() {
        return new Promise((res, rej) => {
            const server = net.createServer()
            server.once('error', (e) => {
                res(false)
            })
            server.once('listening', () => {
                server.close()
                res(true)
            })
            server.listen(this.config.port)
        })
    }

    /**
     * Stops the LDAP server.
     * @param {module:libflitter/app/FlitterApp~FlitterApp} app
     * @returns {Promise<void>}
     */
    async cleanup(app) {
        this.server.close()
    }
}

module.exports = exports = LDAPServerUnit
