var assert = require('assert');

var _ = require('underscore');
var step = require('step');
var debug = require('debug')('windshaft:cartodb');

var LZMA = require('lzma').LZMA;
var lzmaWorker = new LZMA();

// Whitelist query parameters and attach format
var REQUEST_QUERY_PARAMS_WHITELIST = [
    'config',
    'map_key',
    'api_key',
    'auth_token',
    'callback',
    'zoom',
    'lon',
    'lat',
    // widgets & filters
    'filters', // json
    'own_filter', // 0, 1
    'bbox', // w,s,e,n
    'bins', // number
    'start', // number
    'end', // number
    'column_type', // string
    // widgets search
    'q'
];

function BaseController(authApi, pgConnection) {
    this.authApi = authApi;
    this.pgConnection = pgConnection;
}

module.exports = BaseController;

// jshint maxcomplexity:10
/**
 * Whitelist input and get database name & default geometry type from
 * subdomain/user metadata held in CartoDB Redis
 * @param req - standard express request obj. Should have host & table
 * @param callback
 */
BaseController.prototype.req2params = function(req, callback){
    var self = this;

    if ( req.query.lzma ) {

        // Decode (from base64)
        var lzma = new Buffer(req.query.lzma, 'base64')
            .toString('binary')
            .split('')
            .map(function(c) {
                return c.charCodeAt(0) - 128;
            });


        // Decompress
        lzmaWorker.decompress(
            lzma,
            function(result) {
                req.profiler.done('lzma');
                try {
                    delete req.query.lzma;
                    _.extend(req.query, JSON.parse(result));
                    self.req2params(req, callback);
                } catch (err) {
                    req.profiler.done('req2params');
                    callback(new Error('Error parsing lzma as JSON: ' + err));
                }
            }
        );
        return;
    }

    var allowedQueryParams = REQUEST_QUERY_PARAMS_WHITELIST;
    if (Array.isArray(req.context.allowedQueryParams)) {
        allowedQueryParams = allowedQueryParams.concat(req.context.allowedQueryParams);
    }
    req.query = _.pick(req.query, allowedQueryParams);
    req.params = _.extend({}, req.params); // shuffle things as request is a strange array/object

    var user = req.context.user;

    if ( req.params.token ) {
        // Token might match the following patterns:
        // - {user}@{tpl_id}@{token}:{cache_buster}
        var tksplit = req.params.token.split(':');
        req.params.token = tksplit[0];
        if ( tksplit.length > 1 ) {
            req.params.cache_buster= tksplit[1];
        }
        tksplit = req.params.token.split('@');
        if ( tksplit.length > 1 ) {
            req.params.signer = tksplit.shift();
            if ( ! req.params.signer ) {
                req.params.signer = user;
            }
            else if ( req.params.signer !== user ) {
                var err = new Error(
                        'Cannot use map signature of user "' + req.params.signer + '" on db of user "' + user + '"'
                );
                err.http_status = 403;
                req.profiler.done('req2params');
                callback(err);
                return;
            }
            if ( tksplit.length > 1 ) {
                /*var template_hash = */tksplit.shift(); // unused
            }
            req.params.token = tksplit.shift();
        }
    }

    // bring all query values onto req.params object
    _.extend(req.params, req.query);

    req.profiler.done('req2params.setup');

    step(
        function getPrivacy(){
            self.authApi.authorize(req, this);
        },
        function validateAuthorization(err, authorized) {
            req.profiler.done('authorize');
            assert.ifError(err);
            if(!authorized) {
                err = new Error("Sorry, you are unauthorized (permission denied)");
                err.http_status = 403;
                throw err;
            }
            return null;
        },
        function getDatabase(err){
            assert.ifError(err);
            self.pgConnection.setDBConn(user, req.params, this);
        },
        function finishSetup(err) {
            if ( err ) {
                req.profiler.done('req2params');
                return callback(err, req);
            }

            // Add default database connection parameters
            // if none given
            _.defaults(req.params, {
                dbuser: global.environment.postgres.user,
                dbpassword: global.environment.postgres.password,
                dbhost: global.environment.postgres.host,
                dbport: global.environment.postgres.port
            });

            req.profiler.done('req2params');
            callback(null, req);
        }
    );
};
// jshint maxcomplexity:6

