'use strict';
var libVersion = require('../package.json').version,
    request = require('request'),
    throttledRequestLib = require('throttled-request');
/**
 * Creates instance of {@link RedditApi}.
 * @class
 * @classdesc Reddit API Controller which facilitates authentication and API endpoint queries.
 * @param {Object} options - Options object
 * @param {String} options.app_id - Reddit app ID
 * @param {String} options.app_secret - Reddit app secret
 * @param {String} [options.redirect_uri=null] - Reddit app redirect URI
 * @param {String} [options.user_agent=reddit-oauth/x.y.z by aihamh] - HTTP user agent header sent to Reddit for each request
 * @param {String} [options.access_token=null] - OAuth access token
 * @param {String} [options.refresh_token=null] - OAuth refresh token
 * @param {Number} [options.request_buffer=2000] - Time in milliseconds to buffer for each request
 */
function RedditApi(options) {
    if (!options || typeof options !== 'object') {
        throw 'Invalid options: ' + options;
    }
    if (typeof options.app_id !== 'string' || options.app_id.length < 1) {
        throw 'Invalid app ID: ' + options.app_id;
    }
    if (typeof options.app_secret !== 'string' || options.app_secret.length < 1) {
        throw 'Invalid app secret: ' + options.app_secret;
    }
    /**
     * Reddit app ID
     * @type {String}
     */
    this.app_id = options.app_id;
    /**
     * Reddit app secret
     * @type {String}
     */
    this.app_secret = options.app_secret;
    /**
     * Reddit app redirect URI
     * @type {?String}
     * @default null
     */
    this.redirect_uri = options.redirect_uri || null;
    /**
     * HTTP user agent header sent to Reddit for each request
     * @type {String}
     * @default reddit-oauth/x.y.z by aihamh
     */
    this.user_agent = options.user_agent || 'reddit-oauth/' + libVersion + ' by aihamh';
    /**
     * OAuth access token
     * @type {?String}
     * @default null
     */
    this.access_token = options.access_token || null;
    /**
     * OAuth refresh token
     * @type {?String}
     * @default null
     */
    this.refresh_token = options.refresh_token || null;
    /**
     * Throttled request function
     * Refer to: https://www.npmjs.com/package/throttled-request
     * @type {Function}
     */
    this.throttled_request = throttledRequestLib(request);
    this.throttled_request.configure({
        requests: 1,
        milliseconds: options.request_buffer || 2000
    });
}
RedditApi.prototype = {
    constructor: RedditApi,
    /**
     * Checks if user is authenticated based on the presence of an access token.
     * @return {Boolean}
     */
    isAuthed: function RedditApi__isAuthed() {
        return typeof this.access_token === 'string' &&
            this.access_token.length > 0;
    },
    /**
     * @callback RedditApi~ApiRequestCallback
     * @param {?Object} error
     * @param {Object} incomingMessage
     * @param {String|Buffer|Object} responseBody
     */
    /**
     * @callback RedditApi~ApiListingRequestCallback
     * @param {?Object} error
     * @param {Object} incomingMessage
     * @param {String|Buffer|Object} responseBody
     * @param {?Function} next - Invoke to retrieve the next page in the listing, until next equals null
     */
    /**
     * @callback RedditApi~ApiTokenCallback
     * @param {Boolean} success
     */
    /**
     * Create new API request to specified API endpoint with custom options and callback to be invoked on request completion. If authentication error occurs, is_refreshing_token is false and a refresh token is currently defined, then it will automatically attempt to retrieve a new access token then try again.
     * @param {String} path - API endpoint path
     * @param {external:Request~Options} [options={}] - Request options
     * @param {String} [options.method=GET] - HTTP method
     * @param {String} [options.url=https://(oauth|ssl).reddit.com/:path] - Request URL. ssl subdomain used for authentication; oauth for authenticated queries
     * @param {Object} [options.headers={}] - HTTP headers
     * @param {String} [options.headers.User-Agent] - User agent
     * @param {String} [options.headers.Authorization] - Bearer token if available
     * @param {RedditApi~ApiRequestCallback} callback - Callback function
     * @param {Boolean} [is_refreshing_token=false] - If false, will attempt to refresh tokens then retry request
     */
    request: function RedditApi__request(path, options, callback, is_refreshing_token) {
        if (!options) {
            options = {};
        }
        if (!options.headers) {
            options.headers = {};
        }
        options.headers['User-Agent'] = this.user_agent;
        if (this.isAuthed()) {
            options.headers['Authorization'] = 'bearer ' + this.access_token;
        }
        if (!options.url) {
            var subdomain = this.isAuthed() ? 'oauth' : 'ssl';
            options.url = 'https://' + subdomain + '.reddit.com' + path;
        }
        if (!options.method) {
            options.method = 'GET';
        }
        this.throttled_request(options, (function (api) {
            return function (error, response, body) {
                if (!error && response.statusCode === 200) {
                    try {
                        response.jsonData = JSON.parse(body);
                    } catch (e) {
                        error = e;
                    }
                } else if (!is_refreshing_token && response.statusCode === 401 && api.refresh_token) {
                    api.refreshToken(function (success) {
                        if (success) {
                            api.request(path, options, callback);
                        } else {
                            callback.call(api, error, response, data);
                        }
                    });
                    return;
                } else {
                    console.log(
                        'reddit-oauth Error:', error,
                        ', Status code:', response.statusCode,
                        ', Status message:', response.statusMessage
                    );
                }
                callback.call(api, error, response, body);
            };
        })(this));
    },
    /**
     * Authenticate with username and password
     * @param {String} username - Reddit username
     * @param {String} password - Reddit password
     * @param {RedditApi~ApiRequestCallback} callback - Request callback
     */
    passAuth: function RedditApi__passAuth(username, password, callback) {
        this.access_token = null;
        this.refresh_token = null;
        this.request('/api/v1/access_token', {
            method: 'POST',
            form: {
                grant_type: 'password',
                username: username,
                password: password
            },
            auth: {
                username: this.app_id,
                password: this.app_secret
            }
        }, function (error, response, body) {
            var success = !error &&
                typeof response.jsonData === 'object' &&
                typeof response.jsonData.access_token === 'string' &&
                response.jsonData.access_token.length > 0;
            if (success) {
                this.access_token = response.jsonData.access_token;
            }
            if (callback) {
                callback(success);
            }
        });
    },
    /**
     * Get OAuth authorization URL for specific scope
     * @param {String} state - An arbitrary string that is checked when user returns with the code
     * @param {String|Array.<String>} scope - Array or comma separated list of scopes to request from user
     * @return {String} URL to send user's browser to
     */
    oAuthUrl: function RedditApi__oAuthUrl(state, scope) {
        if (Array.isArray(scope)) {
            scope = scope.join(',');
        }
        if (typeof scope !== 'string') {
            throw 'Invalid scope: ' + scope;
        }
        var url = 'https://ssl.reddit.com/api/v1/authorize' +
            '?client_id=' + encodeURIComponent(this.app_id) +
            '&response_type=code' +
            '&state=' + encodeURIComponent(state) +
            '&redirect_uri=' + encodeURIComponent(this.redirect_uri || '') +
            '&duration=permanent' +
            '&scope=' + encodeURIComponent(scope);
        return url;
    },
    /**
     * Upon user returning from authorization URL, use supplied code to request access and fresh tokens.
     * @param {String} state - The same arbitrary string that was used in {@link RedditApi#oAuthUrl}
     * @param {Object} query - Key/value pairs from HTTP query string constructed by Reddit
     * @param {String} query.state - Should be the string passed into {@link RedditApi#oAuthUrl}
     * @param {String} query.code - A one time use token provided by Reddit to be exchanged for access and refresh tokens
     * @param {RedditApi~ApiTokenCallback} callback - Callback function to invoke after tokens are retrieved
     */
    oAuthTokens: function RedditApi__oAuthTokens(state, query, callback) {
        if (query.state !== state || !query.code) {
            callback(false);
            return;
        }
        this.access_token = null;
        this.refresh_token = null;
        this.request('/api/v1/access_token', {
            method: 'POST',
            form: {
                grant_type: 'authorization_code',
                code: query.code,
                redirect_uri: this.redirect_uri || ''
            },
            auth: {
                username: this.app_id,
                password: this.app_secret
            }
        }, function (error, response, body) {
            var success = !error &&
                typeof response.jsonData === 'object' &&
                typeof response.jsonData.access_token === 'string' &&
                typeof response.jsonData.refresh_token === 'string' &&
                response.jsonData.access_token.length > 0 &&
                response.jsonData.refresh_token.length > 0;
            if (success) {
                this.access_token = response.jsonData.access_token;
                this.refresh_token = response.jsonData.refresh_token;
            }
            if (callback) {
                callback(success);
            }
        });
    },
    /**
     * Request a new access token using the existing refresh token.
     * @param {RedditApi~ApiTokenCallback} callback - Callback function to invoke after the access token is retrieved
     */
    refreshToken: function RedditApi__refreshToken(callback) {
        this.access_token = null;
        this.request('/api/v1/access_token', {
            method: 'POST',
            form: {
                grant_type: 'refresh_token',
                refresh_token: this.refresh_token
            },
            auth: {
                username: this.app_id,
                password: this.app_secret
            }
        }, function (error, response, body) {
            var success = !error &&
                typeof response.jsonData === 'object' &&
                typeof response.jsonData.access_token === 'string' &&
                response.jsonData.access_token.length > 0;
            if (success) {
                this.access_token = response.jsonData.access_token;
            }
            if (callback) {
                callback(!error);
            }
        }, true);
    },
    /**
     * Execute an authenticated GET request to the specified API endpoint.
     * @param {String} path - API endpoint path
     * @param {Object} params - Key/value pairs to send as the request query string
     * @param {RedditApi~ApiRequestCallback} callback - Callback function
     */
    get: function RedditApi__get(path, params, callback) {
        var options = null;
        if (params) {
            for (var key in params) {
                if (params.hasOwnProperty(key)) {
                    if (!options) options = {};
                    options.form = params;
                    break;
                }
            }
        }
        this.request(path, options, callback);
    },
    /**
     * Execute an authenticated POST request to the specified API endpoint.
     * @param {String} path - API endpoint path
     * @param {Object} params - Key/value pairs to send as the request POST body
     * @param {RedditApi~ApiRequestCallback} callback - Callback function
     */
    post: function RedditApi__post(path, params, callback) {
        var options = {
            method: 'POST'
        };
        if (params) {
            for (var key in params) {
                if (params.hasOwnProperty(key)) {
                    options.form = params;
                    break;
                }
            }
        }
        this.request(path, options, callback);
    },
    /**
     * Request a page of values from the specified listing endpoint. Use the additional 'next' callback argument to request the next page, repeatedly until 'next' equals null.
     * @param {String} path
     * @param {Object} params
     * @param {RedditApi~ApiListingRequestCallback} callback - Invoke the next callback to retrieve the next page of the list
     */
    getListing: function RedditApi__getListing(path, params, callback, after, count) {
        if (!count) {
            count = 0;
        }
        var fullPath = path;
        if (after) {
            fullPath += '?after=' + encodeURIComponent(after) + '&count=' + encodeURIComponent(count);
        }
        this.get(fullPath, params, (function (reddit) {
            return function (error, response, body) {
                if (error || response.statusCode !== 200) {
                    callback(error, response, body);
                    return;
                }
                var nextAfter = response.jsonData.data.after;
                var nextCount = count + response.jsonData.data.children.length;
                var next = nextAfter === null ? null : function () {
                    reddit.getListing(path, params, callback, nextAfter, nextCount);
                };
                if (callback) {
                    callback(error, response, body, next);
                }
            };
        })(this));
    }
};
module.exports = RedditApi;