/* eslint-disable no-console */

// System documented in https://zulip.readthedocs.io/en/latest/subsystems/logging.html

// This must be included before the first call to $(document).ready
// in order to be able to report exceptions that occur during their
// execution.

const blueslip_stacktrace = require("./blueslip_stacktrace");

if (Error.stackTraceLimit !== undefined) {
    Error.stackTraceLimit = 100000;
}

function Logger() {
    this._memory_log = [];
}

Logger.prototype = (function () {
    function pad(num, width) {
        let ret = num.toString();
        while (ret.length < width) {
            ret = "0" + ret;
        }
        return ret;
    }

    function make_logger_func(name) {
        return function Logger_func(...args) {
            const now = new Date();
            const date_str =
                now.getUTCFullYear() + '-' +
                pad(now.getUTCMonth() + 1, 2) + '-' +
                pad(now.getUTCDate(), 2) + ' ' +
                pad(now.getUTCHours(), 2) + ':' +
                pad(now.getUTCMinutes(), 2) + ':' +
                pad(now.getUTCSeconds(), 2) + '.' +
                pad(now.getUTCMilliseconds(), 3) + ' UTC';

            const str_args = args.map(x => typeof x === "object" ? JSON.stringify(x) : x);

            const log_entry = date_str + " " + name.toUpperCase() +
                ': ' + str_args.join("");
            this._memory_log.push(log_entry);

            // Don't let the log grow without bound
            if (this._memory_log.length > 1000) {
                this._memory_log.shift();
            }

            if (console[name] !== undefined) {
                return console[name](...args);
            }
            return;
        };
    }

    const proto = {
        get_log: function Logger_get_log() {
            return this._memory_log;
        },
    };

    const methods = ['debug', 'log', 'info', 'warn', 'error'];
    let i;
    for (i = 0; i < methods.length; i += 1) {
        proto[methods[i]] = make_logger_func(methods[i]);
    }

    return proto;
}());

const logger = new Logger();

exports.get_log = function blueslip_get_log() {
    return logger.get_log();
};

const reported_errors = new Set();
const last_report_attempt = new Map();

function report_error(msg, stack, opts) {
    opts = { show_ui_msg: false, ...opts };

    if (stack === undefined) {
        stack = 'No stacktrace available';
    }

    if (page_params.debug_mode) {
        // In development, we display blueslip errors in the web UI,
        // to make them hard to miss.
        blueslip_stacktrace.display_stacktrace(msg, stack);
    }

    const key = ':' + msg + stack;
    if (reported_errors.has(key)
        || last_report_attempt.has(key)
            // Only try to report a given error once every 5 minutes
            && Date.now() - last_report_attempt.get(key) <= 60 * 5 * 1000) {
        return;
    }

    last_report_attempt.set(key, Date.now());

    // TODO: If an exception gets thrown before we setup ajax calls
    // to include the CSRF token, our ajax call will fail.  The
    // elegant thing to do in that case is to either wait until that
    // setup is done or do it ourselves and then retry.
    //
    // Important: We don't use channel.js here so that exceptions
    // always make it to the server even if reload_state.is_in_progress.
    $.ajax({
        type: 'POST',
        url: '/json/report/error',
        dataType: 'json',
        data: {
            message: msg,
            stacktrace: stack,
            ui_message: opts.show_ui_msg,
            more_info: JSON.stringify(opts.more_info),
            href: window.location.href,
            user_agent: window.navigator.userAgent,
            log: logger.get_log().join("\n"),
        },
        timeout: 3 * 1000,
        success: function () {
            reported_errors.add(key);
            if (opts.show_ui_msg && ui_report !== undefined) {
                // There are a few races here (and below in the error
                // callback):
                // 1) The ui_report module or something it requires might
                //    not have been compiled or initialized yet.
                // 2) The DOM might not be ready yet and so fetching
                //    the #home-error div might fail.

                // For (1) we just don't show the message if the ui
                // hasn't been loaded yet.  The user will probably
                // get another error once it does.  We can't solve
                // (2) by using $(document).ready because the
                // callback never gets called (I think what's going
                // on here is if the exception was raised by a
                // function that was called as a result of the DOM
                // becoming ready then the internal state of jQuery
                // gets messed up and our callback never gets
                // invoked).  In any case, it will pretty clear that
                // something is wrong with the page and the user will
                // probably try to reload anyway.
                ui_report.message("Oops.  It seems something has gone wrong. " +
                                  "The error has been reported to the fine " +
                                  "folks at Zulip, but, in the mean time, " +
                                  "please try reloading the page.",
                                  $("#home-error"), "alert-error");
            }
        },
        error: function () {
            if (opts.show_ui_msg && ui_report !== undefined) {
                ui_report.message("Oops.  It seems something has gone wrong. " +
                                  "Please try reloading the page.",
                                  $("#home-error"), "alert-error");
            }
        },
    });

    if (page_params.save_stacktraces) {
        // Save the stacktrace so it can be examined even in
        // development servers.  (N.B. This assumes you have set DEBUG
        // = False on your development server, or else this code path
        // won't execute to begin with -- useful for testing
        // (un)minification.)
        window.last_stacktrace = stack;
    }
}

