const AuthUser = require('flitter-auth/model/User')
const LDAP = require('ldapjs')

const ActiveScope = require('../scopes/ActiveScope')
const MFAToken = require('./MFAToken.model')
const PasswordReset = require('./PasswordReset.model')
const AppAuthorization = require('./AppAuthorization.model')
const AppPassword = require('./AppPassword.model')
const uuid = require('uuid').v4
const NotifyConfig = require('../system/NotifyConfig.model')

/*
 * Auth user model. This inherits fields and methods from the default
 * flitter-auth/model/User model, however you can override methods and
 * properties here as you need.
 */
class User extends AuthUser {
    static get services() {
        return [...super.services, 'auth', 'ldap_server', 'configs', 'models', 'app']
    }

    static get schema() {
        return {...super.schema, ...{
            // other schema fields here
            first_name: String,
            last_name: String,
            tagline: String,
            email: String,
            email_verified: {type: Boolean, default: false},
            ldap_visible: {type: Boolean, default: true},
            active: {type: Boolean, default: true},
            mfa_token: MFAToken,
            password_resets: [PasswordReset],
            app_passwords: [AppPassword],
            app_authorizations: [AppAuthorization],
            mfa_enabled: {type: Boolean, default: false},
            mfa_enable_date: Date,
            create_date: {type: Date, default: () => new Date},
            photo_file_id: String,
            trap: String,
            notify_config: NotifyConfig,
            uid_number: Number,
            login_shell: String,

            is_default_user_for_coreid: { type: Boolean, default: false },
        }}
    }

    async grant_defaults() {
        const default_user = await this.constructor.findOne({is_default_user_for_coreid: true, active: true})
        this.login_shell = default_user.login_shell
        this.roles = default_user.roles
        this.permissions = default_user.permissions

        const groups = await default_user.groups()
        for ( const group of groups ) {
            group.user_ids.push(this.id)
            await group.save()
        }
    }

    async get_uid_number() {
        if ( !this.uid_number ) {
            const Setting = this.models.get('Setting')
            let last_uid = await Setting.get('ldap.last_alloc_uid')
            if ( last_uid < 1 ) {
                last_uid = this.configs.get('ldap:server.schema.start_uid')
            }

            this.uid_number = last_uid + 1
            await Setting.set('ldap.last_alloc_uid', this.uid_number)
            await this.save()
        }

        return this.uid_number
    }

    async photo() {
        const File = this.models.get('upload::File')
        return File.findById(this.photo_file_id)
    }

    has_authorized(client) {
        return this.app_authorizations.some(x => x.client_id === client.id)
    }

    get_authorization(client) {
        for ( const auth of this.app_authorizations ) {
            if ( auth.client_id === client.id )
                return auth
        }
    }

    authorize(client) {
        if ( !this.has_authorized(client) ) {
            const client_rec = new AppAuthorization({
                client_id: client.id,
                api_scopes: client.api_scopes,
            }, this)

            this.app_authorizations.push(client_rec)
        } else {
            const client_rec = this.get_authorization(client)
            client_rec.api_scopes = client.api_scopes
        }
    }

    async to_api() {
        return {
            id: this.id,
            uid: this.uid,
            first_name: this.first_name,
            last_name: this.last_name,
            name: `${this.first_name} ${this.last_name}`,
            email: this.email,
            tagline: this.tagline,
            trap: this.trap,
            group_ids: (await this.groups()).map(x => x.id),
        }
    }

    static scopes = [
        new ActiveScope({})
    ]

    static async ldap_directory() {
        return this.find({ldap_visible: true})
    }

    // TODO just in case we need this later
    get can_login() {
        return true
    }

    async sessions() {
        const Session = require('../Session')
        this.app.di().inject(Session)
        return Session.find({ 'session.auth.user_id': this.id })
    }

    async kickout() {
        // TODO handle SAML session participants
        const sessions = await this.sessions()
        for ( const session of sessions ) {
            delete session.session.auth
            delete session.session.mfa_remember
            await session.save()
        }
    }

    // Prefer soft delete because of the active scope
    async delete() {
        this.active = false
        await this.save()
    }

    async check_password(password) {
        return this.get_provider().check_user_auth(this, password)
    }

    async check_app_password(password) {
        for ( const pw of this.app_passwords ) {
            if ( await pw.verify(password) ) return true
        }

        return false
    }

    async reset_password(new_password, reason = 'user') {
        const reset = new PasswordReset({
            reason,
            old_hash: this.password,
        }, this)

        await reset.set_hash(new_password)
        this.password = reset.hash
        this.password_resets.push(reset)
        return reset
    }

