const { Controller } = require('libflitter')
const zxcvbn = require('zxcvbn')

class LDAPController extends Controller {
    static get services() {
        return [...super.services, 'models', 'utility', 'configs']
    }

    async get_config(req, res, next) {
        // ldap port
        // user base dn
        // group base dn
        const config = this.configs.get('ldap:server')
        return res.api({
            port: config.port,
            base_dc: config.schema.base_dc,
            authentication_base: config.schema.authentication_base,
            group_base: config.schema.group_base,
            login_field: config.schema.auth.user_id,
        })
    }

    async get_clients(req, res, next) {
        const Client = this.models.get('ldap:Client')
        const clients = await Client.find({active: true})
        const data = []

        for ( const client of clients ) {
            if ( !req.user.can(`ldap:client:${client.id}:view`) ) continue
            data.push(await client.to_api())
        }

        return res.api(data)
    }

    async get_groups(req, res, next) {
        const Group = this.models.get('ldap:Group')
        const groups = await Group.find({active: true})
        const data = []

        for ( const group of groups ) {
            if ( !req.user.can(`ldap:group:${group.id}:view`) ) continue
            data.push(await group.to_api())
        }

        return res.api(data)
    }

    async get_machines(req, res, next) {
        const Machine = this.models.get('ldap:Machine')
        const machines = await Machine.find({active: true})
        const data = []

        for ( const machine of machines ) {
            if ( !req.user.can(`ldap:machine:${machine.id}:view`) ) continue
            data.push(await machine.to_api())
        }

        return res.api(data)
    }

    async get_machine_groups(req, res, next) {
        const MachineGroup = this.models.get('ldap:MachineGroup')
        const groups = await MachineGroup.find({active: true})
        const data = []

        for ( const group of groups ) {
            if ( !req.user.can(`ldap:machine_group:${group.id}:view`) ) continue
            data.push(await group.to_api())
        }

        return res.api(data)
    }

    async get_client(req, res, next) {
        const Client = this.models.get('ldap:Client')
        const client = await Client.findById(req.params.id)

        if ( !client || !client.active )
            return res.status(404)
                .message(req.T('api.client_not_found'))
                .api()

        if ( !req.user.can(`ldap:client:${client.id}:view`) )
            return res.status(401)
                .message(req.T('api.insufficient_permissions'))
                .api()

        return res.api(await client.to_api())
    }

    async get_group(req, res, next) {
        const Group = this.models.get('ldap:Group')
        const group = await Group.findById(req.params.id)

        if ( !group || !group.active )
            return res.status(404)
                .message(req.T('api.group_not_found'))
                .api()

        if ( !req.user.can(`ldap:group:${group.id}:view`) )
            return res.status(401)
                .message(req.T('api.insufficient_permissions'))
                .api()

        return res.api(await group.to_api())
    }

    async get_machine(req, res, next) {
        const Machine = this.models.get('ldap:Machine')
        const machine = await Machine.findById(req.params.id)

        if ( !machine || !machine.active )
            return res.status(404)
                .message(req.T('api.machine_not_found'))
                .api()

        if ( !req.user.can(`ldap:machine:${machine.id}:view`) )
            return res.status(401)
                .message(req.T('api.insufficient_permissions'))
                .api()

        return res.api(await machine.to_api())
    }

    async get_machine_group(req, res, next) {
        const MachineGroup = this.models.get('ldap:MachineGroup')
        const group = await MachineGroup.findById(req.params.id)

        if ( !group || !group.active )
            return res.status(404)
                .message(req.T('api.group_not_found'))
                .api()

        if ( !req.user.can(`ldap:machine_group:${group.id}:view`) )
            return res.status(401)
                .message(req.T('api.insufficient_permissions'))
                .api()

        return res.api(await group.to_api())
    }

