/************************************************************************
 * This file is part of EspoCRM.
 *
 * EspoCRM – Open Source CRM application.
 * Copyright (C) 2014-2025 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
 * Website: https://www.espocrm.com
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * The interactive user interfaces in modified source and object code versions
 * of this program must display Appropriate Legal Notices, as required under
 * Section 5 of the GNU Affero General Public License version 3.
 *
 * In accordance with Section 7(b) of the GNU Affero General Public License version 3,
 * these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
 ************************************************************************/

/** @module controllers/record */

import Controller from 'controller';

/**
 * A record controller.
 * @template {module:model} TModel
 */
class RecordController extends Controller {

    /** @inheritDoc */
    defaultAction = 'list'

    constructor(params, injections) {
        super(params, injections);

        /**
         * @private
         * @type {Object}
         */
        this.collectionMap = {};
    }

    /** @inheritDoc */
    checkAccess(action) {
        if (this.getAcl().check(this.name, action)) {
            return true;
        }

        return false;
    }

    /**
     * Get a view name/path.
     *
     * @protected
     * @param {'list'|'detail'|'edit'|'create'|'listRelated'|string} type A type.
     * @returns {string}
     */
    getViewName(type) {
        return this.getMetadata().get(['clientDefs', this.name, 'views', type]) ||
            'views/' + Espo.Utils.camelCaseToHyphen(type);
    }

    // noinspection JSUnusedGlobalSymbols
    beforeList() {
        this.handleCheckAccess('read');
    }

    /**
     * @param {{
     *     isReturn?: boolean,
     *     primaryFilter?: string,
     * } | Record} options
     */
    actionList(options) {
        const isReturn = options.isReturn || this.getRouter().backProcessed;

        let key = 'list';

        if (options.primaryFilter) {
            key += 'Filter' + Espo.Utils.upperCaseFirst(options.primaryFilter);
        }

        if (!isReturn && this.getStoredMainView(key)) {
            this.clearStoredMainView(key);
        }

        this.getCollection().then(collection => {
            const mediator = {};

            const abort = () => {
                collection.abortLastFetch();
                mediator.abort = true;

                Espo.Ui.notify(false);
            };

            this.listenToOnce(this.baseController, 'action', abort);
            this.listenToOnce(collection, 'sync', () => this.stopListening(this.baseController, 'action', abort));

            const viewOptions = {
                scope: this.name,
                collection: collection,
                params: options,
                mediator: mediator,
            };

            const viewName = this.getViewName('list');

            const params = {
                useStored: isReturn,
                key: key,
            };

            this.main(viewName, viewOptions, null, params);
        });
    }

    beforeView() {
        this.handleCheckAccess('read');
    }

    /**
     * @protected
     * @param {{
     *     returnUrl?: string,
     *     returnDispatchParams?: Record,
     * } | Record} options
     * @param {module:model} model
     * @param {string|null} [view]
     */
    createViewView(options, model, view) {
        view = view || this.getViewName('detail');

        this.main(view, {
            scope: this.name,
            model: model,
            returnUrl: options.returnUrl,
            returnDispatchParams: options.returnDispatchParams,
            params: options,
        });
    }

    /**
     * @protected
     * @param {module:model} model
     * @param {Record} options
     */
    prepareModelView(model, options) {}

