const { Controller } = require('libflitter')

class IAMController extends Controller {
    static get services() {
        return [...super.services, 'models', 'canon']
    }

    async check_entity_access(req, res, next) {
        const Policy = this.models.get('iam:Policy')

        if ( !req.body.entity_id && !req.body.target_id )
            return res.status(400)
                .message(`${req.T('api.missing_field', true)} entity_id, target_id`)
                .api()

        return res.api(await Policy.check_entity_access(req.body.entity_id, req.body.target_id, req.body.permission || undefined))
    }

    async check_user_access(req, res, next) {
        const User = this.models.get('auth:User')
        const Policy = this.models.get('iam:Policy')

        if ( !req.body.target_id )
            return res.status(400)
                .message(`${req.T('api.missing_field')} target_id`)
                .api()

        let user = req.user
        if ( req.body.user_id && req.body.user_id !== 'me' )
            user = await User.findById(req.body.user_id)

        if ( !user )
            return res.status(404)
                .message(req.T('api.user_not_found'))
                .api()

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

        return res.api(await Policy.check_user_access(user, req.body.target_id, req.body.permission || undefined))
    }

    async get_policies(req, res, next) {
        const Policy = this.models.get('iam:Policy')
        const policies = await Policy.find({ active: true })
        const data = []

        for ( const policy of policies ) {
            if ( req.user.can(`iam:policy:${policy.id}:view`) ) {
                data.push(await policy.to_api())
            }
        }

        return res.api(data)
    }

    async get_permissions(req, res, next) {
        const Permission = this.models.get('iam:Permission')
        const permissions = await Permission.find({
            active: true,
            ...(req.query.target_type ? {
                target_type: req.query.target_type,
            } : {})
        })
        const data = []

        for ( const perm of permissions ) {
            if ( req.user.can(`iam:permission:${perm.target_type}:view`) ) {
                data.push(await perm.to_api())
            }
        }

        if ( req.query.include_unset ) {
            data.reverse().push({
                permission: '',
            })

            data.reverse()
        }

        return res.api(data)
    }

