import { Model } from '@converse/skeletor';
import { _converse, api, converse, log, u } from '@converse/headless';
import { getOpenPromise } from '@converse/openpromise';

const { Strophe, $build, $iq, sizzle } = converse.env;

/**
 * @namespace _converse.DeviceList
 * @memberOf _converse
 */
class DeviceList extends Model {
    get idAttribute () { // eslint-disable-line class-methods-use-this
        return 'jid';
    }

    async initialize () {
        super.initialize();
        this.initialized = getOpenPromise();
        await this.initDevices();
        this.initialized.resolve();
    }

    initDevices () {
        this.devices = new _converse.exports.Devices();
        const bare_jid = _converse.session.get('bare_jid');
        const id = `converse.devicelist-${bare_jid}-${this.get('jid')}`;
        u.initStorage(this.devices, id);
        return this.fetchDevices();
    }

    async onDevicesFound (collection) {
        if (collection.length === 0) {
            let ids = [];
            try {
                ids = await this.fetchDevicesFromServer();
            } catch (e) {
                if (e === null) {
                    log.error(`Timeout error while fetching devices for ${this.get('jid')}`);
                } else {
                    log.error(`Could not fetch devices for ${this.get('jid')}`);
                    log.error(e);
                }
                this.destroy();
            }
            const bare_jid = _converse.session.get('bare_jid');
            if (this.get('jid') === bare_jid) {
                this.publishCurrentDevice(ids);
            }
        }
    }

    fetchDevices () {
        if (this._devices_promise === undefined) {
            this._devices_promise = new Promise(resolve => {
                this.devices.fetch({
                    'success': c => resolve(this.onDevicesFound(c)),
                    'error': (_, e) => {
                        log.error(e);
                        resolve();
                    }
                });
            });
        }
        return this._devices_promise;
    }

    async getOwnDeviceId () {
        const { omemo_store } = _converse.state;
        let device_id = omemo_store.get('device_id');
        if (!this.devices.get(device_id)) {
            // Generate a new bundle if we cannot find our device
            await omemo_store.generateBundle();
            device_id = omemo_store.get('device_id');
        }
        return device_id;
    }

    async publishCurrentDevice (device_ids) {
        const bare_jid = _converse.session.get('bare_jid');
        if (this.get('jid') !== bare_jid) {
            return; // We only publish for ourselves.
        }
        await api.omemo.session.restore();

        if (!_converse.state.omemo_store) {
            // Happens during tests. The connection gets torn down
            // before publishCurrentDevice has time to finish.
            log.warn('publishCurrentDevice: omemo_store is not defined, likely a timing issue');
            return;
        }
        if (!device_ids.includes(await this.getOwnDeviceId())) {
            return this.publishDevices();
        }
    }

    async fetchDevicesFromServer () {
        const bare_jid = _converse.session.get('bare_jid');
        const stanza = $iq({
            'type': 'get',
            'from': bare_jid,
            'to': this.get('jid')
        }).c('pubsub', { 'xmlns': Strophe.NS.PUBSUB })
          .c('items', { 'node': Strophe.NS.OMEMO_DEVICELIST });

        const iq = await api.sendIQ(stanza);
        const selector = `list[xmlns="${Strophe.NS.OMEMO}"] device`;
        const device_ids = sizzle(selector, iq).map(d => d.getAttribute('id'));
        const jid = this.get('jid');
        return Promise.all(device_ids.map(id => this.devices.create({ id, jid }, { 'promise': true })));
    }

    /**
     * Send an IQ stanza to the current user's "devices" PEP node to
     * ensure that all devices are published for potential chat partners to see.
     * See: https://xmpp.org/extensions/xep-0384.html#usecases-announcing
     */
    publishDevices () {
        const item = $build('item', { 'id': 'current' }).c('list', { 'xmlns': Strophe.NS.OMEMO });
        this.devices.filter(d => d.get('active')).forEach(d => item.c('device', { 'id': d.get('id') }).up());
        const options = { 'pubsub#access_model': 'open' };
        return api.pubsub.publish(null, Strophe.NS.OMEMO_DEVICELIST, item, options, false);
    }

    async removeOwnDevices (device_ids) {
        const bare_jid = _converse.session.get('bare_jid');
        if (this.get('jid') !== bare_jid) {
            throw new Error("Cannot remove devices from someone else's device list");
        }
        await Promise.all(device_ids.map(id => this.devices.get(id)).map(d =>
            new Promise(resolve => d.destroy({
                'success': resolve,
                'error': (_, e) => { log.error(e); resolve(); }
            }))
        ));
        return this.publishDevices();
    }
}

export default DeviceList;