function BlueslipError(msg, more_info) {
    // One can't subclass Error normally so we have to play games
    // with setting __proto__
    const self = new Error(msg);
    self.name = "BlueslipError";

    // Indirect access to __proto__ keeps jslint quiet
    const proto = '__proto__';
    self[proto] = BlueslipError.prototype;

    if (more_info !== undefined) {
        self.more_info = more_info;
    }
    return self;
}

BlueslipError.prototype = Object.create(Error.prototype);

exports.exception_msg = function blueslip_exception_msg(ex) {
    let message = ex.message;
    if (ex.hasOwnProperty('fileName')) {
        message += " at " + ex.fileName;
        if (ex.hasOwnProperty('lineNumber')) {
            message += ":" + ex.lineNumber;
        }
    }
    return message;
};

$(window).on('error', function (event) {
    const ex = event.originalEvent.error;
    if (!ex || ex instanceof BlueslipError) {
        return;
    }
    const message = exports.exception_msg(ex);
    report_error(message, ex.stack);
});

function build_arg_list(msg, more_info) {
    const args = [msg];
    if (more_info !== undefined) {
        args.push("\nAdditional information: ", more_info);
    }
    return args;
}

exports.debug = function blueslip_debug(msg, more_info) {
    const args = build_arg_list(msg, more_info);
    logger.debug(...args);
};

exports.log = function blueslip_log(msg, more_info) {
    const args = build_arg_list(msg, more_info);
    logger.log(...args);
};

exports.info = function blueslip_info(msg, more_info) {
    const args = build_arg_list(msg, more_info);
    logger.info(...args);
};

exports.warn = function blueslip_warn(msg, more_info) {
    const args = build_arg_list(msg, more_info);
    logger.warn(...args);
    if (page_params.debug_mode) {
        console.trace();
    }
};

exports.error = function blueslip_error(msg, more_info, stack) {
    if (stack === undefined) {
        stack = Error().stack;
    }
    const args = build_arg_list(msg, more_info);
    logger.error(...args);
    report_error(msg, stack, {more_info: more_info});

    if (page_params.debug_mode) {
        throw new BlueslipError(msg, more_info);
    }
};

exports.fatal = function blueslip_fatal(msg, more_info) {
    report_error(msg, Error().stack, {more_info: more_info});
    throw new BlueslipError(msg, more_info);
};

exports.timings = new Map();

exports.start_timing = function (label) {
    const t1 = performance.now();

    return function () {
        const t2 = performance.now();
        const elapsed = t2 - t1;
        exports.timings.set(label, elapsed);
    };
};

// Produces an easy-to-read preview on an HTML element.  Currently
// only used for including in error report emails; be sure to discuss
// with other developers before using it in a user-facing context
// because it is not XSS-safe.
exports.preview_node = function (node) {
    if (node.constructor === jQuery) {
        node = node[0];
    }

    const tag = node.tagName.toLowerCase();
    const className = node.className.length ? node.className : false;
    const id = node.id.length ? node.id : false;

    const node_preview = "<" + tag +
       (id ? " id='" + id + "'" : "") +
       (className ? " class='" + className + "'" : "") +
       "></" + tag + ">";

    return node_preview;
};

window.blueslip = exports;