    async app_password(name) {
        const gen = uuid().replace(/-/g, '')
        const pw = new AppPassword({ name }, this)
        await pw.set_hash(gen)
        this.app_passwords.push(pw)
        return { password: gen, record: pw }
    }

    async groups() {
        const Group = this.models.get('auth:Group')
        return Group.find({ active: true, user_ids: this.id })
    }

    async oidc_sessions() {
        const Session = this.models.get('openid:Session')
        return Session.find({ 'payload.account': this.id })
    }

    async logout(request) {
        for ( const session of (await this.oidc_sessions()) ) {
            await session.delete()
        }

        this.get_provider().logout(request)
    }

    async has_sudo() {
        const groups = await this.groups()
        return groups.some(group => group.grants_sudo)
    }

    async to_sudo(iam_targets = []) {
        const Policy = this.models.get('iam:Policy')
        const granted = []
        for ( const target of iam_targets ) {
            if ( await Policy.check_user_access(this, target, 'sudo') ) {
                granted.push(target)
            }
        }

        return {
            objectClass: ['sudoRole'],
            cn: `sudo_${this.uid.toLowerCase()}`,
            sudoUser: this.uid.toLowerCase(),
            ...(granted.length ? {
                iamtarget: granted,
                sudoHost: 'ALL',
                sudoRunAs: 'ALL',
                sudoCommand: 'ALL',
            } : {})
        }
    }

    async to_ldap(iam_targets = []) {
        const Policy = this.models.get('iam:Policy')

        const uid_number = await this.get_uid_number()
        const shell = this.login_shell || this.configs.get('ldap:server.schema.default_shell')
        const domain = this.configs.get('ldap:server.schema.base_dc').split(',').map(x => x.replace('dc=', '')).join('.')

        const group_ids = []
        for ( const group of await this.groups() ) {
            group_ids.push(await group.get_gid_number())
        }

        const ldap_data = {
            uid: this.uid.toLowerCase(),
            uuid: this.uuid,
            cn: this.first_name,
            sn: this.last_name,
            gecos: `${this.first_name} ${this.last_name}`,
            mail: this.email,
            objectClass: ['inetOrgPerson', 'person', 'posixaccount'],
            objectclass: ['inetOrgPerson', 'person', 'posixaccount'],
            entryuuid: this.uuid,
            entryUUID: this.uuid,
            objectGuid: this.uuid,
            objectguid: this.uuid,
            uidNumber: uid_number,
            gidNumber: String(await this.get_uid_number()), // group_ids.map(x => String(x)),
            loginShell: shell,
            homeDirectory: `/home/${this.uid}@${domain}`
        }

        if ( this.tagline ) ldap_data.extras_tagline = this.tagline

        const addl_data = JSON.parse(this.data)
        for ( const key in addl_data ) {
            if ( !addl_data.hasOwnProperty(key) || !key.startsWith('ldap_') ) continue
            ldap_data[`data${key.substr(4)}`] = `${addl_data[key]}`
        }

        const groups = await this.groups()
        if ( groups.length > 0 ) {
            const group_data = groups.map(x => x.dn.format(this.configs.get('ldap:server.format')))
            ldap_data.memberOf = group_data
            ldap_data.memberof = group_data
        }

        const iamtarget = []
        for ( const target_id of iam_targets ) {
            if ( await Policy.check_user_access(this, target_id) ) {
                iamtarget.push(target_id)
            }
        }

        ldap_data.iamtarget = iamtarget

        return ldap_data
    }

    get dn() {
        return LDAP.parseDN(`uid=${this.uid.toLowerCase()},${this.ldap_server.auth_dn().format(this.configs.get('ldap:server.format'))}`)
    }

    get sudo_dn() {
        return LDAP.parseDN(`cn=sudo_${this.uid.toLowerCase()},${this.ldap_server.sudo_dn().format(this.configs.get('ldap:server.format'))}`)
    }

    // The following are used by OpenID connect

    async claims(use, scope) {
        return {
            sub: this.id,
            email: this.email,
            email_verified: true, // TODO
            family_name: this.last_name,
            given_name: this.first_name,
            locale: 'en_US', // TODO
            name: `${this.first_name} ${this.last_name}`,
            preferred_username: this.uid.toLowerCase(),
            username: this.uid.toLowerCase(),
        }
    }

    static async findByLogin(login) {
        return this.findOne({
            active: true,
            uid: login.toLowerCase(),
        })
    }

    static async findAccount(ctx, id, token) {
        return this.findById(id)
    }

    get accountId() {
        return this.id
    }
}

module.exports = exports = User