    // noinspection JSUnusedGlobalSymbols
    /**
     * @param {{
     *     model?: module:model,
     *     id?: string,
     *     isReturn?: boolean,
     *     isAfterCreate?: boolean,
     * } | Record} options
     */
    actionView(options) {
        const id = options.id;

        const isReturn = this.getRouter().backProcessed;

        if (isReturn) {
            if (this.lastViewActionOptions && this.lastViewActionOptions.id === id) {
                options = Espo.Utils.clone(this.lastViewActionOptions);

                if (options.model && options.model.get('deleted')) {
                    delete options.model;
                }
            }

            options.isReturn = true;
        }
        else {
            delete this.lastViewActionOptions;
        }

        this.lastViewActionOptions = options;

        const createView = model => {
            this.prepareModelView(model, options);

            this.createViewView.call(this, options, model);
        };

        if ('model' in options) {
            const model = options.model;

            createView(model);

            this.showLoadingNotification();

            model.fetch()
                .then(() => this.hideLoadingNotification())
                .catch(xhr => {
                    if (
                        xhr.status === 403 &&
                        options.isAfterCreate
                    ) {
                        this.hideLoadingNotification();
                        xhr.errorIsHandled = true;

                        model.trigger('fetch-forbidden');
                    }
                });

            this.listenToOnce(this.baseController, 'action', () => {
                model.abortLastFetch();
                this.hideLoadingNotification();
            });

            return;
        }

        this.getModel().then(model => {
            model.id = id;

            this.showLoadingNotification();

            model.fetch({main: true})
                .then(() => {
                    this.hideLoadingNotification();

                    if (model.get('deleted')) {
                        this.listenToOnce(model, 'after:restore-deleted', () => {
                            createView(model);
                        });

                        this.prepareModelView(model, options);
                        this.createViewView(options, model, 'views/deleted-detail');

                        return;
                    }

                    createView(model);
                });

            this.listenToOnce(this.baseController, 'action', () => {
                model.abortLastFetch();
            });
        });
    }

    // noinspection JSUnusedGlobalSymbols
    beforeCreate() {
        this.handleCheckAccess('create');
    }

    // noinspection JSUnusedLocalSymbols
    /**
     * @protected
     * @param {module:model} model
     * @param {Record} options
     */
    prepareModelCreate(model, options) {
        this.listenToOnce(model, 'before:save', () => {
            const key = 'list';

            const stored = this.getStoredMainView(key);

            if (!stored) {
                return;
            }

            if (!('storeViewAfterCreate' in stored) || !stored.storeViewAfterCreate) {
                this.clearStoredMainView(key);
            }
        });

        this.listenToOnce(model, 'after:save', () => {
            const key = 'list';

            const stored = this.getStoredMainView(key);

            if (!stored) {
                return;
            }

            if (!('storeViewAfterCreate' in stored) || !stored.storeViewAfterCreate) {
                return;
            }

            if (!('collection' in stored) || !stored.collection) {
                return;
            }

            this.listenToOnce(stored, 'after:render', () => stored.collection.fetch());
        });
    }

    /**
     * @param {{
     *     options?: Record,
     *     relate?: model:model~setRelateItem | model:model~setRelateItem[],
     *     returnUrl?: string,
     *     returnDispatchParams?: Record,
     *     attributes?: Record,
     * } | Record} [options]
     */
    create(options) {
        options = options || {};

        const optionsOptions = options.options || {};

        this.getModel().then(model => {
            if (options.relate) {
                model.setRelate(options.relate);
            }

            const o = {
                scope: this.name,
                model: model,
                returnUrl: options.returnUrl,
                returnDispatchParams: options.returnDispatchParams,
                params: options,
            };

            for (const k in optionsOptions) {
                o[k] = optionsOptions[k];
            }

            if (options.attributes) {
                model.set(options.attributes);
            }

            this.prepareModelCreate(model, options);

            this.main(this.getViewName('edit'), o);
        });
    }

    /**
     * @param {{
     *     options?: Record,
     *     relate?: model:model~setRelateItem | model:model~setRelateItem[],
     *     returnUrl?: string,
     *     returnDispatchParams?: Record,
     *     options?: Record,
     *     attributes?: Record,
     * } | Record} [options]
     */
    actionCreate(options) {
        this.create(options);
    }

    // noinspection JSUnusedGlobalSymbols
    beforeEdit() {
        this.handleCheckAccess('edit');
    }

    // noinspection JSUnusedLocalSymbols
    /**
     * @protected
     * @param {module:model} model
     * @param {Record} options
     */
    prepareModelEdit(model, options) {
        this.listenToOnce(model, 'before:save', () => {
            const key = 'list';

            const stored = this.getStoredMainView(key);

            if (!stored) {
                return;
            }

            if (!('storeViewAfterUpdate' in stored) || !stored.storeViewAfterUpdate) {
                this.clearStoredMainView(key);
            }
        });
    }

