const debug = require('ghost-ignition').debug('services:url:resources'),
    Promise = require('bluebird'),
    _ = require('lodash'),
    Resource = require('./Resource'),
    config = require('../../config'),
    models = require('../../models'),
    common = require('../../lib/common');

/**
 * These are the default resources and filters.
 * These are the minimum filters for public accessibility of resources.
 */
const resourcesConfig = [
    {
        type: 'posts',
        modelOptions: {
            modelName: 'Post',
            filter: 'visibility:public+status:published+page:false',
            exclude: [
                'title',
                'mobiledoc',
                'html',
                'plaintext',
                'amp',
                'codeinjection_head',
                'codeinjection_foot',
                'meta_title',
                'meta_description',
                'custom_excerpt',
                'og_image',
                'og_title',
                'og_description',
                'twitter_image',
                'twitter_title',
                'twitter_description',
                'custom_template',
                'feature_image',
                'locale'
            ],
            withRelated: ['tags', 'authors'],
            withRelatedPrimary: {
                primary_tag: 'tags',
                primary_author: 'authors'
            },
            withRelatedFields: {
                tags: ['tags.id', 'tags.slug'],
                authors: ['users.id', 'users.slug']
            }
        },
        events: {
            add: 'post.published',
            update: 'post.published.edited',
            remove: 'post.unpublished'
        }
    },
    {
        type: 'pages',
        modelOptions: {
            modelName: 'Post',
            exclude: [
                'title',
                'mobiledoc',
                'html',
                'plaintext',
                'amp',
                'codeinjection_head',
                'codeinjection_foot',
                'meta_title',
                'meta_description',
                'custom_excerpt',
                'og_image',
                'og_title',
                'og_description',
                'twitter_image',
                'twitter_title',
                'twitter_description',
                'custom_template',
                'feature_image',
                'locale',
                'tags',
                'authors',
                'primary_tag',
                'primary_author'
            ],
            filter: 'visibility:public+status:published+page:true'
        },
        events: {
            add: 'page.published',
            update: 'page.published.edited',
            remove: 'page.unpublished'
        }
    },
    {
        type: 'tags',
        keep: ['id', 'slug', 'updated_at', 'created_at'],
        modelOptions: {
            modelName: 'Tag',
            exclude: [
                'description',
                'meta_title',
                'meta_description'
            ],
            filter: 'visibility:public'
        },
        events: {
            add: 'tag.added',
            update: 'tag.edited',
            remove: 'tag.deleted'
        }
    },
    {
        type: 'users',
        modelOptions: {
            modelName: 'User',
            exclude: [
                'bio',
                'website',
                'location',
                'facebook',
                'twitter',
                'accessibility',
                'meta_title',
                'meta_description',
                'tour'
            ],
            filter: 'visibility:public'
        },
        events: {
            add: 'user.activated',
            update: 'user.activated.edited',
            remove: 'user.deactivated'
        }
    }
];

/**
 * NOTE: We are querying knex directly, because the Bookshelf ORM overhead is too slow.
 */
class Resources {
    constructor(queue) {
        this.queue = queue;
        this.data = {};

        this.listeners = [];
        this._listeners();
    }

    _listenOn(eventName, listener) {
        this.listeners.push({
            eventName: eventName,
            listener: listener
        });

        common.events.on(eventName, listener);
    }

    _listeners() {
        /**
         * We fetch the resources as early as possible.
         * Currently the url service needs to use the settings cache,
         * because we need to `settings.permalink`.
         */
        this._listenOn('db.ready', this._onDatabaseReady.bind(this));
    }

    _onDatabaseReady() {
        const ops = [];
        debug('db ready. settings cache ready.');

        _.each(resourcesConfig, (resourceConfig) => {
            this.data[resourceConfig.type] = [];
            ops.push(this._fetch(resourceConfig));

            this._listenOn(resourceConfig.events.add, (model) => {
                return this._onResourceAdded.bind(this)(resourceConfig.type, model);
            });

            this._listenOn(resourceConfig.events.update, (model) => {
                return this._onResourceUpdated.bind(this)(resourceConfig.type, model);
            });

            this._listenOn(resourceConfig.events.remove, (model) => {
                return this._onResourceRemoved.bind(this)(resourceConfig.type, model);
            });
        });

        Promise.all(ops)
            .then(() => {
                // CASE: all resources are fetched, start the queue
                this.queue.start({
                    event: 'init',
                    tolerance: 100,
                    requiredSubscriberCount: 1
                });
            });
    }

    _fetch(resourceConfig, options = {offset: 0, limit: 999}) {
        debug('_fetch', resourceConfig.type, resourceConfig.modelOptions);

        let modelOptions = _.cloneDeep(resourceConfig.modelOptions);
        const isSQLite = config.get('database:client') === 'sqlite3';

        // CASE: prevent "too many SQL variables" error on SQLite3
        if (isSQLite) {
            modelOptions.offset = options.offset;
            modelOptions.limit = options.limit;
        }

        return models.Base.Model.raw_knex.fetchAll(modelOptions)
            .then((objects) => {
                debug('fetched', resourceConfig.type, objects.length);

                _.each(objects, (object) => {
                    this.data[resourceConfig.type].push(new Resource(resourceConfig.type, object));
                });

                if (objects.length && isSQLite) {
                    options.offset = options.offset + options.limit;
                    return this._fetch(resourceConfig, {offset: options.offset, limit: options.limit});
                }
            });
    }

