const { Injectable } = require('flitter-di')
const { ImplementationError } = require('libflitter')
const LDAP = require('ldapjs')

class LDAPController extends Injectable {
    resource_type = 'items'

    // TODO make use of this better
    check_attribute = 'ldap_visible' // or false

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

    async compare(req, res, next) {
        if ( !req.user.can(`ldap:compare:${this.resource_type}`)) {
            return next(new LDAP.InsufficientAccessRightsError())
        }

        // Make sure the resource exists
        const item = await this.get_resource_from_dn(req.dn)
        if ( !item ) {
            return next(LDAP.NoSuchObjectError())
        }

        // Make sure it has the requested attribute
        const value = (await item.to_ldap())[req.attribute]
        if ( typeof value === 'undefined' ) {
            return next(new LDAP.NoSuchAttributeError())
        }

        // Check if it matches the request value
        const values = Array.isArray(value) ? value : [value]
        const matches = values.some(x => x === req.value)

        res.end(matches)
        return next()
    }

    async bind(req, res, next) {
        const auth_dn = this.ldap_server.auth_dn()

        // Make sure the DN is valid
        if ( !req.dn.childOf(auth_dn) ) {
            this.output.debug(`Bind failure: ${req.dn} not in ${auth_dn}`)
            return next(new LDAP.InvalidCredentialsError('Cannot bind to DN outsize of the authentication base.'))
        }

        // Get the item
        const item = await this.get_resource_from_dn(req.dn)
        if ( !item ) {
            this.output.debug(`Bind failure: ${req.dn} not found`)
            return next(new LDAP.NoSuchObjectError())
        }

        // If the object is can-able, make sure it can bind
        if ( typeof item.can === 'function' && !item.can('ldap:bind') ) {
            this.output.debug(`Bind failure: User not allowed to bind`)
            return next(new LDAP.InsufficientAccessRightsError())
        }

        // Check if the credentials are an app_password
        const app_password_verified = Array.isArray(item.app_passwords)
            && item.app_passwords.length > 0
            && await item.check_app_password(req.credentials)

        // Check if the user has MFA enabled.
        // If so, split the incoming password to fetch the MFA code
        // e.g. normalPassword:123456
        if ( !app_password_verified && item.mfa_enabled ) {
            const parts = req.credentials.split(':')
            const mfa_code = parts.pop()
            const actual_password = parts.join(':')

            // Check the credentials
            if ( !await item.check_password(actual_password) ) {
                this.output.debug(`Bind failure: user w/ MFA provided invalid credentials`)
                return next(new LDAP.InvalidCredentialsError('Invalid credentials. Make sure MFA code is included at the end of your password (e.g. password:123456)'))
            }

            // Now, check the MFA code
            if ( !item.mfa_token.verify(mfa_code) ) {
                this.output.debug(`Bind failure: user w/ MFA provided invalid MFA token`)
                return next(new LDAP.InvalidCredentialsError('Invalid credentials. Verification of the MFA token failed.'))
            }

        // If not MFA, just check the credentials
        } else if (!app_password_verified && !await item.check_password(req.credentials)) {
            this.output.debug(`Bind failure: user w/ simple auth provided invalid credentials`)
            return next(new LDAP.InvalidCredentialsError())
        }

        // Check if the resource has a trap. If so, deny access.
        if ( item.trap ) {
            return next(new LDAP.InvalidCredentialsError('This resource currently has a login trap set. Please visit the web UI to release.'))
        }

        this.output.info(`Successfully bound resource as DN: ${req.dn.format(this.configs.get('ldap:server.format'))}.`)
        res.end()
        return next()
    }

    async delete(req, res, next) {
        if ( !req.user.can(`ldap:delete:${this.resource_type}`) ) {
            return next(new LDAP.InsufficientAccessRightsError())
        }

        // Get the base DN
        const base_dn = await this.get_base_dn()

        // Make sure it's a parent of the request DN
        if ( !base_dn.parentOf(req.dn) ) {
            this.output.warn(`Attempted to perform resource deletion on invalid DN: ${req.dn.format(this.configs.get('ldap:server.format'))}`)
            return next(new LDAP.InsufficientAccessRightsError(`Target DN must be a member of the base DN: ${base_dn.format(this.configs.get('ldap:server.format'))}.`))
        }

        // Fetch the resource (error if not found)
        const item = await this.get_resource_from_dn(req.dn)
        if ( !item ) {
            return next(new LDAP.NoSuchObjectError())
        }

        // Delete it - TODO full soft delete, or just ldap_visible = false?
        await item.delete()

        res.end()
        return next()
    }

    async get_resource_from_dn(dn) {
        throw new ImplementationError()
    }

    async get_base_dn() {
        throw new ImplementationError()
    }
}

module.exports = exports = LDAPController