// jshint maxcomplexity:9
BaseController.prototype.send = function(req, res, body, status, headers) {
    if (req.params.dbhost) {
        res.set('X-Served-By-DB-Host', req.params.dbhost);
    }

    res.set('X-Tiler-Profiler', req.profiler.toJSONString());

    if (headers) {
        res.set(headers);
    }

    res.status(status);

    if (!Buffer.isBuffer(body) && typeof body === 'object') {
        if (req.query && req.query.callback) {
            res.jsonp(body);
        } else {
            res.json(body);
        }
    } else {
        res.send(body);
    }

    try {
        // May throw due to dns, see
        // See http://github.com/CartoDB/Windshaft/issues/166
        req.profiler.sendStats();
    } catch (err) {
        debug("error sending profiling stats: " + err);
    }
};
// jshint maxcomplexity:6

BaseController.prototype.sendError = function(req, res, err, label) {
    var allErrors = Array.isArray(err) ? err : [err];
    label = label || 'UNKNOWN';
    err = allErrors[0] || new Error(label);
    allErrors[0] = err;

    var statusCode = findStatusCode(err);

    debug('[%s ERROR] -- %d: %s, %s', label, statusCode, err, err.stack);

    // If a callback was requested, force status to 200
    if (req.query && req.query.callback) {
        statusCode = 200;
    }

    var errorResponseBody = {
        errors: allErrors.map(errorMessage),
        errors_with_context: allErrors.map(errorMessageWithContext)
    };

    this.send(req, res, errorResponseBody, statusCode);
};

function stripConnectionInfo(message) {
    // Strip connection info, if any
    return message
        // See https://github.com/CartoDB/Windshaft/issues/173
        .replace(/Connection string: '[^']*'\n\s/im, '')
        // See https://travis-ci.org/CartoDB/Windshaft/jobs/20703062#L1644
        .replace(/is the server.*encountered/im, 'encountered');
}

function errorMessage(err) {
    // See https://github.com/Vizzuality/Windshaft-cartodb/issues/68
    var message = (_.isString(err) ? err : err.message) || 'Unknown error';

    return stripConnectionInfo(message);
}

function errorMessageWithContext(err) {
    // See https://github.com/Vizzuality/Windshaft-cartodb/issues/68
    var message = (_.isString(err) ? err : err.message) || 'Unknown error';

    var error = {
        type: err.type || 'unknown',
        message: stripConnectionInfo(message),
    };

    for (var prop in err) {
        // type & message are properties from Error's prototype and will be skipped
        if (err.hasOwnProperty(prop)) {
            error[prop] = err[prop];
        }
    }

    return error;
}
module.exports.errorMessage = errorMessage;

function findStatusCode(err) {
    var statusCode;
    if ( err.http_status ) {
        statusCode = err.http_status;
    } else {
        statusCode = statusFromErrorMessage('' + err);
    }
    return statusCode;
}
module.exports.findStatusCode = findStatusCode;

function statusFromErrorMessage(errMsg) {
    // Find an appropriate statusCode based on message
    // jshint maxcomplexity:7
    var statusCode = 400;
    if ( -1 !== errMsg.indexOf('permission denied') ) {
        statusCode = 403;
    }
    else if ( -1 !== errMsg.indexOf('authentication failed') ) {
        statusCode = 403;
    }
    else if (errMsg.match(/Postgis Plugin.*[\s|\n].*column.*does not exist/)) {
        statusCode = 400;
    }
    else if ( -1 !== errMsg.indexOf('does not exist') ) {
        if ( -1 !== errMsg.indexOf(' role ') ) {
            statusCode = 403; // role 'xxx' does not exist
        } else if ( errMsg.match(/function .* does not exist/) ) {
            statusCode = 400; // invalid SQL (SQL function does not exist)
        } else {
            statusCode = 404;
        }
    }
    return statusCode;
}