    _onResourceAdded(type, model) {
        const resourceConfig = _.find(resourcesConfig, {type: type});
        const exclude = resourceConfig.modelOptions.exclude;
        const withRelatedFields = resourceConfig.modelOptions.withRelatedFields;
        const obj = _.omit(model.toJSON(), exclude);

        if (withRelatedFields) {
            _.each(withRelatedFields, (fields, key) => {
                if (!obj[key]) {
                    return;
                }

                obj[key] = _.map(obj[key], (relation) => {
                    const relationToReturn = {};

                    _.each(fields, (field) => {
                        const fieldSanitized = field.replace(/^\w+./, '');
                        relationToReturn[fieldSanitized] = relation[fieldSanitized];
                    });

                    return relationToReturn;
                });
            });

            const withRelatedPrimary = resourceConfig.modelOptions.withRelatedPrimary;

            if (withRelatedPrimary) {
                _.each(withRelatedPrimary, (relation, primaryKey) => {
                    if (!obj[primaryKey] || !obj[relation]) {
                        return;
                    }

                    const targetTagKeys = Object.keys(obj[relation].find((item) => {return item.id === obj[primaryKey].id;}));
                    obj[primaryKey] = _.pick(obj[primaryKey], targetTagKeys);
                });
            }
        }

        const resource = new Resource(type, obj);

        debug('_onResourceAdded', type);
        this.data[type].push(resource);

        this.queue.start({
            event: 'added',
            action: 'added:' + model.id,
            eventData: {
                id: model.id,
                type: type
            }
        });
    }

    /**
     * CASE:
     *  - post was fetched on bootstrap
     *  - that means, the post is already published
     *  - resource exists, but nobody owns it
     *  - if the model changes, it can be that somebody will then own the post
     *
     * CASE:
     *   - post was fetched on bootstrap
     *   - that means, the post is already published
     *   - resource exists and is owned by somebody
     *   - but the data changed and is maybe no longer owned?
     *   - e.g. featured:false changes and your filter requires featured posts
     */
    _onResourceUpdated(type, model) {
        debug('_onResourceUpdated', type);

        this.data[type].every((resource) => {
            if (resource.data.id === model.id) {
                const resourceConfig = _.find(resourcesConfig, {type: type});
                const exclude = resourceConfig.modelOptions.exclude;
                const withRelatedFields = resourceConfig.modelOptions.withRelatedFields;
                const obj = _.omit(model.toJSON(), exclude);

                if (withRelatedFields) {
                    _.each(withRelatedFields, (fields, key) => {
                        if (!obj[key]) {
                            return;
                        }

                        obj[key] = _.map(obj[key], (relation) => {
                            const relationToReturn = {};

                            _.each(fields, (field) => {
                                const fieldSanitized = field.replace(/^\w+./, '');
                                relationToReturn[fieldSanitized] = relation[fieldSanitized];
                            });

                            return relationToReturn;
                        });
                    });

                    const withRelatedPrimary = resourceConfig.modelOptions.withRelatedPrimary;

                    if (withRelatedPrimary) {
                        _.each(withRelatedPrimary, (relation, primaryKey) => {
                            if (!obj[primaryKey] || !obj[relation]) {
                                return;
                            }

                            const targetTagKeys = Object.keys(obj[relation].find((item) => {return item.id === obj[primaryKey].id;}));
                            obj[primaryKey] = _.pick(obj[primaryKey], targetTagKeys);
                        });
                    }
                }

                resource.update(obj);

                // CASE: pretend it was added
                if (!resource.isReserved()) {
                    this.queue.start({
                        event: 'added',
                        action: 'added:' + model.id,
                        eventData: {
                            id: model.id,
                            type: type
                        }
                    });
                }

                // break!
                return false;
            }

            return true;
        });
    }

    _onResourceRemoved(type, model) {
        let index = null;
        let resource;

        this.data[type].every((_resource, _index) => {
            if (_resource.data.id === model._previousAttributes.id) {
                resource = _resource;
                index = _index;
                // break!
                return false;
            }

            return true;
        });

        // CASE: there are possible cases that the resource was not fetched e.g. visibility is internal
        if (index === null) {
            debug('can\'t find resource', model._previousAttributes.id);
            return;
        }

        this.data[type].splice(index, 1);
        resource.remove();
    }

    getAll() {
        return this.data;
    }

    getAllByType(type) {
        return this.data[type];
    }

    getByIdAndType(type, id) {
        return _.find(this.data[type], {data: {id: id}});
    }

    reset() {
        _.each(this.listeners, (obj) => {
            common.events.removeListener(obj.eventName, obj.listener);
        });

        this.listeners = [];
        this.data = {};
    }

    softReset() {
        this.data = {};

        _.each(resourcesConfig, (resourceConfig) => {
            this.data[resourceConfig.type] = [];
        });
    }
}

module.exports = Resources;