    async create_client(req, res, next) {
        if ( !req.user.can('ldap:client:create') )
            return res.status(401)
                .message(req.T('api.insufficient_permissions'))
                .api()

        // validate inputs
        const required_fields = ['name', 'uid', 'password']
        for ( const field of required_fields ) {
            if ( !req.body[field] )
                return res.status(400)
                    .message(`${req.T('api.missing_field')} ${field}`)
        }

        // Make sure the uid is free
        const User = this.models.get('auth:User')
        const existing_user = await User.findOne({ uid: req.body.uid.toLowerCase() })
        if ( existing_user )
            return res.status(400)
                .message(req.T('api.user_already_exists'))
                .api()

        // Verify password complexity
        const min_score = 3
        const result = zxcvbn(req.body.password)
        if ( result.score < min_score )
            return res.status(400)
                .message(req.T('auth.password_complexity_fail').replace('MIN_SCORE', min_score))
                .api()

        // Create the client
        const Client = this.models.get('ldap:Client')
        const client = await Client.create({
            uid: req.body.uid.toLowerCase(),
            password: req.body.password,
            name: req.body.name,
        })

        return res.api(await client.to_api())
    }

    async create_machine(req, res, next) {
        // validate inputs
        const required_fields = ['name', 'description']
        for ( const field of required_fields ) {
            if ( !req.body[field] )
                return res.status(400)
                    .message(`${req.T('api.missing_field')} ${field}`)
                    .api()
        }

        // Make sure the machine name is free
        const Machine = this.models.get('ldap:Machine')
        const existing_machine = await Machine.findOne({ name: req.body.name })
        if ( existing_machine )
            return res.status(400)
                .message(req.T('api.machine_already_exists'))
                .api()

        const machine = new Machine({
            name: req.body.name,
            description: req.body.description,
            host_name: req.body.host_name,
            location: req.body.location,
        })

        if ( req.body.bind_password ) {
            await machine.set_bind_password(req.body.bind_password)
        }

        if ( 'ldap_visible' in req.body ) {
            machine.ldap_visible = !!req.body.ldap_visible
        }

        await machine.save()
        return res.api(await machine.to_api())
    }

    async create_machine_group(req, res, next) {
        // validate inputs
        const required_fields = ['name']
        for ( const field of required_fields ) {
            if ( !req.body[field] )
                return res.status(400)
                    .message(`${req.T('api.missing_field')} ${field}`)
                    .api()
        }

        // Make sure the machine name is free
        const MachineGroup = this.models.get('ldap:MachineGroup')
        const existing_group = await MachineGroup.findOne({ name: req.body.name })
        if ( existing_group )
            return res.status(400)
                .message(req.T('api.group_already_exists'))
                .api()

        const group = new MachineGroup({
            name: req.body.name,
            description: req.body.description,
        })

        if ( 'ldap_visible' in req.body ) {
            group.ldap_visible = !!req.body.ldap_visible
        }

        const Machine = this.models.get('ldap:Machine')
        const machine_ids = Array.isArray(req.body.machine_ids) ? req.body.machine_ids : []
        group.machine_ids = []
        for ( const potential of machine_ids ) {
            const machine = await Machine.findOne({
                _id: Machine.to_object_id(potential),
                active: true,
            })

            if ( machine ) {
                group.machine_ids.push(potential)
            }
        }

        await group.save()
        return res.api(await group.to_api())
    }

    async create_group(req, res, next) {
        // validate inputs
        const required_fields = ['role', 'name']
        for ( const field of required_fields ) {
            if ( !req.body[field] )
                return res.status(400)
                    .message(`${req.T('api.missing_field')} ${field}`)
                    .api()
        }

        // Make sure the group name is free
        const Group = this.models.get('ldap:Group')
        const User = this.models.get('auth:User')
        const existing_group = await Group.findOne({ name: req.body.name })
        if ( existing_group )
            return res.status(400)
                .message(req.T('api.group_already_exists'))
                .api()

        // Make sure the role exists
        if ( !this.configs.get('auth.roles')[req.body.role] )
            return res.status(400)
                .message(`${req.T('common.invalid')} role.`)
                .api()

        const group = new Group({
            name: req.body.name,
            role: req.body.role,
        })

        if ( 'ldap_visible' in req.body ) group.ldap_visible = !!req.body.ldap_visible
        if ( 'user_ids' in req.body ) {
            // Attempt to parse the user_ids
            const parsed = typeof req.body.user_ids === 'string' ? this.utility.infer(req.body.user_ids) : req.body.user_ids
            const user_ids = Array.isArray(parsed) ? parsed : [parsed]

            // Make sure all the user IDs are valid
            for ( const user_id of user_ids ) {
                const user = await User.findById(user_id)
                if ( !user )
                    return res.status(400)
                        .message(`${req.T('common.invalid')} user_id: ${user_id}`)
                        .api()
            }

            group.user_ids = user_ids
        }

        await group.save()
        return res.api(await group.to_api())
    }