    /**
     * @param {{
     *     id: string,
     *     options?: Record,
     *     model?: import('model').default,
     *     returnUrl?: string,
     *     attributes?: Record,
     *     highlightFieldList?: string[],
     *     returnDispatchParams?: Record,
     * }} options
     */
    actionEdit(options) {
        const id = options.id;

        const optionsOptions = options.options || {};

        this.getModel().then(model => {
            model.id = id;

            if (options.model) {
                model = options.model;
            }

            this.prepareModelEdit(model, options);

            this.showLoadingNotification();

            model
                .fetch({main: true})
                .then(() => {
                    this.hideLoadingNotification();

                    const o = {
                        scope: this.name,
                        model: model,
                        returnUrl: options.returnUrl,
                        returnDispatchParams: options.returnDispatchParams,
                        params: options,
                    };

                    for (const k in optionsOptions) {
                        o[k] = optionsOptions[k];
                    }

                    if (options.attributes) {
                        o.attributes = options.attributes;
                    }

                    if (options.highlightFieldList) {
                        o.highlightFieldList = options.highlightFieldList;
                    }

                    this.main(this.getViewName('edit'), o);
                });

            this.listenToOnce(this.baseController, 'action', () => {
                model.abortLastFetch();
            });
        });
    }

    // noinspection JSUnusedGlobalSymbols
    beforeMerge() {
        this.handleCheckAccess('edit');
    }

    // noinspection JSUnusedGlobalSymbols
    /**
     * @param {{
     *     ids: string,
     *     collection: import('collection').default,
     * }} options
     */
    actionMerge(options) {
        const ids = options.ids.split(',');

        this.getModel().then((model) => {
            const models = [];

            const proceed = () => {
                this.main('views/merge', {
                    models: models,
                    scope: this.name,
                    collection: options.collection
                });
            };

            let i = 0;

            ids.forEach(id => {
                const current = model.clone();

                current.id = id;
                models.push(current);

                this.listenToOnce(current, 'sync', () => {
                    i++;

                    if (i === ids.length) {
                        proceed();
                    }
                });

                current.fetch();
            });
        });
    }

    // noinspection JSUnusedGlobalSymbols
    /**
     * @param {{
     *     id: string,
     *     link: string,
     * }} options
     */
    actionRelated(options) {
        const id = options.id;
        const link = options.link;

        const viewName = this.getViewName('listRelated');

        let model;

        this.getModel()
            .then(m => {
                model = m;
                model.id = id;

                return model.fetch({main: true});
            })
            .then(() => {
                const foreignEntityType = model.getLinkParam(link, 'entity');

                if (!foreignEntityType) {
                    this.baseController.error404();

                    throw new Error(`Bad link '${link}'.`);
                }

                return this.collectionFactory.create(foreignEntityType);
            })
            .then(collection => {
                collection.url = model.entityType + '/' + id + '/' + link;

                this.main(viewName, {
                    scope: this.name,
                    model: model,
                    collection: collection,
                    link: link,
                });
            })
    }

    /**
     * Get a collection for the current controller.
     *
     * @protected
     * @param {boolean} [usePreviouslyFetched=false] Use a previously fetched. @todo Revise.
     * @return {Promise<module:collection>}
     */
    getCollection(usePreviouslyFetched) {
        if (!this.name) {
            throw new Error('No collection for unnamed controller');
        }

        const entityType = this.entityType || this.name;

        if (usePreviouslyFetched && entityType in this.collectionMap) {
            const collection = this.collectionMap[entityType];

            return Promise.resolve(collection);
        }

        return this.collectionFactory.create(entityType, collection => {
            this.collectionMap[entityType] = collection;

            this.listenTo(collection, 'sync', () => collection.isFetched = true);
        });
    }

    /**
     * Get a model for the current controller.
     *
     * @protected
     * @param {Function} [callback]
     * @param {Object} [context]
     * @return {Promise<module:model>}
     */
    getModel(callback, context) {
        context = context || this;

        if (!this.name) {
            throw new Error('No collection for unnamed controller');
        }

        const modelName = this.entityType || this.name;

        return this.modelFactory.create(modelName, model => {
            if (callback) {
                callback.call(context, model);
            }
        });
    }
}

export default RecordController;