    async get_policy(req, res, next) {
        const Policy = this.models.get('iam:Policy')
        const policy = await Policy.findById(req.params.id)

        if ( !policy )
            return res.status(404)
                .message(req.T('iam.policy_not_found'))
                .api()

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

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

    async get_permission(req, res, next) {
        const Permission = this.models.get('iam:Permission')
        const permission = await Permission.findById(req.params.id)

        if ( !permission )
            return res.status(404)
                .message(req.T('iam.permission_not_found'))
                .api()

        if ( !req.user.can(`iam:permission:${permission.target_type}:view`) )
            return res.status(401)
                .message(req.T('api.insufficient_permissions'))
                .api()

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

    async create_policy(req, res, next) {
        const Policy = this.models.get('iam:Policy')

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

        if ( !['user', 'group'].includes(req.body.entity_type) )
            return res.status(400)
                .message(`${req.T('iam.invalid_entity')} user, group`)
                .api()

        // Make sure the entity_id is valid
        if ( req.body.entity_type === 'user' ) {
            const User = this.models.get('auth:User')
            const user = await User.findById(req.body.entity_id)
            if ( !user || !req.user.can(`auth:user:${user.id}:view`) )
                return res.status(400)
                    .message(`${req.T('common.invalid')} entity_id.`)
                    .api()
        } else if ( req.body.entity_type === 'group' ) {
            const Group = this.models.get('auth:Group')
            const group = await Group.findById(req.body.entity_id)
            if ( !group || !group.active || !req.user.can(`auth:group:${group.id}:view`) )
                return res.status(400)
                    .message(`${req.T('common.invalid')} entity_id.`)
                    .api()
        }

        if ( !['allow', 'deny'].includes(req.body.access_type) )
            return res.status(400)
                .message(`${req.T('common.invalid')} access_type. ${req.T('api.must_one')} allow, deny.`)
                .api()

        if ( !['application', 'api_scope', 'machine', 'machine_group', 'vault'].includes(req.body.target_type) )
            return res.status(400)
                .message(`${req.T('common.invalid')} target_type. ${req.T('api.must_one')} application, api_scope, machine, machine_group, vault.`)
                .api()

        // Make sure the target_id is valid
        if ( req.body.target_type === 'application' ) {
            const Application = this.models.get('Application')
            const app = await Application.findById(req.body.target_id)
            if ( !app || !app.active || !req.user.can(`app:${app.id}:view`) )
                return res.status(400)
                    .message(`${req.T('common.invalid')} target_id.`)
                    .api()
        } else if ( req.body.target_type === 'api_scope' ) {
            const api_scopes = this.canon.get('controller::api:v1:Reflect.api_scopes')()
            if ( !api_scopes.includes(req.body.target_id) )
                return res.status(400)
                    .message(`${req.T('common.invalid')} target_id.`)
                    .api()
        } else if ( req.body.target_type === 'machine' ) {
            const Machine = this.models.get('ldap:Machine')
            const machine = await Machine.findById(req.body.target_id)
            if ( !machine || !machine.active || !req.user.can(`ldap:machine:${machine.id}:view`) )
                return res.status(400)
                    .message(`${req.T('common.invalid')} target_id.`)
                    .api()
        } else if ( req.body.target_type === 'machine_group' ) {
            const MachineGroup = this.models.get('ldap:MachineGroup')
            const group = await MachineGroup.findById(req.body.target_id)
            if ( !group || !group.active || !req.user.can(`ldap:machine_group:${group.id}:view`) )
                return res.status(400)
                    .message(`${req.T('common.invalid')} target_id.`)
                    .api()
        } else if ( req.body.target_type === 'vault' ) {
            const Vault = this.models.get('vault:Vault')
            const vault = await Vault.findById(req.body.target_id)
            if ( !vault?.active || !(await Policy.check_user_access(req.user, vault.id, 'update')) )
                return res.status(400)
                    .message(`${req.T('common.invalid')} target_id.`)
                    .api()
        }

        const policy = new Policy({
            entity_type: req.body.entity_type,
            entity_id: req.body.entity_id,
            access_type: req.body.access_type,
            target_type: req.body.target_type,
            target_id: req.body.target_id,
        })

        if ( req.body.permission ) {
            // Validate the permission and set it, if it is valid
            const Permission = this.models.get('iam:Permission')
            const permission = await Permission.findOne({
                active: true,
                target_type: req.body.target_type,
                permission: req.body.permission,
            })

            if ( permission ) {
                policy.for_permission = true
                policy.permission = req.body.permission
            }
        }

        await policy.save()
        req.user.allow(`iam:policy:${policy.id}`)
        await req.user.save()
        return res.api(await policy.to_api())
    }

    async create_permission(req, res, next) {
        const Permission = this.models.get('iam:Permission')

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

        const valid_target_types = ['application', 'api_scope', 'machine', 'machine_group', 'vault']
        if ( !valid_target_types.includes(req.body.target_type) ) {
            return res.status(400)
                .message(`${req.T('api.invalid_target_type')}`)
                .api()
        }

        if ( !req.user.can(`iam:permission${req.body.target_type}:create`) ) {
            return res.status(401).api()
        }

        // Make sure one doesn't already exist
        const existing = await Permission.findOne({
            active: true,
            target_type: req.body.target_type,
            permission: req.body.permission,
        })

        if ( existing ) {
            return res.status(400)
                .message(req.T('api.permission_already_exists'))
                .api()
        }

        const perm = new Permission({
            target_type: req.body.target_type,
            permission: req.body.permission,
        })

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

    async update_policy(req, res, next) {
        const Policy = this.models.get('iam:Policy')
        const policy = await Policy.findById(req.params.id)

        if ( !policy || !policy.active )
            return res.status(404)
                .message(req.T('iam.policy_not_found'))
                .api()

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

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

        if ( !['user', 'group'].includes(req.body.entity_type) )
            return res.status(400)
                .message(`${req.T('common.invalid')} entity_type. ${req.T('api.must_one')} user, group.`)
                .api()

        // Make sure the entity_id is valid
        if ( req.body.entity_type === 'user' ) {
            const User = this.models.get('auth:User')
            const user = await User.findById(req.body.entity_id)
            if ( !user || !req.user.can(`auth:user:${user.id}:view`) )
                return res.status(400)
                    .message(`${req.T('common.invalid')} entity_id.`)
                    .api()
        } else if ( req.body.entity_type === 'group' ) {
            const Group = this.models.get('auth:Group')
            const group = await Group.findById(req.body.entity_id)
            if ( !group || !group.active || !req.user.can(`auth:group:${group.id}:view`) )
                return res.status(400)
                    .message(`${req.T('common.invalid')} entity_id.`)
                    .api()
        }

        if ( !['allow', 'deny'].includes(req.body.access_type) )
            return res.status(400)
                .message(`${req.T('common.invalid')} access_type. ${req.T('api.must_one')} allow, deny.`)
                .api()

        if ( !['application', 'api_scope', 'machine', 'machine_group', 'vault'].includes(req.body.target_type) )
            return res.status(400)
                .message(`${req.T('common.invalid')} target_type. ${req.T('api.must_one')} application, api_scope, machine, machine_group, vault.`)
                .api()

        // Make sure the target_id is valid
        if ( req.body.target_type === 'application' ) {
            const Application = this.models.get('Application')
            const app = await Application.findById(req.body.target_id)
            if ( !app || !app.active || !req.user.can(`app:${app.id}:view`) )
                return res.status(400)
                    .message(`${req.T('common.invalid')} target_id.`)
                    .api()
        } else if ( req.body.target_type === 'api_scope' ) {
            const api_scopes = this.canon.get('controller::api:v1:Reflect.api_scopes')()
            if ( !api_scopes.includes(req.body.target_id) )
                return res.status(400)
                    .message(`${req.T('common.invalid')} target_id.`)
                    .api()
        } else if ( req.body.target_type === 'machine' ) {
            const Machine = this.models.get('ldap:Machine')
            const machine = await Machine.findById(req.body.target_id)
            if ( !machine || !machine.active || !req.user.can(`ldap:machine:${machine.id}:view`) )
                return res.status(400)
                    .message(`${req.T('common.invalid')} target_id.`)
                    .api()
        } else if ( req.body.target_type === 'machine_group' ) {
            const MachineGroup = this.models.get('ldap:MachineGroup')
            const group = await MachineGroup.findById(req.body.target_id)
            if ( !group || !group.active || !req.user.can(`ldap:machine_group:${group.id}:view`) )
                return res.status(400)
                    .message(`${req.T('common.invalid')} target_id.`)
                    .api()
        } else if ( req.body.target_type === 'vault' ) {
            const Vault = this.models.get('vault:Vault')
            const vault = await Vault.findById(req.body.target_id)
            if ( !vault?.active || !(await Policy.check_user_access(req.user, vault.id, 'update')) )
                return res.status(400)
                    .message(`${req.T('common.invalid')} target_id.`)
                    .api()
        }

        policy.entity_type = req.body.entity_type
        policy.entity_id = req.body.entity_id
        policy.access_type = req.body.access_type
        policy.target_type = req.body.target_type
        policy.target_id = req.body.target_id

        if ( req.body.permission ) {
            // Validate the permission and set it, if it is valid
            const Permission = this.models.get('iam:Permission')
            const permission = await Permission.findOne({
                active: true,
                target_type: req.body.target_type,
                permission: req.body.permission,
            })

            if ( permission ) {
                policy.for_permission = true
                policy.permission = req.body.permission
            } else {
                policy.for_permission = false
                policy.permission = undefined
            }
        } else {
            policy.for_permission = false
            policy.permission = undefined
        }

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

    async update_permission(req, res, next) {
        const Permission = this.models.get('iam:Permission')

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

        const valid_target_types = ['application', 'api_scope', 'machine', 'machine_group', 'vault']
        if ( !valid_target_types.includes(req.body.target_type) ) {
            return res.status(400)
                .message(`${req.T('api.invalid_target_type')}`)
                .api()
        }

        if ( !req.user.can(`iam:permission${req.body.target_type}:update`) ) {
            return res.status(401).api()
        }

        // Make sure one doesn't already exist
        const existing = await Permission.findById(req.params.id)
        if ( !existing?.active ) {
            return res.status(404)
                .message(req.T('api.permission_not_found'))
                .api()
        }

        existing.target_type = req.body.target_type
        existing.permission = req.body.permission

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

    async delete_policy(req, res, next) {
        const Policy = this.models.get('iam:Policy')
        const policy = await Policy.findById(req.params.id)

        if ( !policy || !policy.active )
            return res.status(404)
                .message(req.T('iam.policy_not_found'))
                .api()

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

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

    async delete_permission(req, res, next) {
        const Permission = this.models.get('iam:Permission')
        const permission = await Permission.findById(req.params.id)

        if ( !permission?.active ) {
            return res.status(404)
                .message(req.T('api.permission_not_found'))
                .api()
        }

        if ( !req.user.can(`iam:permission:${permission.target_type}:delete`) ) {
            return res.status(401)
                .message(req.T('api.insufficient_permissions'))
                .api()
        }

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

module.exports = exports = IAMController