    async update_client(req, res, next) {
        const Client = this.models.get('ldap:Client')
        const client = await Client.findById(req.params.id)

        if ( !client || !client.active )
            return res.status(404)
                .message(req.T('api.client_not_found'))
                .api()

        if ( !req.user.can(`ldap:client:${client.id}:update`) )
            return res.status(401)
                .message(req.T('api.insufficient_permissions'))
                .api()

        const required_fields = ['name', 'uid']
        for ( const field of required_fields ) {
            if ( !req.body[field] )
                return res.status(400)
                    .message(`${req.T('api.missing_field')} ${field}`)
                    .api()
        }

        const user = await client.user()

        // Update the name
        if ( req.body.name !== client.name ) {
            client.name = req.body.name
            user.first_name = req.body.name
        }

        // Update the uid
        if ( req.body.uid.toLowerCase() !== user.uid ) {
            // Make sure the UID is free
            const User = this.models.get('auth:User')
            const existing_user = await User.findOne({ uid: req.body.uid.toLowerCase() })
            if ( existing_user )
                return res.status(400)
                    .message(req.T('api.user_already_exists'))
                    .api()

            user.uid = req.body.uid.toLowerCase()
        }

        // Update the password
        if ( req.body.password && !(await user.check_password(req.body.password)) ) {
            // Verify the password's complexity
            const min_score = 3
            const result = zxcvbn(req.body.password)
            if ( result.score < min_score )
                return res.status(400)
                    .message(req.T('auth.password_complexity_fail').replace('MIN_SCORE', min_score))
                    .api()

            await user.reset_password(req.body.password)
        }

        await user.save()
        await client.save()
        return res.api()
    }

    async update_machine(req, res, next) {
        const Machine = this.models.get('ldap:Machine')

        const machine = await Machine.findById(req.params.id)
        if ( !machine || !machine.active )
            return res.status(404)
                .message(req.T('api.machine_not_found'))
                .api()

        if ( !req.user.can(`ldap:machine:${machine.id}:update`) )
            return res.status(401)
                .message(req.T('api.insufficient_permissions'))
                .api()

        const required_fields = ['name', 'description']
        for ( const field of required_fields ) {
            if ( !req.body[field] )
                return res.status(400)
                    .message(`${req.T('api.missing_field')} ${field}`)
                    .api()
        }

        // Make sure the machine name is free
        const existing_machine = await Machine.findOne({ name: req.body.name })
        if ( existing_machine && existing_machine.id !== machine.id )
            return res.status(400)
                .message(req.T('api.machine_already_exists'))
                .api()

        machine.name = req.body.name
        machine.description = req.body.description
        machine.host_name = req.body.host_name
        machine.location = req.body.location

        if ( req.body.bind_password ) {
            await machine.set_bind_password(req.body.bind_password)
        }

        if ( 'ldap_visible' in req.body ) {
            machine.ldap_visible = !!req.body.ldap_visible
        }

        await machine.save()
        return res.api(await machine.to_api())
    }

    async update_machine_group(req, res, next) {
        const MachineGroup = this.models.get('ldap:MachineGroup')

        const group = await MachineGroup.findById(req.params.id)
        if ( !group || !group.active )
            return res.status(404)
                .message(req.T('api.group_not_found'))
                .api()

        if ( !req.user.can(`ldap:machine_group:${group.id}:update`) )
            return res.status(401)
                .message(req.T('api.insufficient_permissions'))
                .api()

        const required_fields = ['name']
        for ( const field of required_fields ) {
            if ( !req.body[field] )
                return res.status(400)
                    .message(`${req.T('api.missing_field')} ${field}`)
                    .api()
        }

        // Make sure the machine name is free
        const existing_group = await MachineGroup.findOne({ name: req.body.name })
        if ( existing_group && existing_group.id !== group.id )
            return res.status(400)
                .message(req.T('api.group_already_exists'))
                .api()

        group.name = req.body.name
        group.description = req.body.description

        if ( 'ldap_visible' in req.body ) {
            group.ldap_visible = !!req.body.ldap_visible
        }

        const Machine = this.models.get('ldap:Machine')
        const machine_ids = Array.isArray(req.body.machine_ids) ? req.body.machine_ids : []
        group.machine_ids = []
        for ( const potential of machine_ids ) {
            const machine = await Machine.findOne({
                _id: Machine.to_object_id(potential),
                active: true,
            })

            if ( machine ) {
                group.machine_ids.push(potential)
            }
        }

        await group.save()
        return res.api(await group.to_api())
    }

