const _ = require('lodash');
const querystring = require('querystring');
const random = require('./random');
const maskSensitive = require('./maskSensitive');

const LOG_LEVELS = [
    'debug', 'info', 'warning', 'error'
];

const LOG_STYLES = {
    default: { fields: 2 },
    warning: { timestamp: 33, message: 33, fields: [ 33, 2 ] },
    error: { timestamp: 31, message: 31, fields: [ 31, 2 ] }
};

const ENV = process.env.CATALOG_ENVIRONMENT || process.env.NODE_ENV;
const EXTENDED_LOGS_ENABLED = [ true, 1, 'true', '1' ].indexOf(process.env.CATALOG_EXTENDED_LOGS_ENABLED) !== -1;

function log(...args) {
    // log(fetch) - логирование fetch-запроса
    if (typeof args[0] === 'function')
        return (url, options) => fetchVerbosely(args[0], url, options);

    // log(req) - логирование объекта запроса
    if (args[0] && args[0].headers && args[0].url) {
        let req = args[0];
        let url = req.originalUrl || req.url;
        let options = { method: req.method, headers: req.headers, body: req.body };
        return log(`⚑ ${getRequestString(url, options)}`, getRequestOptions(url, options));
    }

    let messages = [];
    let output = { message: '', fields: {} };

    args.forEach(arg => {
        if (arg !== null && typeof arg === 'object') {
            messages.push(_.get(arg, '_response.message', arg.message));
            _.merge(output.fields, arg);
        }
        else if (arg !== undefined && arg !== '')
            messages.push(arg);
    });

    if (messages.length)
        output.message = messages.filter(Boolean).join(' - ');

    let level = (output.message.match(/^@(\w+)/) || [])[1];

    if (level && LOG_LEVELS.indexOf(level) !== -1) {
        output.level = level;
        output.message = output.message.replace(/^@(\w+)\s*(\-\s*)?/, '');
    }

    Object.keys(output).forEach(key => {
        output[key] = maskSensitive(output[key]);
    });

    if (ENV === 'development') {
        let styles = LOG_STYLES[level] || LOG_STYLES.default;

        console.log(style(getDateString(), styles.timestamp));

        if (output.message)
            console.log(style(output.message, styles.message));

        if (!_.isEmpty(output.fields)) {
            let indentation = level === 'json' ? 3 : undefined;
            console.log(style(JSON.stringify(output.fields, null, indentation), styles.fields));
        }

        console.log();
    }
    else {
        output['@fields'] = output.fields;
        delete output.fields;
        console.log(JSON.stringify(output));
    }
}

function style(text, escapeSequences) {
    if (!escapeSequences)
        return text;

    let openingSequence = (
        escapeSequences instanceof Array ? escapeSequences : [ escapeSequences ]
    ).map(value => `\x1b[${value}m`).join('');

    return openingSequence + text + '\x1b[0m';
}

function fetchVerbosely(fetch, url, options) {
    let requestId = _.get(options, 'headers.x-request-id');

    if (!requestId) {
        requestId = random.getString(16);
        _.set(options, 'headers.x-request-id', requestId);
    }

    let requestString = getRequestString(url, options);
    let requestOptions = getRequestOptions(url, options);
    let t0 = Date.now();

    log(`→ ${requestString}`, requestOptions);

    return fetch(url, options)
        .then(res => {
            res.headers.set('x-request-id', requestId);
            return handleResponse(res, url, options, t0);
        })
        .catch(err => {
            let error = new Error(err);
            log(`@error ☠ ${requestString} - [${error.message}]`, requestOptions);
            throw error;
        });
}

function handleResponse(res, url, options, t0) {
    let shouldIntercept = res && (
        (!res.ok || EXTENDED_LOGS_ENABLED) &&
        res.headers && /^application\/json\b/.test(res.headers.get('Content-Type'))
    );

    let statusString = `${res.ok ? '✔' : '@error ✘'} ${getRequestString(url, options)} - [${res.status}: ${res.statusText}]`;
    let requestOptions = getRequestOptions(url, options, t0);

    if (!shouldIntercept) {
        log(statusString, requestOptions);
        return res;
    }

    if (res.status === 204) { // No content
        return Promise.resolve()
            .then(() => {
                log(statusString, requestOptions, { _response: null });
                res.json = () => Promise.resolve(null);
                return res;
            });
    }

    return res.text()
        .then(content => {
            try {
                let data = JSON.parse(content);

                log(statusString, requestOptions, { _response: data });

                res.json = () => Promise.resolve(data);
            }
            catch(e) {
                res.json = () => { throw e; };
            }

            res.text = () => content;

            return res;
        });
}

function getRequestString(url, options) {
    let urlComponents = parseUrl(url);
    let method = _.get(options, 'method', 'GET').toUpperCase();
    let requestId = _.get(options, 'headers.x-request-id');

    return `${method} ${urlComponents.base} [${requestId}]`;
}

function getRequestOptions(url, options, t0) {
    let output = { _request: { options, url: parseUrl(url) } };
    if (t0 !== undefined) output._duration = Date.now() - t0;
    return output;
}

function parseUrl(url) {
    let components = String(url).match(/^([^\?]*)(\?([^#]*))?(#(.*))?$/);
    return {
        base: components[1],
        query: components[3] ? querystring.parse(components[3]) : null,
        hash: components[5]
    };
}

function getDateString(date) {
    if (date === undefined) date = new Date();

    let components = {};

    components.date = [ date.getFullYear(), date.getMonth() + 1, date.getDate() ]
        .map(value => fillDigits(value, 2))
        .join('-');
    components.time = date.toTimeString().split(' ')
        .map((value, i) => i === 0 ? value + '.' + fillDigits(date.getMilliseconds(), 3) : value)
        .join(' ');

    return `${components.date} ${components.time}`;
}

function fillDigits(value, length) {
    let output = String(value);
    while (output.length < length) output = '0' + output;
    return output;
}

module.exports = log;
