// # Ghost Data API
// Provides access from anywhere to the Ghost data layer.
//
// Ghost's JSON API is integral to the workings of Ghost, regardless of whether you want to access data internally,
// from a theme, an app, or from an external app, you'll use the Ghost JSON API to do so.

var _              = require('lodash'),
    Promise        = require('bluebird'),
    config         = require('../config'),
    // Include Endpoints
    configuration  = require('./configuration'),
    db             = require('./db'),
    mail           = require('./mail'),
    notifications  = require('./notifications'),
    posts          = require('./posts'),
    roles          = require('./roles'),
    settings       = require('./settings'),
    tags           = require('./tags'),
    themes         = require('./themes'),
    users          = require('./users'),
    slugs          = require('./slugs'),
    authentication = require('./authentication'),
    uploads        = require('./upload'),
    dataExport     = require('../data/export'),
    errors         = require('../errors'),

    http,
    formatHttpErrors,
    addHeaders,
    cacheInvalidationHeader,
    locationHeader,
    contentDispositionHeader,
    init;

/**
 * ### Init
 * Initialise the API - populate the settings cache
 * @return {Promise(Settings)} Resolves to Settings Collection
 */
init = function () {
    return settings.updateSettingsCache();
};

/**
 * ### Cache Invalidation Header
 * Calculate the header string for the X-Cache-Invalidate: header.
 * The resulting string instructs any cache in front of the blog that request has occurred which invalidates any cached
 * versions of the listed URIs.
 *
 * `/*` is used to mean the entire cache is invalid
 *
 * @private
 * @param {Express.request} req Original HTTP Request
 * @param {Object} result API method result
 * @return {Promise(String)} Resolves to header string
 */
cacheInvalidationHeader = function (req, result) {
    var parsedUrl = req._parsedUrl.pathname.replace(/^\/|\/$/g, '').split('/'),
        method = req.method,
        endpoint = parsedUrl[0],
        id = parsedUrl[1],
        cacheInvalidate,
        jsonResult = result.toJSON ? result.toJSON() : result,
        post,
        hasStatusChanged,
        wasDeleted,
        wasPublishedUpdated;

    if (method === 'POST' || method === 'PUT' || method === 'DELETE') {
        if (endpoint === 'settings' || endpoint === 'users' || endpoint === 'db') {
            cacheInvalidate = '/*';
        } else if (endpoint === 'posts') {
            post = jsonResult.posts[0];
            hasStatusChanged = post.statusChanged;
            wasDeleted = method === 'DELETE';
            // Invalidate cache when post was updated but not when post is draft
            wasPublishedUpdated = method === 'PUT' && post.status === 'published';

            // Remove the statusChanged value from the response
            delete post.statusChanged;

            // Don't set x-cache-invalidate header for drafts
            if (hasStatusChanged || wasDeleted || wasPublishedUpdated) {
                cacheInvalidate = '/, /page/*, /rss/, /rss/*, /tag/*, /author/*, /sitemap-*.xml';
                if (id && post.slug && post.url) {
                    cacheInvalidate +=  ', ' + post.url;
                }
            }
        }
    }

    return Promise.resolve(cacheInvalidate);
};

/**
 * ### Location Header
 *
 * If the API request results in the creation of a new object, construct a Location: header which points to the new
 * resource.
 *
 * @private
 * @param {Express.request} req Original HTTP Request
 * @param {Object} result API method result
 * @return {Promise(String)} Resolves to header string
 */
locationHeader = function (req, result) {
    var apiRoot = config.urlFor('api'),
        location,
        newObject;

    if (req.method === 'POST') {
        if (result.hasOwnProperty('posts')) {
            newObject = result.posts[0];
            location = apiRoot + '/posts/' + newObject.id + '/?status=' + newObject.status;
        } else if (result.hasOwnProperty('notifications')) {
            newObject = result.notifications[0];
            location = apiRoot + '/notifications/' + newObject.id;
        } else if (result.hasOwnProperty('users')) {
            newObject = result.users[0];
            location = apiRoot + '/users/' + newObject.id;
        }
    }

    return Promise.resolve(location);
};

/**
 * ### Content Disposition Header
 * create a header that invokes the 'Save As' dialog in the browser when exporting the database to file. The 'filename'
 * parameter is governed by [RFC6266](http://tools.ietf.org/html/rfc6266#section-4.3).
 *
 * For encoding whitespace and non-ISO-8859-1 characters, you MUST use the "filename*=" attribute, NOT "filename=".
 * Ideally, both. Examples: http://tools.ietf.org/html/rfc6266#section-5
 *
 * We'll use ISO-8859-1 characters here to keep it simple.
 *
 * @private
 * @see http://tools.ietf.org/html/rfc598
 * @return {string}
 */