    async update_group(req, res, next) {
        const User = await this.models.get('auth:User')
        const Group = await this.models.get('ldap:Group')

        const group = await Group.findById(req.params.id)
        if ( !group || !group.active )
            return res.status(404)
                .message(req.T('api.group_not_found'))
                .api()

        if ( !req.user.can(`ldap:group:${group.id}:update`) )
            return res.status(401)
                .message(req.T('api.insufficient_permissions'))
                .api()

        const required_fields = ['role', 'name']
        for ( const field of required_fields ) {
            if ( !req.body[field] )
                return res.status(400)
                    .message(`${req.T('api.missing_field')} ${field}`)
                    .api()
        }

        // Make sure the name is free
        const existing_group = await Group.findOne({ name: req.body.name })
        if ( existing_group && existing_group.id !== group.id )
            return res.status(400)
                .message(req.T('api.group_already_exists'))
                .api()

        group.name = req.body.name
        group.role = req.body.name
        group.ldap_visible = !!req.body.ldap_visible

        if ( req.body.user_ids ) {
            const parsed = typeof req.body.user_ids === 'string' ? this.utility.infer(req.body.user_ids) : req.body.user_ids
            const user_ids = Array.isArray(parsed) ? parsed : [parsed]

            for ( const user_id of user_ids ) {
                const user = await User.findById(user_id)
                if ( !user )
                    return res.status(400)
                        .message(`${req.T('common.invalid')} user_id: ${user_id}`)
                        .api()
            }

            group.user_ids = user_ids
        } else {
            group.user_ids = []
        }

        await group.save()
        return res.api()
    }

    async delete_client(req, res, next) {
        const Client = this.models.get('ldap:Client')
        const client = await Client.findById(req.params.id)

        if ( !client || !client.active )
            return res.status(404)
                .message(req.T('api.client_not_found'))
                .api()

        if ( !req.user.can(`ldap:client:${client.id}:delete`) )
            return res.status(401)
                .message(req.T('api.insufficient_permissions'))
                .api()

        const user = await client.user()
        client.active = false
        user.active = false
        user.block_login = true

        await user.save()
        await client.save()
        return res.api()
    }

    async delete_group(req, res, next) {
        const Group = this.models.get('ldap:Group')
        const group = await Group.findById(req.params.id)

        if ( !group || !group.active )
            return res.status(404)
                .message(req.T('api.group_not_found'))
                .api()

        if ( !req.user.can(`ldap:group:${group.id}:delete`) )
            return res.status(401)
                .message(req.T('api.insufficient_permissions'))
                .api()

        group.active = false
        await group.save()
        return res.api()
    }

    async delete_machine(req, res, next) {
        const Machine = this.models.get('ldap:Machine')
        const machine = await Machine.findById(req.params.id)

        if ( !machine || !machine.active )
            return res.status(404)
                .message(req.T('api.machine_not_found'))
                .api()

        if ( !req.user.can(`ldap:machine:${machine.id}:delete`) )
            return res.status(401)
                .message(req.T('api.insufficient_permissions'))
                .api()

        machine.active = false
        await machine.save()
        return res.api()
    }

    async delete_machine_group(req, res, next) {
        const MachineGroup = this.models.get('ldap:MachineGroup')
        const group = await MachineGroup.findById(req.params.id)

        if ( !group || !group.active )
            return res.status(404)
                .message(req.T('api.group_not_found'))
                .api()

        if ( !req.user.can(`ldap:machine_group:${group.id}:delete`) )
            return res.status(401)
                .message(req.T('api.insufficient_permissions'))
                .api()

        group.active = false
        await group.save()
        return res.api()
    }
}

module.exports = exports = LDAPController
