// # Get Helper
// Usage: `{{#get "posts" limit="5"}}`, `{{#get "tags" limit="all"}}`
// Fetches data from the API
const {config, api, prepareContextResource} = require('../services/proxy');
const {hbs} = require('../services/handlebars');

const logging = require('@tryghost/logging');
const errors = require('@tryghost/errors');
const tpl = require('@tryghost/tpl');

const messages = {
    mustBeCalledAsBlock: 'The {\\{{helperName}}} helper must be called as a block. E.g. {{#{helperName}}}...{{/{helperName}}}',
    invalidResource: 'Invalid "{resource}" resource given to get helper'
};

const createFrame = hbs.handlebars.createFrame;

/**
 * ## Parse Options
 * Ensure options passed in make sense
 *
 * @param {Object} options
 * @returns {*}
 */
function parseOptions(options) {
    if (options.limit === 'all' || !options.limit) {
        return {
            limit: 3
        };
    }

    return {
        limit: options.limit
    };
}

/**
 *
 * @param {string} resource
 * @param {string} controllerName
 * @param {string} action
 * @param {Object} apiOptions
 * @returns {Promise<Object>}
 */
async function makeAPICall(resource, controllerName, action, apiOptions) {
    const controller = api[controllerName];

    let timer;

    try {
        let response;

        if (config.get('optimization:getHelper:timeout:threshold')) {
            const logLevel = config.get('optimization:getHelper:timeout:level') || 'error';
            const threshold = config.get('optimization:getHelper:timeout:threshold');

            const apiResponse = controller[action](apiOptions);
            // consume rejections that happen after the timeout has already won
            // the race — they'd otherwise crash the process as unhandled
            apiResponse.catch(() => {});

            const timeout = new Promise((resolve) => {
                timer = setTimeout(() => {
                    logging[logLevel](new errors.HelperWarning({
                        message: `{{#get}} took longer than ${threshold}ms and was aborted`,
                        code: 'ABORTED_GET_HELPER',
                        errorDetails: {
                            api: `${controllerName}.${action}`,
                            apiOptions
                        }
                    }));

                    resolve({[resource]: [], '@@ABORTED_GET_HELPER@@': true});
                }, threshold);
            });

            response = await Promise.race([apiResponse, timeout]);
            clearTimeout(timer);
        } else {
            response = await controller[action](apiOptions);
        }

        return response;
    } catch (err) {
        clearTimeout(timer);
        throw err;
    }
}

/**
 * ## Get
 * @param {string} slug
 * @param {object} options
 * @returns {Promise<any>}
 */
module.exports = async function collection(slug, options) {
    options = options || {};
    options.hash = options.hash || {};
    options.data = options.data || {};

    const self = this;
    const data = createFrame(options.data);

    let apiOptions = options.hash;

    if (!options.fn) {
        data.error = tpl(messages.mustBeCalledAsBlock, {helperName: 'collection'});
        logging.warn(data.error);
        return;
    }

    const resource = 'posts';
    const controllerName = 'postsPublic';
    const action = 'browse';

    // Parse the options we're going to pass to the API
    apiOptions = parseOptions(apiOptions);
    apiOptions.context = {member: data.member};
    apiOptions.collection = slug;

    try {
        const response = await makeAPICall(resource, controllerName, action, apiOptions);

        // consume the internal abort marker so it doesn't leak into the template context
        const degraded = response['@@ABORTED_GET_HELPER@@'];
        delete response['@@ABORTED_GET_HELPER@@'];
        if (degraded && options.data?.root?._locals) {
            options.data.root._locals.degradedRender = true;
        }

        // prepare data properties for use with handlebars
        if (response[resource] && response[resource].length) {
            response[resource].forEach(prepareContextResource);
        }

        // block params allows the theme developer to name the data using something like
        // `{{#get "posts" as |result pageInfo|}}`
        const blockParams = [response[resource]];
        if (response.meta && response.meta.pagination) {
            response.pagination = response.meta.pagination;
            blockParams.push(response.meta.pagination);
        }

        // Call the main template function
        return options.fn(response, {
            data: data,
            blockParams: blockParams
        });
    } catch (error) {
        logging.error(error);
        data.error = error.message;
        return options.inverse(self, {data: data});
    }
};

module.exports.async = true;