contentDispositionHeader = function () {
    return dataExport.fileName().then(function (filename) {
        return 'Attachment; filename="' + filename + '"';
    });
};

/**
 * ### Format HTTP Errors
 * Converts the error response from the API into a format which can be returned over HTTP
 *
 * @private
 * @param {Array} error
 * @return {{errors: Array, statusCode: number}}
 */
formatHttpErrors = function (error) {
    var statusCode = 500,
        errors = [];

    if (!_.isArray(error)) {
        error = [].concat(error);
    }

    _.each(error, function (errorItem) {
        var errorContent = {};

        // TODO: add logic to set the correct status code
        statusCode = errorItem.code || 500;

        errorContent.message = _.isString(errorItem) ? errorItem :
            (_.isObject(errorItem) ? errorItem.message : 'Unknown API Error');
        errorContent.type = errorItem.type || 'InternalServerError';
        errors.push(errorContent);
    });

    return {errors: errors, statusCode: statusCode};
};

addHeaders = function (apiMethod, req, res, result) {
    var ops = [],
        cacheInvalidation,
        location,
        contentDisposition;

    cacheInvalidation = cacheInvalidationHeader(req, result)
        .then(function addCacheHeader(header) {
            if (header) {
                res.set({'X-Cache-Invalidate': header});
            }
        });

    ops.push(cacheInvalidation);

    if (req.method === 'POST') {
        location = locationHeader(req, result)
            .then(function addLocationHeader(header) {
                if (header) {
                    res.set({Location: header});
                    // The location header indicates that a new object was created.
                    // In this case the status code should be 201 Created
                    res.status(201);
                }
            });
        ops.push(location);
    }

    if (apiMethod === db.exportContent) {
        contentDisposition = contentDispositionHeader()
            .then(function addContentDispositionHeader(header) {
                // Add Content-Disposition Header
                if (apiMethod === db.exportContent) {
                    res.set({
                        'Content-Disposition': header
                    });
                }
            });
        ops.push(contentDisposition);
    }

    return Promise.all(ops);
};

/**
 * ### HTTP
 *
 * Decorator for API functions which are called via an HTTP request. Takes the API method and wraps it so that it gets
 * data from the request and returns a sensible JSON response.
 *
 * @public
 * @param {Function} apiMethod API method to call
 * @return {Function} middleware format function to be called by the route when a matching request is made
 */
http = function (apiMethod) {
    return function (req, res) {
        // We define 2 properties for using as arguments in API calls:
        var object = req.body,
            response,
            options = _.extend({}, req.files, req.query, req.params, {
                context: {
                    user: (req.user && req.user.id) ? req.user.id : null
                }
            });

        // If this is a GET, or a DELETE, req.body should be null, so we only have options (route and query params)
        // If this is a PUT, POST, or PATCH, req.body is an object
        if (_.isEmpty(object)) {
            object = options;
            options = {};
        }

        return apiMethod(object, options)
            // Handle adding headers
            .then(function onSuccess(result) {
                response = result;
                // Add X-Cache-Invalidate header
                return addHeaders(apiMethod, req, res, result);
            }).then(function () {
                // #### Success
                // Send a properly formatting HTTP response containing the data with correct headers
                res.json(response || {});
            }).catch(function onError(error) {
                errors.logError(error);
                // #### Error
                var httpErrors = formatHttpErrors(error);
                // Send a properly formatted HTTP response containing the errors
                res.status(httpErrors.statusCode).json({errors: httpErrors.errors});
            });
    };
};

/**
 * ## Public API
 */
module.exports = {
    // Extras
    init: init,
    http: http,
    // API Endpoints
    configuration: configuration,
    db: db,
    mail: mail,
    notifications: notifications,
    posts: posts,
    roles: roles,
    settings: settings,
    tags: tags,
    themes: themes,
    users: users,
    slugs: slugs,
    authentication: authentication,
    uploads: uploads
};

/**
 * ## API Methods
 *
 * Most API methods follow the BREAD pattern, although not all BREAD methods are available for all resources.
 * Most API methods have a similar signature, they either take just `options`, or both `object` and `options`.
 * For RESTful resources `object` is always a model object of the correct type in the form `name: [{object}]`
 * `options` is an object with several named properties, the possibilities are listed for each method.
 *
 * Read / Edit / Destroy routes expect some sort of identifier (id / slug / key) for which object they are handling
 *
 * All API methods take a context object as one of the options:
 *
 * @typedef context
 * Context provides information for determining permissions. Usually a user, but sometimes an app, or the internal flag
 * @param {Number} user (optional)
 * @param {String} app (optional)
 * @param {Boolean} internal (optional)
 */
